Files
MaliangAINovalWriter/AINoval/lib/widgets/common/multi_select_instructions_with_presets.dart
2025-09-10 00:07:52 +08:00

419 lines
12 KiB
Dart
Raw Permalink 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:flutter/material.dart';
import 'package:ainoval/utils/web_theme.dart';
import 'custom_text_editor.dart';
/// 指令预设选项
class InstructionPreset {
/// 构造函数
const InstructionPreset({
required this.id,
required this.title,
required this.content,
this.description,
});
/// 唯一标识
final String id;
/// 显示标题
final String title;
/// 指令内容
final String content;
/// 描述
final String? description;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InstructionPreset &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}
/// 多选指令预设组件
/// 类似于HTML中的多选下拉框支持选择多个预设
/// 选中的预设以badges/chips形式显示
class MultiSelectInstructionsWithPresets extends StatefulWidget {
/// 构造函数
const MultiSelectInstructionsWithPresets({
super.key,
this.controller,
this.presets = const [],
this.placeholder = 'e.g. You are a...',
this.dropdownPlaceholder = 'Select Instructions...',
this.onExpand,
this.onCopy,
this.onSelectionChanged,
});
/// 文本控制器
final TextEditingController? controller;
/// 预设选项列表
final List<InstructionPreset> presets;
/// 输入框占位符
final String placeholder;
/// 下拉框占位符
final String dropdownPlaceholder;
/// 展开回调
final VoidCallback? onExpand;
/// 复制回调
final VoidCallback? onCopy;
/// 选择改变回调
final ValueChanged<List<InstructionPreset>>? onSelectionChanged;
@override
State<MultiSelectInstructionsWithPresets> createState() => _MultiSelectInstructionsWithPresetsState();
}
class _MultiSelectInstructionsWithPresetsState extends State<MultiSelectInstructionsWithPresets> {
final Set<InstructionPreset> _selectedPresets = <InstructionPreset>{};
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
final GlobalKey _dropdownKey = GlobalKey();
@override
void dispose() {
_removeOverlay();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// 第一行:多选下拉框
if (widget.presets.isNotEmpty) ...[
_buildMultiSelectDropdown(),
const SizedBox(height: 8),
],
// 第二行:文本编辑器
CustomTextEditor(
controller: widget.controller,
placeholder: widget.placeholder,
onExpand: widget.onExpand,
onCopy: widget.onCopy,
),
],
);
}
/// 构建多选下拉框
Widget _buildMultiSelectDropdown() {
final isDark = Theme.of(context).brightness == Brightness.dark;
return CompositedTransformTarget(
link: _layerLink,
child: GestureDetector(
key: _dropdownKey,
onTap: _toggleDropdown,
child: Container(
width: double.infinity,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
width: 1,
),
borderRadius: BorderRadius.circular(6),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
child: Row(
children: [
// 选中的badges区域
Expanded(
child: _selectedPresets.isEmpty
? Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
widget.dropdownPlaceholder,
style: TextStyle(
fontSize: 14,
color: isDark ? WebTheme.darkGrey400 : WebTheme.grey500,
),
),
)
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
const SizedBox(width: 4),
..._selectedPresets.map((preset) => _buildPresetBadge(preset)),
],
),
),
),
// 下拉箭头
Padding(
padding: const EdgeInsets.only(right: 8),
child: Icon(
Icons.keyboard_arrow_down,
size: 16,
color: isDark ? WebTheme.darkGrey400 : WebTheme.grey400,
),
),
],
),
),
),
),
);
}
/// 构建预设badge
Widget _buildPresetBadge(InstructionPreset preset) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
margin: const EdgeInsets.only(right: 4),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isDark
? const Color(0xFF3A3A3A).withOpacity(0.8)
: const Color(0xFFF4F4F5),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
preset.title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isDark
? const Color(0xFFA1A1AA)
: const Color(0xFF52525B),
),
),
const SizedBox(width: 4),
GestureDetector(
onTap: () => _removePreset(preset),
child: Icon(
Icons.close,
size: 12,
color: isDark
? const Color(0xFFA1A1AA)
: const Color(0xFF52525B),
),
),
],
),
);
}
/// 切换下拉菜单显示状态
void _toggleDropdown() {
if (_overlayEntry != null) {
_removeOverlay();
} else {
_showOverlay();
}
}
/// 显示下拉菜单覆盖层
void _showOverlay() {
final RenderBox? renderBox = _dropdownKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final size = renderBox.size;
final overlay = Overlay.of(context);
_overlayEntry = OverlayEntry(
builder: (context) => Stack(
children: [
// 透明背景,点击关闭
Positioned.fill(
child: GestureDetector(
onTap: _removeOverlay,
child: Container(color: Colors.transparent),
),
),
// 下拉菜单内容
CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: Offset(0, size.height + 4),
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.surfaceContainer,
child: Container(
width: size.width,
constraints: const BoxConstraints(maxHeight: 300),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: _buildDropdownContent(),
),
),
),
],
),
);
overlay.insert(_overlayEntry!);
}
/// 移除覆盖层
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
/// 构建下拉菜单内容
Widget _buildDropdownContent() {
final isDark = Theme.of(context).brightness == Brightness.dark;
return ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: widget.presets.length,
itemBuilder: (context, index) {
final preset = widget.presets[index];
final isSelected = _selectedPresets.contains(preset);
return InkWell(
onTap: () => _togglePreset(preset),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
// 复选框
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.transparent,
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: (isDark ? WebTheme.darkGrey500 : WebTheme.grey400),
width: 1,
),
borderRadius: BorderRadius.circular(3),
),
child: isSelected
? const Icon(
Icons.check,
size: 12,
color: Colors.white,
)
: null,
),
const SizedBox(width: 12),
// 预设信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
preset.title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isDark ? WebTheme.darkGrey100 : WebTheme.grey900,
),
),
if (preset.description != null) ...[
const SizedBox(height: 2),
Text(
preset.description!,
style: TextStyle(
fontSize: 12,
color: isDark ? WebTheme.darkGrey400 : WebTheme.grey600,
),
),
],
],
),
),
],
),
),
);
},
);
}
/// 切换预设选择状态
void _togglePreset(InstructionPreset preset) {
setState(() {
if (_selectedPresets.contains(preset)) {
_selectedPresets.remove(preset);
} else {
_selectedPresets.add(preset);
}
});
_updateInstructions();
widget.onSelectionChanged?.call(_selectedPresets.toList());
}
/// 移除预设
void _removePreset(InstructionPreset preset) {
setState(() {
_selectedPresets.remove(preset);
});
_updateInstructions();
widget.onSelectionChanged?.call(_selectedPresets.toList());
}
/// 更新指令文本
void _updateInstructions() {
if (widget.controller != null && _selectedPresets.isNotEmpty) {
final contents = _selectedPresets.map((preset) => preset.content).toList();
final newText = contents.join('\n\n');
// 只有当前文本为空或者只包含预设内容时才更新
final currentText = widget.controller!.text.trim();
if (currentText.isEmpty || _isOnlyPresetContent(currentText)) {
widget.controller!.text = newText;
} else {
// 如果有自定义内容,追加到末尾
widget.controller!.text = '$currentText\n\n$newText';
}
} else if (_selectedPresets.isEmpty && widget.controller != null) {
// 如果没有选中任何预设,检查是否只有预设内容,如果是则清空
final currentText = widget.controller!.text.trim();
if (_isOnlyPresetContent(currentText)) {
widget.controller!.clear();
}
}
}
/// 检查当前文本是否只包含预设内容
bool _isOnlyPresetContent(String text) {
if (text.isEmpty) return true;
// 这里可以实现更复杂的逻辑来检测是否只包含预设内容
// 暂时简化处理
for (final preset in widget.presets) {
if (text.contains(preset.content)) {
return true;
}
}
return false;
}
}