feat(diary): 优化日记AI协助交互体验

- 添加promptBanner作为未开始协作时的醒目邀请横幅,包含圆形图标、标题和说明文字
- 重构assistSection使用switch语句处理careState不同状态,区分隐藏、prompt和其他状态
- 增加动画过渡效果消除聚焦/失焦切换时的布局跳动
- 优化thinking状态下的UI展示,添加AIFlowBar彩色呼吸条显示推理进度
- 修改requestSuggestions逻辑,进入推理时收起键盘以完整显示协作卡片

refactor(inference): 优化性能自检界面样式

- 将性能自检入口改为描边动作按钮(TjGhostButton),与引擎选择在视觉上区分开
- 调整未就绪状态下的禁用样式和提示文案

feat(localization): 添加新的本地化字符串

- 新增"追踪"和"记一笔"的多语言翻译,包括英语、日语和韩语

fix(diary): 增强AI问答解析稳定性

- 将最大token数从400提升至512,避免中文问题JSON被截断导致解析失败
- 实现salvageQuestionObjects方法作为终极兜底机制,逐个解析平衡的{...}对象
- 当外层wrapper解析失败时,仍可救回内部已闭合的问题对象,确保用户不被AI错误卡住

test(diary): 补充AI问答解析测试用例

- 添加截断对象恢复测试,验证maxTokens截断时前序完整问题的救回能力
- 添加wrapper key错误情况的恢复测试,确保模型输出格式异常时的容错性
```
This commit is contained in:
link2026
2026-06-17 09:21:47 +08:00
parent 52db6fb85a
commit abacf5c4f5
6 changed files with 244 additions and 93 deletions

View File

@@ -73,7 +73,9 @@ struct DiaryAssistService {
for _ in 0..<2 {
try Task.checkCancellation()
var collected = ""
let stream = await AIRuntime.shared.generate(prompt: prompt, maxTokens: 400)
// 512( 400):3-4 dim/q/fill JSON 250-320 token,400
// /
let stream = await AIRuntime.shared.generate(prompt: prompt, maxTokens: 512)
for try await chunk in stream {
collected += chunk.text
if chunk.decodeRate > 0 { lastRate = chunk.decodeRate }
@@ -116,6 +118,13 @@ struct DiaryAssistService {
rawQuestions = arr
}
}
// : / maxTokens ,
// {}, "q" 便
// , §3.2 / 线 #5退, AI
if rawQuestions == nil {
let salvaged = salvageQuestionObjects(from: stripped)
if !salvaged.isEmpty { rawQuestions = salvaged }
}
guard let rawQuestions else { return nil }
return rawQuestions.compactMap { d -> Question? in
@@ -131,6 +140,43 @@ struct DiaryAssistService {
}
}
/// : `{}` (),,
/// `"q"` :
/// - `{"questions":[]}` `}` question ,;
/// -
/// wrapper `"q"`( `"questions"`),,
/// q (),
private static func salvageQuestionObjects(from raw: String) -> [[String: Any]] {
var openStack: [String.Index] = []
var collected: [[String: Any]] = []
var seenQ = Set<String>()
var inString = false
var escape = false
var idx = raw.startIndex
while idx < raw.endIndex {
let ch = raw[idx]
if escape { escape = false }
else if ch == "\\" { escape = true }
else if ch == "\"" { inString.toggle() }
else if !inString {
if ch == "{" {
openStack.append(idx)
} else if ch == "}", let open = openStack.popLast() {
let sub = CaptureService.repairJSON(String(raw[open...idx]))
if let data = sub.data(using: .utf8),
let dict = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
let q = dict["q"] as? String,
!q.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
seenQ.insert(q).inserted {
collected.append(dict)
}
}
}
idx = raw.index(after: idx)
}
return collected
}
/// 稿稿(spec 2026-06-10-voice-diary)
/// ( / ),退使,
/// suggest AIRuntime actor ,/