# SPEC: ChatBox 上下文接入与 UI 美化 > **版本**: v1.0 > **前置依赖**: 后端 `POST /deepview/chat` 和 `GET /deepview/chat/history` 已就绪 > **状态**: 草案 (Proposal) --- ## 1. 问题陈述 当前 ChatBox 存在两大核心缺陷: ### 1.1 上下文接入是假的(Mock 硬编码) 打开 `app.ts` 的 `openChatSheet()` 和 `sendChatMessage()` 可知: - 打招呼消息**硬编码**为"李女士(铂金会员/建档3年)"——与当前报告完全无关。 - 用户发消息后,回复也是**硬编码**(永远返回"连续2次拒绝乔雅登"那段话)。 - 没有调用任何后端 API (`POST /deepview/chat`),也没有通过 SSE 接收真实 AI 回复。 - ChatBox 的 `chatId` 没有与当前报告的 `reportId` 或 `contextId` 绑定,意味着**所有页面共享同一个假对话**。 ### 1.2 ChatBox UI 缺乏角色区分 当前的消息气泡 `.msg-bubble` 仅通过左对齐/右对齐和背景色做了最基础的区分,没有: - AI 头像(应为方形,体现机器人身份) - 用户头像(应为圆形,复用 TopBar 的 `avatar-circle` 风格) - 消息中 Markdown 的富文本渲染(AI 回复通常包含列表、加粗等) ### 1.3 ChatBox Header 没有指明上下文来源 Header 固定写死"💬 X光片追踪深打": - 不知道当前追问的是**哪一份录音报告**,还是**哪位客户的全景档案**。 - 用户无法确认 AI 的回答是否基于正确的上下文。 ### 1.4 但后端 SSE 桥接层已经完备!(Xinzong 审计交叉验证) 经与馨总智能体的 [SSE 事件路由全链路审计](file:///Users/lidongfang/.gemini/antigravity/brain/c489626d-a001-4aff-9e29-13dec0e6ffd4/xinzong_sse_event_routing_audit.md.resolved) 进行交叉比对,确认 **Deepview 后端已完整实现了与 Xinzong 相同的 Hermes SSE 桥接层**(见 `deepview_sse.py` 文件头 L12-19 的翻译规则声明): | Hermes 原生回调 | Deepview SSE 事件 | 实现位置 | 状态 | |---|---|---|---| | `stream_delta_callback(text)` | `agent:chunk {chatId, text}` | `_makeStreamDeltaCallback` L216-233 | ✅ 已实现 | | `stream_delta_callback(None)` | **丢弃** | L225-226 | ✅ 正确过滤 | | `tool_progress("tool.started")` | `agent:thinking {chatId, step, message}` | `_makeToolProgressCallback` L246-257 | ✅ 已实现 | | `tool_progress("tool.completed")` | **丢弃** | L258 | ✅ 正确过滤 | | `tool_progress("reasoning.available")` | **丢弃** | L258 | ✅ 正确过滤(防重复渲染) | | `run_conversation()` 返回 | `agent:done {chatId, fullAnswer}` | `_runChat` L627-630 | ✅ 已实现 | | `run_conversation()` 异常 | `agent:error {chatId, message}` | `_runChat` L634-636 | ✅ 已实现 | **关键结论**:Deepview 的 SSE 管线与 Xinzong 是**同源同构**的。 - 翻译规则完全一致(丢弃 `None`、丢弃 `tool.completed`、丢弃 `reasoning.available`) - `_pushEvent` 基于 `userId` 路由到正确的 SSE 连接 - 前端已注册了 `agent:chunk`、`agent:thinking`、`agent:done` 的 `addEventListener`(`api.service.ts` L90) **因此,后端需要的改动为零。全部工作量集中在前端的「从 Mock 切换到真实 SSE 消费」**。 --- ## 2. 改造目标 将 ChatBox 从一个静态 Mock 面板升级为**真实接入后端 Hermes Agent 的上下文对话终端**,同时在 UI 层面达到专业级的对话体验。 --- ## 3. 上下文路由机制 ### 3.1 chatId 与 contextId 的绑定规则 ChatBox 的核心是两个 ID: | ID | 来源 | 用途 | |----|------|------| | `contextId` | 从当前页面 URL 解析 | 告诉后端"AI 应该读哪些文件"作为 RAG 上下文 | | `chatId` | 前端生成,与 contextId 绑定 | 标识当前对话会话,用于加载历史和持久化 | **URL → contextId 的解析规则**(在 `app.ts` 路由监听中执行): ``` /deepview/report/rep_xxx → contextId = "recording:{rep_xxx 对应的 asrId}" 或 contextId = "recording:{clientId}/{asrId}" (已归档) /deepview/client/cli_xxx → contextId = "client:cli_xxx" ``` **chatId 的生成规则**: ``` chatId = "chat_" + contextId.replace(/[:/]/g, "_") ``` 例如:`chat_recording_abc123` 或 `chat_client_cli_001` 这样保证同一个报告/客户的对话始终复用同一个 chatId,回来能看到之前的追问记录。 ### 3.2 从报告中提取 contextId 当前 `report-detail.ts` 已经从 API 加载了 `this.report` 对象。该对象中包含 `context_id` 字段(后端 DB `deepview_reports_v2` 表中存储的原始上下文标识)。 **数据流**: ``` report-detail.ts 加载 report → 提取 report.context_id → 存入共享 Service → app.ts 在 openChatSheet() 时读取 ``` 需要一个轻量的 `ChatContextService`(单例 Injectable)在 report-detail 和 app.ts 之间桥接。 --- ## 4. 前端改造清单 ### 4.1 新增 `ChatContextService` ```typescript // src/app/core/chat-context.service.ts @Injectable({ providedIn: 'root' }) export class ChatContextService { /** 当前页面的上下文 ID */ contextId = signal(''); /** 当前上下文的人类可读标题 */ contextTitle = signal(''); /** 上下文类型:recording | client */ contextType = signal<'recording' | 'client' | ''>(''); /** 基于 contextId 派生 chatId */ get chatId(): string { const ctx = this.contextId(); return ctx ? 'chat_' + ctx.replace(/[:/]/g, '_') : ''; } } ``` ### 4.2 修改 `report-detail.ts` 在 `loadReportFromApi` 成功后: ```typescript this.chatCtx.contextId.set(data.context_id || `recording:${reportId}`); this.chatCtx.contextTitle.set(data.clientName || '未归档录音'); this.chatCtx.contextType.set('recording'); ``` ### 4.3 修改 `app.ts` 的 Chat 逻辑 #### 删除所有 Mock 代码 删除 `openChatSheet()` 中的硬编码欢迎词和 `sendChatMessage()` 中的 `setTimeout` 假回复。 #### `openChatSheet()` 改为: 1. 从 `ChatContextService` 读取 `chatId` 和 `contextId`。 2. 调用 `GET /deepview/chat/history?chatId=xxx` 加载历史消息。 3. 如果历史为空,推一条系统欢迎消息(基于 `contextType` 和 `contextTitle` 动态生成)。 #### `sendChatMessage()` 改为: 1. 将用户消息推入 `chatMessages` 数组(即时展示)。 2. 推入一条 `{ role: 'agent', content: '', isStreaming: true }` 占位。 3. 调用 `POST /deepview/chat` 传递 `{ chatId, text, contextId }`。 4. 通过已有的 SSE 监听 (`api.listenToEvents()`) 接收 `agent:chunk` 和 `agent:done` 事件,实时追加到占位消息的 content 中。 #### 消息接口扩展: ```typescript interface ChatMessage { role: 'user' | 'agent'; content: string; isStreaming?: boolean; // 正在流式接收中 } ``` ### 4.4 `ApiService` 新增方法 ```typescript async getChatHistory(chatId: string): Promise<{messages: any[]}> { return this.http.get(`${this.apiUrl}/chat/history`, { headers: this.getHeaders(), params: { chatId } }).toPromise(); } async sendChat(chatId: string, text: string, contextId: string): Promise<{received: boolean}> { return this.http.post(`${this.apiUrl}/chat`, { chatId, text, contextId }, { headers: this.getHeaders() }).toPromise(); } ``` --- ## 5. UI 改造规格 ### 5.1 ChatBox Header 动态化 **Before**: ```html

