马良AI写作初始化仓库
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user