马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View File

@@ -0,0 +1,953 @@
import 'package:ainoval/models/user_ai_model_config_model.dart';
import 'package:ainoval/models/context_selection_models.dart';
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/models/setting_group.dart';
import 'package:ainoval/models/novel_snippet.dart';
import 'package:ainoval/models/ai_request_models.dart';
import 'package:ainoval/models/preset_models.dart';
import 'package:ainoval/models/unified_ai_model.dart';
import 'package:ainoval/services/ai_preset_service.dart';
import 'package:ainoval/config/app_config.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:ainoval/widgets/common/top_toast.dart';
import 'package:ainoval/widgets/common/model_display_selector.dart';
import 'package:ainoval/widgets/common/context_selection_dropdown_menu_anchor.dart';
import 'package:ainoval/widgets/common/credit_display.dart';
import 'package:flutter/material.dart';
import 'package:ainoval/utils/web_theme.dart';
// import 'package:flutter_bloc/flutter_bloc.dart';
class ChatInput extends StatefulWidget {
const ChatInput({
Key? key,
required this.controller,
required this.onSend,
this.isGenerating = false,
this.onCancel,
this.onModelSelected,
this.initialModel,
this.novel,
this.contextData,
this.onContextChanged,
this.settings = const [],
this.settingGroups = const [],
this.snippets = const [],
this.chatConfig,
this.onConfigChanged,
this.onCreditError, // 🚀 新增:积分不足错误回调
this.initialChapterId,
this.initialSceneId,
}) : super(key: key);
final TextEditingController controller;
final VoidCallback onSend;
final Function(String)? onCreditError; // 🚀 新增:积分不足错误回调
final bool isGenerating;
final VoidCallback? onCancel;
final Function(UserAIModelConfigModel?)? onModelSelected;
final UserAIModelConfigModel? initialModel;
final dynamic novel;
final ContextSelectionData? contextData;
final ValueChanged<ContextSelectionData>? onContextChanged;
final List<NovelSettingItem> settings;
final List<SettingGroup> settingGroups;
final List<NovelSnippet> snippets;
final UniversalAIRequest? chatConfig;
final ValueChanged<UniversalAIRequest>? onConfigChanged;
final String? initialChapterId;
final String? initialSceneId;
@override
State<ChatInput> createState() => _ChatInputState();
}
class _ChatInputState extends State<ChatInput> {
OverlayEntry? _presetOverlay;
final LayerLink _layerLink = LayerLink();
bool _isComposing = false;
// 预设相关状态
// final GlobalKey _presetButtonKey = GlobalKey();
List<AIPromptPreset> _availablePresets = [];
bool _isLoadingPresets = false;
AIPromptPreset? _currentPreset;
@override
void initState() {
super.initState();
widget.controller.addListener(_handleTextChange);
_handleTextChange();
_loadPresets();
}
@override
void dispose() {
widget.controller.removeListener(_handleTextChange);
_removePresetOverlay();
super.dispose();
}
/// 加载预设数据
Future<void> _loadPresets() async {
if (_isLoadingPresets) return;
setState(() {
_isLoadingPresets = true;
});
try {
final presetService = AIPresetService();
// 直接获取AI_CHAT类型的预设
final chatPresets = await presetService.getUserPresets(featureType: 'AI_CHAT');
setState(() {
_availablePresets = chatPresets;
_isLoadingPresets = false;
});
AppLogger.i('ChatInput', '加载了 ${_availablePresets.length} 个聊天预设');
} catch (e) {
setState(() {
_isLoadingPresets = false;
});
AppLogger.e('ChatInput', '加载预设失败', e);
}
}
void _handleTextChange() {
final bool composingNow = widget.controller.text.trim().isNotEmpty;
if (composingNow != _isComposing) {
// 只有从空 → 非空 或 非空 → 空 时才重建,避免输入过程中频繁 setState
setState(() {
_isComposing = composingNow;
});
}
}
/// 显示预设下拉菜单
void _showPresetOverlay() {
if (_presetOverlay != null) {
_removePresetOverlay();
return;
}
_presetOverlay = OverlayEntry(
builder: (context) => Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: _removePresetOverlay,
child: Container(color: Colors.transparent),
),
),
CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
targetAnchor: Alignment.topRight,
followerAnchor: Alignment.bottomRight,
offset: const Offset(0, -8),
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surfaceContainer,
shadowColor: WebTheme.getShadowColor(context, opacity: 0.15),
child: Container(
width: 240,
constraints: const BoxConstraints(maxHeight: 320),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.3),
),
),
child: _buildPresetMenuContent(),
),
),
),
],
),
);
Overlay.of(context).insert(_presetOverlay!);
}
/// 移除预设下拉菜单
void _removePresetOverlay() {
_presetOverlay?.remove();
_presetOverlay = null;
}
/// 构建预设菜单内容
Widget _buildPresetMenuContent() {
if (_isLoadingPresets) {
return Container(
height: 120,
padding: const EdgeInsets.all(16),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(height: 8),
Text(
'加载预设中...',
style: TextStyle(fontSize: 12),
),
],
),
),
);
}
if (_availablePresets.isEmpty) {
return Container(
height: 120,
padding: const EdgeInsets.all(16),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.auto_awesome_outlined,
size: 32,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
),
const SizedBox(height: 8),
Text(
'暂无可用预设',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
'可在设置中创建预设',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.7),
),
),
],
),
),
);
}
// 对预设进行分组
final Map<String, List<AIPromptPreset>> groupedPresets = {
'最近使用': _availablePresets.where((p) => p.lastUsedAt != null).take(3).toList(),
'收藏预设': _availablePresets.where((p) => p.isFavorite).toList(),
'所有预设': _availablePresets,
};
return ListView(
padding: const EdgeInsets.all(8),
shrinkWrap: true,
children: [
// 标题
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
child: Row(
children: [
Icon(
Icons.auto_awesome,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'快速预设',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
const Divider(height: 1),
// 预设分组列表
...groupedPresets.entries.where((entry) => entry.value.isNotEmpty).map((entry) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (entry.key != '所有预设' || (entry.key == '所有预设' && groupedPresets['最近使用']!.isEmpty && groupedPresets['收藏预设']!.isEmpty))
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
child: Text(
entry.key,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant,
letterSpacing: 0.5,
),
),
),
...entry.value.map((preset) => _buildPresetMenuItem(preset)).toList(),
],
);
}).toList(),
],
);
}
/// 构建预设菜单项
Widget _buildPresetMenuItem(AIPromptPreset preset) {
final colorScheme = Theme.of(context).colorScheme;
final isSelected = _currentPreset?.presetId == preset.presetId;
return InkWell(
onTap: () => _handlePresetSelected(preset),
borderRadius: BorderRadius.circular(8),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? colorScheme.primaryContainer.withOpacity(0.3) : null,
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
// 预设图标
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.auto_awesome,
size: 12,
color: colorScheme.primary,
),
),
const SizedBox(width: 8),
// 预设信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
preset.displayName,
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? colorScheme.primary : colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
),
if (preset.isFavorite) ...[
const SizedBox(width: 4),
Icon(
Icons.star,
size: 10,
color: Colors.amber.shade600,
),
],
],
),
if (preset.presetDescription != null && preset.presetDescription!.isNotEmpty)
Text(
preset.presetDescription!,
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
// 选中标识
if (isSelected)
Icon(
Icons.check_circle,
size: 14,
color: colorScheme.primary,
),
],
),
),
);
}
/// 处理预设选择
void _handlePresetSelected(AIPromptPreset preset) {
_removePresetOverlay();
try {
setState(() {
_currentPreset = preset;
});
// 解析预设并应用到聊天配置
final parsedRequest = preset.parsedRequest;
if (parsedRequest != null && widget.onConfigChanged != null) {
// 创建新的配置,保留现有的基础信息
final baseConfig = widget.chatConfig ?? UniversalAIRequest(
requestType: AIRequestType.chat,
userId: AppConfig.userId ?? 'unknown',
novelId: widget.novel?.id,
);
// 应用预设配置
final updatedConfig = baseConfig.copyWith(
modelConfig: parsedRequest.modelConfig ?? baseConfig.modelConfig,
instructions: parsedRequest.instructions?.isNotEmpty == true
? parsedRequest.instructions
: preset.effectiveUserPrompt.isNotEmpty ? preset.effectiveUserPrompt : null,
contextSelections: parsedRequest.contextSelections ?? baseConfig.contextSelections,
enableSmartContext: parsedRequest.enableSmartContext,
parameters: {
...baseConfig.parameters,
...parsedRequest.parameters,
},
metadata: {
...baseConfig.metadata,
'appliedPreset': preset.presetId,
'presetName': preset.presetName,
'lastPresetApplied': DateTime.now().toIso8601String(),
},
);
widget.onConfigChanged!(updatedConfig);
// 如果预设包含模型配置,也要通知模型选择器
if (parsedRequest.modelConfig != null) {
widget.onModelSelected?.call(parsedRequest.modelConfig);
}
AppLogger.i('ChatInput', '预设已应用: ${preset.displayName}');
// 记录预设使用
AIPresetService().applyPreset(preset.presetId);
// 显示成功提示
TopToast.success(context, '已应用预设: ${preset.displayName}');
} else {
AppLogger.w('ChatInput', '预设解析失败或缺少配置变更回调');
TopToast.error(context, '应用预设失败');
}
} catch (e) {
AppLogger.e('ChatInput', '应用预设失败', e);
TopToast.error(context, '应用预设失败: $e');
}
}
void _updateContextData(ContextSelectionData newData, {bool isAddOperation = true}) {
if (widget.onConfigChanged != null) {
if (widget.chatConfig != null) {
// 🚀 修复使用完整的菜单结构而不是可能不完整的currentSelections
final currentSelections = widget.chatConfig!.contextSelections;
// 🚀 获取完整的菜单结构数据
ContextSelectionData? fullContextData;
if (widget.contextData != null) {
fullContextData = widget.contextData;
} else if (widget.novel != null) {
fullContextData = ContextSelectionDataBuilder.fromNovelWithContext(
widget.novel!,
settings: widget.settings,
settingGroups: widget.settingGroups,
snippets: widget.snippets,
);
}
if (fullContextData != null) {
ContextSelectionData updatedSelections;
if (isAddOperation && currentSelections != null) {
// 🚀 添加操作:将现有选择应用到完整结构,然后添加新选择
// 先应用现有选择到完整结构
updatedSelections = fullContextData.applyPresetSelections(currentSelections);
// 再添加新选择的项目
for (final newItem in newData.selectedItems.values) {
if (!updatedSelections.selectedItems.containsKey(newItem.id)) {
updatedSelections = updatedSelections.selectItem(newItem.id);
}
}
} else if (!isAddOperation && currentSelections != null) {
// 🚀 删除操作:将现有选择应用到完整结构,然后移除指定项目
updatedSelections = fullContextData.applyPresetSelections(currentSelections);
// 找出被删除的项目并移除
for (final existingId in currentSelections.selectedItems.keys) {
if (!newData.selectedItems.containsKey(existingId)) {
updatedSelections = updatedSelections.deselectItem(existingId);
}
}
} else {
// 🚀 如果当前没有选择,直接使用新数据(但保持完整结构)
updatedSelections = fullContextData;
for (final newItem in newData.selectedItems.values) {
updatedSelections = updatedSelections.selectItem(newItem.id);
}
}
final updatedConfig = widget.chatConfig!.copyWith(
contextSelections: updatedSelections,
);
widget.onConfigChanged!(updatedConfig);
} else {
// 如果无法获取完整菜单结构,回退到原来的逻辑
final updatedConfig = widget.chatConfig!.copyWith(
contextSelections: newData,
);
widget.onConfigChanged!(updatedConfig);
}
} else {
// 如果没有chatConfig创建一个基础配置
final newConfig = UniversalAIRequest(
requestType: AIRequestType.chat,
userId: 'unknown', // 这应该从某个地方获取
novelId: widget.novel?.id,
contextSelections: newData,
);
widget.onConfigChanged!(newConfig);
}
} else {
// 🚀 如果没有onConfigChanged回调则使用传统的onContextChanged
widget.onContextChanged?.call(newData);
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final bool canSend = _isComposing && !widget.isGenerating;
ContextSelectionData? currentContextData;
if (widget.contextData != null) {
// 🚀 使用EditorScreenController维护的级联菜单数据静态结构
currentContextData = widget.contextData;
} else if (widget.novel != null) {
// 备用方案如果EditorScreenController还没有准备好数据则临时构建
currentContextData = ContextSelectionDataBuilder.fromNovelWithContext(
widget.novel!,
settings: widget.settings,
settingGroups: widget.settingGroups,
snippets: widget.snippets,
);
}
// final contextSelectionCount = widget.chatConfig?.contextSelections?.selectedCount ?? 0;
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
border: Border(
top: BorderSide(
color: colorScheme.outlineVariant.withOpacity(0.5),
width: 1.0,
),
),
),
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 上下文选择区域 - 始终显示,以便用户可以点击添加
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: colorScheme.outline.withOpacity(0.1),
width: 1.0,
),
),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center, // 垂直居中对齐
children: [
// 使用完整的上下文选择组件 - 包含完整的级联菜单
if (currentContextData != null)
ContextSelectionDropdownBuilder.buildMenuAnchor(
data: currentContextData,
onSelectionChanged: _updateContextData,
placeholder: '+ Context',
maxHeight: 400,
initialChapterId: widget.initialChapterId,
initialSceneId: widget.initialSceneId,
)
else
// 当没有数据时显示占位符
Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.pending_outlined,
size: 16,
color: colorScheme.onSurface.withOpacity(0.5),
),
const SizedBox(width: 8),
Text(
'等待级联菜单数据...',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurface.withOpacity(0.5),
),
),
],
),
),
// 🚀 修复:使用完整菜单结构中的已选择项目显示标签
if (currentContextData != null && widget.chatConfig?.contextSelections != null)
..._buildSelectedContextTags(currentContextData, widget.chatConfig!.contextSelections!).map((item) {
return Container(
height: 36,
constraints: const BoxConstraints(maxWidth: 200),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.75),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colorScheme.outline.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
item.type.icon,
size: 16,
color: colorScheme.onSurface.withOpacity(0.7),
),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item.title,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (item.displaySubtitle.isNotEmpty)
Text(
item.displaySubtitle,
style: TextStyle(
fontSize: 9,
color: colorScheme.onSurface.withOpacity(0.6),
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 4),
InkWell(
onTap: () {
// 🚀 修复:使用完整菜单结构进行删除操作
if (currentContextData != null && widget.chatConfig!.contextSelections != null) {
// 将当前选择应用到完整结构,然后删除指定项目
final fullDataWithSelections = currentContextData.applyPresetSelections(widget.chatConfig!.contextSelections!);
final newData = fullDataWithSelections.deselectItem(item.id);
_updateContextData(newData, isAddOperation: false);
}
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(2),
child: Icon(
Icons.close,
size: 14,
color: colorScheme.onSurface.withOpacity(0.5),
),
),
),
],
),
);
}).toList(),
],
),
),
const SizedBox(height: 8.0),
// 输入框行 - 独占一行,去掉圆角,紧贴边缘
Container(
width: double.infinity,
child: TextField(
controller: widget.controller,
decoration: InputDecoration(
hintText: widget.isGenerating ? 'AI 正在回复...' : '输入消息...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(0), // 去掉圆角
borderSide: BorderSide(
color: colorScheme.outline.withOpacity(0.5)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(0), // 去掉圆角
borderSide: BorderSide(
color: colorScheme.outline.withOpacity(0.3)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(0), // 去掉圆角
borderSide:
BorderSide(color: colorScheme.primary, width: 1.5),
),
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12), // 增加垂直内边距
isDense: false, // 改为false以获得更多空间
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(0), // 去掉圆角
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline.withOpacity(0.3)),
),
),
readOnly: widget.isGenerating,
minLines: 1,
maxLines: 5,
textInputAction: TextInputAction.newline,
style: TextStyle(fontSize: 14, color: colorScheme.onSurface),
onSubmitted: (_) {
if (canSend) {
widget.onSend();
}
},
),
),
const SizedBox(height: 8.0),
// 预设按钮、积分显示、模型选择器和发送按钮行
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 预设快捷按钮 - 使用PopupMenuButton实现精准定位
CompositedTransformTarget(
link: _layerLink,
child: GestureDetector(
onTap: _showPresetOverlay,
child: Container(
width: 40,
height: 36, // 与模型选择器保持一致的高度
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.surfaceContainerHighest // 深色容器
: Theme.of(context).colorScheme.surface, // 浅色容器
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.4),
width: 1.0,
),
borderRadius: BorderRadius.circular(20), // rounded-full
boxShadow: [
BoxShadow(
color: WebTheme.getShadowColor(context, opacity: 0.1),
blurRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: _showPresetOverlay,
borderRadius: BorderRadius.circular(20),
hoverColor: Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.8),
child: Container(
width: 40,
height: 36,
child: Center(
child: Icon(
Icons.auto_awesome,
size: 16,
color: _currentPreset != null
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
),
),
),
),
),
),
),
const SizedBox(width: 8),
// 🚀 积分显示组件
const CreditDisplay(
size: CreditDisplaySize.small,
showRefreshButton: false,
),
const SizedBox(width: 8),
// 模型选择按钮 - 使用统一的显示/选择组件
Expanded(
child: ModelDisplaySelector(
selectedModel: widget.initialModel != null ? PrivateAIModel(widget.initialModel!) : null,
onModelSelected: (unifiedModel) {
// 将UnifiedAIModel转换为UserAIModelConfigModel以保持兼容性
UserAIModelConfigModel? compatModel;
if (unifiedModel != null) {
if (unifiedModel.isPublic) {
final publicModel = (unifiedModel as PublicAIModel).publicConfig;
compatModel = UserAIModelConfigModel.fromJson({
'id': 'public_${publicModel.id}',
'userId': AppConfig.userId ?? 'unknown',
'alias': publicModel.displayName,
'modelName': publicModel.modelId,
'provider': publicModel.provider,
'apiEndpoint': '',
'isDefault': false,
'isValidated': true,
'createdAt': DateTime.now().toIso8601String(),
'updatedAt': DateTime.now().toIso8601String(),
});
} else {
compatModel = (unifiedModel as PrivateAIModel).userConfig;
}
}
widget.onModelSelected?.call(compatModel);
},
chatConfig: widget.chatConfig,
onConfigChanged: widget.onConfigChanged,
novel: widget.novel,
settings: widget.settings,
settingGroups: widget.settingGroups,
snippets: widget.snippets,
size: ModelDisplaySize.medium,
showIcon: true,
showTags: true,
showSettingsButton: true,
placeholder: '选择模型',
),
),
const SizedBox(width: 8),
// 发送/停止按钮 - 改为纯黑/灰黑主题
SizedBox(
height: 36, // 与模型选择器保持一致的高度
width: 36,
child: widget.isGenerating
? Material(
color: colorScheme.primary, // 使用主色
borderRadius: BorderRadius.circular(18),
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: widget.onCancel,
child: Container(
width: 36,
height: 36,
child: const Icon(
Icons.stop_rounded,
size: 20,
color: Colors.white,
),
),
),
)
: Material(
color: canSend
? colorScheme.primary
: colorScheme.onSurfaceVariant,
borderRadius: BorderRadius.circular(18),
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: canSend ? _handleSendWithCreditCheck : null,
child: Container(
width: 36,
height: 36,
child: Icon(
Icons.arrow_upward_rounded,
size: 20,
color: canSend
? colorScheme.onPrimary
: colorScheme.onPrimary.withOpacity(0.5),
),
),
),
),
),
],
),
],
),
);
}
/// 🚀 新增:带积分检查的发送处理
void _handleSendWithCreditCheck() {
try {
// 调用原发送方法,积分校验将在后端处理
widget.onSend();
} catch (e) {
// 如果发送失败,检查是否为积分不足错误
final errorMessage = e.toString();
if (errorMessage.contains('积分不足') || errorMessage.contains('InsufficientCredits')) {
// 积分不足,调用错误回调
widget.onCreditError?.call('积分不足,无法发送消息。请充值后重试。');
// 同时显示Toast提示
TopToast.error(context, '积分不足,无法发送消息');
} else {
// 其他错误,显示通用错误提示
TopToast.error(context, '发送失败: $errorMessage');
}
}
}
/// 🚀 构建已选择的上下文标签,使用完整菜单结构中的数据
List<ContextSelectionItem> _buildSelectedContextTags(
ContextSelectionData fullContextData,
ContextSelectionData currentSelections,
) {
// 将当前选择应用到完整菜单结构中
final updatedContextData = fullContextData.applyPresetSelections(currentSelections);
// 返回应用后的选中项目列表
return updatedContextData.selectedItems.values.toList();
}
}