Compare commits

...

2 Commits

Author SHA1 Message Date
7125753ca2 feat: 重构AI消息气泡结构,集成thinking、tool和content为统一block
- AI消息使用统一的可折叠容器(message-block)
- thinking和tool使用相同样式,支持展开/收起
- 工具调用显示input/output内容,区分入参和出参
- 添加状态徽章样式(running/completed/error + 脉冲动画)
- 消息气泡添加毛玻璃效果和精细阴影
- 删除file场景(分析上传的报表)
- 同步更新main specs
2026-04-19 16:34:50 +08:00
9dd2d4a1fc feat: 扩展工作空间侧边栏宽度约束并统一拖动手柄样式
- 侧边栏最大宽度从 800px 扩展至 1200px(含动态计算)
- 文件树最小宽度从 180px 提升至 300px
- 文件树最大宽度扩展为侧边栏宽度
- 预览关闭时文件树自动占满侧边栏,分隔线隐藏
- 外部拖动手柄改为与内部分隔线一致的样式(1px、hover 变色)
2026-04-19 15:19:26 +08:00
6 changed files with 335 additions and 162 deletions

View File

@@ -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

View File

@@ -82,7 +82,8 @@ function WorkspaceSidebar({ isOpen, onClose }) {
if (isResizingSidebarRef.current) {
setSidebarWidth(prev => {
const newWidth = prev - deltaX; // 注意:向左拖动是负数
return Math.max(400, Math.min(800, newWidth));
const maxWidth = Math.min(1200, window.innerWidth - 480);
return Math.max(400, Math.min(maxWidth, newWidth));
});
}
@@ -90,7 +91,9 @@ function WorkspaceSidebar({ isOpen, onClose }) {
if (isResizingTreeRef.current) {
setFileTreeWidth(prev => {
const newWidth = prev + deltaX;
return Math.max(180, Math.min(300, newWidth));
const minWidth = 300;
const maxWidth = previewFile ? sidebarWidth - 32 : sidebarWidth;
return Math.max(minWidth, Math.min(maxWidth, newWidth));
});
}
});
@@ -368,7 +371,10 @@ function WorkspaceSidebar({ isOpen, onClose }) {
{/* FileTree */}
<div
className="workspace-sidebar__file-tree"
style={{ width: `${fileTreeWidth}px` }}
style={{
width: previewFile ? `${fileTreeWidth}px` : 'auto',
flex: previewFile ? 'none' : '1'
}}
>
<FileTree
onFileClick={handleFileClick}
@@ -377,10 +383,12 @@ function WorkspaceSidebar({ isOpen, onClose }) {
</div>
{/* 分隔线(可拖动调整) */}
<div
className="workspace-sidebar__divider"
onMouseDown={handleDividerMouseDown}
/>
{previewFile && (
<div
className="workspace-sidebar__divider"
onMouseDown={handleDividerMouseDown}
/>
)}
{/* Preview Panel */}
{previewFile && (
@@ -397,13 +405,7 @@ function WorkspaceSidebar({ isOpen, onClose }) {
<div
className="workspace-sidebar__resize-handle"
onMouseDown={handleSidebarMouseDown}
>
<div className="workspace-sidebar__resize-lines">
<div></div>
<div></div>
<div></div>
</div>
</div>
/>
</div>
{/* 右键菜单 */}

View File

@@ -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() {
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
<div class="message-block expanded">
<div class="message-block-header">
<span class="message-block-header-icon">▶</span>
<span class="message-block-header-title">已深度思考</span>
<span class="message-block-header-status">2.1s</span>
</div>
<div class="message-thinking-content">
<div class="message-block-content">
<p>让我分析一下代码重构的思路:</p>
<ul>
<li>首先识别代码中的重复模式和冗余逻辑</li>
@@ -90,6 +90,7 @@ export function getChatScenes() {
</ul>
</div>
</div>
<div class="message-bubble">
<p>好的,我来帮你重构这段代码。首先让我分析一下现有代码的问题,然后提供优化方案。</p>
<p style="margin-top: 12px;"><strong>优化建议:</strong></p>
@@ -114,23 +115,13 @@ export function getChatScenes() {
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-bubble">
<p style="display: flex; align-items: center; gap: 8px; color: #3B82F6;">
🧩 已加载CRM客户查询技能
</p>
</div>
<div class="message-time">10:15</div>
</div>
</div>
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
<div class="message-block expanded">
<div class="message-block-header">
<span class="message-block-header-icon">▶</span>
<span class="message-block-header-title">已深度思考</span>
<span class="message-block-header-status">1.8s</span>
</div>
<div class="message-thinking-content">
<div class="message-block-content">
<p>正在调用CRM客户查询技能...</p>
<ul>
<li>识别用户意图:查询客户"张三"的订单信息</li>
@@ -141,6 +132,56 @@ export function getChatScenes() {
</ul>
</div>
</div>
<div class="message-block message-tool">
<div class="message-block-header">
<span class="message-block-header-icon">▶</span>
<span class="message-block-header-title">🧩 使用工具: CRM客户查询</span>
<span class="message-block-header-status message-tool-completed">✓ 0.4s</span>
</div>
<div class="message-block-content">
<div class="message-tool-io">
<div class="message-tool-io-label">INPUT</div>
<div class="message-tool-io-content"><pre>{ "name": "张三", "timeRange": "1year" }</pre></div>
</div>
<div class="message-tool-io">
<div class="message-tool-io-label">OUTPUT</div>
<div class="message-tool-io-content"><pre>{ "customerId": "C001", "orders": 3, "totalAmount": 28560 }</pre></div>
</div>
</div>
</div>
<div class="message-block message-tool">
<div class="message-block-header">
<span class="message-block-header-icon">▶</span>
<span class="message-block-header-title">🧩 使用工具: 订单数据分析</span>
<span class="message-block-header-status message-tool-completed">✓ 0.3s</span>
</div>
<div class="message-block-content">
<div class="message-tool-io">
<div class="message-tool-io-label">INPUT</div>
<div class="message-tool-io-content"><pre>{ "orders": [...], "timeRange": "1year" }</pre></div>
</div>
<div class="message-tool-io">
<div class="message-tool-io-label">OUTPUT</div>
<div class="message-tool-io-content"><pre>{ "totalAmount": 28560, "orderCount": 3, "lastOrderDate": "2026-03-10" }</pre></div>
</div>
</div>
</div>
<div class="message-block expanded">
<div class="message-block-header">
<span class="message-block-header-icon">▶</span>
<span class="message-block-header-title">已深度思考</span>
<span class="message-block-header-status">0.6s</span>
</div>
<div class="message-block-content">
<p>根据分析结果,用户可能还想知道:</p>
<ul>
<li>订单详情列表</li>
<li>客户等级权益</li>
</ul>
</div>
</div>
<div class="message-bubble">
<p><strong>客户信息查询结果:</strong></p>
<div style="margin-top: 12px; padding: 12px; background: #F8FAFC; border-radius: 8px;">
@@ -154,70 +195,6 @@ export function getChatScenes() {
</div>
</div>
`,
file: `
<div class="message user">
<div class="message-avatar user">张</div>
<div class="message-content">
<div class="message-bubble">
<p>帮我分析一下这个 Excel 文件里的销售数据</p>
<div style="margin-top: 12px; padding: 10px 14px; background: #F8FAFC; border-radius: 8px; display: inline-flex; align-items: center; gap: 10px; color: #475569;">
<span style="font-size: 20px;">📊</span>
<div>
<div style="font-weight: 600;">Q1销售数据.xlsx</div>
<div style="font-size: 12px; color: #94A3B8;">2.4 MB</div>
</div>
</div>
</div>
<div class="message-time">16:20</div>
</div>
</div>
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-bubble">
<p>文件已接收!正在分析数据...</p>
<div style="margin-top: 14px;">
<div style="height: 6px; background: #E2E8F0; border-radius: 3px; overflow: hidden;">
<div style="width: 65%; height: 100%; background: linear-gradient(90deg, #3B82F6, #8B5CF6); border-radius: 3px;"></div>
</div>
</div>
</div>
<div class="message-time">16:20</div>
</div>
</div>
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
</div>
<div class="message-thinking-content">
<p>正在分析 Excel 文件中的销售数据...</p>
<ul>
<li>文件格式识别Excel (.xlsx),大小 2.4MB</li>
<li>数据结构解析:包含日期、产品、销售额等字段</li>
<li>时间范围2026年 Q11-3月</li>
<li>计算指标:总销售额、同比增长、月度趋势、产品占比</li>
<li>异常检测:无异常值或缺失数据</li>
<li>关键洞察提取3月表现突出产品C增长势头强劲</li>
</ul>
</div>
</div>
<div class="message-bubble">
<p><strong>数据分析完成!以下是关键发现:</strong></p>
<ul style="margin-top: 12px; padding-left: 20px;">
<li>Q1 总销售额:<strong>¥128.5 万</strong>,同比增长 18%</li>
<li>3 月份表现最佳,单月突破 50 万</li>
<li>产品 A 占比最高42%),产品 C 增长最快</li>
</ul>
<p style="margin-top: 12px;">需要生成可视化图表吗?</p>
</div>
<div class="message-time">16:22</div>
</div>
</div>
`,
code: `
<div class="message user">
<div class="message-avatar user">张</div>
@@ -229,12 +206,13 @@ export function getChatScenes() {
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
<div class="message-block expanded">
<div class="message-block-header">
<span class="message-block-header-icon">▶</span>
<span class="message-block-header-title">已深度思考</span>
<span class="message-block-header-status">0.8s</span>
</div>
<div class="message-thinking-content">
<div class="message-block-content">
<p>分析需求:列表去重并保持顺序</p>
<ul>
<li>使用字典键的有序性Python 3.7+</li>
@@ -243,6 +221,7 @@ export function getChatScenes() {
</ul>
</div>
</div>
<div class="message-bubble">
<p>好的,这是一个常用的工具函数。以下提供两种实现方式:</p>
<pre style="margin-top: 12px; padding: 16px; background: #1E293B; border-radius: 8px; overflow-x: auto;"><code style="color: #E2E8F0; font-family: 'Fira Code', monospace; font-size: 13px; line-height: 1.6;"><span style="color: #60A5FA;">def</span> <span style="color: #34D399;">dedupe_ordered</span>(lst):
@@ -274,12 +253,13 @@ data = [<span style="color: #A78BFA;">1</span>, <span style="color: #A78BFA;">2<
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
<div class="message-block expanded">
<div class="message-block-header">
<span class="message-block-header-icon">▶</span>
<span class="message-block-header-title">已深度思考</span>
<span class="message-block-header-status">1.2s</span>
</div>
<div class="message-thinking-content">
<div class="message-block-content">
<p>正在查询销售数据...</p>
<ul>
<li>时间范围2026年3月1日 - 3月19日</li>
@@ -288,6 +268,7 @@ data = [<span style="color: #A78BFA;">1</span>, <span style="color: #A78BFA;">2<
</ul>
</div>
</div>
<div class="message-bubble">
<p><strong>本月各部门销售业绩汇总:</strong></p>
<table class="msg-table">
@@ -423,12 +404,13 @@ data = [<span style="color: #A78BFA;">1</span>, <span style="color: #A78BFA;">2<
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
<div class="message-block expanded">
<div class="message-block-header">
<span class="message-block-header-icon">▶</span>
<span class="message-block-header-title">已深度思考</span>
<span class="message-block-header-status">30.2s</span>
</div>
<div class="message-thinking-content">
<div class="message-block-content">
<p>正在连接日志数据库...</p>
<ul>
<li>目标数据库log-db-prod-01</li>
@@ -437,6 +419,7 @@ data = [<span style="color: #A78BFA;">1</span>, <span style="color: #A78BFA;">2<
</ul>
</div>
</div>
<div class="message-bubble" style="border-left: 3px solid #EF4444; background: #FEF2F2;">
<p style="display: flex; align-items: center; gap: 8px; color: #EF4444; font-weight: 600;">
❌ 请求失败

View File

@@ -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);
});
};

View File

@@ -6,7 +6,7 @@
.workspace-sidebar {
width: 500px;
min-width: 400px;
max-width: 800px;
max-width: 1200px;
background: var(--color-bg-2);
border-left: 1px solid var(--color-border-2);
display: flex;
@@ -131,8 +131,8 @@
// ========== FileTree ==========
&__file-tree {
min-width: 180px;
max-width: 300px;
min-width: 300px;
max-width: 100%;
flex-shrink: 0;
overflow-y: auto;
padding: 8px;
@@ -177,39 +177,28 @@
&__resize-handle {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
top: 0;
bottom: 0;
width: 1px;
background: var(--color-border-2);
cursor: ew-resize;
opacity: 0.5;
transition: opacity 0.2s;
transition: background 0.2s;
z-index: 10;
&::before {
content: '';
position: absolute;
top: 0;
left: -4px;
right: -4px;
bottom: 0;
cursor: ew-resize;
}
&:hover {
opacity: 1;
background: var(--color-primary);
}
}
&__resize-lines {
display: flex;
flex-direction: column;
gap: 2px;
div {
width: 4px;
height: 2px;
background: var(--color-text-3);
border-radius: 1px;
}
}
&__resize-handle:hover &__resize-lines div {
background: var(--color-text-1);
}
}
// 工作空间展开按钮

View File

@@ -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;