01 — 项目概览与架构
一句话描述
dodo-agent 前端 是一个多 Agent AI 对话 Web 界面(类似 ChatGPT),用户可以选择 5 种 AI 助手(对话 / 文件问答 / PPT生成 / 深度研究 / 技能),进行流式多轮对话、上传文件、查看思考过程。支持 6 套主题切换和完整移动端适配。
技术栈
| 技术 | 版本 | 用途 | 为什么选它 |
|---|
| React | 18.3 | UI 框架 | 组件化、生态丰富 |
| TypeScript | 5.5 | 类型系统 | 减少运行时错误 |
| Vite | 5.4 | 构建工具 | 极快的热更新 |
| Zustand | 4.5 | 状态管理 | 比 Redux 轻量得多 |
| Immer | 10.2 | 不可变数据中间件 | 让 Zustand 支持直接赋值语法 |
| CSS Modules | - | 样式方案 | 天然避免样式冲突 |
| Lucide React | 0.460 | 图标 | 轻量、纯净 SVG |
| react-markdown | 9.0 | Markdown 渲染 | 显示 AI 富文本回复 |
| rehype-sanitize | 6.0 | HTML 安全过滤 | 防 XSS |
| Noto Serif SC | Google Fonts | 中文字体(思源宋体) | 标题宋体营造书卷气 |
目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| agent/dodo-agent-frontend/ ├── index.html ← 入口 HTML(含 viewport-fit=cover + 字体预加载) ├── package.json ← 依赖和脚本 ├── vite.config.ts ← 构建配置(代理后端 /api) ├── tsconfig.json ← TS 配置(含 @ 别名) │ └── src/ ├── main.tsx ← React 应用入口 ├── App.tsx ← 根组件(协调所有面板) ├── App.module.css ← 根布局(position: fixed + safe-area) │ ├── styles/ │ ├── tokens.css ← 🎨 设计令牌(6 主题分组,data-theme 切换) │ └── global.css ← 🌐 全局 reset、滚动条、背景水墨装饰 │ ├── types/api.ts ← 后端 API 类型定义 │ ├── constants/ │ ├── agents.ts ← Agent 列表(5 个 AI 助手) │ └── streamTypes.ts ← SSE 事件类型 │ ├── api/ │ ├── client.ts ← HTTP 请求封装(GET/DELETE/POST FormData) │ ├── session.ts ← 会话相关 API │ ├── file.ts ← 文件上传 API │ └── stream.ts ← SSE 流式 URL 构建 │ ├── store/ │ ├── chatStore.ts ← 🗄️ 全局聊天状态(Zustand + Immer)⭐ │ └── themeStore.ts ← 🗄️ 全局主题状态(6 主题切换)⭐ │ ├── hooks/ │ ├── useAutoScroll.ts ← 消息更新时自动滚到底部 │ ├── useSseStream.ts ← SSE 流式数据解析(含 parseSseStream 工具函数) │ └── useTextareaAutosize.ts ← textarea 自动调整高度 │ ├── utils/ │ ├── id.ts ← 生成唯一 ID │ └── file.ts ← 文件工具(大小格式化、类型校验) │ └── components/ ├── Sidebar/ │ ├── Sidebar.tsx ← 📂 侧边栏(桌面 flex 子项 / 移动端 fixed 抽屉) │ ├── Sidebar.module.css │ ├── ChatItem.tsx ← 单个会话项 │ └── ChatItem.module.css │ ├── MessageList/ │ ├── MessageList.tsx ← 💬 消息列表容器 │ ├── MessageList.module.css │ ├── EmptyState.tsx ← 空状态欢迎页(渐变大书法「豆豆」) │ └── EmptyState.module.css │ ├── Message/ │ ├── Message.tsx ← 单条消息(金色左竖线) │ ├── Message.module.css │ ├── Markdown.tsx ← Markdown 渲染 │ ├── Markdown.module.css │ ├── Timeline.tsx ← AI 思考时间线 │ ├── Timeline.module.css │ ├── RecommendQuestions.tsx ← 推荐问题 │ ├── RecommendQuestions.module.css │ ├── ReferenceList.tsx ← 参考来源列表 │ ├── ReferenceList.module.css │ ├── PptDownload.tsx ← PPT 下载按钮 │ └── PptDownload.module.css │ ├── InputArea/ │ ├── InputArea.tsx ← ⌨️ 底部输入区(案台式) │ ├── InputArea.module.css │ ├── AgentSelector.tsx ← Agent 选择器(横向滚动) │ ├── AgentSelector.module.css │ ├── FilePreview.tsx ← 文件预览 │ └── FilePreview.module.css │ ├── Topbar/ ← 🆕 顶部栏(豆豆 logo + 主题切换) │ ├── Topbar.tsx │ └── Topbar.module.css │ ├── ThemeSwitcher/ ← 🆕 主题切换面板 │ ├── ThemeSwitcher.tsx │ └── ThemeSwitcher.module.css │ ├── ConfirmDialog/ │ ├── ConfirmDialog.tsx ← ⚠️ 确认对话框 │ └── ConfirmDialog.module.css │ └── ConnectionError/ ├── ConnectionError.tsx ← 🔴 连接错误提示 └── ConnectionError.module.css
|
架构分层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| ┌─────────────────────────────────────────────────────────┐ │ App.tsx (根布局) │ │ │ │ bg-blob (背景水墨晕染装饰) │ ├──────────────────────────────────────────────────────────┤ │ │ │ ┌─ Sidebar ───┐ ┌─ Main ──────────────────────────┐ │ │ │ │ │ Topbar (豆豆 logo + 标题) │ │ │ │ 桌面端: │ ├────────────────────────────────┤ │ │ │ flex 子项 │ │ │ │ │ │ │ │ MessageList (消息列表) │ │ │ │ 移动端: │ │ - EmptyState (空状态) │ │ │ │ fixed 抽屉 │ │ - Message × N │ │ │ │ │ │ ├ Timeline (思考过程) │ │ │ │ │ │ ├ Markdown (富文本回复) │ │ │ │ │ │ ├ ReferenceList (参考) │ │ │ │ │ │ ├ RecommendQuestions │ │ │ │ │ │ └ PptDownload │ │ │ │ │ │ │ │ │ │ │ ├────────────────────────────────┤ │ │ │ │ │ InputArea (案台式输入) │ │ │ │ │ │ - AgentSelector │ │ │ │ │ │ - FilePreview │ │ │ │ │ │ - textarea + sendBtn │ │ │ └─────────────┘ └────────────────────────────────┘ │ │ │ ├──────────────────────────────────────────────────────────┤ │ <ConfirmDialog /> <ConnectionError /> │ │ <ThemeSwitcher /> (全局浮层) │ └──────────────────────────────────────────────────────────┘ ↓ 数据流方向 ↓ ┌──────────────────────────────────────────────────────────┐ │ Zustand Store │ │ - chatStore.ts (聊天状态) │ │ - themeStore.ts (主题状态) │ └──────────────────────────────────────────────────────────┘ ↓ Cross-component events ↓ CustomEvent: dodo:sidebar-toggle CustomEvent: dodo:open-theme-switcher ↓ API 调用 ↓ ┌──────────────────────────────────────────────────────────┐ │ API 层 (client.ts / session.ts / stream.ts) │ │ 与后端通信(HTTP + SSE) │ └──────────────────────────────────────────────────────────┘
|
三大核心数据流
1. 启动初始化流
1 2 3 4 5 6 7
| App.tsx mount ↓ useEffect testConnection() → 检查后端连接 ↓ loadChats() → 加载会话列表 ↓ createNewChat() → 创建一个新对话(默认)
|
2. 消息发送流(核心业务)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 用户输入文字 → InputArea ↓ useChatStore.sendMessage() ├─ 1. 校验(防重复) ├─ 2. 创建用户消息对象 → chat.messages.push(userMsg) ├─ 3. 创建 AI 占位消息(空内容) ├─ 4. set({ isSending: true }) ├─ 5. 构建 SSE URL └─ 6. fetch + parseSseStream ↓ 每收到一个事件 applyEvent(aiMsg, event) ├─ 'text' → 累积文本 ├─ 'thinking' → 追加 timeline ├─ 'tool_start/end' → 更新工具状态 ├─ 'reference' → 解析参考来源 ├─ 'recommend' → 解析推荐问题 └─ 'error' → 加入 timeline ↓ set({ chatList: [...] }) 触发 React 重渲染
|
3. 主题切换流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 用户点击 Topbar 的主题标签 ↓ dispatchEvent('dodo:open-theme-switcher') ↓ App.tsx 监听到事件 → setThemeOpen(true) ↓ ThemeSwitcher 渲染(6 主题卡片) ↓ 用户点击某个主题 ↓ useThemeStore.setTheme(id) ├─ document.documentElement.setAttribute('data-theme', id) ├─ localStorage.setItem('dodo.theme', id) └─ set({ current: id }) ↓ 所有 CSS 变量自动应用新主题
|
关键技术决策
为什么用 Zustand 而不是 Redux
- 项目数据量小,不需要 Redux 复杂的中间件
- Zustand API 极简,TypeScript 友好
- Immer 中间件让更新逻辑更直观
为什么用 CSS Modules 而不是 Tailwind
- 项目 UI 风格统一(不需要 Tailwind 大量工具类)
- CSS 变量系统(tokens.css)便于主题切换
- 避免引入构建工具链额外开销
为什么用 fetch + ReadableStream 而不是 EventSource
- 需要自定义请求方法(GET + URL params)
- 需要 AbortController 取消请求
- EventSource 是只读 GET,自定义能力弱
为什么用 CustomEvent 跨组件通信
- Topbar(main 内)和 Sidebar(main 外)是兄弟组件
- 不方便把状态提升到 App(会重渲染太多)
- CustomEvent 解耦且实现简单
为什么 6 主题而不是 1 个
- 用户偏好差异大(有人喜欢暗色护眼,有人喜欢亮色清晰)
- 不同场景(写作/会议/演示)需要不同氛围
- 主题切换无需重启(CSS 变量实时生效)
移动端适配核心策略
| 断点 | 行为 |
|---|
| > 768px | 桌面布局:Sidebar 作为 flex 子项永久参与布局 |
| ≤ 768px | 移动布局:Sidebar 变 fixed 抽屉,点击 hamburger 滑出 |
关键技术点:
- 真实视口:
position: fixed; inset: 0 替代 100vh(修复 Chrome 移动端 100vh 含地址栏高度的 bug) - iOS 安全区:
padding-bottom: env(safe-area-inset-bottom, 0px) 适配 home indicator - 字体防缩放:iOS input 字号 ≥ 16px 防止 focus 自动缩放
- 横向滚动:Agent 选择器
overflow-x: auto 支持多个 agent 时滑动
详见 17-移动端适配实战.md。
核心要点
- 🎨 设计令牌化:6 主题通过 CSS 变量分组切换,不改组件代码
- 🗄️ Zustand 双 Store:chatStore(业务)+ themeStore(主题)
- 📨 SSE 流式:fetch + ReadableStream + AbortController
- 📱 移动优先:抽屉式侧边栏 + iOS 安全区 + 真实视口绑定
- 🎯 跨组件通信:CustomEvent 解耦兄弟组件
下一步
继续阅读 02-HTML基础回顾,了解本项目用到的 HTML 语法。