import 'json_util.dart'; /// 一套 AI 人设(system prompt) /// /// 与 [AiConfigItem](模型配置)是**平行的两套配置**:一个管「用哪个模型」, /// 一个管「用什么人设」。两者正交 —— App 里切模型不会换人设,切人设也不会换模型。 /// /// ⚠️ [systemPrompt] 里**不含日期等环境信息**。那些是运行时事实, /// 由服务端在每次对话时追加(今天是几号、「这个月」指哪个月)。 /// 所以「新建时预填」拿到的是可编辑的那部分,用户不用管日期。 class AiPersonaItem { const AiPersonaItem({ required this.id, required this.name, required this.systemPrompt, required this.promptLength, required this.isDefault, required this.editable, required this.defaultPrompt, this.avatar, }); /// null = 虚拟的「默认」项(内容是服务端代码内置的) final int? id; final String name; /// 人设 + 运行规则。默认项上是内置默认文本,用户项上是自己配的 final String systemPrompt; /// 服务端数好的字数,前端直接显示 final int promptLength; /// 是否是「默认」那一项 final bool isDefault; /// 是否可编辑/删除。默认项为 false final bool editable; /// 内置默认文本。**只有默认项上才有值**,供「恢复默认」按钮使用。 /// /// 放服务端下发而不是 App 里硬编码,是为了两边永远一致 —— /// 以后改了默认人设,App 不用跟着改版本。 final String? defaultPrompt; /// 头像**相对路径**(如 `persona/2026/09/29/xxx.jpg`)。 /// 用 [Avatar] 组件显示,它会自己拼 host。null = 用默认头像。 final String? avatar; /// 用于「显示已选中的是哪一项」的稳定标识。 /// /// 默认项没有 id,用 -1 代替 —— 不能直接用 null 当 map key 去比较。 int get selectionKey => id ?? -1; factory AiPersonaItem.fromJson(Map json) { return AiPersonaItem( id: asNullableInt(json['id']), name: asString(json['name'], fallback: '未命名'), systemPrompt: asString(json['systemPrompt']), promptLength: asInt(json['promptLength']), isDefault: json['isDefault'] == true, editable: json['editable'] != false, // 用户项上这个字段是 null —— 不能直接用 asNonEmpty(它要 fallback 参数, // 而我们恰恰需要保留「null = 没有」这个语义,供「恢复默认」按钮判断) defaultPrompt: _nullableNonEmpty(json['defaultPrompt']), avatar: _nullableNonEmpty(json['avatar']), ); } /// 列表里显示的两行预览:把换行压平,太长的截断 String get preview { final flat = systemPrompt.replaceAll(RegExp(r'\s+'), ' ').trim(); if (flat.length <= 50) return flat; return '${flat.substring(0, 50)}…'; } } /// 空串/null 都返回 null,否则原样返回。 /// /// 不用 `asNonEmpty` 是因为它需要一个 fallback 字符串,而这里 /// 「有没有值」本身是有意义的(defaultPrompt 只有默认项才有)。 String? _nullableNonEmpty(Object? value) { if (value == null) return null; final s = value.toString(); return s.isEmpty ? null : s; }