Files
MaliangAINovalWriter/AINoval/lib/screens/chat/widgets/chat_input.dart
2025-09-10 00:07:52 +08:00

954 lines
36 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}