From 7125753ca21e64d01dbb65a90929e92c542b8186 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Sun, 19 Apr 2026 16:34:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84AI=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=B0=94=E6=B3=A1=E7=BB=93=E6=9E=84=EF=BC=8C=E9=9B=86=E6=88=90?= =?UTF-8?q?thinking=E3=80=81tool=E5=92=8Ccontent=E4=B8=BA=E7=BB=9F?= =?UTF-8?q?=E4=B8=80block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AI消息使用统一的可折叠容器(message-block) - thinking和tool使用相同样式,支持展开/收起 - 工具调用显示input/output内容,区分入参和出参 - 添加状态徽章样式(running/completed/error + 脉冲动画) - 消息气泡添加毛玻璃效果和精细阴影 - 删除file场景(分析上传的报表) - 同步更新main specs --- openspec/specs/ai-message-blocks/spec.md | 89 +++++++++++ src/data/conversations.js | 185 ++++++++++------------- src/pages/console/ChatPage.jsx | 10 +- src/styles/pages/_console.scss | 134 ++++++++++++++-- 4 files changed, 300 insertions(+), 118 deletions(-) create mode 100644 openspec/specs/ai-message-blocks/spec.md diff --git a/openspec/specs/ai-message-blocks/spec.md b/openspec/specs/ai-message-blocks/spec.md new file mode 100644 index 0000000..9cc99e8 --- /dev/null +++ b/openspec/specs/ai-message-blocks/spec.md @@ -0,0 +1,89 @@ +## Purpose + +定义AI消息中思考过程、工具调用和对话正文的多Block结构规范。 + +## ADDED Requirements + +### Requirement: AI消息Block结构 +AI消息气泡内 SHALL 支持多个Block的灵活组织,Block类型包括thinking、tool、content三种。 + +#### Scenario: 单个Thinking Block +- **WHEN** AI消息包含thinking过程 +- **THEN** 在消息气泡内显示thinking Block,包含标题、执行时间、可展开的详细内容 + +#### Scenario: 单个Tool Block +- **WHEN** AI消息包含工具调用 +- **THEN** 在消息气泡内显示tool Block,包含工具名称、状态(input/output) + +#### Scenario: 多个Block组合 +- **WHEN** AI消息包含多个thinking和tool +- **THEN** 按声明顺序依次显示,最后显示content +- **AND** 所有thinking和tool必须在content之前 + +### Requirement: 可折叠容器 +Thinking和Tool Block SHALL 使用统一的可折叠容器样式。 + +#### Scenario: 展开状态 +- **WHEN** 用户点击Block头部 +- **THEN** 显示详细内容区域 + +#### Scenario: 收起状态 +- **WHEN** Block初始状态为收起 +- **THEN** 只显示头部标题和状态,点击后展开 + +#### Scenario: Thinking默认展开 +- **WHEN** 渲染thinking Block +- **THEN** 默认展开显示内容 + +#### Scenario: Tool默认收起 +- **WHEN** 渲染tool Block +- **THEN** 默认收起,只显示头部 + +### Requirement: 工具调用显示 +Tool Block SHALL 清晰展示input和output内容。 + +#### Scenario: 显示输入参数 +- **WHEN** tool Block展开 +- **THEN** 显示INPUT区域,包含JSON格式化的调用参数 + +#### Scenario: 显示返回结果 +- **WHEN** tool执行完成 +- **THEN** 显示OUTPUT区域,包含JSON格式化的返回值 + +#### Scenario: 执行状态 +- **WHEN** 渲染tool Block +- **THEN** 显示执行状态:running(运行中)/completed(完成)/error(失败) +- **AND** running状态带脉冲动画 + +### Requirement: 时间显示 +时间显示在Block头部的status区域中。 + +#### Scenario: Block执行时间 +- **WHEN** 渲染thinking或tool Block +- **THEN** 在头部status区域显示执行耗时,如"2.1s" + +#### Scenario: Content时间 +- **WHEN** 渲染content Block +- **THEN** 在Block底部显示消息发送时间,如"14:31" + +### Requirement: 消息气泡样式 +AI消息气泡 SHALL 使用毛玻璃效果和精细阴影。 + +#### Scenario: 毛玻璃效果 +- **WHEN** 渲染AI消息气泡 +- **THEN** 使用backdrop-filter: blur(12px)实现毛玻璃效果 + +#### Scenario: 精细阴影 +- **WHEN** 渲染AI消息气泡 +- **THEN** 使用多层阴影实现精细立体效果 + +### Requirement: 场景数据 +消息场景数据 SHALL 符合新的Block结构。 + +#### Scenario: 删除File场景 +- **WHEN** 系统加载场景列表 +- **THEN** 不包含file场景 + +#### Scenario: 场景消息结构 +- **WHEN** 渲染场景消息 +- **THEN** 使用blocks结构组织thinking、tool、content \ No newline at end of file diff --git a/src/data/conversations.js b/src/data/conversations.js index 4408164..0f4c9a5 100644 --- a/src/data/conversations.js +++ b/src/data/conversations.js @@ -4,7 +4,6 @@ export const conversations = [ { id: 'welcome', title: '新对话', time: '欢迎页', scene: 'welcome', status: 'running' }, { id: 'text', title: '代码重构方案讨论', time: '普通对话', scene: 'text', status: 'running' }, { id: 'skill', title: '查询客户数据', time: '调用 Skill', scene: 'skill', status: 'running' }, - { id: 'file', title: '分析上传的报表', time: '上传文件', scene: 'file', status: 'running' }, { id: 'code', title: '生成 Python 函数', time: '代码展示', scene: 'code', status: 'running' }, { id: 'table', title: '查询销售报表', time: '表格数据', scene: 'table', status: 'running' }, { id: 'multiTurn', title: '产品方案讨论', time: '多轮对话', scene: 'multiTurn', status: 'running' }, @@ -74,12 +73,13 @@ export function getChatScenes() {
🤖
-
-
- - 已深度思考 +
+
+ + 已深度思考 + 2.1s
-
+