💬 X光片追踪深打

上下文已在此锁定,免去前提

``` **After**: ```html

💬 X光片追踪深打

🎙️ 上下文锁定:本次面诊录音报告

👤 上下文锁定:{{ chatContextTitle }} 全景档案

``` ### 5.2 消息气泡 + 头像重构 **目标效果**: ``` ┌──────────────────────────────────────┐ │ [ AI ] 消息内容巴拉巴拉... │ ← AI: 方形头像,左对齐 │ ■ │ │ │ │ 用户消息巴拉巴拉... [ 👤 ] │ ← 用户: 圆形头像,右对齐 │ ● │ └──────────────────────────────────────┘ ``` **HTML 结构改造** (`app.html` 中 chat-history 区域): ```html @for (msg of chatMessages; track $index) {
...
{{ auth.user()?.name?.charAt(0) || '我' }}
} ``` **CSS 规格**: ```css .msg-avatar { width: 32px; height: 32px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 800; } /* AI: 方形圆角 + 品牌蓝渐变 */ .ai-avatar { border-radius: 8px; background: linear-gradient(135deg, #3b82f6, #1d4ed8); color: white; } /* 用户: 圆形 + 淡紫 */ .user-avatar { border-radius: 50%; background: #f3e8ff; color: #7c3aed; } /* AI 消息行:左对齐 */ .chat-message:not(.user) { justify-content: flex-start; gap: 8px; } /* 用户消息行:右对齐 */ .chat-message.user { justify-content: flex-end; gap: 8px; } ``` ### 5.3 流式打字效果(Streaming Indicator) 当 AI 消息处于 `isStreaming: true` 状态时,在气泡尾部追加呼吸光标: ```css .msg-bubble.streaming::after { content: '▋'; animation: blink 1s infinite; color: var(--color-primary); } @keyframes blink { 50% { opacity: 0; } } ``` --- ## 6. 后端现有能力清点(经 Xinzong 审计交叉验证,无需改动) | 端点/事件 | 实现位置 | 状态 | 说明 | |------|------|------|------| | `POST /deepview/chat` | `_handleChat` L482 | ✅ | 接收 `{chatId, text, contextId}`,异步 `create_task(_runChat)` | | `GET /deepview/chat/history` | `_handleChatHistory` | ✅ | 从 SessionDB 返回 `{messages: [{role, content}, ...]}` | | SSE `agent:chunk` | `_makeStreamDeltaCallback` L216 | ✅ | 流式推送 `{chatId, text}` | | SSE `agent:thinking` | `_makeToolProgressCallback` L245 | ✅ | 推送 `{chatId, step, message}`(如"正在读取 profile.md") | | SSE `agent:done` | `_runChat` L627 | ✅ | 推送 `{chatId, fullAnswer}` 标记回答完成 | | SSE `agent:error` | `_runChat` L634 | ✅ | 推送 `{chatId, message}` 错误信息 | ### 与 Xinzong 的管线同构性 ``` Xinzong SSE Pipeline Deepview SSE Pipeline ═══════════════════ ═════════════════════ stream_delta → chunk ←→ stream_delta → chunk ← 同源 tool.started → thinking ←→ tool.started → thinking ← 同源 tool.completed → 丢弃 ←→ tool.completed → 丢弃 ← 同源 reasoning → 丢弃 ←→ reasoning → 丢弃 ← 同源 None → 丢弃 ←→ None → 丢弃 ← 同源 return → done ←→ return → done ← 同源 exception → error ←→ exception → error ← 同源 ``` > [!TIP] > **试错成本归零**:无需猜测后端推送什么事件、携带什么字段——它们与 Xinzong 完全一致。前端只需将 Xinzong 的 `StateService` 订阅模式(一事件一消费者)复制到 Deepview 的 `app.ts` 中即可。 后端无需任何改动。所有工作在前端 Angular 完成。 --- ## 7. 修改文件清单 | 文件 | 动作 | 说明 | |------|------|------| | `src/app/core/chat-context.service.ts` | **[NEW]** | 跨组件共享当前上下文 | | `src/app/core/api.service.ts` | **[MODIFY]** | 新增 `getChatHistory` 和 `sendChat` | | `src/app/pages/report-detail/report-detail.ts` | **[MODIFY]** | 注入 ChatContextService,加载报告后设置上下文 | | `src/app/app.ts` | **[MODIFY]** | 重写 chat 逻辑,接入真实 API 和 SSE | | `src/app/app.html` | **[MODIFY]** | 重构消息气泡结构(头像 + 动态 Header) | | `src/app/app.css` | **[MODIFY]** | 新增头像、流式光标等样式 | --- ## 8. 验证计划 ### 自动验证 1. 打开 `/deepview/report/rep_xxx` 页面,点击 ChatBox,观察 Header 是否显示"🎙️ 上下文锁定:本次面诊录音报告"。 2. 发一条消息,确认 `POST /deepview/chat` 被调用且参数中 `contextId` 正确。 3. 确认 SSE `agent:chunk` 事件被正确接收并实时渲染到气泡中。 ### 视觉验证 1. AI 消息:方形蓝色头像在左,气泡在右。 2. 用户消息:圆形紫色头像在右,蓝色气泡在左。 3. 流式打字时,气泡尾部有呼吸光标 `▋`。 > *本规范指导 ChatBox 从 Mock 阶段进化为真实 AI 对话终端的全部前端改造工作。*