第 12 章 Web 前端¶
本章学习目标 - 理解 TTD Web 前端的架构(Next.js 16 + React 19 + Zustand + shadcn/ui)与各技术选型理由 - 掌握 Better Auth 认证链路与 SSE 流式契约的设计 - 理解可视化组件体系与数据交互模式 - 对比 SSE / WebSocket / Vercel AI SDK 的流式通信选型 - 认清当前前端设计的工程权衡与改进方向
前置知识:第 11 章 Backend(API 契约) 关联代码:
web/(app/、components/、stores/、hooks/、lib/)
12.1 背景与动机¶
Web 前端是 TTD 的交互层(L1),承担三职责:
- 对话式分析:流式展示 Agent 思考过程与结果(SSE)——这是核心体验,用户需要看到"系统在一步步思考"
- 管理后台:Admin Panel 管理模型/知识库/评估/反馈/语义资产/few-shot 审批等
- 统一认证:托管 Better Auth,签发 JWT 给后端(第 2 章认证链路)
12.2 设计原理¶
12.2.1 技术栈与选型理由¶
| 层 | 技术 | 版本 | 用途 | 选型理由 |
|---|---|---|---|---|
| 框架 | Next.js | 16.2 | App Router, SSR, RSC, Turbopack dev | RSC + API 路由(反代 + Auth)+ Better Auth 原生集成 |
| UI 库 | React | 19.2 | 组件渲染 | 生态最大,Next.js 原生 |
| 状态 | Zustand | 5 | 全局状态 | 轻量、无 boilerplate,适合 UI 状态 |
| 数据 | TanStack Query | 5 | 服务端状态 | 缓存、重试、失效管理,适合 API 数据 |
| UI 组件 | shadcn/ui + Radix | — | 62 个组件 | 可定制(复制源码而非 npm 依赖),无运行时样式开销 |
| 样式 | Tailwind | 4 | 原子 CSS | 开发效率高,与 shadcn/ui 配合 |
| 图表 | ECharts / Recharts / @antv/g6 | 6 / — / — | 数据可视化 | ECharts 覆盖广,g6 做图谱 |
| 认证 | better-auth | 1.6 | JWT + admin + nextCookies | Next.js 原生集成,RS256 JWT |
| API 客户端 | @hey-api/openapi-ts | — | 从后端 OpenAPI 生成 | 类型安全,与后端 schema 同步 |
12.2.2 路由结构¶
web/app/
├── (auth)/ # 登录/注册(login, signup)
├── (chat)/
│ ├── chat/ # 对话主界面(history, c/[id] 会话)
│ └── admin/ # 管理后台(models/llms/embeddings/...)
└── api/
├── auth/[...all] # Better Auth 路由
├── token # 签发 JWT
└── v1/[...path] # 反向代理到 FastAPI
web/app/api/v1/[...path]/route.ts 将前端请求代理到 TTD_API_INTERNAL_BASE_URL——这是"Next.js 作为 BFF(Backend for Frontend)"的模式,前端不直连后端,统一经过 Next.js 转发,便于注入认证头。
12.3 实现详解¶
12.3.1 Better Auth 认证¶
Better Auth 托管在 Next.js,是统一身份源(ADR-17):
- 邮箱密码 + 可选 GitHub OAuth
- 签发 RS256 JWT(1h TTL)
- Backend 通过 JWKS 本地验签(详见 第 2 章 认证链路)
web/lib/auth.ts配置 Better Auth 服务端
12.3.2 状态管理¶
TTD 用两个状态库分工——这是"服务端状态与客户端状态分离"的最佳实践:
| Store | 文件 | 职责 | 状态类型 |
|---|---|---|---|
| chat-store | stores/chat-store.ts |
对话状态(消息、流式) | 客户端 UI 状态 |
| query-workspace-store | stores/query-workspace-store.ts |
查询工作区 | 客户端 UI 状态 |
| session-detail-store | stores/session-detail-store.ts |
会话详情 | 客户端 UI 状态 |
| ui-preferences-store | stores/ui-preferences-store.ts |
UI 偏好 | 客户端 UI 状态 |
| TanStack Query | hooks/ | 服务端数据(模型列表、知识库等) | 服务端缓存状态 |
StreamingAssistant 类型管理 SSE 流式状态。
12.3.3 API 集成与 SSE 契约¶
API 客户端用 @hey-api/openapi-ts 从后端 OpenAPI 生成类型化客户端(web/lib/api/generated/)——这保证了前后端类型一致,后端 API 变更自动反映到前端类型。
SSE 采用 POST-based SSE(POST /api/v1/chats/{id}/messages/stream),29 种事件类型,前端用 Zod 校验:
| 事件类别 | 事件类型 |
|---|---|
| 状态 | status, session, node_complete, pipeline_plan |
| 检索/规划 | retrieval_step, semantic_guidance, semantic_plan |
| SQL | sql, intent, validation |
| 结果 | data, result |
| 后处理 | visualization, insights, sql_business_context |
| 知识/图 | knowledge, graph |
| ML | ml_result, ml_chart, ml_summary |
| 路由 | route_decision |
| 评估 | evaluator |
| 交互 | clarification, suggested_followups |
| 错误 | errors, error |
事件 schema 定义在 web/lib/api/sse-schema.ts,流处理在 query-stream.ts / chat-stream.ts,hook 在 hooks/use-chat-stream.ts。
为什么用 POST-based SSE 而非标准 EventSource
标准 EventSource 只支持 GET 请求,无法传 request body(查询问题、选项等)。TTD 需要在请求体中传递复杂参数,所以用 fetch + ReadableStream 手动解析 SSE 流(POST-based SSE)。
代价:失去浏览器原生 EventSource 的自动重连。改进方向是用 Last-Event-ID 头实现断点续传。
12.3.4 数据可视化¶
| 组件 | 用途 | 技术 |
|---|---|---|
| 结果表 | 数据表格展示 | 自研 |
| 图表 | ECharts 渲染 | 后端推荐配置(见 第 8 章),前端渲染 |
| 下钻 | 层级探索 | 自研 |
| ML 面板 | ML 分析结果展示 | 自研 |
| 知识面板 | 业务知识问答 | Markdown 渲染 |
| 图结果面板 | 图谱可视化 | @antv/g6 |
前端图表模板在 web/lib/chart-templates/(data-extractor/themes/templates),与后端推荐引擎配合——后端推荐 chart_type + ECharts option,前端直接渲染或按模板微调。
12.3.5 交互模式¶
- 流式渐进渲染:Thinking Steps 面板实时展示 Agent 推理过程(node_complete 事件)
- HITL checkpoint:可视化低置信度时,前端展示候选确认(用户选择 accept/select_alternative/fallback_table)
- 会话回放:历史会话可重放完整管道(基于持久化的 turn 数据)
12.3.6 组件架构¶
前端 React 组件按 Server/Client 组件区分,关键组件树如下:
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e8f0fe','primaryTextColor':'#202124','primaryBorderColor':'#1a73e8','lineColor':'#5f6368','secondaryColor':'#e0f2f1','tertiaryColor':'#f8f9fa'}}}%%
flowchart TB
subgraph Server["Server Components(RSC)"]
direction LR
RootLayout["app/layout.tsx<br/>根布局"]
ChatPage["app/(chat)/chat/page.tsx<br/>会话列表 + 主区"]
AdminPage["app/(chat)/admin/page.tsx<br/>管理后台"]
end
subgraph Client["Client Components(use client)"]
direction TB
ChatShell["ChatShell<br/>对话主容器"]
MsgList["MessageList<br/>消息流"]
Thinking["ThinkingSteps<br/>推理过程面板"]
ResultPanel["ResultPanel<br/>结果区"]
VizPanel["ChartPanel<br/>ECharts 渲染"]
GraphPanel["GraphPanel<br/>@antv/g6 图谱"]
InputBar["ChatInput<br/>输入栏"]
end
subgraph Hooks["Hooks(状态消费)"]
direction LR
UseChat["use-chat-stream<br/>SSE 流状态"]
UseQuery["use-query-workspace<br/>查询工作区"]
end
RootLayout --> ChatPage
RootLayout --> AdminPage
ChatPage --> ChatShell
ChatShell --> MsgList & InputBar
MsgList --> Thinking & ResultPanel
ResultPanel --> VizPanel & GraphPanel
UseChat -.->|"驱动 SSE 流"| MsgList & Thinking
UseQuery -.->|"驱动查询工作区"| ResultPanel
classDef bpProcess fill:#e8f0fe,stroke:#1a73e8,stroke-width:2px,color:#202124
classDef bpInfo fill:#f3e8fd,stroke:#9334e6,stroke-width:2px,color:#202124
classDef bpGroup fill:#f8f9fa,stroke:#dadce0,stroke-width:1px,color:#3c4043
class RootLayout,ChatPage,AdminPage bpProcess
class ChatShell,MsgList,Thinking,ResultPanel,VizPanel,GraphPanel,InputBar bpInfo
class UseChat,UseQuery bpProcess
| 组件 | 类型 | 职责 | props 流 |
|---|---|---|---|
ChatShell |
Client | 对话主容器,协调消息流与输入 | session_id → use-chat-stream |
MessageList |
Client | 渲染消息列表(user/assistant) | messages[] from store |
ThinkingSteps |
Client | 实时展示 node_complete 事件 | pipeline events from SSE |
ResultPanel |
Client | 结果区(表格 + 图表 + 洞察) | query result from store |
ChartPanel |
Client | ECharts 渲染(接收后端 option) | echarts_option prop |
ChatInput |
Client | 输入栏 + 发送 | onSubmit callback |
12.3.7 SSE 流处理管道¶
29 种 SSE 事件从后端到 UI 的处理管道:
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e8f0fe','primaryTextColor':'#202124','primaryBorderColor':'#1a73e8','lineColor':'#5f6368','secondaryColor':'#e0f2f1','tertiaryColor':'#f8f9fa'}}}%%
flowchart LR
Backend["Backend FastAPI<br/>SSE 29 事件类型"] -->|"POST · ReadableStream"| Fetch["fetch()<br/>手动解析 SSE"]
Fetch -->|"逐事件"| Schema["sse-schema.ts<br/>Zod 校验<br/>discriminated union"]
Schema -->|"类型安全事件"| QStream["query-stream.ts<br/>查询结果流处理<br/>sql · data · viz"]
Schema -->|"类型安全事件"| CStream["chat-stream.ts<br/>对话状态流处理<br/>status · thinking · insights"]
QStream -->|"更新查询结果"| Store["Zustand stores<br/>chat-store · query-workspace<br/>session-detail"]
CStream -->|"更新对话状态"| Store
Store -->|"响应式订阅"| Hook["use-chat-stream.ts<br/>React hook<br/>生命周期管理"]
Hook -->|"驱动渲染"| UI["React 组件<br/>ThinkingSteps · ResultPanel<br/>ChartPanel · GraphPanel"]
classDef bpProcess fill:#e8f0fe,stroke:#1a73e8,stroke-width:2px,color:#202124
classDef bpData fill:#e0f2f1,stroke:#0d7377,stroke-width:2px,color:#202124
classDef bpInfo fill:#f3e8fd,stroke:#9334e6,stroke-width:2px,color:#202124
classDef bpSuccess fill:#e6f4ea,stroke:#1e8e3e,stroke-width:2px,color:#202124
class Backend,Fetch bpProcess
class Schema,QStream,CStream bpData
class Store,Hook bpInfo
class UI bpSuccess
| 文件 | 职责 |
|---|---|
sse-schema.ts |
29 种事件的 Zod schema 定义 + discriminated union |
query-stream.ts |
处理查询类事件(sql/data/result/visualization)→ 更新结果状态 |
chat-stream.ts |
处理对话类事件(status/node_complete/insights)→ 更新思考过程状态 |
use-chat-stream.ts |
React hook,封装 SSE 连接生命周期 + 状态订阅 |
为何拆分 query-stream 与 chat-stream
查询结果与对话思考过程是两个独立的 UI 关注点——查询结果更新结果区(表格/图表),思考过程更新 ThinkingSteps 面板。拆分处理让各自的状态更新路径清晰,避免单一处理器过于庞大。
12.3.8 认证流逐步演练¶
用户从登录到发起查询的完整认证链路:
sequenceDiagram
autonumber
box rgb(248,249,250) 用户浏览器
participant U as 用户浏览器
end
box rgb(232,240,254) Next.js BFF
participant Next as Next.js (BFF)
participant BA as Better Auth
end
box rgb(224,242,241) Backend
participant API as FastAPI Backend
participant JWKS as JWKS 端点
end
U->>Next: 邮箱密码登录
Next->>BA: 验证凭据
BA-->>Next: 创建 session(HttpOnly cookie)
Next->>BA: 签发 RS256 JWT(1h TTL)
BA-->>Next: JWT(私钥签名)
Next-->>U: JWT 存储于 cookie
U->>Next: 发起查询(带 cookie)
Next->>Next: 从 cookie 取 JWT
Next->>API: 反代请求 + Authorization: Bearer <JWT>
API->>JWKS: 首次拉取 JWKS 公钥(缓存 3600s)
JWKS-->>API: RS256 公钥
API->>API: 验签 JWT + 校验 audience/issuer
alt 验签通过
API-->>U: SSE 流式响应
else 验签失败
API-->>U: 401 Unauthorized
end
关键点:① JWT 由 Better Auth 用私钥在 Next.js 侧签发(RS256);② Backend 用 JWKS 公钥本地验签,私钥不离开签发方;③ JWKS 公钥缓存 1 小时(TTD_JWKS_CACHE_TTL_SECONDS=3600),避免每次请求拉取。详见 ADR-17 与 第 2 章认证链路。
12.3.9 SSE 边缘情况处理¶
流式通信在真实网络环境下会遇到多种边缘情况,以下是 TTD 的处理策略:
| 边缘情况 | 现象 | 当前处理 | 改进方向 |
|---|---|---|---|
| 断线重连 | 网络中断,SSE 连接断开 | 当前无自动重连——连接断开后前端显示"连接中断"提示,用户需手动重发 | 用 Last-Event-ID 头实现断点续传(16 章 P1) |
| 事件乱序 | 网络抖动导致后发事件先到 | SSE 基于 TCP 有序传输,理论不乱序;但若客户端用多连接并发可能乱序 | 单连接串行处理,use-chat-stream 按序消费 |
| 部分事件丢失 | 连接中断时部分事件未到达 | 最后一个 status 事件若丢失,前端无法判断是否完成 |
心跳机制(每 5s 发 heartbeat 事件),超时无心跳判定为断连 |
| 连接超时 | 后端处理超过 HAProxy timeout |
HAProxy 返回 504;前端显示"请求超时" | 前端显示部分已收到结果 + 重试按钮 |
| JWT 过期 | 1h TTL 到期,SSE 流中断 | 前端检测 401 后刷新 token 重连 | silent refresh(过期前 5min 自动刷新) |
12.3.10 流式渲染性能优化¶
长查询(deep_analysis ~8s)期间,Thinking Steps 面板会接收大量 node_complete 事件,需优化渲染性能:
| 策略 | 实现方式 | 效果 |
|---|---|---|
| 增量 DOM 更新 | Zustand selector 精确订阅,只重渲染变化的节点 | 避免全列表重渲染 |
| 虚拟滚动 | Thinking Steps 超过 20 条时启用 @tanstack/react-virtual |
DOM 节点数恒定,不随事件增长 |
| 批量更新 | requestAnimationFrame 内批量 flush 事件到 store |
避免高频事件触发逐次渲染 |
| 结果区延迟渲染 | React.lazy + Suspense,ECharts/g6 按需加载 |
首屏不含重型可视化库 |
| 代码分割 | next/dynamic 按路由分割,Admin Panel 独立 chunk |
主对话页 bundle 更小 |
性能预算(设计目标)
- 主对话页 JS bundle:< 300KB(gzip)
- 首次内容绘制(FCP):< 1.5s
- Thinking Steps 单事件渲染:< 16ms(60fps 一帧)
- ECharts 图表渲染:< 100ms
上述为设计目标,尚未用 Lighthouse 实测验证。
12.3.11 错误状态 UX¶
不同错误场景下的前端表现设计:
| 错误类型 | 前端表现 | 用户操作 |
|---|---|---|
| 网络错误 | 消息气泡显示"连接中断" + 重试按钮 | 点击重试重新发送 |
| SQL 被拒绝(护栏拦截) | 显示"查询被安全策略拦截" + 原因(如"检测到 DELETE 语句") | 用户修改问题重试 |
| 查询超时 | 显示"查询执行超时,建议缩小范围" + 已收到的部分结果 | 重试或细化问题 |
| 路由到 clarification | 显示澄清问题(如"您说的'销量'是指交易次数还是交易金额?") | 用户选择消歧选项 |
| 越权访问 | 显示"您没有权限查询该数据" | 无(需联系管理员) |
| LLM 不可用 | 显示"AI 服务暂时不可用,请稍后重试" | 重试 |
| SQL 语法错误(修复循环耗尽) | 显示"无法生成有效查询,请尝试换一种问法" | 用户改写问题 |
所有错误状态都通过 SSE error 事件类型传递,前端在 use-chat-stream 中统一捕获并映射到对应 UI。
12.4 方案选型对比¶
12.4.1 前端框架对比¶
| 维度 | Next.js(TTD) | Remix | Nuxt |
|---|---|---|---|
| SSR/RSC | 强(React 19 RSC) | 强 | 强 |
| 生态 | 最大 | 中 | 中 |
| API 路由 | 强(反代 + Auth) | 强 | 中 |
| Better Auth 集成 | 原生 | 需适配 | 需适配 |
| TTD 选择理由 | RSC + API 路由(BFF 模式)+ Better Auth 原生集成 |
12.4.2 流式通信对比¶
| 方案 | 优势 | 劣势 | TTD 状态 |
|---|---|---|---|
| SSE over POST | 可传 body、简单、HTTP 兼容 | 非标准 EventSource、无自动重连 | |
| 标准 EventSource (GET) | 浏览器原生、自动重连 | 不能传 body | |
| WebSocket | 双向、低延迟 | 复杂、需维护连接、代理兼容性 | |
| Vercel AI SDK | 封装好、生态 | 绑定 Vercel、抽象层重 |
当前实现可改进之处
- 缺 UI 截图与线框图:本章讨论前端却无任何视觉参考(截图/线框图),读者难以建立对实际界面的直观认知。改进:补充关键页面截图(对话主界面、Thinking Steps、结果区、Admin Panel)与组件线框图。
- SSE 事件类型膨胀(29 种):事件类型多,前端处理逻辑分散在多个文件(sse-schema/query-stream/chat-stream/use-chat-stream),维护成本高。改进:事件分类聚合(用 discriminated union),统一处理器;或减少事件类型,合并相似事件。
- 状态管理 Zustand + TanStack Query 重叠:两者都管理服务端状态,职责边界模糊,易出现双源真理。改进:明确分工——TanStack Query 管服务端缓存,Zustand 仅管纯客户端 UI 状态。
- SSE 无断点续传:网络中断后无法从断点恢复,需重新发起整个流。改进:用
Last-Event-ID头实现断点续传,提升弱网体验。 - OpenAPI codegen 与手写 schema 并存:
generated/是 codegen,sse-schema.ts是手写 Zod,两套 schema 易漂移。改进:SSE schema 也从后端生成,单一来源。
改进方向
前端的演进方向是收敛状态管理 + 简化流式契约。短期:明确 Zustand/TanStack 分工、SSE 断点续传。长期:评估 Vercel AI SDK 或自研流式抽象统一 29 种事件,降低维护成本;SSE schema 纳入 codegen 单一来源。
12.5 小结与延伸阅读¶
本章要点:
- Web 前端用 Next.js 16 + React 19 + Zustand + shadcn/ui,承担对话/管理/认证三职责。
- Better Auth 托管在 Next.js(BFF 模式),签发 RS256 JWT,Backend JWKS 本地验签。
- SSE over POST 流式 29 种事件,前端 Zod 校验,Thinking Steps 面板实时展示推理过程。
- 可视化用 ECharts(后端推荐配置)+ @antv/g6(图谱),规则推荐为主、LLM 回退为辅。
- 状态管理 Zustand(UI)+ TanStack Query(服务端)分工。
延伸阅读:
- Next.js 16 — nextjs.org/docs
- Better Auth — better-auth.com
- shadcn/ui — ui.shadcn.com
- SSE 规范 — developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
下一章:第 13 章 开发与协作流程——进入第五部工程实践,环境搭建、五步启动、CI/CD 与 Task 命令。