doctorAI/docs/SPEC_chatbox_context_integration.md

13 KiB
Raw Blame History

SPEC: ChatBox 上下文接入与 UI 美化

版本: v1.0
前置依赖: 后端 POST /deepview/chatGET /deepview/chat/history 已就绪
状态: 草案 (Proposal)


1. 问题陈述

当前 ChatBox 存在两大核心缺陷:

1.1 上下文接入是假的Mock 硬编码)

打开 app.tsopenChatSheet()sendChatMessage() 可知:

  • 打招呼消息硬编码为"李女士(铂金会员/建档3年"——与当前报告完全无关。
  • 用户发消息后,回复也是硬编码(永远返回"连续2次拒绝乔雅登"那段话)。
  • 没有调用任何后端 API (POST /deepview/chat),也没有通过 SSE 接收真实 AI 回复。
  • ChatBox 的 chatId 没有与当前报告的 reportIdcontextId 绑定,意味着所有页面共享同一个假对话

1.2 ChatBox UI 缺乏角色区分

当前的消息气泡 .msg-bubble 仅通过左对齐/右对齐和背景色做了最基础的区分,没有:

  • AI 头像(应为方形,体现机器人身份)
  • 用户头像(应为圆形,复用 TopBar 的 avatar-circle 风格)
  • 消息中 Markdown 的富文本渲染AI 回复通常包含列表、加粗等)

1.3 ChatBox Header 没有指明上下文来源

Header 固定写死"💬 X光片追踪深打"

  • 不知道当前追问的是哪一份录音报告,还是哪位客户的全景档案
  • 用户无法确认 AI 的回答是否基于正确的上下文。

1.4 但后端 SSE 桥接层已经完备Xinzong 审计交叉验证)

经与馨总智能体的 SSE 事件路由全链路审计 进行交叉比对,确认 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:chunkagent:thinkingagent:doneaddEventListenerapi.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_abc123chat_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

// src/app/core/chat-context.service.ts
@Injectable({ providedIn: 'root' })
export class ChatContextService {
  /** 当前页面的上下文 ID */
  contextId = signal<string>('');
  /** 当前上下文的人类可读标题 */
  contextTitle = signal<string>('');
  /** 上下文类型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 成功后:

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 读取 chatIdcontextId
  2. 调用 GET /deepview/chat/history?chatId=xxx 加载历史消息。
  3. 如果历史为空,推一条系统欢迎消息(基于 contextTypecontextTitle 动态生成)。

sendChatMessage() 改为:

  1. 将用户消息推入 chatMessages 数组(即时展示)。
  2. 推入一条 { role: 'agent', content: '', isStreaming: true } 占位。
  3. 调用 POST /deepview/chat 传递 { chatId, text, contextId }
  4. 通过已有的 SSE 监听 (api.listenToEvents()) 接收 agent:chunkagent:done 事件,实时追加到占位消息的 content 中。

消息接口扩展:

interface ChatMessage {
  role: 'user' | 'agent';
  content: string;
  isStreaming?: boolean;   // 正在流式接收中
}

4.4 ApiService 新增方法

async getChatHistory(chatId: string): Promise<{messages: any[]}> {
  return this.http.get<any>(`${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<any>(`${this.apiUrl}/chat`, {
    chatId, text, contextId
  }, { headers: this.getHeaders() }).toPromise();
}

5. UI 改造规格

5.1 ChatBox Header 动态化

Before:

<h4>💬 X光片追踪深打</h4>
<p>上下文已在此锁定,免去前提</p>

After:

<h4>💬 X光片追踪深打</h4>
<p *ngIf="chatContextType === 'recording'">
  🎙️ 上下文锁定:本次面诊录音报告
</p>
<p *ngIf="chatContextType === 'client'">
  👤 上下文锁定:{{ chatContextTitle }} 全景档案
</p>

5.2 消息气泡 + 头像重构

目标效果

┌──────────────────────────────────────┐
│  [ AI ]  消息内容巴拉巴拉...           │  ← AI: 方形头像,左对齐
│  ■                                    │
│                                      │
│         用户消息巴拉巴拉...  [ 👤 ]   │  ← 用户: 圆形头像,右对齐
│                              ●       │
└──────────────────────────────────────┘

HTML 结构改造 (app.html 中 chat-history 区域)

@for (msg of chatMessages; track $index) {
  <div class="chat-message" [class.user]="msg.role === 'user'">
    <!-- AI 头像:方形 -->
    <div class="msg-avatar ai-avatar" *ngIf="msg.role !== 'user'">
      <svg>...</svg>  <!-- 或用文字 "深" -->
    </div>
    
    <div class="msg-bubble" [innerHTML]="msg.content"></div>
    
    <!-- 用户头像:圆形 -->
    <div class="msg-avatar user-avatar" *ngIf="msg.role === 'user'">
      {{ auth.user()?.name?.charAt(0) || '我' }}
    </div>
  </div>
}

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 状态时,在气泡尾部追加呼吸光标:

.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] 新增 getChatHistorysendChat
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 对话终端的全部前端改造工作。