01 — 项目概览与架构

一句话描述

dodo-agent 前端 是一个多 Agent AI 对话 Web 界面(类似 ChatGPT),用户可以选择 5 种 AI 助手(对话 / 文件问答 / PPT生成 / 深度研究 / 技能),进行流式多轮对话、上传文件、查看思考过程。支持 6 套主题切换和完整移动端适配


技术栈

技术版本用途为什么选它
React18.3UI 框架组件化、生态丰富
TypeScript5.5类型系统减少运行时错误
Vite5.4构建工具极快的热更新
Zustand4.5状态管理比 Redux 轻量得多
Immer10.2不可变数据中间件让 Zustand 支持直接赋值语法
CSS Modules-样式方案天然避免样式冲突
Lucide React0.460图标轻量、纯净 SVG
react-markdown9.0Markdown 渲染显示 AI 富文本回复
rehype-sanitize6.0HTML 安全过滤防 XSS
Noto Serif SCGoogle 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 语法。