让我分析一下代码重构的思路:

  • 首先识别代码中的重复模式和冗余逻辑
  • @@ -90,6 +90,7 @@ export function getChatScenes() {
+

好的,我来帮你重构这段代码。首先让我分析一下现有代码的问题,然后提供优化方案。

优化建议:

@@ -114,23 +115,13 @@ export function getChatScenes() {
🤖
-
-

- 🧩 已加载CRM客户查询技能 -

-
-
10:15
-
-
-
-
🤖
-
-
-
- - 已深度思考 +
+
+ + 已深度思考 + 1.8s
-
+

正在调用CRM客户查询技能...

  • 识别用户意图:查询客户"张三"的订单信息
  • @@ -141,6 +132,56 @@ export function getChatScenes() {
+ +
+
+ + 🧩 使用工具: CRM客户查询 + ✓ 0.4s +
+
+
+
INPUT
+
{ "name": "张三", "timeRange": "1year" }
+
+
+
OUTPUT
+
{ "customerId": "C001", "orders": 3, "totalAmount": 28560 }
+
+
+
+
+
+ + 🧩 使用工具: 订单数据分析 + ✓ 0.3s +
+
+
+
INPUT
+
{ "orders": [...], "timeRange": "1year" }
+
+
+
OUTPUT
+
{ "totalAmount": 28560, "orderCount": 3, "lastOrderDate": "2026-03-10" }
+
+
+
+
+
+ + 已深度思考 + 0.6s +
+
+

根据分析结果,用户可能还想知道:

+
    +
  • 订单详情列表
  • +
  • 客户等级权益
  • +
+
+
+

客户信息查询结果:

@@ -154,70 +195,6 @@ export function getChatScenes() {
`, - file: ` -
-
-
-
-

帮我分析一下这个 Excel 文件里的销售数据

-
- 📊 -
-
Q1销售数据.xlsx
-
2.4 MB
-
-
-
-
16:20
-
-
-
-
🤖
-
-
-

文件已接收!正在分析数据...

-
-
-
-
-
-
-
16:20
-
-
-
-
🤖
-
-
-
- - 已深度思考 -
-
-

正在分析 Excel 文件中的销售数据...

-
    -
  • 文件格式识别:Excel (.xlsx),大小 2.4MB
  • -
  • 数据结构解析:包含日期、产品、销售额等字段
  • -
  • 时间范围:2026年 Q1(1-3月)
  • -
  • 计算指标:总销售额、同比增长、月度趋势、产品占比
  • -
  • 异常检测:无异常值或缺失数据
  • -
  • 关键洞察提取:3月表现突出,产品C增长势头强劲
  • -
-
-
-
-

数据分析完成!以下是关键发现:

-
    -
  • Q1 总销售额:¥128.5 万,同比增长 18%
  • -
  • 3 月份表现最佳,单月突破 50 万
  • -
  • 产品 A 占比最高(42%),产品 C 增长最快
  • -
-

需要生成可视化图表吗?

-
-
16:22
-
-
- `, code: `
@@ -229,12 +206,13 @@ export function getChatScenes() {
🤖
-
-
- - 已深度思考 +
+
+ + 已深度思考 + 0.8s
-
+

分析需求:列表去重并保持顺序

  • 使用字典键的有序性(Python 3.7+)
  • @@ -243,6 +221,7 @@ export function getChatScenes() {
+

好的,这是一个常用的工具函数。以下提供两种实现方式:

def dedupe_ordered(lst):
@@ -274,12 +253,13 @@ data = [1, 2<
             
🤖
-
-
- - 已深度思考 +
+
+ + 已深度思考 + 1.2s
-
+

正在查询销售数据...

  • 时间范围:2026年3月1日 - 3月19日
  • @@ -288,6 +268,7 @@ data = [1, 2<
+

本月各部门销售业绩汇总:

@@ -423,12 +404,13 @@ data = [1, 2<
🤖
-
-
- - 已深度思考 +
+
+ + 已深度思考 + 30.2s
-
+

正在连接日志数据库...

  • 目标数据库:log-db-prod-01
  • @@ -437,6 +419,7 @@ data = [1, 2<
+

❌ 请求失败 diff --git a/src/pages/console/ChatPage.jsx b/src/pages/console/ChatPage.jsx index 55fc3da..2ec00e5 100644 --- a/src/pages/console/ChatPage.jsx +++ b/src/pages/console/ChatPage.jsx @@ -155,20 +155,20 @@ function ChatPage() { useEffect(() => { if (!chatMessagesRef.current) return; - const thinkingElements = chatMessagesRef.current.querySelectorAll('.message-thinking'); + const blockElements = chatMessagesRef.current.querySelectorAll('.message-block'); const handleClick = (event) => { - const thinkingElement = event.currentTarget; - thinkingElement.classList.toggle('expanded'); + const blockElement = event.currentTarget; + blockElement.classList.toggle('expanded'); }; - thinkingElements.forEach(el => { + blockElements.forEach(el => { el.addEventListener('click', handleClick); el.style.cursor = 'pointer'; }); return () => { - thinkingElements.forEach(el => { + blockElements.forEach(el => { el.removeEventListener('click', handleClick); }); }; diff --git a/src/styles/pages/_console.scss b/src/styles/pages/_console.scss index b518b63..101d8ef 100644 --- a/src/styles/pages/_console.scss +++ b/src/styles/pages/_console.scss @@ -105,7 +105,13 @@ } .message.assistant .message-bubble { - background: var(--color-bg-2); + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.6); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.04), + 0 4px 12px rgba(0, 0, 0, 0.06); border-bottom-left-radius: 4px; } @@ -122,17 +128,27 @@ padding: 0 4px; } -// AI 思考过程 -.message-thinking { +// AI 消息Block容器(thinking和tool通用) +.message-block { margin-bottom: 12px; border: 1px solid var(--color-border-3); border-radius: 12px; overflow: hidden; background: #FFFBEB; cursor: pointer; + + &.expanded { + .message-block-header-icon { + transform: rotate(90deg); + } + + .message-block-content { + display: block; + } + } } -.message-thinking-header { +.message-block-header { display: flex; align-items: center; gap: 8px; @@ -150,26 +166,31 @@ } } -.message-thinking-icon { +.message-block-header-icon { transition: transform 0.2s ease; font-size: 12px; + flex-shrink: 0; } -.message-thinking.expanded .message-thinking-icon { - transform: rotate(90deg); +.message-block-header-title { + flex: 1; } -.message-thinking-content { +.message-block-header-status { + font-size: 12px; + color: var(--color-text-3); + display: flex; + align-items: center; + gap: 4px; +} + +.message-block-content { padding: 12px 14px; font-size: 14px; line-height: 1.6; color: #78350F; display: none; - .message-thinking.expanded & { - display: block; - } - ul { margin: 8px 0 0 0; padding-left: 20px; @@ -180,6 +201,95 @@ } } +.message-block-time { + font-size: 12px; + color: var(--color-text-4); + padding: 8px 14px 12px; + border-top: 1px solid rgba(0, 0, 0, 0.05); +} + +// 工具调用Block +.message-tool { + background: #F0F9FF; + border-color: #BFDBFE; + + .message-block-header { + color: #1E40AF; + background: rgba(219, 234, 254, 0.5); + } + + .message-block-header-status { + color: #1D4ED8; + } +} + +.message-tool-running { + color: #3B82F6; + + &::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; + animation: pulse 1.5s ease-in-out infinite; + } +} + +.message-tool-completed { + color: #10B981; +} + +.message-tool-error { + color: #EF4444; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } +} + +// 工具调用的input/output分隔 +.message-tool-io { + border-top: 1px solid rgba(0, 0, 0, 0.05); + padding-top: 12px; + margin-top: 12px; + + &:first-child { + border-top: none; + padding-top: 0; + margin-top: 0; + } +} + +.message-tool-io-label { + font-size: 12px; + font-weight: 600; + color: #64748B; + margin-bottom: 8px; +} + +.message-tool-io-content { + background: #1E293B; + border-radius: 8px; + padding: 12px; + overflow-x: auto; + + pre, code { + font-family: 'Fira Code', monospace; + font-size: 13px; + color: #E2E8F0; + margin: 0; + white-space: pre-wrap; + word-break: break-all; + } +} + // 输入区 .chat-input-wrapper { padding: 16px 24px 24px;