```
feat(DiaryQuickSheet): 添加AI追问问答功能和底部协作入口 - 新增currentAnswer状态管理追问输入,添加answerFocused状态独立处理键盘避让 - 移除键盘工具条的关心条,将AI协作入口固定到底部按钮 - 添加完整的问答式追问卡片组件,支持自由回答输入和加入日记功能 - 修改prompt阶段行为,不再在正文区显示邀请横幅 - 更新recordCurrent为answerCurrent,实现问题+答案一同加入日记的逻辑 - 调整底部操作栏布局,间距和内边距优化 refactor(InferenceSettingsView): 性能自检改为内联展开模式 - 将性能自检视图从导航链接改为当前页就地展开 - 添加showSelfTest状态控制展开收起动画 - 支持ModelSelfTestView内联嵌入模式,去除外层导航和背景 chore(Localizable): 同步更新本地化字符串资源 - 添加新的UI文本:加入日记、在这儿写下你的回答、康康帮你一起填等 - 修复部分字符串位置调整和翻译映射问题 - 同步更新多语言版本的翻译内容 style(RootView): 优化记一笔标签页视觉设计 - 为记一笔标签添加语音识别角标标识 - 使用麦克风图标配合加号突出长按语音直达功能 ```
This commit is contained in:
@@ -31,6 +31,8 @@ struct DiaryQuickSheet: View {
|
|||||||
}
|
}
|
||||||
@State private var phase: AssistPhase = .idle
|
@State private var phase: AssistPhase = .idle
|
||||||
@State private var questions: [DiaryAssistService.Question] = []
|
@State private var questions: [DiaryAssistService.Question] = []
|
||||||
|
/// 当前这道追问的「自由回答」输入。采纳(加入日记)或跳过、切到下一题时清空。
|
||||||
|
@State private var currentAnswer: String = ""
|
||||||
@State private var lastRate: Double = 0
|
@State private var lastRate: Double = 0
|
||||||
@State private var currentRound: Int = 0
|
@State private var currentRound: Int = 0
|
||||||
/// 累积已覆盖的问诊维度(question.dim),回传下一轮 prompt 用于按维度去重。
|
/// 累积已覆盖的问诊维度(question.dim),回传下一轮 prompt 用于按维度去重。
|
||||||
@@ -45,6 +47,9 @@ struct DiaryQuickSheet: View {
|
|||||||
/// 仍保留 medium,用户可手动下拉收回为半屏(纯写文本时更轻量)。
|
/// 仍保留 medium,用户可手动下拉收回为半屏(纯写文本时更轻量)。
|
||||||
@State private var detent: PresentationDetent = .large
|
@State private var detent: PresentationDetent = .large
|
||||||
@FocusState private var contentFocused: Bool
|
@FocusState private var contentFocused: Bool
|
||||||
|
/// 追问答案输入框的聚焦态。与 contentFocused 分开:答题时聚焦的是答案框、不是正文框,
|
||||||
|
/// 两者各自独立避让键盘,互不打架。
|
||||||
|
@FocusState private var answerFocused: Bool
|
||||||
|
|
||||||
// MARK: 语音输入状态(spec 2026-06-10-voice-diary)
|
// MARK: 语音输入状态(spec 2026-06-10-voice-diary)
|
||||||
|
|
||||||
@@ -184,12 +189,6 @@ struct DiaryQuickSheet: View {
|
|||||||
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||||||
.strokeBorder(Tj.Palette.line, lineWidth: 1)
|
.strokeBorder(Tj.Palette.line, lineWidth: 1)
|
||||||
)
|
)
|
||||||
// ①「关心条」主舞台:贴键盘正上方,随写随冒、一次只问一句。
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItemGroup(placement: .keyboard) {
|
|
||||||
careBarRow(compact: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if voicePhase != .idle {
|
if voicePhase != .idle {
|
||||||
DiaryVoicePanel(
|
DiaryVoicePanel(
|
||||||
@@ -248,11 +247,39 @@ struct DiaryQuickSheet: View {
|
|||||||
}
|
}
|
||||||
.scrollDismissesKeyboard(.interactively)
|
.scrollDismissesKeyboard(.interactively)
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 8) {
|
||||||
Button("取消") { dismiss() }
|
Button("取消") { dismiss() }
|
||||||
.buttonStyle(TjGhostButton(height: 44, fontSize: 15, horizontalPadding: 18))
|
.buttonStyle(TjGhostButton(height: 44, fontSize: 15, horizontalPadding: 14))
|
||||||
|
// 「康康帮你一起填」:AI 协作主入口,移到底部和保存同一行(spec 2026-06-17)。
|
||||||
|
// 中间 flexible 占主宽、最醒目;想想中时禁用并显示进度脉冲。
|
||||||
|
Button(action: requestSuggestions) {
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Image(systemName: "sparkles")
|
||||||
|
.font(.tjScaled( 13, weight: .semibold))
|
||||||
|
.symbolEffect(.pulse, options: .repeating, isActive: isLoading)
|
||||||
|
Text(isLoading ? String(appLoc: "想想中…") : String(appLoc: "康康帮你一起填"))
|
||||||
|
.font(.tjScaled( 14, weight: .semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.8)
|
||||||
|
}
|
||||||
|
.foregroundStyle(canRequestSuggest ? Tj.Palette.brick : Tj.Palette.text3)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 44)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||||||
|
.fill(Tj.Palette.brick.opacity(canRequestSuggest ? 0.12 : 0.05))
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||||||
|
.strokeBorder(Tj.Palette.brick.opacity(canRequestSuggest ? 0.4 : 0.15),
|
||||||
|
lineWidth: 1)
|
||||||
|
)
|
||||||
|
.contentShape(RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(!canRequestSuggest)
|
||||||
Button("保存") { submit() }
|
Button("保存") { submit() }
|
||||||
.buttonStyle(TjPrimaryButton(height: 44, fontSize: 15, horizontalPadding: 18))
|
.buttonStyle(TjPrimaryButton(height: 44, fontSize: 15, horizontalPadding: 14))
|
||||||
.disabled(!canSubmit)
|
.disabled(!canSubmit)
|
||||||
.opacity(canSubmit ? 1 : 0.4)
|
.opacity(canSubmit ? 1 : 0.4)
|
||||||
}
|
}
|
||||||
@@ -321,12 +348,10 @@ struct DiaryQuickSheet: View {
|
|||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
if !contentFocused {
|
if !contentFocused {
|
||||||
switch careState {
|
switch careState {
|
||||||
case .hidden:
|
case .hidden, .prompt:
|
||||||
|
// prompt(有内容、还没生成)不在正文区显示卡片:AI 入口已常驻底部
|
||||||
|
//「康康帮你一起填」按钮,正文区保持干净。
|
||||||
EmptyView()
|
EmptyView()
|
||||||
case .prompt:
|
|
||||||
// 还没开始协作:醒目的整行邀请 banner(自带标题,不再挂「康康帮你记」抬头,
|
|
||||||
// 免得两行都在说「帮你记」)。这是「智能协作不明显」的主补强点。
|
|
||||||
promptBanner
|
|
||||||
default:
|
default:
|
||||||
// 已在协作(想想 / 追问 / 问完 / 失败):挂抬头 + tok/s + 关心条卡片。
|
// 已在协作(想想 / 追问 / 问完 / 失败):挂抬头 + tok/s + 关心条卡片。
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
@@ -365,49 +390,6 @@ struct DiaryQuickSheet: View {
|
|||||||
.animation(.snappy(duration: 0.22), value: contentFocused)
|
.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` 给键盘正上方那条(单行紧凑);
|
/// 关心条的统一渲染。`compact = true` 给键盘正上方那条(单行紧凑);
|
||||||
/// `compact = false` 给正文回落卡(问题可换两行、留白更松)。两处共用同一 careState 与动作。
|
/// `compact = false` 给正文回落卡(问题可换两行、留白更松)。两处共用同一 careState 与动作。
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -454,28 +436,8 @@ struct DiaryQuickSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .asking(let q):
|
case .asking(let q):
|
||||||
HStack(spacing: 10) {
|
// 键盘工具条已移除,asking 始终落在正文里的「问答卡片」。
|
||||||
Image(systemName: "text.bubble.fill")
|
askingCard(q)
|
||||||
.font(.tjScaled( compact ? 12 : 13, weight: .semibold))
|
|
||||||
.foregroundStyle(Tj.Palette.brick)
|
|
||||||
Text(q.q)
|
|
||||||
.font(.tjScaled( compact ? 13 : 14, weight: .medium))
|
|
||||||
.foregroundStyle(Tj.Palette.text)
|
|
||||||
.lineLimit(compact ? 1 : 2)
|
|
||||||
.fixedSize(horizontal: false, vertical: !compact)
|
|
||||||
Spacer(minLength: 6)
|
|
||||||
Button { skipCurrent(q) } label: {
|
|
||||||
Text("跳过")
|
|
||||||
.font(.tjScaled( 12, weight: .semibold))
|
|
||||||
.foregroundStyle(Tj.Palette.text3)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
Button { recordCurrent(q) } label: {
|
|
||||||
careCapsule(icon: "plus", text: String(appLoc: "记一下"),
|
|
||||||
tint: Tj.Palette.ink, style: .filled, compact: compact)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
|
|
||||||
case .caughtUp(let exhausted):
|
case .caughtUp(let exhausted):
|
||||||
Button(action: requestSuggestions) {
|
Button(action: requestSuggestions) {
|
||||||
@@ -512,7 +474,7 @@ struct DiaryQuickSheet: View {
|
|||||||
|
|
||||||
private enum CareCapsuleStyle { case filled, soft }
|
private enum CareCapsuleStyle { case filled, soft }
|
||||||
|
|
||||||
/// 关心条里的胶囊。filled = 实心(主动作「记一下」);soft = 浅色底(邀请类)。
|
/// 关心条里的胶囊。filled = 实心强调;soft = 浅色底(邀请类,prompt / caughtUp 用)。
|
||||||
private func careCapsule(icon: String, text: String, tint: Color,
|
private func careCapsule(icon: String, text: String, tint: Color,
|
||||||
style: CareCapsuleStyle, compact: Bool) -> some View {
|
style: CareCapsuleStyle, compact: Bool) -> some View {
|
||||||
HStack(spacing: 5) {
|
HStack(spacing: 5) {
|
||||||
@@ -529,6 +491,80 @@ struct DiaryQuickSheet: View {
|
|||||||
.contentShape(Capsule())
|
.contentShape(Capsule())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 当前答案去掉首尾空白(判空 / 提交用)。
|
||||||
|
private var answerTrimmed: String {
|
||||||
|
currentAnswer.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func answerPlaceholder(for q: DiaryAssistService.Question) -> String {
|
||||||
|
q.dim.isEmpty
|
||||||
|
? String(appLoc: "在这儿写下你的回答…")
|
||||||
|
: String(appLoc: "说说「\(q.dim)」的情况…")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「问答式」追问卡片(正文回落卡里):康康问一句 → 用户自由打字回答 →
|
||||||
|
/// 「加入日记」把「· 维度:答案」追加到正文。问题与答案都完整留痕(spec 2026-06-17)。
|
||||||
|
@ViewBuilder
|
||||||
|
private func askingCard(_ q: DiaryAssistService.Question) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||||
|
Image(systemName: "text.bubble.fill")
|
||||||
|
.font(.tjScaled( 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Tj.Palette.brick)
|
||||||
|
Text(q.q)
|
||||||
|
.font(.tjScaled( 14, weight: .medium))
|
||||||
|
.foregroundStyle(Tj.Palette.text)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
Spacer(minLength: 6)
|
||||||
|
if pendingQuestions.count > 1 {
|
||||||
|
Text(String(format: String(appLoc: "还有 %d 个"), pendingQuestions.count - 1))
|
||||||
|
.font(.tjScaled( 10, weight: .medium))
|
||||||
|
.foregroundStyle(Tj.Palette.text3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextField(answerPlaceholder(for: q), text: $currentAnswer, axis: .vertical)
|
||||||
|
.font(.tjScaled( 13))
|
||||||
|
.lineLimit(1...4)
|
||||||
|
.focused($answerFocused)
|
||||||
|
.submitLabel(.done)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 9)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||||||
|
.fill(Tj.Palette.sand2)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||||||
|
.strokeBorder(answerFocused ? Tj.Palette.brick.opacity(0.5) : Tj.Palette.line,
|
||||||
|
lineWidth: 1)
|
||||||
|
)
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Button { skipCurrent(q) } label: {
|
||||||
|
Text("跳过这条")
|
||||||
|
.font(.tjScaled( 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Tj.Palette.text3)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Button { answerCurrent(q) } label: {
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Image(systemName: "text.append")
|
||||||
|
.font(.tjScaled( 12, weight: .semibold))
|
||||||
|
Text("加入日记")
|
||||||
|
.font(.tjScaled( 13, weight: .semibold))
|
||||||
|
}
|
||||||
|
.foregroundStyle(Tj.Palette.paper)
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Capsule().fill(answerTrimmed.isEmpty ? Tj.Palette.text3 : Tj.Palette.ink))
|
||||||
|
.contentShape(Capsule())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(answerTrimmed.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
private func sectionLabel(_ text: String) -> some View {
|
private func sectionLabel(_ text: String) -> some View {
|
||||||
@@ -709,11 +745,12 @@ struct DiaryQuickSheet: View {
|
|||||||
/// 要求本轮避开这些维度,从结构上压住跨轮换皮重复。
|
/// 要求本轮避开这些维度,从结构上压住跨轮换皮重复。
|
||||||
/// 进入推理即**收起键盘**:把舞台让给正文里的协作卡片,让「康康在想想」+ 彩色呼吸条
|
/// 进入推理即**收起键盘**:把舞台让给正文里的协作卡片,让「康康在想想」+ 彩色呼吸条
|
||||||
/// 的本地推理过程完整可见(键盘挡住卡片时呼吸条就白跑了,正是「推理时没有呼吸条」的成因)。
|
/// 的本地推理过程完整可见(键盘挡住卡片时呼吸条就白跑了,正是「推理时没有呼吸条」的成因)。
|
||||||
/// 出结果后点「记一下」会自动重新聚焦续写(recordCurrent 里 contentFocused = true),
|
/// 出结果后在正文卡片以「问答卡片」呈现:用户自由回答、点「加入日记」把
|
||||||
/// 书写节奏照样接得上。
|
///「· 维度:答案」追加进正文(answerCurrent),问题与答案一并留痕。
|
||||||
private func requestSuggestions() {
|
private func requestSuggestions() {
|
||||||
suggestTask?.cancel()
|
suggestTask?.cancel()
|
||||||
contentFocused = false
|
contentFocused = false
|
||||||
|
answerFocused = false
|
||||||
let snapshotContent = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
let snapshotContent = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let covered = Array(coveredDims)
|
let covered = Array(coveredDims)
|
||||||
exhaustedNote = false
|
exhaustedNote = false
|
||||||
@@ -789,25 +826,30 @@ struct DiaryQuickSheet: View {
|
|||||||
phase = hasQuestions ? .ready : .idle
|
phase = hasQuestions ? .ready : .idle
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 「记一下」:把这条问题对应的补充句落进正文,标记已采纳,关心条自动滑到下一句。
|
/// 「加入日记」:把用户对这道追问的【自由回答】按「· 维度:答案」追加到正文,
|
||||||
/// 用 `assemble(values: [])` 取「去方括号、占位回退为原词」的干净句子——
|
/// 标记已采纳,关心条自动滑到下一题(答案框清空、保持聚焦,顺势接着答)。
|
||||||
/// 即便用户不再细填,也不会把 `[时间]` 这种机器括号留进日记。
|
/// 维度名取自 question.dim(受控词表「起病诱因/症状性质/…」);模型偶尔没给 dim
|
||||||
private func recordCurrent(_ question: DiaryAssistService.Question) {
|
/// 时退化为「· 答案」。这是「问题+答案一起进日记」的落点(spec 2026-06-17)。
|
||||||
let stub = question.fill.isEmpty
|
private func answerCurrent(_ question: DiaryAssistService.Question) {
|
||||||
? question.q
|
let answer = currentAnswer.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
: DiaryFillTemplate.assemble(question.fill, values: [])
|
guard !answer.isEmpty else { return }
|
||||||
appendToContent(stub)
|
let dim = question.dim.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
appendToContent(dim.isEmpty ? "· \(answer)" : "· \(dim):\(answer)")
|
||||||
if let idx = questions.firstIndex(where: { $0.id == question.id }) {
|
if let idx = questions.firstIndex(where: { $0.id == question.id }) {
|
||||||
questions[idx].adopted = true
|
questions[idx].adopted = true
|
||||||
}
|
}
|
||||||
// 落字后把键盘留住:用户顺势接着写,关心条已切到下一句。
|
currentAnswer = ""
|
||||||
contentFocused = true
|
// 还有下一题就把答案框留住、顺势接着答;答完了收键盘,把整理好的正文交还用户。
|
||||||
|
if pendingQuestions.isEmpty { answerFocused = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 「跳过」:这句先不记,关心条滑到下一句。该维度已在生成时计入 coveredDims,
|
/// 「跳过这条」:这句先不记,关心条滑到下一句。该维度已在生成时计入 coveredDims,
|
||||||
/// 下一轮 prompt 不会再问它,所以跳过的不必从 questions 里删。
|
/// 下一轮 prompt 不会再问它,所以跳过的不必从 questions 里删。清空当前答案输入;
|
||||||
|
/// 队列清空(没有下一题了)则收起键盘。
|
||||||
private func skipCurrent(_ question: DiaryAssistService.Question) {
|
private func skipCurrent(_ question: DiaryAssistService.Question) {
|
||||||
skippedQuestionIDs.insert(question.id)
|
skippedQuestionIDs.insert(question.id)
|
||||||
|
currentAnswer = ""
|
||||||
|
if pendingQuestions.isEmpty { answerFocused = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 把一段补充文本追加到正文末尾(自动补换行,空文本忽略)。
|
/// 把一段补充文本追加到正文末尾(自动补换行,空文本忽略)。
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import SwiftUI
|
|||||||
struct InferenceSettingsView: View {
|
struct InferenceSettingsView: View {
|
||||||
@AppStorage("kk.inferenceEngine") private var engineRaw = EnginePreference.auto.rawValue
|
@AppStorage("kk.inferenceEngine") private var engineRaw = EnginePreference.auto.rawValue
|
||||||
@State private var modelService = ModelDownloadService.shared
|
@State private var modelService = ModelDownloadService.shared
|
||||||
|
/// 性能自检改为当前页就地展开,不再 push 新页面。
|
||||||
|
@State private var showSelfTest = false
|
||||||
|
|
||||||
private var selected: EnginePreference {
|
private var selected: EnginePreference {
|
||||||
EnginePreference(rawValue: engineRaw) ?? .auto
|
EnginePreference(rawValue: engineRaw) ?? .auto
|
||||||
@@ -48,19 +50,27 @@ struct InferenceSettingsView: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var selfTestSection: some View {
|
private var selfTestSection: some View {
|
||||||
if modelReady {
|
if modelReady {
|
||||||
NavigationLink {
|
VStack(spacing: 12) {
|
||||||
ModelSelfTestView()
|
Button {
|
||||||
|
withAnimation(.easeInOut(duration: 0.22)) { showSelfTest.toggle() }
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: "gauge.with.needle")
|
Image(systemName: "gauge.with.needle")
|
||||||
.font(.tjScaled(15, weight: .semibold))
|
.font(.tjScaled(15, weight: .semibold))
|
||||||
Text("性能自检")
|
Text("性能自检")
|
||||||
Image(systemName: "arrow.right")
|
Image(systemName: "chevron.down")
|
||||||
.font(.tjScaled(13, weight: .semibold))
|
.font(.tjScaled(13, weight: .semibold))
|
||||||
|
.rotationEffect(.degrees(showSelfTest ? 180 : 0))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
.buttonStyle(TjGhostButton())
|
.buttonStyle(TjGhostButton())
|
||||||
|
|
||||||
|
if showSelfTest {
|
||||||
|
ModelSelfTestView(embedded: true)
|
||||||
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||||
|
}
|
||||||
|
}
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
|
|||||||
@@ -33,8 +33,23 @@ struct ModelSelfTestView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 内联模式:嵌进「推理引擎」页就地展开,去掉外层 ScrollView / 背景 / 导航标题。
|
||||||
|
var embedded = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
if embedded {
|
||||||
|
content
|
||||||
|
} else {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
content.padding(16)
|
||||||
|
}
|
||||||
|
.background(Tj.Palette.sand.ignoresSafeArea())
|
||||||
|
.navigationTitle("性能自检")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var content: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
promptCard
|
promptCard
|
||||||
|
|
||||||
@@ -67,11 +82,6 @@ struct ModelSelfTestView: View {
|
|||||||
|
|
||||||
if !history.isEmpty { historyCard }
|
if !history.isEmpty { historyCard }
|
||||||
}
|
}
|
||||||
.padding(16)
|
|
||||||
}
|
|
||||||
.background(Tj.Palette.sand.ignoresSafeArea())
|
|
||||||
.navigationTitle("性能自检")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.onAppear { history = BenchmarkService.load() }
|
.onAppear { history = BenchmarkService.load() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3806,6 +3806,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"加入日记" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"加入记录" : {
|
"加入记录" : {
|
||||||
|
|
||||||
@@ -4530,6 +4533,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"在这儿写下你的回答…" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"在这里输入主诉……" : {
|
"在这里输入主诉……" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
@@ -5887,6 +5893,9 @@
|
|||||||
},
|
},
|
||||||
"康康在想想…" : {
|
"康康在想想…" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"康康帮你一起填" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"康康帮你记" : {
|
"康康帮你记" : {
|
||||||
|
|
||||||
@@ -6466,6 +6475,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"想想中…" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"慢性肾病" : {
|
"慢性肾病" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -10034,9 +10046,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"用上方选中的引擎跑固定 prompt,实测 prefill / 生成 tok/s" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"用于自动判定 正常/偏高/偏低" : {
|
"用于自动判定 正常/偏高/偏低" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -11211,8 +11220,27 @@
|
|||||||
"让康康帮你把这条记得更全" : {
|
"让康康帮你把这条记得更全" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"记一下" : {
|
"记一笔" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Add"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "追加"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "추가"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"记剂量与时间" : {
|
"记剂量与时间" : {
|
||||||
|
|
||||||
@@ -11720,6 +11748,9 @@
|
|||||||
},
|
},
|
||||||
"说完了,整理成日记" : {
|
"说完了,整理成日记" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"说说「%@」的情况…" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"说说你想给医生看什么" : {
|
"说说你想给医生看什么" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
@@ -11938,50 +11969,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"追踪" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Tracking"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "推移"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "추적"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"记一笔" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Add"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "追加"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "추가"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"跟随系统" : {
|
"跟随系统" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -12005,6 +11992,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"跳过" : {
|
"跳过" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -12028,6 +12016,9 @@
|
|||||||
},
|
},
|
||||||
"跳过 · 手动录入" : {
|
"跳过 · 手动录入" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"跳过这条" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"身体档案" : {
|
"身体档案" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -12343,6 +12334,9 @@
|
|||||||
},
|
},
|
||||||
"还想到几个想问你 · 再来一轮" : {
|
"还想到几个想问你 · 再来一轮" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"还有 %d 个" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"还没有任何记录\n点底部 + 号开始" : {
|
"还没有任何记录\n点底部 + 号开始" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -12575,6 +12569,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"追踪" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Tracking"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "推移"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "추적"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"通俗解读:设备本地 AI 把指标与趋势转述为易懂的说明" : {
|
"通俗解读:设备本地 AI 把指标与趋势转述为易懂的说明" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@@ -292,6 +292,16 @@ private struct TabBar: View {
|
|||||||
.foregroundStyle(Tj.Palette.paper)
|
.foregroundStyle(Tj.Palette.paper)
|
||||||
}
|
}
|
||||||
.frame(width: slotHeight, height: slotHeight)
|
.frame(width: slotHeight, height: slotHeight)
|
||||||
|
// 语音识别角标:体现「长按语音直达」,加号 + 麦克风双标识
|
||||||
|
.overlay(alignment: .bottomTrailing) {
|
||||||
|
Image(systemName: "mic.fill")
|
||||||
|
.font(.tjScaled( 8, weight: .bold))
|
||||||
|
.foregroundStyle(Tj.Palette.paper)
|
||||||
|
.frame(width: 15, height: 15)
|
||||||
|
.background(Circle().fill(Tj.Palette.brick))
|
||||||
|
.overlay(Circle().strokeBorder(Tj.Palette.paper, lineWidth: 1.5))
|
||||||
|
.offset(x: 3, y: 2)
|
||||||
|
}
|
||||||
|
|
||||||
Text("记一笔")
|
Text("记一笔")
|
||||||
.font(.tjScaled( 11, weight: .semibold))
|
.font(.tjScaled( 11, weight: .semibold))
|
||||||
|
|||||||
Reference in New Issue
Block a user