AI助手优化-人设与会话绑定
This commit is contained in:
@@ -27,7 +27,6 @@ class AiPersonaTab extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final personas = ref.watch(aiPersonasProvider);
|
||||
final selectedId = ref.watch(selectedAiPersonaProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFFAF8F5),
|
||||
@@ -52,7 +51,7 @@ class AiPersonaTab extends ConsumerWidget {
|
||||
_introCard(),
|
||||
const SizedBox(height: 14),
|
||||
for (final item in list) ...[
|
||||
_personaTile(context, ref, item, selectedId, custom),
|
||||
_personaTile(context, ref, item, custom),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
if (custom == 0) _emptyHint(),
|
||||
@@ -74,7 +73,7 @@ class AiPersonaTab extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'换个说话的人',
|
||||
'管理人设',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -83,8 +82,8 @@ class AiPersonaTab extends ConsumerWidget {
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'人设决定助手的说话风格和做事规则,和用哪个模型是两件独立的事 ——\n'
|
||||
'换模型不会换人设。\n'
|
||||
'这里只负责「有哪些人设」。用哪个是在新建对话时选的 ——\n'
|
||||
'一段对话选定之后不会中途换人,想换就开个新对话。\n'
|
||||
'提示词里不用写日期,服务端每次会自动补上今天几号、「这个月」指哪个月。',
|
||||
style: TextStyle(fontSize: 12, height: 1.6, color: Color(0xFF8D6E63)),
|
||||
),
|
||||
@@ -105,7 +104,8 @@ class AiPersonaTab extends ConsumerWidget {
|
||||
),
|
||||
child: const Text(
|
||||
'还没有自己的人设,用的就是「默认」那套。\n'
|
||||
'想换个风格就点右下角「+」—— 会预填一份能用的,改一改就行。',
|
||||
'想加一个就点右下角「+」—— 会预填一份能用的,改一改就行。\n'
|
||||
'加完之后,新建对话时就能选它。',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12.5, height: 1.7, color: Color(0xFFBCAAA4)),
|
||||
),
|
||||
@@ -113,119 +113,97 @@ class AiPersonaTab extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 一条人设卡片。
|
||||
///
|
||||
/// **点整行不再是「选用」** —— 用哪个人设是新建对话时决定的,
|
||||
/// 这里只负责管理(改名、改提示词、换头像、删除)。
|
||||
/// 之前这里有个单选圈 + 点击切换,会让人误以为「在这儿选了就生效」,
|
||||
/// 而实际说话的人跟着会话走,两者不是一回事。
|
||||
Widget _personaTile(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AiPersonaItem item,
|
||||
int? selectedId,
|
||||
int customCount,
|
||||
) {
|
||||
// 默认项的 selectionKey 是 -1,不能用 id 比较
|
||||
final active = selectedId == null
|
||||
? item.isDefault
|
||||
: item.selectionKey == selectedId;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: active ? const Color(0xFFC8956E) : const Color(0xFFF0E6D8),
|
||||
width: active ? 1.5 : 1,
|
||||
),
|
||||
border: Border.all(color: const Color(0xFFF0E6D8)),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: () {
|
||||
ref.read(selectedAiPersonaProvider.notifier).select(item.id);
|
||||
showToast(context, '已切换到「${item.name}」', icon: 'check');
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 8, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 1),
|
||||
child: Icon(
|
||||
active
|
||||
? Icons.radio_button_checked
|
||||
: Icons.radio_button_unchecked,
|
||||
size: 19,
|
||||
color:
|
||||
active ? const Color(0xFFC8956E) : const Color(0xFFD7CCC8),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 人设头像。没设过就是默认小人图标
|
||||
Avatar(
|
||||
assetPath: item.avatar,
|
||||
size: 38,
|
||||
fallbackIcon: 'assistant',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 8, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 人设头像。没设过就是默认小人图标
|
||||
Avatar(
|
||||
assetPath: item.avatar,
|
||||
size: 38,
|
||||
fallbackIcon: 'assistant',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
if (item.isDefault) ...[
|
||||
const SizedBox(width: 6),
|
||||
_tag('默认', const Color(0xFF8D6E63)),
|
||||
],
|
||||
if (item.promptLength > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
_tag('${item.promptLength} 字', const Color(0xFFBCAAA4)),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 提示词预览:两行封顶,别把列表撑散
|
||||
Text(
|
||||
item.preview.isEmpty ? '(空提示词)' : item.preview,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
height: 1.6,
|
||||
color: Color(0xFFBCAAA4),
|
||||
),
|
||||
if (item.isDefault) ...[
|
||||
const SizedBox(width: 6),
|
||||
_tag('默认', const Color(0xFF8D6E63)),
|
||||
],
|
||||
if (item.promptLength > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
_tag('${item.promptLength} 字', const Color(0xFFBCAAA4)),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 提示词预览:两行封顶,别把列表撑散
|
||||
Text(
|
||||
item.preview.isEmpty ? '(空提示词)' : item.preview,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
height: 1.6,
|
||||
color: Color(0xFFBCAAA4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 默认项不给编辑/删除的入口 —— 它的内容是服务端代码里内置的
|
||||
if (item.editable)
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert,
|
||||
size: 19, color: Color(0xFFBCAAA4)),
|
||||
onSelected: (v) {
|
||||
if (v == 'edit') {
|
||||
_openEditor(context, ref, item);
|
||||
} else {
|
||||
_confirmDelete(context, ref, item, customCount);
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(value: 'edit', child: Text('编辑')),
|
||||
PopupMenuItem(value: 'delete', child: Text('删除')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 默认项不给编辑/删除的入口 —— 它的内容是服务端代码里内置的
|
||||
if (item.editable)
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert,
|
||||
size: 19, color: Color(0xFFBCAAA4)),
|
||||
onSelected: (v) {
|
||||
if (v == 'edit') {
|
||||
_openEditor(context, ref, item);
|
||||
} else {
|
||||
_confirmDelete(context, ref, item, customCount);
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(value: 'edit', child: Text('编辑')),
|
||||
PopupMenuItem(value: 'delete', child: Text('删除')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -297,9 +275,10 @@ class AiPersonaTab extends ConsumerWidget {
|
||||
await ref.read(aiApiProvider).deletePersona(item.id!);
|
||||
if (!context.mounted) return;
|
||||
|
||||
// 删掉的正好是当前选中的 → 退回默认
|
||||
if (ref.read(selectedAiPersonaProvider) == item.id) {
|
||||
ref.read(selectedAiPersonaProvider.notifier).select(null);
|
||||
// 删掉的正好是「新对话预选的那条」→ 忘掉它,别让下次新对话
|
||||
// 预选一个已经不存在的 id(服务端会兜底退回默认,但界面上会显得莫名其妙)
|
||||
if (ref.read(preferredPersonaProvider) == item.id) {
|
||||
ref.read(preferredPersonaProvider.notifier).forget();
|
||||
}
|
||||
ref.invalidate(aiPersonasProvider);
|
||||
showToast(context, '已删除', icon: 'check');
|
||||
|
||||
Reference in New Issue
Block a user