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

@@ -320,32 +320,40 @@ struct DiaryQuickSheet: View {
private var assistSection: some View {
VStack(alignment: .leading, spacing: 10) {
if !contentFocused {
if case .hidden = careState {
switch careState {
case .hidden:
EmptyView()
} else {
HStack(spacing: 6) {
Image(systemName: "sparkles")
.font(.tjScaled( 11, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
sectionLabel(String(appLoc: "康康帮你记"))
Spacer(minLength: 0)
if lastRate > 0 {
Text(String(format: "%.1f tok/s", lastRate))
.font(.tjScaled( 10, design: .monospaced))
.foregroundStyle(Tj.Palette.leaf)
case .prompt:
// : banner(,,
// )
promptBanner
default:
// ( / / / ): + tok/s +
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 6) {
Image(systemName: "sparkles")
.font(.tjScaled( 11, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
sectionLabel(String(appLoc: "康康帮你记"))
Spacer(minLength: 0)
if lastRate > 0 {
Text(String(format: "%.1f tok/s", lastRate))
.font(.tjScaled( 10, design: .monospaced))
.foregroundStyle(Tj.Palette.leaf)
}
}
careBarRow(compact: false)
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
}
careBarRow(compact: false)
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
}
}
// AI ,(,)
@@ -353,6 +361,51 @@ struct DiaryQuickSheet: View {
AIDisclaimerFooter()
}
}
// /( ),
.animation(.snappy(duration: 0.22), value: contentFocused)
}
/// banner()
/// : + + + ,
/// AI (:)
private var promptBanner: some View {
Button(action: requestSuggestions) {
HStack(spacing: 11) {
Image(systemName: "sparkles")
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.paper)
.frame(width: 32, height: 32)
.background(Circle().fill(Tj.Palette.brick))
VStack(alignment: .leading, spacing: 2) {
Text("让康康帮你把这条记得更全")
.font(.tjScaled( 14, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("从医生问诊角度提几个值得补充的细节 · 本机推理")
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
}
Spacer(minLength: 0)
Image(systemName: "arrow.right")
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.fill(Tj.Palette.brick.opacity(0.08))
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.strokeBorder(Tj.Palette.brick.opacity(0.45), lineWidth: 1)
)
.contentShape(RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous))
}
.buttonStyle(.plain)
.disabled(!canRequestSuggest)
.opacity(canRequestSuggest ? 1 : 0.5)
}
/// `compact = true` ();
@@ -373,23 +426,31 @@ struct DiaryQuickSheet: View {
.disabled(!canRequestSuggest)
case .thinking:
HStack(spacing: 8) {
Image(systemName: "sparkles")
.font(.tjScaled( compact ? 12 : 13, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
.symbolEffect(.pulse, options: .repeating)
Text(lastRate > 0
? String(format: String(appLoc: "康康在想想 · %.1f tok/s"), lastRate)
: String(appLoc: "康康在想想"))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.text2)
Spacer(minLength: 0)
Button(action: cancelSuggestions) {
Text("")
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
VStack(alignment: .leading, spacing: 9) {
HStack(spacing: 8) {
Image(systemName: "sparkles")
.font(.tjScaled( compact ? 12 : 13, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
.symbolEffect(.pulse, options: .repeating)
Text(lastRate > 0
? String(format: String(appLoc: "康康在想想 · %.1f tok/s"), lastRate)
: String(appLoc: "康康在想想…"))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.text2)
Spacer(minLength: 0)
Button(action: cancelSuggestions) {
Text("")
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
}
.buttonStyle(.plain)
}
// (Apple 线, / AIFlowBar)
// ,;keyboard toolbar(compact) GeometryReader
// , requestSuggestions ,thinking , compact
if !compact {
AIFlowBar()
}
.buttonStyle(.plain)
}
case .asking(let q):
@@ -646,14 +707,17 @@ struct DiaryQuickSheet: View {
/// AI (coveredDims) LLM,
/// ,
/// ****:,
/// ,
/// ****:,+
/// (,)
/// (recordCurrent contentFocused = true),
///
private func requestSuggestions() {
suggestTask?.cancel()
contentFocused = false
let snapshotContent = content.trimmingCharacters(in: .whitespacesAndNewlines)
let covered = Array(coveredDims)
exhaustedNote = false
phase = .loading
withAnimation(.snappy(duration: 0.2)) { phase = .loading }
suggestTask = Task { @MainActor in
do {
let result = try await DiaryAssistService.shared.suggest(