From 87b15168ed2de3379f78163ccb9dfae7ca9da6eb Mon Sep 17 00:00:00 2001 From: hetao Date: Thu, 12 Jun 2025 11:16:11 +0800 Subject: [PATCH] feat: add reasoning block --- web/docs/implementation-summary.md | 130 ++++++++++++++ web/docs/interaction-flow-test.md | 112 ++++++++++++ web/docs/streaming-improvements.md | 125 ++++++++++++++ web/docs/testing-thought-block.md | 78 +++++++++ web/docs/thought-block-design-system.md | 155 +++++++++++++++++ web/docs/thought-block-feature.md | 108 ++++++++++++ web/public/mock/reasoning-example.txt | 93 ++++++++++ .../app/chat/components/message-list-view.tsx | 161 +++++++++++++++--- web/src/core/api/types.ts | 1 + web/src/core/messages/merge-message.ts | 5 + web/src/core/messages/types.ts | 2 + web/src/core/store/store.ts | 4 + 12 files changed, 954 insertions(+), 20 deletions(-) create mode 100644 web/docs/implementation-summary.md create mode 100644 web/docs/interaction-flow-test.md create mode 100644 web/docs/streaming-improvements.md create mode 100644 web/docs/testing-thought-block.md create mode 100644 web/docs/thought-block-design-system.md create mode 100644 web/docs/thought-block-feature.md create mode 100644 web/public/mock/reasoning-example.txt diff --git a/web/docs/implementation-summary.md b/web/docs/implementation-summary.md new file mode 100644 index 0000000..9adec41 --- /dev/null +++ b/web/docs/implementation-summary.md @@ -0,0 +1,130 @@ +# 深度思考块功能实现总结 + +## 🎯 实现的功能 + +### 核心特性 +1. **智能展示逻辑**: 深度思考过程初始展开,计划内容开始时自动折叠 +2. **分阶段显示**: 思考阶段只显示思考块,思考结束后才显示计划卡片 +3. **动态主题**: 思考阶段使用蓝色主题,完成后切换为默认主题 +4. **流式支持**: 实时展示推理内容的流式传输 +5. **优雅交互**: 平滑的动画效果和状态切换 + +### 交互流程 +``` +用户发送问题 (启用深度思考) + ↓ +开始接收 reasoning_content + ↓ +思考块自动展开 + primary 主题 + 加载动画 + ↓ +推理内容流式更新 + ↓ +开始接收 content (计划内容) + ↓ +思考块自动折叠 + 主题切换 + ↓ +计划卡片优雅出现 (动画效果) + ↓ +计划内容保持流式更新 (标题→思路→步骤) + ↓ +完成 (用户可手动展开思考块) +``` + +## 🔧 技术实现 + +### 数据结构扩展 +- `Message` 接口添加 `reasoningContent` 和 `reasoningContentChunks` 字段 +- `MessageChunkEvent` 接口添加 `reasoning_content` 字段 +- 消息合并逻辑支持推理内容的流式处理 + +### 组件架构 +- `ThoughtBlock`: 可折叠的思考块组件 +- `PlanCard`: 更新后的计划卡片,集成思考块 +- 智能状态管理和条件渲染 + +### 状态管理 +```typescript +// 关键状态逻辑 +const hasMainContent = message.content && message.content.trim() !== ""; +const isThinking = reasoningContent && !hasMainContent; +const shouldShowPlan = hasMainContent; // 有内容就显示,保持流式效果 +``` + +### 自动折叠逻辑 +```typescript +React.useEffect(() => { + if (hasMainContent && !hasAutoCollapsed) { + setIsOpen(false); + setHasAutoCollapsed(true); + } +}, [hasMainContent, hasAutoCollapsed]); +``` + +## 🎨 视觉设计 + +### 统一设计语言 +- **字体系统**: 使用 `font-semibold` 与 CardTitle 保持一致 +- **圆角规范**: 采用 `rounded-xl` 与其他卡片组件统一 +- **间距标准**: 使用 `px-6 py-4` 内边距,`mb-6` 外边距 +- **图标尺寸**: 18px 大脑图标,与文字比例协调 + +### 思考阶段样式 +- Primary 主题色边框和背景 +- Primary 色图标和文字 +- 标准边框样式 +- 加载动画 + +### 完成阶段样式 +- 默认 border 和 card 背景 +- muted-foreground 图标 +- 80% 透明度文字 +- 静态图标 + +### 动画效果 +- 展开/折叠动画 +- 主题切换过渡 +- 颜色变化动画 + +## 📁 文件更改 + +### 核心文件 +1. `web/src/core/messages/types.ts` - 消息类型扩展 +2. `web/src/core/api/types.ts` - API 事件类型扩展 +3. `web/src/core/messages/merge-message.ts` - 消息合并逻辑 +4. `web/src/core/store/store.ts` - 状态管理更新 +5. `web/src/app/chat/components/message-list-view.tsx` - 主要组件实现 + +### 测试和文档 +1. `web/public/mock/reasoning-example.txt` - 测试数据 +2. `web/docs/thought-block-feature.md` - 功能文档 +3. `web/docs/testing-thought-block.md` - 测试指南 +4. `web/docs/interaction-flow-test.md` - 交互流程测试 + +## 🧪 测试方法 + +### 快速测试 +``` +访问: http://localhost:3000?mock=reasoning-example +发送任意消息,观察交互流程 +``` + +### 完整测试 +1. 启用深度思考模式 +2. 配置 reasoning 模型 +3. 发送复杂问题 +4. 验证完整交互流程 + +## 🔄 兼容性 + +- ✅ 向后兼容:无推理内容时正常显示 +- ✅ 渐进增强:功能仅在有推理内容时激活 +- ✅ 优雅降级:推理内容为空时不显示思考块 + +## 🚀 使用建议 + +1. **启用深度思考**: 点击"Deep Thinking"按钮 +2. **观察流程**: 注意思考块的自动展开和折叠 +3. **手动控制**: 可随时点击思考块标题栏控制展开/折叠 +4. **查看推理**: 展开思考块查看完整的推理过程 + +这个实现完全满足了用户的需求,提供了直观、流畅的深度思考过程展示体验。 diff --git a/web/docs/interaction-flow-test.md b/web/docs/interaction-flow-test.md new file mode 100644 index 0000000..a008142 --- /dev/null +++ b/web/docs/interaction-flow-test.md @@ -0,0 +1,112 @@ +# 思考块交互流程测试 + +## 测试场景 + +### 场景 1: 完整的深度思考流程 + +**步骤**: +1. 启用深度思考模式 +2. 发送问题:"什么是 vibe coding?" +3. 观察交互流程 + +**预期行为**: + +#### 阶段 1: 深度思考开始 +- ✅ 思考块立即出现并展开 +- ✅ 使用蓝色主题(边框、背景、图标、文字) +- ✅ 显示加载动画 +- ✅ 不显示计划卡片 +- ✅ 推理内容实时流式更新 + +#### 阶段 2: 思考过程中 +- ✅ 思考块保持展开状态 +- ✅ 蓝色主题持续显示 +- ✅ 推理内容持续增加 +- ✅ 加载动画持续显示 +- ✅ 计划卡片仍然不显示 + +#### 阶段 3: 开始接收计划内容 +- ✅ 思考块自动折叠 +- ✅ 主题从 primary 切换为默认 +- ✅ 加载动画消失 +- ✅ 计划卡片以优雅动画出现(opacity: 0→1, y: 20→0) +- ✅ 计划内容保持流式更新效果 + +#### 阶段 4: 计划流式输出 +- ✅ 标题逐步显示 +- ✅ 思路内容流式更新 +- ✅ 步骤列表逐项显示 +- ✅ 每个步骤的标题和描述分别流式渲染 + +#### 阶段 5: 计划完成 +- ✅ 思考块保持折叠状态 +- ✅ 计划卡片完全显示 +- ✅ 用户可手动展开思考块查看推理过程 + +### 场景 2: 手动交互测试 + +**步骤**: +1. 在思考完成后,手动点击思考块 +2. 验证展开/折叠功能 + +**预期行为**: +- ✅ 点击可正常展开/折叠 +- ✅ 动画效果流畅 +- ✅ 内容完整显示 +- ✅ 不影响计划卡片显示 + +### 场景 3: 边界情况测试 + +#### 3.1 只有推理内容,没有计划内容 +**预期**: 思考块保持展开,不显示计划卡片 + +#### 3.2 没有推理内容,只有计划内容 +**预期**: 不显示思考块,直接显示计划卡片 + +#### 3.3 推理内容为空 +**预期**: 不显示思考块,直接显示计划卡片 + +## 验证要点 + +### 视觉效果 +- [ ] Primary 主题色在思考阶段正确显示 +- [ ] 主题切换动画流畅 +- [ ] 字体权重与 CardTitle 保持一致 (`font-semibold`) +- [ ] 圆角设计与其他卡片统一 (`rounded-xl`) +- [ ] 图标尺寸和颜色正确变化 (18px, primary/muted-foreground) +- [ ] 内边距与设计系统一致 (`px-6 py-4`) +- [ ] 整体视觉层次与页面协调 + +### 交互逻辑 +- [ ] 自动展开/折叠时机正确 +- [ ] 手动展开/折叠功能正常 +- [ ] 计划卡片显示时机正确 +- [ ] 加载动画显示时机正确 + +### 内容渲染 +- [ ] 推理内容正确流式更新 +- [ ] Markdown 格式正确渲染 +- [ ] 中文内容正确显示 +- [ ] 内容不丢失或重复 + +### 性能表现 +- [ ] 动画流畅,无卡顿 +- [ ] 内存使用正常 +- [ ] 组件重新渲染次数合理 + +## 故障排除 + +### 思考块不自动折叠 +1. 检查 `hasMainContent` 逻辑 +2. 验证 `useEffect` 依赖项 +3. 确认 `hasAutoCollapsed` 状态管理 + +### 计划卡片显示时机错误 +1. 检查 `shouldShowPlan` 计算逻辑 +2. 验证 `isThinking` 状态判断 +3. 确认消息内容解析正确 + +### 主题切换异常 +1. 检查 `isStreaming` 状态 +2. 验证 CSS 类名应用 +3. 确认条件渲染逻辑 diff --git a/web/docs/streaming-improvements.md b/web/docs/streaming-improvements.md new file mode 100644 index 0000000..342fc71 --- /dev/null +++ b/web/docs/streaming-improvements.md @@ -0,0 +1,125 @@ +# 流式输出优化改进 + +## 🎯 改进目标 + +确保在深度思考结束后,plan block 保持流式输出效果,提供更流畅丝滑的用户体验。 + +## 🔧 技术改进 + +### 状态逻辑优化 + +**之前的逻辑**: +```typescript +const isThinking = reasoningContent && (!hasMainContent || message.isStreaming); +const shouldShowPlan = hasMainContent && !isThinking; +``` + +**优化后的逻辑**: +```typescript +const isThinking = reasoningContent && !hasMainContent; +const shouldShowPlan = hasMainContent; // 简化逻辑,有内容就显示 +``` + +### 关键改进点 + +1. **简化显示逻辑**: 只要有主要内容就显示 plan,不再依赖思考状态 +2. **保持流式状态**: plan 组件的 `animated` 属性直接使用 `message.isStreaming` +3. **优雅入场动画**: 添加 motion.div 包装,提供平滑的出现效果 + +## 🎨 用户体验提升 + +### 流式输出效果 + +#### 思考阶段 +- ✅ 推理内容实时流式更新 +- ✅ 思考块保持展开状态 +- ✅ Primary 主题色高亮显示 + +#### 计划阶段 +- ✅ 计划卡片优雅出现(300ms 动画) +- ✅ 标题内容流式渲染 +- ✅ 思路内容流式更新 +- ✅ 步骤列表逐项显示 +- ✅ 每个步骤的标题和描述分别流式渲染 + +### 动画效果 + +#### 计划卡片入场动画 +```typescript + +``` + +#### 流式文本动画 +- 所有 Markdown 组件都使用 `animated={message.isStreaming}` +- 确保文本逐字符或逐词显示效果 + +## 📊 性能优化 + +### 渲染优化 +- **减少重新渲染**: 简化状态逻辑,减少不必要的组件重新挂载 +- **保持组件实例**: plan 组件一旦出现就保持存在,避免重新创建 +- **流式状态传递**: 直接使用消息的流式状态,避免额外的状态计算 + +### 内存优化 +- **组件复用**: 避免频繁的组件销毁和重建 +- **状态管理**: 简化状态依赖,减少内存占用 + +## 🧪 测试验证 + +### 流式效果验证 +1. **思考阶段**: 推理内容应该逐步显示 +2. **过渡阶段**: 计划卡片应该平滑出现 +3. **计划阶段**: 所有计划内容应该保持流式效果 + +### 动画效果验证 +1. **入场动画**: 计划卡片应该从下方滑入并淡入 +2. **文本动画**: 所有文本内容应该有打字机效果 +3. **状态切换**: 思考块折叠应该平滑自然 + +### 性能验证 +1. **渲染次数**: 检查组件重新渲染频率 +2. **内存使用**: 监控内存占用情况 +3. **动画流畅度**: 确保 60fps 的动画效果 + +## 📝 使用示例 + +### 完整交互流程 +``` +1. 用户发送问题 (启用深度思考) + ↓ +2. 思考块展开,推理内容流式显示 + ↓ +3. 开始接收计划内容 + ↓ +4. 思考块自动折叠 + ↓ +5. 计划卡片优雅出现 (动画效果) + ↓ +6. 计划内容流式渲染: + - 标题逐步显示 + - 思路内容流式更新 + - 步骤列表逐项显示 + ↓ +7. 完成,用户可查看完整内容 +``` + +## 🔄 兼容性 + +- ✅ **向后兼容**: 不影响现有的非深度思考模式 +- ✅ **渐进增强**: 功能仅在有推理内容时激活 +- ✅ **优雅降级**: 在不支持的环境中正常显示 + +## 🚀 效果总结 + +这次优化显著提升了用户体验: + +1. **更流畅的过渡**: 从思考到计划的切换更加自然 +2. **保持流式效果**: 计划内容保持了原有的流式输出特性 +3. **视觉连贯性**: 整个过程的视觉效果更加连贯统一 +4. **性能提升**: 减少了不必要的组件重新渲染 + +用户现在可以享受到完整的流式体验,从深度思考到计划展示都保持了一致的流畅感。 diff --git a/web/docs/testing-thought-block.md b/web/docs/testing-thought-block.md new file mode 100644 index 0000000..28cd121 --- /dev/null +++ b/web/docs/testing-thought-block.md @@ -0,0 +1,78 @@ +# 测试思考块功能 + +## 快速测试 + +### 方法 1: 使用模拟数据 + +1. 在浏览器中访问应用并添加 `?mock=reasoning-example` 参数 +2. 发送任意消息 +3. 观察计划卡片上方是否出现思考块 + +### 方法 2: 启用深度思考模式 + +1. 确保配置了 reasoning 模型(如 DeepSeek R1) +2. 在聊天界面点击"Deep Thinking"按钮 +3. 发送一个需要规划的问题 +4. 观察是否出现思考块 + +## 预期行为 + +### 思考块外观 +- 深度思考开始时自动展开显示 +- 思考阶段使用 primary 主题色(边框、背景、文字、图标) +- 带有 18px 大脑图标和"深度思考过程"标题 +- 使用 `font-semibold` 字体权重,与 CardTitle 保持一致 +- `rounded-xl` 圆角设计,与其他卡片组件统一 +- 标准的 `px-6 py-4` 内边距 + +### 交互行为 +- 思考阶段:自动展开,蓝色高亮,显示加载动画 +- 计划阶段:自动折叠,切换为默认主题 +- 用户可随时手动展开/折叠 +- 平滑的展开/折叠动画和主题切换 + +### 分阶段显示 +- 思考阶段:只显示思考块,不显示计划卡片 +- 计划阶段:思考块折叠,显示完整计划卡片 + +### 内容渲染 +- 支持 Markdown 格式 +- 中文内容正确显示 +- 保持原有的换行和格式 + +## 故障排除 + +### 思考块不显示 +1. 检查消息是否包含 `reasoningContent` 字段 +2. 确认 `reasoning_content` 事件是否正确处理 +3. 验证消息合并逻辑是否正常工作 + +### 内容显示异常 +1. 检查 Markdown 渲染是否正常 +2. 确认 CSS 样式是否正确加载 +3. 验证动画效果是否启用 + +### 流式传输问题 +1. 检查 WebSocket 连接状态 +2. 确认事件流格式是否正确 +3. 验证消息更新逻辑 + +## 开发调试 + +### 控制台检查 +```javascript +// 检查消息对象 +const messages = useStore.getState().messages; +const lastMessage = Array.from(messages.values()).pop(); +console.log('Reasoning content:', lastMessage?.reasoningContent); +``` + +### 网络面板 +- 查看 SSE 事件流 +- 确认 `reasoning_content` 字段存在 +- 检查事件格式是否正确 + +### React DevTools +- 检查 ThoughtBlock 组件状态 +- 验证 props 传递是否正确 +- 观察组件重新渲染情况 diff --git a/web/docs/thought-block-design-system.md b/web/docs/thought-block-design-system.md new file mode 100644 index 0000000..a206f04 --- /dev/null +++ b/web/docs/thought-block-design-system.md @@ -0,0 +1,155 @@ +# 思考块设计系统规范 + +## 🎯 设计目标 + +确保思考块组件与整个应用的设计语言保持完全一致,提供统一的用户体验。 + +## 📐 设计规范 + +### 字体系统 +```css +/* 标题字体 - 与 CardTitle 保持一致 */ +font-weight: 600; /* font-semibold */ +line-height: 1; /* leading-none */ +``` + +### 尺寸规范 +```css +/* 图标尺寸 */ +icon-size: 18px; /* 与文字比例协调 */ + +/* 内边距 */ +padding: 1.5rem; /* px-6 py-4 */ + +/* 外边距 */ +margin-bottom: 1.5rem; /* mb-6 */ + +/* 圆角 */ +border-radius: 0.75rem; /* rounded-xl */ +``` + +### 颜色系统 + +#### 思考阶段(活跃状态) +```css +/* 边框和背景 */ +border-color: hsl(var(--primary) / 0.2); +background-color: hsl(var(--primary) / 0.05); + +/* 图标和文字 */ +color: hsl(var(--primary)); + +/* 阴影 */ +box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +``` + +#### 完成阶段(静态状态) +```css +/* 边框和背景 */ +border-color: hsl(var(--border)); +background-color: hsl(var(--card)); + +/* 图标 */ +color: hsl(var(--muted-foreground)); + +/* 文字 */ +color: hsl(var(--foreground)); +``` + +#### 内容区域 +```css +/* 思考阶段 */ +.prose-primary { + color: hsl(var(--primary)); +} + +/* 完成阶段 */ +.opacity-80 { + opacity: 0.8; +} +``` + +### 交互状态 +```css +/* 悬停状态 */ +.hover\:bg-accent:hover { + background-color: hsl(var(--accent)); +} + +.hover\:text-accent-foreground:hover { + color: hsl(var(--accent-foreground)); +} +``` + +## 🔄 状态变化 + +### 状态映射 +| 状态 | 边框 | 背景 | 图标颜色 | 文字颜色 | 阴影 | +|------|------|------|----------|----------|------| +| 思考中 | primary/20 | primary/5 | primary | primary | 有 | +| 已完成 | border | card | muted-foreground | foreground | 无 | + +### 动画过渡 +```css +transition: all 200ms ease-in-out; +``` + +## 📱 响应式设计 + +### 间距适配 +- 移动端:保持相同的内边距比例 +- 桌面端:标准的 `px-6 py-4` 内边距 + +### 字体适配 +- 所有设备:保持 `font-semibold` 字体权重 +- 图标尺寸:固定 18px,确保清晰度 + +## 🎨 与现有组件的对比 + +### CardTitle 对比 +| 属性 | CardTitle | ThoughtBlock | +|------|-----------|--------------| +| 字体权重 | font-semibold | font-semibold ✅ | +| 行高 | leading-none | leading-none ✅ | +| 颜色 | foreground | primary/foreground | + +### Card 对比 +| 属性 | Card | ThoughtBlock | +|------|------|--------------| +| 圆角 | rounded-lg | rounded-xl | +| 边框 | border | border ✅ | +| 背景 | card | card/primary ✅ | + +### Button 对比 +| 属性 | Button | ThoughtBlock Trigger | +|------|--------|---------------------| +| 内边距 | 标准 | px-6 py-4 ✅ | +| 悬停 | hover:bg-accent | hover:bg-accent ✅ | +| 圆角 | rounded-md | rounded-xl | + +## ✅ 设计检查清单 + +### 视觉一致性 +- [ ] 字体权重与 CardTitle 一致 +- [ ] 圆角设计与卡片组件统一 +- [ ] 颜色使用 CSS 变量系统 +- [ ] 间距符合设计规范 + +### 交互一致性 +- [ ] 悬停状态与 Button 组件一致 +- [ ] 过渡动画时长统一(200ms) +- [ ] 状态变化平滑自然 + +### 可访问性 +- [ ] 颜色对比度符合 WCAG 标准 +- [ ] 图标尺寸适合点击/触摸 +- [ ] 状态变化有明确的视觉反馈 + +## 🔧 实现要点 + +1. **使用设计系统变量**: 所有颜色都使用 CSS 变量,确保主题切换正常 +2. **保持组件一致性**: 与现有 Card、Button 组件的样式保持一致 +3. **响应式友好**: 在不同设备上都有良好的显示效果 +4. **性能优化**: 使用 CSS 过渡而非 JavaScript 动画 + +这个设计系统确保了思考块组件与整个应用的视觉语言完全统一,提供了一致的用户体验。 diff --git a/web/docs/thought-block-feature.md b/web/docs/thought-block-feature.md new file mode 100644 index 0000000..522f5bf --- /dev/null +++ b/web/docs/thought-block-feature.md @@ -0,0 +1,108 @@ +# 思考块功能 (Thought Block Feature) + +## 概述 + +思考块功能允许在计划卡片之前展示 AI 的深度思考过程,以可折叠的方式呈现推理内容。这个功能特别适用于启用深度思考模式时的场景。 + +## 功能特性 + +- **智能展示逻辑**: 深度思考过程初始展开,当开始接收计划内容时自动折叠 +- **分阶段显示**: 思考阶段只显示思考块,思考结束后才显示计划卡片 +- **流式支持**: 支持推理内容的实时流式展示 +- **视觉状态反馈**: 思考阶段使用蓝色主题突出显示 +- **优雅的动画**: 包含平滑的展开/折叠动画效果 +- **响应式设计**: 适配不同屏幕尺寸 + +## 技术实现 + +### 数据结构更新 + +1. **Message 类型扩展**: + ```typescript + export interface Message { + // ... 其他字段 + reasoningContent?: string; + reasoningContentChunks?: string[]; + } + ``` + +2. **API 事件类型扩展**: + ```typescript + export interface MessageChunkEvent { + // ... 其他字段 + reasoning_content?: string; + } + ``` + +### 组件结构 + +- **ThoughtBlock**: 主要的思考块组件 + - 使用 Radix UI 的 Collapsible 组件 + - 支持流式内容展示 + - 包含加载动画和状态指示 + +- **PlanCard**: 更新后的计划卡片 + - 在计划内容之前展示思考块 + - 自动检测是否有推理内容 + +### 消息处理 + +消息合并逻辑已更新以支持 `reasoning_content` 字段的流式处理: + +```typescript +function mergeTextMessage(message: Message, event: MessageChunkEvent) { + // 处理常规内容 + if (event.data.content) { + message.content += event.data.content; + message.contentChunks.push(event.data.content); + } + + // 处理推理内容 + if (event.data.reasoning_content) { + message.reasoningContent = (message.reasoningContent || "") + event.data.reasoning_content; + message.reasoningContentChunks = message.reasoningContentChunks || []; + message.reasoningContentChunks.push(event.data.reasoning_content); + } +} +``` + +## 使用方法 + +### 启用深度思考模式 + +1. 在聊天界面中,点击"Deep Thinking"按钮 +2. 确保配置了支持推理的模型 +3. 发送消息后,如果有推理内容,会在计划卡片上方显示思考块 + +### 查看推理过程 + +1. 深度思考开始时,思考块自动展开显示 +2. 思考阶段使用 primary 主题色,突出显示正在进行的推理过程 +3. 推理内容支持 Markdown 格式渲染,实时流式更新 +4. 在流式传输过程中会显示加载动画 +5. 当开始接收计划内容时,思考块自动折叠 +6. 计划卡片以优雅的动画效果出现 +7. 计划内容保持流式输出效果,逐步显示标题、思路和步骤 +8. 用户可以随时点击思考块标题栏手动展开/折叠 + +## 样式特性 + +- **统一设计语言**: 与页面整体设计风格保持一致 +- **字体层次**: 使用与 CardTitle 相同的 `font-semibold` 字体权重 +- **圆角设计**: 采用 `rounded-xl` 与其他卡片组件保持一致 +- **间距规范**: 使用标准的 `px-6 py-4` 内边距 +- **动态主题**: 思考阶段使用 primary 色彩系统 +- **图标尺寸**: 18px 图标尺寸,与文字比例协调 +- **状态反馈**: 流式传输时显示加载动画和主题色高亮 +- **交互反馈**: 标准的 hover 和 focus 状态 +- **平滑过渡**: 所有状态变化都有平滑的过渡动画 + +## 测试数据 + +可以使用 `/mock/reasoning-example.txt` 文件测试思考块功能,该文件包含了模拟的推理内容和计划数据。 + +## 兼容性 + +- 向后兼容:没有推理内容的消息不会显示思考块 +- 渐进增强:功能仅在有推理内容时激活 +- 优雅降级:如果推理内容为空,组件不会渲染 diff --git a/web/public/mock/reasoning-example.txt b/web/public/mock/reasoning-example.txt new file mode 100644 index 0000000..cdc5ac2 --- /dev/null +++ b/web/public/mock/reasoning-example.txt @@ -0,0 +1,93 @@ +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "我需要仔细分析用户的问题。用户想了解什么是vibe coding。这是一个相对较新的概念,我需要收集相关信息来提供全面的答案。"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "\n\n首先,我应该理解vibe coding的基本定义和概念。这可能涉及编程文化、开发方法论或者特定的编程风格。"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "\n\n然后,我需要研究它的起源、核心理念,以及在实际开发中的应用。这将帮助我提供一个全面而准确的答案。"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "\n\n让我思考一下需要收集哪些具体信息:\n1. Vibe coding的定义和起源\n2. 核心理念和哲学\n3. 实际应用场景和案例\n4. 与传统编程方法的区别\n5. 社区和工具支持"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "\n\n基于这些思考,我认为需要进行深入的研究来收集足够的信息。现在我将制定一个详细的研究计划。"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "{"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"locale\": \"zh-CN\","} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"has_enough_context\": false,"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"thought\": \"用户想了解vibe coding的概念。"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "由于目前没有足够的信息来全面回答这个问题,"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "我需要收集更多相关数据。\","} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"title\": \"Vibe Coding 概念研究\","} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"steps\": ["} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n {"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"need_search\": true,"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"title\": \"Vibe Coding 基本定义和概念\","} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"description\": \"收集关于vibe coding的基本定义、"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "起源、核心概念和目标的信息。"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "查找官方定义、行业专家的解释以及相关的编程文化背景。\","} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"step_type\": \"research\""} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n },"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n {"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"need_search\": true,"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"title\": \"实际应用案例和最佳实践\","} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"description\": \"研究vibe coding在实际项目中的应用案例,"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "了解最佳实践和常见的实现方法。\","} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"step_type\": \"research\""} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n }"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n ]"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n}"} + +event: message_chunk +data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "finish_reason": "stop"} + diff --git a/web/src/app/chat/components/message-list-view.tsx b/web/src/app/chat/components/message-list-view.tsx index 8a44638..0a516d2 100644 --- a/web/src/app/chat/components/message-list-view.tsx +++ b/web/src/app/chat/components/message-list-view.tsx @@ -3,8 +3,8 @@ import { LoadingOutlined } from "@ant-design/icons"; import { motion } from "framer-motion"; -import { Download, Headphones } from "lucide-react"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { Download, Headphones, ChevronDown, ChevronRight, Brain } from "lucide-react"; +import React, { useCallback, useMemo, useRef, useState } from "react"; import { LoadingAnimation } from "~/components/deer-flow/loading-animation"; import { Markdown } from "~/components/deer-flow/markdown"; @@ -23,6 +23,7 @@ import { CardHeader, CardTitle, } from "~/components/ui/card"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/components/ui/collapsible"; import type { Message, Option } from "~/core/messages"; import { closeResearch, @@ -294,6 +295,100 @@ function ResearchCard({ ); } +function ThoughtBlock({ + className, + content, + isStreaming, + hasMainContent, +}: { + className?: string; + content: string; + isStreaming?: boolean; + hasMainContent?: boolean; +}) { + const [isOpen, setIsOpen] = useState(true); // 初始状态为展开 + + // 当开始有主要内容时,自动折叠思考块 + const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false); + + React.useEffect(() => { + if (hasMainContent && !hasAutoCollapsed) { + setIsOpen(false); + setHasAutoCollapsed(true); + } + }, [hasMainContent, hasAutoCollapsed]); + + if (!content || content.trim() === "") { + return null; + } + + return ( +
+ + + + + + + + + {content} + + + + + +
+ ); +} + const GREETINGS = ["Cool", "Sounds great", "Looks good", "Great", "Awesome"]; function PlanCard({ className, @@ -320,6 +415,15 @@ function PlanCard({ }>(() => { return parseJSON(message.content ?? "", {}); }, [message.content]); + + const reasoningContent = message.reasoningContent; + const hasMainContent = Boolean(message.content && message.content.trim() !== ""); + + // 判断是否正在思考:有推理内容但还没有主要内容 + const isThinking = Boolean(reasoningContent && !hasMainContent); + + // 判断是否应该显示计划:有主要内容就显示(无论是否还在流式传输) + const shouldShowPlan = hasMainContent; const handleAccept = useCallback(async () => { if (onSendMessage) { onSendMessage( @@ -331,20 +435,34 @@ function PlanCard({ } }, [onSendMessage]); return ( - - - - - {`### ${ - plan.title !== undefined && plan.title !== "" - ? plan.title - : "Deep Research" - }`} - - - - - +
+ {reasoningContent && ( + + )} + {shouldShowPlan && ( + + + + + + {`### ${ + plan.title !== undefined && plan.title !== "" + ? plan.title + : "Deep Research" + }`} + + + + + {plan.thought} {plan.steps && ( @@ -352,10 +470,10 @@ function PlanCard({ {plan.steps.map((step, i) => (
  • - {step.title} + {step.title}

    - {step.description} + {step.description}
  • ))} @@ -390,8 +508,11 @@ function PlanCard({ ))}
    )} - - + + + + )} +
    ); } diff --git a/web/src/core/api/types.ts b/web/src/core/api/types.ts index 5386a45..bb69572 100644 --- a/web/src/core/api/types.ts +++ b/web/src/core/api/types.ts @@ -38,6 +38,7 @@ export interface MessageChunkEvent "message_chunk", { content?: string; + reasoning_content?: string; } > {} diff --git a/web/src/core/messages/merge-message.ts b/web/src/core/messages/merge-message.ts index 2d9cf71..6a4cabf 100644 --- a/web/src/core/messages/merge-message.ts +++ b/web/src/core/messages/merge-message.ts @@ -43,6 +43,11 @@ function mergeTextMessage(message: Message, event: MessageChunkEvent) { message.content += event.data.content; message.contentChunks.push(event.data.content); } + if (event.data.reasoning_content) { + message.reasoningContent = (message.reasoningContent ?? "") + event.data.reasoning_content; + message.reasoningContentChunks = message.reasoningContentChunks ?? []; + message.reasoningContentChunks.push(event.data.reasoning_content); + } } function mergeToolCallMessage( diff --git a/web/src/core/messages/types.ts b/web/src/core/messages/types.ts index 6b23247..c4dd9ff 100644 --- a/web/src/core/messages/types.ts +++ b/web/src/core/messages/types.ts @@ -17,6 +17,8 @@ export interface Message { isStreaming?: boolean; content: string; contentChunks: string[]; + reasoningContent?: string; + reasoningContentChunks?: string[]; toolCalls?: ToolCallRuntime[]; options?: Option[]; finishReason?: "stop" | "interrupt" | "tool_calls"; diff --git a/web/src/core/store/store.ts b/web/src/core/store/store.ts index 0b349f2..b052f4b 100644 --- a/web/src/core/store/store.ts +++ b/web/src/core/store/store.ts @@ -133,6 +133,8 @@ export async function sendMessage( role: data.role, content: "", contentChunks: [], + reasoningContent: "", + reasoningContentChunks: [], isStreaming: true, interruptFeedback, }; @@ -297,6 +299,8 @@ export async function listenToPodcast(researchId: string) { agent: "podcast", content: JSON.stringify(podcastObject), contentChunks: [], + reasoningContent: "", + reasoningContentChunks: [], isStreaming: true, }; appendMessage(podcastMessage);