feat(Ask): 检索过程可视化 — RAG 命中记录以 chips 展示,生成前先看见
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ struct HealthExportSheet: View {
|
||||
@State private var completed: Bool = false
|
||||
@State private var copiedFlash: Bool = false
|
||||
@State private var answeringTurnID: UUID?
|
||||
@State private var retrieval: HealthExportService.RetrievalSummary?
|
||||
@State private var turnRetrievals: [UUID: HealthExportService.RetrievalSummary] = [:]
|
||||
@FocusState private var questionFocused: Bool
|
||||
|
||||
// 快捷问答
|
||||
@@ -234,9 +236,14 @@ struct HealthExportSheet: View {
|
||||
Text(turn.role.transcriptLabel)
|
||||
.font(.tjScaled( 11, weight: .semibold))
|
||||
.foregroundStyle(isUser ? Tj.Palette.paper.opacity(0.8) : Tj.Palette.text3)
|
||||
if !isUser, let summary = turnRetrievals[turn.id] {
|
||||
RetrievalChipsView(summary: summary)
|
||||
}
|
||||
if turn.id == answeringTurnID && turn.text.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("正在查看本地记录…")
|
||||
Text(turnRetrievals[turn.id] == nil
|
||||
? "正在查看本地记录…"
|
||||
: "正在根据这些记录回答…")
|
||||
.font(.tjScaled( 13))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
AIFlowBar()
|
||||
@@ -298,6 +305,9 @@ struct HealthExportSheet: View {
|
||||
arrow
|
||||
phasePill(.generating)
|
||||
}
|
||||
if let retrieval {
|
||||
RetrievalChipsView(summary: retrieval)
|
||||
}
|
||||
if phase == .generating && rate > 0 {
|
||||
Text(String(format: String(appLoc: "本地推理 · %.1f tok/s"), rate))
|
||||
.font(.tjScaled( 11, design: .monospaced))
|
||||
@@ -477,9 +487,18 @@ struct HealthExportSheet: View {
|
||||
task?.cancel()
|
||||
task = Task { @MainActor in
|
||||
do {
|
||||
for try await chunk in stream {
|
||||
appendToTurn(id: assistantTurn.id, text: chunk.text)
|
||||
if chunk.decodeRate > 0 { rate = chunk.decodeRate }
|
||||
for try await event in stream {
|
||||
switch event {
|
||||
case .retrieved(let summary):
|
||||
withAnimation(.snappy(duration: 0.25)) {
|
||||
turnRetrievals[assistantTurn.id] = summary
|
||||
}
|
||||
case .token(let chunk):
|
||||
appendToTurn(id: assistantTurn.id, text: chunk.text)
|
||||
if chunk.decodeRate > 0 { rate = chunk.decodeRate }
|
||||
case .phaseChanged, .completed:
|
||||
break
|
||||
}
|
||||
}
|
||||
answeringTurnID = nil
|
||||
questionFocused = true
|
||||
@@ -512,6 +531,7 @@ struct HealthExportSheet: View {
|
||||
rate = 0 // 重新生成时清零,避免旧 tok/s 残留显示
|
||||
error = nil
|
||||
completed = false
|
||||
retrieval = nil
|
||||
phase = .retrieving
|
||||
|
||||
let stream = HealthExportService.shared.export(conversation: turns, in: ctx)
|
||||
@@ -522,6 +542,8 @@ struct HealthExportSheet: View {
|
||||
switch event {
|
||||
case .phaseChanged(let ph):
|
||||
phase = ph
|
||||
case .retrieved(let summary):
|
||||
withAnimation(.snappy(duration: 0.25)) { retrieval = summary }
|
||||
case .token(let chunk):
|
||||
content += chunk.text
|
||||
if chunk.decodeRate > 0 { rate = chunk.decodeRate }
|
||||
@@ -549,6 +571,7 @@ struct HealthExportSheet: View {
|
||||
rate = 0
|
||||
completed = false
|
||||
content = ""
|
||||
retrieval = nil
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
@@ -560,6 +583,8 @@ struct HealthExportSheet: View {
|
||||
error = nil
|
||||
completed = false
|
||||
answeringTurnID = nil
|
||||
retrieval = nil
|
||||
turnRetrievals = [:]
|
||||
questionFocused = true
|
||||
}
|
||||
|
||||
@@ -577,6 +602,44 @@ struct HealthExportSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 检索结果 chips(本地 RAG 可视化)
|
||||
|
||||
/// 生成开始前先把「本地 RAG 找到了什么」演出来:N 条记录 + 记录名 chips。
|
||||
/// 结构化检索(不用 embedding)的天然优势 —— 每条命中都可解释、可展示(§12 卖点 3)。
|
||||
private struct RetrievalChipsView: View {
|
||||
let summary: HealthExportService.RetrievalSummary
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if summary.totalCount == 0 {
|
||||
Text("本地档案中暂无相关记录,将仅按你的描述整理")
|
||||
.font(.tjScaled( 11))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
} else {
|
||||
Text(String(appLoc: "已在本地档案中找到 \(summary.totalCount) 条相关记录"))
|
||||
.font(.tjScaled( 11, weight: .medium))
|
||||
.foregroundStyle(Tj.Palette.leaf)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(Array(summary.chips.enumerated()), id: \.offset) { _, chip in
|
||||
Text(chip)
|
||||
.font(.tjScaled( 11))
|
||||
.foregroundStyle(Tj.Palette.text2)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Capsule().fill(Tj.Palette.sand2))
|
||||
.overlay(Capsule().strokeBorder(Tj.Palette.lineSoft, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 简易 Markdown 渲染(行级)
|
||||
|
||||
/// 极简 Markdown 渲染器,够给医生看的报告就行。
|
||||
|
||||
Reference in New Issue
Block a user