实现了一个可用的聊天页面,支持安全的 Markdown 渲染、SSE 流式增量输出,并补齐空状态、加载态、错误提示等基础体验。
1. 功能概览
- 支持 3 种消息角色:system/user/assistant,并做了左右布局区分
- 支持 Markdown(含 GFM)渲染,并通过 sanitize 做基础安全过滤
- 支持 SSE 流式协议:meta / delta / done / error
- 支持会话串联:前端保存后端返回的
conversation_id,后续请求携带 - 支持停止生成:AbortController 取消流式请求
- 支持空状态引导、发送中反馈、错误提示(toast + 气泡兜底)
2. 目录结构与关键文件
- 页面入口:
frontend/src/app/page.tsx - 聊天应用组件:
frontend/src/components/chat/ChatApp.tsx - 消息气泡组件:
frontend/src/components/chat/MessageBubble.tsx - SSE 流式工具:
frontend/src/lib/streaming.ts - 非流式 API client(typed):
frontend/src/lib/api.ts
3. 消息渲染:MessageBubble(Markdown + XSS 防护)
核心点:
- 用
react-markdown+remark-gfm渲染 Markdown(列表、表格、代码块等) - 用
rehype-sanitize对输出做基础过滤,避免直接把不可信内容插入 DOM - 通过
components精细控制标签样式(p/a/ul/ol/pre/code等),统一排版与间距
核心实现:
| |
关键说明:
- 这里的 sanitize 是“最低限度”的安全护栏,能够防止最常见的 XSS 形态;但如果未来要支持更丰富的 HTML/组件渲染,需要明确白名单策略
4. 流式通信:SSE 解析与 streamChat
4.1 解析层:parseSseStream(ReadableStream -> SSE message)
- 输入:
ReadableStream<Uint8Array> - 输出:
AsyncGenerator<SseMessage> - 处理点:
- 按行解析
event:/data:,遇到空行表示一个事件结束 - 保留
buffer,处理 chunk 边界被截断的情况 - 支持通过
AbortSignal停止读取并reader.cancel()
- 按行解析
核心实现:
| |
4.2 业务层:streamChat(SSE message -> ChatStreamEvent)
- 请求:
POST /chat?stream=true - 事件:meta / delta / done / error
- 错误信息更友好:非 2xx 时尝试解析 JSON 错误体,并拼上
x-request-id(如果存在)
核心实现:
| |
5. 聊天交互:ChatApp(会话、发送、停止、自动滚动)
5.1 状态设计
messages: 渲染消息列表(user/assistant),assistant 的增量输出通过“更新最后一条消息”实现conversationId: 保存后端 meta 事件带回的conversation_idstatus: idle / streaming / error / doneabortRef: 保存当前请求的AbortController,用于停止生成
5.2 发送流程
- 先插入两条消息:
- user:当前输入内容
- assistant:空字符串(后续 delta 逐步 append)
- 启动
streamChat(...),逐事件处理:- meta:写入
conversationId - delta:把 assistant 对应消息的内容拼接上增量文本
- done:结束并设置状态
- error:结束并展示错误
- meta:写入
核心实现:
| |
5.3 停止生成
AbortController.abort()取消 fetch- 取消是“用户主动行为”,不应当被当作错误展示,所以在 catch 分支里对
AbortError做了区分处理
核心实现:
| |
5.4 自动滚动
useEffect监听messages/status变化,滚动到bottomRef,保证新增内容可见
6. 体验打磨:空状态、加载态、Toast、错误信息
- 空状态(messages 为空时):
- 展示引导卡片与示例问题,避免用户进来看到一屏空白
- 加载态:
- 顶部状态旁显示 spinner
- 发送按钮显示“发送中”并带 spinner
- streaming 时禁用输入框,避免状态混乱
- Toast(轻量提示条):
- 错误时弹错误 toast
- 点击“停止”弹成功 toast
- 自动消失 + 可手动关闭
- 错误信息:
- toast 用于即时反馈
- 消息区仍保留一条 system 气泡兜底,避免用户错过信息
Toast 核心实现:
| |
7. 环境变量:前后端拆分与 apiUrl 注入
7.1 前端:NEXT_PUBLIC_API_URL
frontend/src/app/page.tsx读取process.env.NEXT_PUBLIC_API_URL,并把apiUrl传给ChatApp- 新增示例文件:
frontend/.env.example
建议做法:
- 本地创建
frontend/.env.local(不提交),参考.env.example填写
7.2 后端:backend/.env(与 compose 对齐)
- compose 已约定后端 env 位置为
./backend/.env - 示例文件:
backend/.env.example
8. 本地验证命令
前端静态检查:
| |
前端启动:
| |
Docker 一键拉起(含 DB + migrate + 后端 + 前端):
| |
如果要看迁移是否执行:
| |
9. 设计取舍与可改进点
- Toast 目前是“页面内简单实现”,足够支撑开发阶段的错误/状态提示;如果后续要做全站统一通知,可以抽到
components/ui/并做全局队列 streamChat当前只处理最小协议字段,后续如果需要 citations/tool-calls 等结构化数据,可在事件模型里扩展并保持向后兼容api.ts已提供非流式 typed client;后续可以把 ChatApp 的非流式模式也接到 api client 上,统一错误策略与 request_id 展示
10.最终效果

