@tfdesign/b-end 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_READ_FIRST.md +131 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/package.json +67 -0
- package/scripts/check-tfds-contract.mjs +334 -0
- package/scripts/check-tfds-integration.mjs +263 -0
- package/scripts/postinstall-cursor-skill.mjs +382 -0
- package/scripts/setup.mjs +520 -0
- package/skills/tfds/CHECKLIST.md +205 -0
- package/skills/tfds/COMMON_FAILURES.md +238 -0
- package/skills/tfds/DESIGN_PRINCIPLES.md +477 -0
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +636 -0
- package/skills/tfds/LAYOUT_RECIPES.md +140 -0
- package/skills/tfds/LAYOUT_RULES.md +1355 -0
- package/skills/tfds/PAGE_ARCHETYPES.md +201 -0
- package/skills/tfds/SKILL.md +188 -0
- package/skills/tfds/components.index.json +7305 -0
- package/skills/tfds/components.summary.json +1809 -0
- package/src/_b_end_runtime/components/AiSuggestionShared.jsx +166 -0
- package/src/_b_end_runtime/components/Avatar.jsx +325 -0
- package/src/_b_end_runtime/components/Avatar.tokens.js +76 -0
- package/src/_b_end_runtime/components/AvatarGridPreview.jsx +56 -0
- package/src/_b_end_runtime/components/AvatarGroup.jsx +80 -0
- package/src/_b_end_runtime/components/AvatarGroup.tokens.js +28 -0
- package/src/_b_end_runtime/components/Button.jsx +144 -0
- package/src/_b_end_runtime/components/Button.tokens.js +90 -0
- package/src/_b_end_runtime/components/Card.jsx +460 -0
- package/src/_b_end_runtime/components/Card.tokens.js +124 -0
- package/src/_b_end_runtime/components/CardPreview.jsx +51 -0
- package/src/_b_end_runtime/components/ChatBubble.jsx +384 -0
- package/src/_b_end_runtime/components/ChatBubble.tokens.js +60 -0
- package/src/_b_end_runtime/components/ChatBubblePreview.jsx +129 -0
- package/src/_b_end_runtime/components/ChatInput.jsx +1399 -0
- package/src/_b_end_runtime/components/ChatInput.tokens.js +75 -0
- package/src/_b_end_runtime/components/ChatMessage.jsx +2215 -0
- package/src/_b_end_runtime/components/ChatMessage.tokens.js +257 -0
- package/src/_b_end_runtime/components/ChatMessagePreview.jsx +388 -0
- package/src/_b_end_runtime/components/Checkbox.jsx +317 -0
- package/src/_b_end_runtime/components/Checkbox.tokens.js +59 -0
- package/src/_b_end_runtime/components/ConversationList.jsx +1264 -0
- package/src/_b_end_runtime/components/ConversationList.tokens.js +135 -0
- package/src/_b_end_runtime/components/ConversationListPreview.jsx +108 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +324 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +69 -0
- package/src/_b_end_runtime/components/DatePicker.jsx +739 -0
- package/src/_b_end_runtime/components/DatePicker.tokens.js +99 -0
- package/src/_b_end_runtime/components/Empty.jsx +141 -0
- package/src/_b_end_runtime/components/Empty.tokens.js +40 -0
- package/src/_b_end_runtime/components/Form.jsx +609 -0
- package/src/_b_end_runtime/components/Form.tokens.js +77 -0
- package/src/_b_end_runtime/components/FormFieldStack.jsx +123 -0
- package/src/_b_end_runtime/components/FormFieldStack.tokens.js +12 -0
- package/src/_b_end_runtime/components/FormTitle.jsx +119 -0
- package/src/_b_end_runtime/components/FormTitle.tokens.js +87 -0
- package/src/_b_end_runtime/components/FullScreenPage.jsx +97 -0
- package/src/_b_end_runtime/components/FullScreenPage.tokens.js +19 -0
- package/src/_b_end_runtime/components/Icon.jsx +172 -0
- package/src/_b_end_runtime/components/Icon.tokens.js +26 -0
- package/src/_b_end_runtime/components/IconGridPreview.jsx +277 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +620 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +71 -0
- package/src/_b_end_runtime/components/InfoDisplayPanelPreview.jsx +133 -0
- package/src/_b_end_runtime/components/Input.jsx +258 -0
- package/src/_b_end_runtime/components/Input.tokens.js +68 -0
- package/src/_b_end_runtime/components/InputNumber.jsx +242 -0
- package/src/_b_end_runtime/components/InputNumber.tokens.js +55 -0
- package/src/_b_end_runtime/components/Modal.jsx +155 -0
- package/src/_b_end_runtime/components/Modal.tokens.js +73 -0
- package/src/_b_end_runtime/components/NavBar.jsx +842 -0
- package/src/_b_end_runtime/components/NavBar.tokens.js +97 -0
- package/src/_b_end_runtime/components/NavBarPreview.jsx +11 -0
- package/src/_b_end_runtime/components/Radio.jsx +227 -0
- package/src/_b_end_runtime/components/Radio.tokens.js +59 -0
- package/src/_b_end_runtime/components/Select.jsx +766 -0
- package/src/_b_end_runtime/components/Select.tokens.js +99 -0
- package/src/_b_end_runtime/components/Sheet.jsx +132 -0
- package/src/_b_end_runtime/components/Sheet.tokens.js +61 -0
- package/src/_b_end_runtime/components/Slider.jsx +346 -0
- package/src/_b_end_runtime/components/Slider.tokens.js +47 -0
- package/src/_b_end_runtime/components/Switch.jsx +124 -0
- package/src/_b_end_runtime/components/Switch.tokens.js +38 -0
- package/src/_b_end_runtime/components/Table.jsx +1338 -0
- package/src/_b_end_runtime/components/Table.tokens.js +147 -0
- package/src/_b_end_runtime/components/TablePreview.jsx +599 -0
- package/src/_b_end_runtime/components/Tabs.jsx +149 -0
- package/src/_b_end_runtime/components/Tabs.tokens.js +102 -0
- package/src/_b_end_runtime/components/Tag.jsx +199 -0
- package/src/_b_end_runtime/components/Tag.tokens.js +171 -0
- package/src/_b_end_runtime/components/TagBar.jsx +1134 -0
- package/src/_b_end_runtime/components/TagBar.tokens.js +75 -0
- package/src/_b_end_runtime/components/TagGridPreview.jsx +23 -0
- package/src/_b_end_runtime/components/TagInput.jsx +382 -0
- package/src/_b_end_runtime/components/TagInput.tokens.js +52 -0
- package/src/_b_end_runtime/components/TextArea.jsx +363 -0
- package/src/_b_end_runtime/components/TextArea.tokens.js +65 -0
- package/src/_b_end_runtime/components/TimePicker.jsx +444 -0
- package/src/_b_end_runtime/components/TimePicker.tokens.js +77 -0
- package/src/_b_end_runtime/components/Toast.jsx +120 -0
- package/src/_b_end_runtime/components/Toast.tokens.js +146 -0
- package/src/_b_end_runtime/components/Tooltip.jsx +282 -0
- package/src/_b_end_runtime/components/Tooltip.tokens.js +48 -0
- package/src/_b_end_runtime/components/TooltipPreview.jsx +50 -0
- package/src/_b_end_runtime/components/Upload.jsx +455 -0
- package/src/_b_end_runtime/components/Upload.tokens.js +47 -0
- package/src/_b_end_runtime/components/avatar-assets/avatar-default.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-1.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-2.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-3.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-4.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-5.png +0 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-1.svg +40 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-2.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/construction.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/failure.svg +49 -0
- package/src/_b_end_runtime/components/empty-assets/idle.svg +34 -0
- package/src/_b_end_runtime/components/empty-assets/no-access.svg +36 -0
- package/src/_b_end_runtime/components/empty-assets/no-content.svg +77 -0
- package/src/_b_end_runtime/components/empty-assets/no-result.svg +61 -0
- package/src/_b_end_runtime/components/empty-assets/not-found.svg +46 -0
- package/src/_b_end_runtime/components/empty-assets/success.svg +38 -0
- package/src/_b_end_runtime/components/file-type-assets/batch-report.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/catcat.svg +21 -0
- package/src/_b_end_runtime/components/file-type-assets/code.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/conversation.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/document.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-card.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-sheet.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/image.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/index.js +105 -0
- package/src/_b_end_runtime/components/file-type-assets/knowledge.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pdf.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pe.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/strategy.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/table.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/webpage.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/xmind.png +0 -0
- package/src/_b_end_runtime/components/icons/icon-data.js +12496 -0
- package/src/_b_end_runtime/components/nav-bar-assets/bytehi-logo-mark.svg +21 -0
- package/src/_b_end_runtime/components/table-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-assets/icon-chevron-down.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/checkbox.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon-chevron-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-handle.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-tree-triangle-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/switch.png +0 -0
- package/src/_b_end_runtime/components/tagShared.js +3 -0
- package/src/_b_end_runtime/components/team-avatar-assets/chengcheng-murphy.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/duan-ran.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/guo-zhezhi.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/li-siru.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/liu-delin.png +0 -0
- package/src/_b_end_runtime/components.js +3499 -0
- package/src/_b_end_runtime/index.js +9 -0
- package/src/_b_end_runtime/page-patterns/BasePageFramePattern.jsx +395 -0
- package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +989 -0
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +281 -0
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +380 -0
- package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +392 -0
- package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +590 -0
- package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +237 -0
- package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +189 -0
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +594 -0
- package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +87 -0
- package/src/_b_end_runtime/page-patterns/pageListShared.jsx +177 -0
- package/src/_b_end_runtime/patterns.js +428 -0
- package/src/_b_end_runtime/preview-registry.jsx +4719 -0
- package/src/_b_end_runtime/teamMembers.js +56 -0
- package/src/_b_end_runtime/tokens.js +500 -0
- package/src/index.d.ts +1073 -0
- package/src/index.js +52 -0
- package/theme.css +350 -0
|
@@ -0,0 +1,2215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatMessage — B 端设计系统 · AI 对话页「一条消息」原子单元
|
|
3
|
+
*
|
|
4
|
+
* 行业对齐:assistant-ui Message / Vercel AI SDK Message / Microsoft Copilot ChatMessage。
|
|
5
|
+
* 与 ChatBubble(仅气泡)、ChatInput(输入器)共同组成 Chat 命名空间。
|
|
6
|
+
* 样式依赖:入口 CSS 须 @import "@tf-designsystem/b-end/theme.css"(提供 --color-border-line-light、--color-blueGrey-*、--tfds-ai-execution-* 等);缺失时 var() 无效,追问/执行流等易出现深黑描边。
|
|
7
|
+
* 一个 ChatMessage = 一条会话消息,按 role 渲染:
|
|
8
|
+
* · role='ai' AI 输出消息(默认):header / thinking / plan / 执行流(title+steps/taskGroups)
|
|
9
|
+
* / 结果(resultText+resultArtifacts+confirms) / taskBadge / actions / followUps
|
|
10
|
+
* · role='user' 用户提问气泡:userQuote + userAttachments + userContent + actions
|
|
11
|
+
*
|
|
12
|
+
* status 状态机:
|
|
13
|
+
* · 'requesting' 请求中(仅显示 header + 三圆点 loading)
|
|
14
|
+
* · 'thinking' 深度思考中(仅显示 header + "深度思考中...")
|
|
15
|
+
* · 'processing' 执行中(标题旋转环 + 步骤骨架屏扫光)
|
|
16
|
+
* · 'completed' 已完成(标题对钩 + 完整展示)
|
|
17
|
+
*
|
|
18
|
+
* 命名设计原则(AI 友好):
|
|
19
|
+
* 1. 名字即结构:<ChatMessage header thinking plan ... result actions /> 读完即知 DOM。
|
|
20
|
+
* 2. 同义短词优先:aiHeader→header / thinkingProcess→thinking / taskPlan→plan
|
|
21
|
+
* / humanConfirmNodes→confirms / followUpQuestions→followUps / messageActions→actions
|
|
22
|
+
* / userMessage→userContent。
|
|
23
|
+
* 3. role 决定形态:role 切换时只渲染该角色合法的 part。
|
|
24
|
+
*
|
|
25
|
+
* @prop {'ai'|'user'} [role='ai'] — 消息角色
|
|
26
|
+
* @prop {null|true|object} [header=null] — AI 头像区(原 aiHeader),true 启用默认 OLA AI + catcat 头像,对象自定义 { name, avatarSrc }
|
|
27
|
+
* @prop {null|object} [thinking=null] — 深度思考块(原 thinkingProcess){ state, durationLabel, content, defaultExpanded }
|
|
28
|
+
* @prop {string} [leadText=''] — 引导文案(在 plan / confirms 之上,用于"我开始规划啦..."这类前导提示)
|
|
29
|
+
* @prop {null|object} [plan=null] — 任务规划卡片(原 taskPlan){ tasks, primaryActionLabel, defaultExpanded, taskCountLabel }
|
|
30
|
+
* @prop {null|string[]|object} [followUps=null] — 追问按钮组(原 followUpQuestions),传字符串数组或 { items, onSelect }
|
|
31
|
+
* @prop {null|true|object} [actions=null] — 消息操作组(原 messageActions),true 启用默认四件套,对象自定义 { showCopy/Quote/Like/Dislike, copyCount }
|
|
32
|
+
* @prop {null|string} [taskBadge=null] — 任务进行中徽章文案
|
|
33
|
+
* @prop {Array|null} [userContent=null] — role='user' 富文本(原 userMessage):[{ type:'text', value }] | [{ type:'entity', icon, label, tone }]
|
|
34
|
+
* @prop {Array|null} [userAttachments=null] — role='user' 附件卡数组
|
|
35
|
+
* @prop {object|null} [userQuote=null] — role='user' 紫色划词条
|
|
36
|
+
* @prop {'auto'|'surface'|'fill'} [userBubbleTone='auto'] — role='user' 气泡背景;auto 会根据最近父级背景自动适配:白底用 fill 灰气泡,非白底用 surface 白气泡
|
|
37
|
+
* @prop {string} [timestamp=''] — 消息时间戳(actions 与用户气泡共用)
|
|
38
|
+
*
|
|
39
|
+
* @prop {string} [title=DEFAULT_CHAT_TITLE] — 执行流标题(role='ai')
|
|
40
|
+
* @prop {'completed'|'processing'|'thinking'|'requesting'} [status='completed'] — 标题节点状态
|
|
41
|
+
* @prop {string} [statusIconName='check-circle-stroked'] — 标题左侧状态图标名(completed 时使用)
|
|
42
|
+
* @prop {Array|null} [steps=DEFAULT_CHAT_STEPS] — 执行步骤数组
|
|
43
|
+
* @prop {Array|null} [taskGroups=null] — 多任务组(每组可独立折叠 + 流式输出)
|
|
44
|
+
* @prop {Array|null} [confirms=null] — 人工确认节点数组(原 humanConfirmNodes)
|
|
45
|
+
* @prop {string} [resultText=''] — 结果节点文案
|
|
46
|
+
* @prop {object|null} [resultArtifact=null] — 结果节点单个产物卡片(兼容旧 API)
|
|
47
|
+
* @prop {Array|null} [resultArtifacts=null] — 结果节点产物卡片数组
|
|
48
|
+
* @prop {boolean} [defaultExpanded=true] — 非受控初始展开态(兼容标记)
|
|
49
|
+
* @prop {boolean|undefined} [expanded=undefined] — 受控展开态(兼容标记)
|
|
50
|
+
* @prop {Function|null} [onExpandedChange=null] — 展开态切换回调(兼容标记)
|
|
51
|
+
* @prop {boolean} [continuedBelow=false] — 下方是否仍有后续节点
|
|
52
|
+
* @prop {string} [className=''] — 额外类名
|
|
53
|
+
* @prop {React.CSSProperties|undefined} [style=undefined] — 内联样式
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
57
|
+
import Icon from './Icon';
|
|
58
|
+
import Tooltip from './Tooltip';
|
|
59
|
+
import catcatSvg from './file-type-assets/catcat.svg';
|
|
60
|
+
import { getFileTypeIcon } from './file-type-assets';
|
|
61
|
+
/* 用户附件场景沿用语义化别名,避免散点修改业务调用 */
|
|
62
|
+
const getUserAttachmentIcon = getFileTypeIcon;
|
|
63
|
+
function formatUserAttachmentSize(bytes) {
|
|
64
|
+
if (typeof bytes !== 'number' || Number.isNaN(bytes)) return '';
|
|
65
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
66
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
67
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
68
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ── 默认示例数据:执行流(保持向后兼容)── */
|
|
72
|
+
export const DEFAULT_CHAT_TITLE = '梳理抖音电商客服售后退换货政策并生成标准答复口径';
|
|
73
|
+
|
|
74
|
+
export const DEFAULT_CHAT_STEPS = [
|
|
75
|
+
{
|
|
76
|
+
id: 'collect-rules',
|
|
77
|
+
title: '收集平台售后规则与商家补充条款',
|
|
78
|
+
actionLabel: '正在调用分析 Agent',
|
|
79
|
+
actionDetail: 'Policy_Agent /douyin_ecommerce/after_sales/rules',
|
|
80
|
+
actionIconName: 'line-chart-up-04-stroked',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'compare-policy',
|
|
84
|
+
title: '归纳退货退款、换货与拒收判责口径',
|
|
85
|
+
actionLabel: '正在调用分析 Agent',
|
|
86
|
+
actionDetail: 'Analyze_agent/API/after_sales/responsibility_rules',
|
|
87
|
+
actionIconName: 'line-chart-up-04-stroked',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'generate-policy',
|
|
91
|
+
title: '输出客服售后退换货政策说明',
|
|
92
|
+
actionLabel: '正在调用生成 Agent',
|
|
93
|
+
actionDetail: 'Code_Agent /doc/douyin-after-sales-policy',
|
|
94
|
+
actionIconName: 'code-square-01-stroked',
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/* 执行中 mock:第1步已完成,第2步加载中(用于预览执行中状态) */
|
|
99
|
+
export const DEFAULT_CHAT_STEPS_PROCESSING = [
|
|
100
|
+
{
|
|
101
|
+
id: 'collect-rules',
|
|
102
|
+
title: '收集平台售后规则与商家补充条款',
|
|
103
|
+
actionLabel: '正在调用分析 Agent',
|
|
104
|
+
actionDetail: 'Policy_Agent /douyin_ecommerce/after_sales/rules',
|
|
105
|
+
actionIconName: 'line-chart-up-04-stroked',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'compare-policy',
|
|
109
|
+
title: '归纳退货退款、换货与拒收判责口径',
|
|
110
|
+
actionLabel: '正在调用分析 Agent',
|
|
111
|
+
actionDetail: 'Analyze_agent/API/after_sales/responsibility_rules',
|
|
112
|
+
actionIconName: 'line-chart-up-04-stroked',
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
/* 多任务组 mock:执行流由多个可独立折叠的任务组组成 */
|
|
117
|
+
export const DEFAULT_CHAT_TASK_GROUPS = [
|
|
118
|
+
{
|
|
119
|
+
id: 'group-0',
|
|
120
|
+
title:
|
|
121
|
+
'梳理抖音电商客服售后退换货政策并生成标准答复口径,覆盖退货退款、仅退款、换货与拒收等全场景判责口径,输出客服一次性可用的统一答复模板,并按订单状态、商品类目、签收时效与凭证材料分别给出处理建议与升级路径',
|
|
122
|
+
status: 'completed',
|
|
123
|
+
defaultExpanded: true,
|
|
124
|
+
steps: [
|
|
125
|
+
{
|
|
126
|
+
id: 'g0-collect',
|
|
127
|
+
title: '收集平台售后规则与商家补充条款',
|
|
128
|
+
actionLabel: '正在调用分析 Agent',
|
|
129
|
+
actionDetail: 'Policy_Agent /douyin_ecommerce/after_sales/rules',
|
|
130
|
+
actionIconName: 'line-chart-up-04-stroked',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'group-1',
|
|
136
|
+
title: '梳理抖音电商客服售后退换货政策并生成标准答复口径',
|
|
137
|
+
status: 'completed',
|
|
138
|
+
defaultExpanded: true,
|
|
139
|
+
steps: [
|
|
140
|
+
{
|
|
141
|
+
id: 'g1-collect',
|
|
142
|
+
title: '收集平台售后规则与商家补充条款',
|
|
143
|
+
actionLabel: '正在调用分析 Agent',
|
|
144
|
+
actionDetail: 'Policy_Agent /douyin_ecommerce/after_sales/rules',
|
|
145
|
+
actionIconName: 'line-chart-up-04-stroked',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'g1-compare',
|
|
149
|
+
title: '归纳退货退款、换货与拒收判责口径',
|
|
150
|
+
actionLabel: '正在调用分析 Agent',
|
|
151
|
+
actionDetail: 'Analyze_agent/API/after_sales/responsibility_rules',
|
|
152
|
+
actionIconName: 'line-chart-up-04-stroked',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'g1-generate',
|
|
156
|
+
title: '输出客服售后退换货政策说明',
|
|
157
|
+
actionLabel: '正在调用生成 Agent',
|
|
158
|
+
actionDetail: 'Code_Agent /doc/douyin-after-sales-policy',
|
|
159
|
+
actionIconName: 'code-square-01-stroked',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'group-2',
|
|
165
|
+
title: '生成 AB 实验方案并评估指标体系',
|
|
166
|
+
status: 'processing',
|
|
167
|
+
defaultExpanded: true,
|
|
168
|
+
steps: [
|
|
169
|
+
{
|
|
170
|
+
id: 'g2-fetch',
|
|
171
|
+
title: '获取近 7 天客服会话核心指标',
|
|
172
|
+
actionLabel: '正在调用查询 Agent',
|
|
173
|
+
actionDetail: 'Metric_Agent /cs/sessions/last7d',
|
|
174
|
+
actionIconName: 'line-chart-up-04-stroked',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'g2-design',
|
|
178
|
+
title: '设计 AB 实验分流与回收链路',
|
|
179
|
+
actionLabel: '正在调用实验 Agent',
|
|
180
|
+
actionDetail: 'Experiment_Agent /ab/design',
|
|
181
|
+
actionIconName: 'code-square-01-stroked',
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
export const DEFAULT_CHAT_RESULT =
|
|
188
|
+
'已整理出抖音电商客服售后退换货政策说明:用户可在平台规则允许范围内申请退货退款、仅退款或换货,客服需优先核验订单状态、商品类目、签收时效、凭证材料与运费归属,再按统一口径答复处理时限、审核条件和升级路径。';
|
|
189
|
+
|
|
190
|
+
export const DEFAULT_CHAT_RESULT_ARTIFACTS = [
|
|
191
|
+
{
|
|
192
|
+
id: 'web',
|
|
193
|
+
type: 'web',
|
|
194
|
+
title: '抖音电商客服售后退换货政策说明与统一答复口径网页',
|
|
195
|
+
meta: '370.3 KB',
|
|
196
|
+
actionIconName: 'dots-horizontal-stroked',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'code',
|
|
200
|
+
type: 'code',
|
|
201
|
+
title: '售后规则解析代码',
|
|
202
|
+
meta: '214.6 KB',
|
|
203
|
+
actionIconName: 'dots-horizontal-stroked',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'table',
|
|
207
|
+
type: 'table',
|
|
208
|
+
title: '退换货判责对照表',
|
|
209
|
+
meta: '158.2 KB',
|
|
210
|
+
actionIconName: 'dots-horizontal-stroked',
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
export const DEFAULT_CHAT_RESULT_ARTIFACT = DEFAULT_CHAT_RESULT_ARTIFACTS[0];
|
|
215
|
+
|
|
216
|
+
/* 全量产物类型(用于「产物组」枚举预览,与 file-type-assets 共享 lookup 一一对应) */
|
|
217
|
+
export const ALL_ARTIFACT_TYPES = [
|
|
218
|
+
{ id: 'art-pdf', type: 'pdf', title: 'AI 品牌提案.pdf', meta: '370.3 KB' },
|
|
219
|
+
{ id: 'art-code', type: 'code', title: '售后规则解析代码', meta: '4 个文件 · 214.6 KB' },
|
|
220
|
+
{ id: 'art-pe', type: 'pe', title: 'PE 工程文件', meta: 'PE · 88.6 KB' },
|
|
221
|
+
{ id: 'art-xmind', type: 'xmind', title: '业务架构脑图.xmind', meta: 'XMind · 124.0 KB' },
|
|
222
|
+
{ id: 'art-image', type: 'image', title: '品牌主视觉设计稿.png', meta: '图片 · 2.3 MB' },
|
|
223
|
+
{ id: 'art-document', type: 'document', title: '抖音电商售后政策汇编.docx', meta: '文档 · 320.0 KB' },
|
|
224
|
+
{ id: 'art-table', type: 'table', title: '退换货判责对照表.xlsx', meta: '表格 · 158.2 KB' },
|
|
225
|
+
{ id: 'art-webpage', type: 'webpage', title: '客服售后政策说明网页', meta: '网页 · 370.3 KB' },
|
|
226
|
+
{ id: 'art-knowledge', type: 'knowledge', title: '电商售后知识库条目', meta: '知识 · 56 条' },
|
|
227
|
+
{ id: 'art-strategy', type: 'strategy', title: '统一答复口径策略', meta: '策略 · v1.2' },
|
|
228
|
+
{ id: 'art-conversation', type: 'conversation', title: '客户会话上下文', meta: '会话 · 12 条' },
|
|
229
|
+
{ id: 'art-batch-report', type: 'batch-report', title: '批量测试报告', meta: '批测 · 240/240 通过' },
|
|
230
|
+
{ id: 'art-feishu', type: 'feishu', title: '飞书任务跟进文档', meta: '飞书 · 项目协作' },
|
|
231
|
+
{ id: 'art-feishu-card', type: 'feishu-card', title: '飞书消息卡片', meta: '飞书 · 卡片' },
|
|
232
|
+
{ id: 'art-feishu-sheet', type: 'feishu-sheet', title: '飞书多维表格', meta: '飞书 · 多维表' },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
export const DEFAULT_CHAT_ARTIFACT_GROUP = ALL_ARTIFACT_TYPES;
|
|
236
|
+
|
|
237
|
+
export const DEFAULT_CHAT_CONFIRMS = [
|
|
238
|
+
{
|
|
239
|
+
id: 'confirm-form',
|
|
240
|
+
mode: 'text-card',
|
|
241
|
+
introText: '已输出抖音电商售后政策审核建议,以下关键信息需要人工确认后再继续生成标准答复口径。',
|
|
242
|
+
title: '售后政策确认',
|
|
243
|
+
description: '请确认退货退款、换货与仅退款的适用范围,以及特殊类目限制、签收时效、凭证要求和运费承担口径;若涉及生鲜、定制、虚拟商品或超时售后,请补充例外说明与升级处理路径。',
|
|
244
|
+
iconName: 'sticker-square-stroked',
|
|
245
|
+
primaryActionLabel: '确认继续',
|
|
246
|
+
secondaryActionLabel: '返回修改',
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
/* ── 默认示例数据:新增模块 ── */
|
|
251
|
+
export const DEFAULT_CHAT_HEADER = {
|
|
252
|
+
name: 'OLA AI',
|
|
253
|
+
avatarSrc: catcatSvg,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const DEFAULT_CHAT_THINKING = {
|
|
257
|
+
state: 'completed',
|
|
258
|
+
durationLabel: '深度思考(用时 21.00 秒)',
|
|
259
|
+
inProgressLabel: '深度思考中 ...',
|
|
260
|
+
content:
|
|
261
|
+
'深度思考结合上下文,猜测用户想问的是在2025年5月16日到5月27日期间人工解决率的变化情况,历史问题中提到了人工指标,所以这里的解决率为人工解决率。\n用户希望查询2025年5月16日到2025年5月27日期间的人工解决率变化情况。需要计算每日的人工解决率,并提供环比和同比变化情况。\n维度:会话开始日期\n指标:解决率 解决率(环比变化) 解决率(同比变化)\n筛选项:会话开始日期(2025-05-16至2025-05-27) 限制1000条',
|
|
262
|
+
defaultExpanded: true,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const DEFAULT_CHAT_PLAN = {
|
|
266
|
+
iconName: 'dotpoints-01-stroked',
|
|
267
|
+
title: '任务规划',
|
|
268
|
+
primaryActionLabel: '开始执行任务',
|
|
269
|
+
secondaryActionLabel: '取消',
|
|
270
|
+
defaultExpanded: true,
|
|
271
|
+
tasks: [
|
|
272
|
+
{
|
|
273
|
+
id: 'plan-1',
|
|
274
|
+
title: '信息准备',
|
|
275
|
+
items: ['分析相关背景,获取接入方基本信息、issue tag 下 kit 能力', '明确 Workflow 改动范围和目标'],
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: 'plan-3',
|
|
279
|
+
title: 'Workflow 迭代',
|
|
280
|
+
items: ['基于飞书文档内容初步生成 DSL 文件', '补充注释说明并检查语法结构/逻辑完整性'],
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: 'plan-4',
|
|
284
|
+
title: '批量测试 case 优化 Workflow',
|
|
285
|
+
items: ['抽样 1000 个相关 case 构建评测集', '测试运行并记录输出结论,根据评测结果提出优化建议,并更新飞书文档 DSL 文件'],
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: 'plan-5',
|
|
289
|
+
title: '总结',
|
|
290
|
+
items: ['产出《社交-私信 Ola Workflow v13》+《优化建议摘要报告》'],
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export const DEFAULT_CHAT_FOLLOW_UPS = ['批量测试当前策略', '批量测试当前节点', '开启 AB 实验'];
|
|
296
|
+
|
|
297
|
+
export const DEFAULT_CHAT_ACTIONS = {
|
|
298
|
+
showCopy: true,
|
|
299
|
+
showQuote: true,
|
|
300
|
+
showLike: true,
|
|
301
|
+
showDislike: true,
|
|
302
|
+
copyCount: 66,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export const DEFAULT_CHAT_USER_CONTENT = [
|
|
306
|
+
{ type: 'text', value: '分析一批' },
|
|
307
|
+
{
|
|
308
|
+
type: 'entity',
|
|
309
|
+
icon: 'message-chat-square-stroked',
|
|
310
|
+
label: '智能会话:社交私信',
|
|
311
|
+
tone: 'teal',
|
|
312
|
+
showChevron: true,
|
|
313
|
+
},
|
|
314
|
+
{ type: 'text', value: '的问题与机会点' },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
export const DEFAULT_CHAT_USER_ATTACHMENTS = [
|
|
318
|
+
{ id: 'att-1', name: '抖音电商售后政策汇编.pdf', size: 320 * 1024 },
|
|
319
|
+
{ id: 'att-2', name: '商家补充协议-生鲜定制.pdf', size: 180 * 1024 },
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
/* 用户消息中的紫色引用 / 划词条(quote bar) */
|
|
323
|
+
export const DEFAULT_CHAT_USER_QUOTE = {
|
|
324
|
+
iconName: 'type-01-stroked',
|
|
325
|
+
cornerIconName: 'corner-up-right-stroked',
|
|
326
|
+
title: '接口出参',
|
|
327
|
+
description: '具体内容具体内容具体内容具体内容具体内容具体内',
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/* ── 基础容器类名 ── */
|
|
331
|
+
const ROOT = 'tfds-chat-message group/chat-msg flex w-full min-w-0 max-w-full flex-col items-start justify-end';
|
|
332
|
+
const BODY = 'relative flex w-full min-w-0 max-w-full flex-col items-start gap-1';
|
|
333
|
+
const HEADER = 'group/exec-header relative flex w-full min-w-0 max-w-full items-start gap-3 py-2 cursor-pointer select-none';
|
|
334
|
+
const HEADER_ICON_WRAP = 'mt-[2px] flex shrink-0 items-center';
|
|
335
|
+
const STATUS_SPINNER_WRAP = 'relative block size-4 shrink-0';
|
|
336
|
+
const STATUS_SPINNER_TRACK = 'absolute inset-[0.67px] rounded-full border border-border-line-light';
|
|
337
|
+
const STATUS_SPINNER_ARC = 'absolute inset-[0.67px] rounded-full animate-[spin_1.1s_linear_infinite]';
|
|
338
|
+
/* 标题:单行截断;不占满宽度,让 chevron 紧贴文字(或省略号)右侧;
|
|
339
|
+
颜色用 text token className(不用 inline style,否则 hover 失效);
|
|
340
|
+
整行 hover 时仅标题色加深为 foreground,其他元素无任何动效 */
|
|
341
|
+
const HEADER_TITLE = [
|
|
342
|
+
'min-w-0 truncate text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]',
|
|
343
|
+
'text-foreground-secondary transition-colors duration-150 group-hover/exec-header:text-foreground',
|
|
344
|
+
].join(' ');
|
|
345
|
+
const ICON_BUTTON_INTERACTIVE = [
|
|
346
|
+
'cursor-pointer transition-all duration-150',
|
|
347
|
+
'[outline:2px_solid_transparent] outline-offset-2',
|
|
348
|
+
'hover:bg-fill',
|
|
349
|
+
'active:bg-fill-hover active:scale-[0.97]',
|
|
350
|
+
'focus-visible:outline-blueGrey-400',
|
|
351
|
+
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
352
|
+
].join(' ');
|
|
353
|
+
/* 折叠 chevron:紧跟标题文字(或省略号)右侧,不参与点击交互(整行可点击触发折叠) */
|
|
354
|
+
const TOGGLE = 'mt-[2px] ml-2 flex size-4 shrink-0 items-center justify-center';
|
|
355
|
+
const GLOBAL_RAIL = 'absolute left-[7.5px] top-[34px] w-px';
|
|
356
|
+
|
|
357
|
+
/* ── 步骤节点类名 ── */
|
|
358
|
+
const STEP_ROW = 'flex min-h-20 w-full min-w-0 max-w-full items-start gap-3';
|
|
359
|
+
const STEP_RAIL_WRAP = 'h-full w-4 shrink-0';
|
|
360
|
+
const STEP_BODY = 'flex min-w-0 flex-1 flex-col items-start gap-1 pt-1 pb-2';
|
|
361
|
+
const STEP_TITLE = ['w-full min-w-0', 'text-sm font-normal leading-5 tracking-[0]'].join(' ');
|
|
362
|
+
|
|
363
|
+
/* ── 结果节点类名 ── */
|
|
364
|
+
const RESULT_ROW = 'w-full min-w-0 max-w-full';
|
|
365
|
+
const RESULT_NODE = 'flex w-full min-w-0 flex-col items-start gap-2';
|
|
366
|
+
const RESULT_TEXT = ['w-full min-w-0 break-words', 'text-sm font-normal leading-5 tracking-[0]'].join(' ');
|
|
367
|
+
const RESULT_ARTIFACT_CARD =
|
|
368
|
+
'relative flex w-[300px] max-w-full items-center gap-2 overflow-visible rounded-lg border border-[0.5px] border-border-default bg-surface p-3';
|
|
369
|
+
const RESULT_ARTIFACT_ICON = 'flex size-8 shrink-0 items-center justify-center';
|
|
370
|
+
const RESULT_ARTIFACT_TEXT_GROUP = 'flex min-w-0 flex-1 flex-col items-start justify-center';
|
|
371
|
+
const RESULT_ARTIFACT_TITLE = 'w-full truncate text-xs [font-weight:var(--font-semibold)] leading-4 tracking-[0]';
|
|
372
|
+
const RESULT_ARTIFACT_META = 'text-xs font-normal leading-4 tracking-[0]';
|
|
373
|
+
const RESULT_ARTIFACT_ACTION = [
|
|
374
|
+
'flex size-8 shrink-0 items-center justify-center rounded-[6px] p-2',
|
|
375
|
+
ICON_BUTTON_INTERACTIVE,
|
|
376
|
+
].join(' ');
|
|
377
|
+
const RESULT_ARTIFACT_TITLE_WRAP = 'group/artifact-title relative w-full min-w-0';
|
|
378
|
+
|
|
379
|
+
/* ── 操作卡片类名 ── */
|
|
380
|
+
const ACTION_CARD = [
|
|
381
|
+
'inline-flex w-fit min-w-0 max-w-full items-center gap-2',
|
|
382
|
+
'rounded-lg border border-border-default px-2 py-2 pr-3',
|
|
383
|
+
].join(' ');
|
|
384
|
+
const ACTION_CARD_SKELETON = 'bg-[length:240%_100%] animate-[tfds-ai-execution-shimmer_3.0s_linear_infinite]';
|
|
385
|
+
const ACTION_ICON_WRAP = ['flex size-6 shrink-0 items-center justify-center rounded-md border border-border-default'].join(' ');
|
|
386
|
+
const ACTION_TEXT_GROUP = 'flex min-w-0 max-w-full items-center gap-2 overflow-hidden';
|
|
387
|
+
const ACTION_TEXT_WRAP = 'flex min-w-0 max-w-full flex-1 items-center';
|
|
388
|
+
const ACTION_LABEL = ['min-w-0 shrink', 'truncate', 'text-xs font-normal leading-4 tracking-[0]'].join(' ');
|
|
389
|
+
const ACTION_DETAIL = [
|
|
390
|
+
'min-w-0 flex-1 shrink truncate',
|
|
391
|
+
'text-xs font-normal leading-4 tracking-[0]',
|
|
392
|
+
].join(' ');
|
|
393
|
+
|
|
394
|
+
/* ── 人工确认节点类名 ── */
|
|
395
|
+
const HUMAN_CONFIRM_SECTION = 'flex w-full min-w-0 max-w-full flex-col items-start gap-2';
|
|
396
|
+
const HUMAN_CONFIRM_STACK = 'flex w-full min-w-0 max-w-full flex-col items-start gap-1';
|
|
397
|
+
const HUMAN_CONFIRM_INTRO = 'w-full min-w-0 text-sm font-normal leading-[26px] tracking-[0]';
|
|
398
|
+
const HUMAN_CONFIRM_CARD = 'flex w-full min-w-0 max-w-full flex-col items-stretch gap-3 rounded-[12px] border border-border-default px-4 py-3';
|
|
399
|
+
const HUMAN_CONFIRM_HEADER = 'flex w-full min-w-0 items-center gap-2';
|
|
400
|
+
const HUMAN_CONFIRM_ICON_WRAP = 'flex size-8 shrink-0 items-center justify-center rounded-md border border-border-default';
|
|
401
|
+
const HUMAN_CONFIRM_TITLE = 'min-w-0 flex-1 text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]';
|
|
402
|
+
/* 24px iconButton(与消息操作栏按钮规格一致) */
|
|
403
|
+
const HUMAN_CONFIRM_TOGGLE = [
|
|
404
|
+
'flex size-6 shrink-0 items-center justify-center rounded-[6px] p-1',
|
|
405
|
+
ICON_BUTTON_INTERACTIVE,
|
|
406
|
+
].join(' ');
|
|
407
|
+
const HUMAN_CONFIRM_BODY = 'flex w-full min-w-0 flex-col items-stretch justify-center rounded-[12px] border border-border-default px-[19px] py-[15px]';
|
|
408
|
+
const HUMAN_CONFIRM_DESCRIPTION = 'w-full min-w-0 text-sm font-normal leading-5 tracking-[0] text-justify';
|
|
409
|
+
const HUMAN_CONFIRM_ACTIONS = 'flex w-full min-w-0 items-center justify-end gap-3';
|
|
410
|
+
const HUMAN_CONFIRM_SECONDARY_BUTTON = [
|
|
411
|
+
'inline-flex h-[36px] shrink-0 items-center justify-center rounded-md border',
|
|
412
|
+
'border-border-default bg-surface px-[11px] py-[5px]',
|
|
413
|
+
'text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0] text-foreground-secondary',
|
|
414
|
+
'cursor-pointer transition-all duration-150',
|
|
415
|
+
'[outline:2px_solid_transparent] outline-offset-2',
|
|
416
|
+
'hover:bg-blueGrey-50 hover:border-border-strong',
|
|
417
|
+
'active:bg-blueGrey-100 active:scale-[0.97]',
|
|
418
|
+
'focus-visible:outline-blueGrey-400',
|
|
419
|
+
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
420
|
+
].join(' ');
|
|
421
|
+
const HUMAN_CONFIRM_PRIMARY_BUTTON = [
|
|
422
|
+
'inline-flex h-[36px] shrink-0 items-center justify-center rounded-md border border-transparent',
|
|
423
|
+
'bg-blueGrey-800 px-3 py-[6px]',
|
|
424
|
+
'text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0] text-white',
|
|
425
|
+
'cursor-pointer transition-all duration-150',
|
|
426
|
+
'[outline:2px_solid_transparent] outline-offset-2',
|
|
427
|
+
'hover:bg-blueGrey-700',
|
|
428
|
+
'active:bg-blueGrey-800 active:scale-[0.97]',
|
|
429
|
+
'focus-visible:outline-blueGrey-400',
|
|
430
|
+
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
431
|
+
].join(' ');
|
|
432
|
+
const HUMAN_CONFIRM_COLLAPSED = 'flex w-full min-w-0 max-w-full items-center gap-2 rounded-[12px] border border-border-default px-4 py-3';
|
|
433
|
+
|
|
434
|
+
/* ── 新增:AI 头像 / 深度思考 / 任务规划 / 追问 / 消息操作 / 任务徽章 / 用户气泡 类名 ── */
|
|
435
|
+
const AI_HEADER = 'flex w-full min-w-0 items-center gap-2';
|
|
436
|
+
const AI_HEADER_AVATAR = 'block size-5 shrink-0 select-none';
|
|
437
|
+
const AI_HEADER_NAME = 'text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]';
|
|
438
|
+
|
|
439
|
+
const THINKING_BLOCK = 'flex w-full min-w-0 flex-col items-start';
|
|
440
|
+
/* 颜色用 text token className(不用 inline style,否则 hover 失效) */
|
|
441
|
+
const THINKING_HEADER = [
|
|
442
|
+
'flex w-full min-w-0 items-center gap-1 py-1',
|
|
443
|
+
'text-xs font-normal leading-4 tracking-[0] text-foreground-muted',
|
|
444
|
+
].join(' ');
|
|
445
|
+
/* 深度思考 header 按钮:仅文字 hover 色加深为 foreground,不要背景/scale/padding 等动效 */
|
|
446
|
+
const THINKING_HEADER_INTERACTIVE = 'cursor-pointer transition-colors duration-150 hover:text-foreground focus-visible:[outline:2px_solid_var(--color-blueGrey-400)] focus-visible:outline-offset-2';
|
|
447
|
+
const THINKING_BODY = 'flex w-full min-w-0 items-start gap-2 pl-px pt-1';
|
|
448
|
+
const THINKING_RAIL = 'w-px self-stretch shrink-0';
|
|
449
|
+
const THINKING_TEXT = [
|
|
450
|
+
'flex-1 min-w-0 whitespace-pre-wrap break-words',
|
|
451
|
+
'text-xs font-normal leading-5 tracking-[0]',
|
|
452
|
+
].join(' ');
|
|
453
|
+
|
|
454
|
+
const TASK_PLAN_CARD = [
|
|
455
|
+
'flex w-full min-w-0 max-w-full flex-col items-stretch overflow-hidden',
|
|
456
|
+
'rounded-[12px] border-[0.5px] border-border-default px-4 py-3',
|
|
457
|
+
].join(' ');
|
|
458
|
+
const TASK_PLAN_HEADER = 'flex w-full min-w-0 items-center gap-2';
|
|
459
|
+
const TASK_PLAN_HEADER_ICON_WRAP = 'flex size-8 shrink-0 items-center justify-center rounded-md border-[0.5px] border-border-default';
|
|
460
|
+
const TASK_PLAN_HEADER_TITLE = 'min-w-0 flex-1 text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]';
|
|
461
|
+
const TASK_PLAN_COUNT_BADGE = [
|
|
462
|
+
'inline-flex shrink-0 items-center justify-center rounded-md border border-border-default px-2 py-1',
|
|
463
|
+
'text-xs font-normal leading-4 tracking-[0]',
|
|
464
|
+
].join(' ');
|
|
465
|
+
const TASK_PLAN_TOGGLE = [
|
|
466
|
+
'flex size-7 shrink-0 items-center justify-center rounded-md',
|
|
467
|
+
ICON_BUTTON_INTERACTIVE,
|
|
468
|
+
].join(' ');
|
|
469
|
+
const TASK_PLAN_BODY = [
|
|
470
|
+
'mt-3 flex w-full min-w-0 flex-col items-stretch rounded-[12px] border border-border-default',
|
|
471
|
+
'px-5 py-4',
|
|
472
|
+
].join(' ');
|
|
473
|
+
const TASK_PLAN_LIST = 'flex w-full min-w-0 flex-col items-stretch gap-4';
|
|
474
|
+
const TASK_PLAN_TASK_ROW = 'flex w-full min-w-0 flex-col items-start gap-1';
|
|
475
|
+
const TASK_PLAN_TASK_HEADER = 'flex w-full min-w-0 items-center gap-2';
|
|
476
|
+
const TASK_PLAN_TASK_BADGE = [
|
|
477
|
+
'flex size-4 shrink-0 items-center justify-center rounded-sm border-[0.5px] border-border-default',
|
|
478
|
+
'text-[10px] font-normal leading-4 text-center',
|
|
479
|
+
].join(' ');
|
|
480
|
+
const TASK_PLAN_TASK_TITLE = 'min-w-0 flex-1 text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]';
|
|
481
|
+
const TASK_PLAN_ITEM_ROW = 'flex w-full min-w-0 items-start gap-2';
|
|
482
|
+
/* 16px 宽 × 20px 高(=单行文本行高)容器 + 8px 灰色空心圆环;
|
|
483
|
+
ROW 用 items-start,靠这里把 dot 中线对齐到首行文本中线 */
|
|
484
|
+
const TASK_PLAN_ITEM_DOT_WRAP = 'flex h-5 w-4 shrink-0 items-center justify-center';
|
|
485
|
+
const TASK_PLAN_ITEM_DOT = 'block size-2 rounded-full border border-border-default';
|
|
486
|
+
const TASK_PLAN_ITEM_TEXT = [
|
|
487
|
+
'flex-1 min-w-0 text-sm font-normal leading-5 tracking-[0]',
|
|
488
|
+
].join(' ');
|
|
489
|
+
const TASK_PLAN_FOOTER = 'mt-3 flex w-full min-w-0 items-center justify-end gap-3';
|
|
490
|
+
/* TASK_PLAN_PRIMARY_BUTTON 已废弃,统一复用 HUMAN_CONFIRM_PRIMARY_BUTTON / HUMAN_CONFIRM_SECONDARY_BUTTON */
|
|
491
|
+
|
|
492
|
+
const FOLLOW_UP_GROUP = 'flex w-full min-w-0 flex-col items-stretch gap-2';
|
|
493
|
+
const FOLLOW_UP_BUTTON = [
|
|
494
|
+
'inline-flex h-10 w-fit max-w-full items-center justify-start',
|
|
495
|
+
'rounded-lg border border-border-line-light bg-surface px-3 py-2',
|
|
496
|
+
'text-sm font-normal leading-5 tracking-[0] text-left',
|
|
497
|
+
'cursor-pointer transition-all duration-150',
|
|
498
|
+
'[outline:2px_solid_transparent] outline-offset-2',
|
|
499
|
+
'hover:bg-blueGrey-50 hover:border-border-strong',
|
|
500
|
+
'active:bg-blueGrey-100 active:scale-[0.99]',
|
|
501
|
+
'focus-visible:outline-blueGrey-400',
|
|
502
|
+
].join(' ');
|
|
503
|
+
|
|
504
|
+
const MESSAGE_ACTIONS_GROUP = 'flex items-center gap-3 transition-opacity duration-150';
|
|
505
|
+
const MESSAGE_ACTIONS_HISTORY_HIDDEN = 'opacity-0 group-hover/chat-msg:opacity-100 group-focus-within/chat-msg:opacity-100';
|
|
506
|
+
const MESSAGE_ACTION_BUTTON = [
|
|
507
|
+
'flex size-6 shrink-0 items-center justify-center rounded-[6px] p-1',
|
|
508
|
+
ICON_BUTTON_INTERACTIVE,
|
|
509
|
+
].join(' ');
|
|
510
|
+
const MESSAGE_ACTION_LABEL = 'text-xs font-normal leading-4 tracking-[0]';
|
|
511
|
+
const MESSAGE_ACTION_TIMESTAMP = 'text-xs font-normal leading-4 tracking-[0]';
|
|
512
|
+
const MESSAGE_ACTION_COPY_COUNT = 'text-xs font-normal leading-4 tracking-[0]';
|
|
513
|
+
|
|
514
|
+
const TASK_BADGE = [
|
|
515
|
+
'inline-flex w-fit items-center gap-2 rounded-lg border border-border-default px-2 py-1',
|
|
516
|
+
'text-sm font-normal leading-5 tracking-[0]',
|
|
517
|
+
].join(' ');
|
|
518
|
+
|
|
519
|
+
const REQUESTING_DOTS_WRAP = 'flex h-2 w-full items-center';
|
|
520
|
+
const REQUESTING_DOT = 'block size-[6px] rounded-full';
|
|
521
|
+
|
|
522
|
+
/* 思考中行内 label:与 THINKING_HEADER 保持同样的 py-1 + text token 颜色,确保三种变体首行视觉一致 */
|
|
523
|
+
const THINKING_INLINE_LABEL = [
|
|
524
|
+
'py-1',
|
|
525
|
+
'text-xs font-normal leading-4 tracking-[0] text-foreground-muted',
|
|
526
|
+
].join(' ');
|
|
527
|
+
|
|
528
|
+
const USER_BUBBLE_WRAP = 'flex w-full min-w-0 flex-col items-end gap-2';
|
|
529
|
+
/* 用户气泡:左上 / 左下 / 右下 12px 圆角,右上尖角(IM 发送方向暗示),
|
|
530
|
+
padding 上下 12 / 左右 16,背景随会话背景选择 */
|
|
531
|
+
const USER_BUBBLE = [
|
|
532
|
+
'flex w-fit max-w-full flex-col items-stretch gap-2 px-4 py-3',
|
|
533
|
+
'rounded-tl-[12px] rounded-bl-[12px] rounded-br-[12px]',
|
|
534
|
+
].join(' ');
|
|
535
|
+
const USER_BUBBLE_TONE_CLASS = {
|
|
536
|
+
surface: 'bg-surface',
|
|
537
|
+
fill: 'bg-fill',
|
|
538
|
+
};
|
|
539
|
+
const DEFAULT_USER_BUBBLE_TONE = 'surface';
|
|
540
|
+
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
541
|
+
|
|
542
|
+
function parseRgbColor(value) {
|
|
543
|
+
if (typeof value !== 'string') return null;
|
|
544
|
+
const match = value.trim().match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i);
|
|
545
|
+
if (!match) return null;
|
|
546
|
+
return {
|
|
547
|
+
r: Number(match[1]),
|
|
548
|
+
g: Number(match[2]),
|
|
549
|
+
b: Number(match[3]),
|
|
550
|
+
a: match[4] == null ? 1 : Number(match[4]),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function isWhiteBackgroundColor(value) {
|
|
555
|
+
const rgba = parseRgbColor(value);
|
|
556
|
+
if (!rgba || rgba.a <= 0.02) return false;
|
|
557
|
+
return rgba.r >= 250 && rgba.g >= 250 && rgba.b >= 250;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function resolveAutoUserBubbleTone(node) {
|
|
561
|
+
if (typeof window === 'undefined' || !node) return DEFAULT_USER_BUBBLE_TONE;
|
|
562
|
+
|
|
563
|
+
let current = node.parentElement;
|
|
564
|
+
while (current && current !== document.documentElement) {
|
|
565
|
+
const backgroundColor = window.getComputedStyle(current).backgroundColor;
|
|
566
|
+
const rgba = parseRgbColor(backgroundColor);
|
|
567
|
+
|
|
568
|
+
if (rgba && rgba.a > 0.02) {
|
|
569
|
+
return isWhiteBackgroundColor(backgroundColor) ? 'fill' : 'surface';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
current = current.parentElement;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return DEFAULT_USER_BUBBLE_TONE;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function getBackgroundAncestorNodes(node) {
|
|
579
|
+
const nodes = [];
|
|
580
|
+
if (!node) return nodes;
|
|
581
|
+
|
|
582
|
+
let current = node.parentElement;
|
|
583
|
+
while (current && current !== document.documentElement) {
|
|
584
|
+
nodes.push(current);
|
|
585
|
+
current = current.parentElement;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return nodes;
|
|
589
|
+
}
|
|
590
|
+
/* 用户附件行:右对齐,flex-wrap,gap-2,与气泡并列;max-w-full 跟随容器 */
|
|
591
|
+
const USER_ATTACHMENT_ROW = 'flex w-full max-w-full flex-wrap items-start justify-end gap-2';
|
|
592
|
+
/* 与 ChatInput 的 ATTACH_CARD 完全对齐:180px 文件卡,bg-fill + border + hover 浅蓝灰;
|
|
593
|
+
展示场景不带删除按钮,左侧只渲染文件类型彩色图标 */
|
|
594
|
+
const USER_ATTACHMENT_CARD = [
|
|
595
|
+
'inline-flex items-center gap-2 shrink-0',
|
|
596
|
+
'w-[180px] max-w-[180px] p-2 rounded-lg',
|
|
597
|
+
'bg-fill border border-border-default transition-colors duration-150',
|
|
598
|
+
'hover:bg-blueGrey-100',
|
|
599
|
+
].join(' ');
|
|
600
|
+
const USER_ATTACHMENT_ICON_WRAP = 'shrink-0 relative w-8 h-[34px] inline-flex items-center justify-center';
|
|
601
|
+
const USER_ATTACHMENT_TEXT_GROUP = 'flex-1 min-w-0 flex flex-col gap-0.5';
|
|
602
|
+
const USER_ATTACHMENT_NAME = 'text-xs leading-4 text-foreground truncate';
|
|
603
|
+
const USER_ATTACHMENT_SIZE = 'text-[10px] leading-3 text-foreground-muted';
|
|
604
|
+
|
|
605
|
+
/* ── 用户引用条(紫色划词条 / quote bar)── */
|
|
606
|
+
const USER_QUOTE_ROW = 'flex w-full max-w-full justify-end';
|
|
607
|
+
const USER_QUOTE_CARD = [
|
|
608
|
+
'inline-flex max-w-full items-center gap-3 p-3 rounded-[8px]',
|
|
609
|
+
'bg-violet-50 border border-violet-100',
|
|
610
|
+
].join(' ');
|
|
611
|
+
const USER_QUOTE_ICON_WRAP = [
|
|
612
|
+
'flex size-8 shrink-0 items-center justify-center rounded-[8px]',
|
|
613
|
+
'border-[0.5px] border-violet-200',
|
|
614
|
+
].join(' ');
|
|
615
|
+
const USER_QUOTE_BODY = 'flex min-w-0 flex-col items-start gap-1';
|
|
616
|
+
const USER_QUOTE_TITLE_ROW = 'flex w-full min-w-0 items-center gap-2';
|
|
617
|
+
const USER_QUOTE_TITLE = 'min-w-0 truncate text-xs [font-weight:var(--font-semibold)] leading-4 text-foreground';
|
|
618
|
+
const USER_QUOTE_DESC = 'min-w-0 truncate text-xs font-normal leading-4 text-foreground-muted';
|
|
619
|
+
const USER_BUBBLE_CONTENT = 'flex flex-wrap items-start content-start gap-x-2 gap-y-1';
|
|
620
|
+
/* 纯文本段允许换行;break-words 处理超长无空格字符串;
|
|
621
|
+
TAG 实体本身是 inline-flex 不会被影响 */
|
|
622
|
+
const USER_BUBBLE_TEXT = [
|
|
623
|
+
'text-sm font-normal leading-[26px] tracking-[0] break-words',
|
|
624
|
+
].join(' ');
|
|
625
|
+
/* 与 ChatInput 的 ENTITY_CHIP 完全对齐:rounded-md 8px, gap-1, h-26, px-2,
|
|
626
|
+
bg-brand-50 + border-brand-100 + text-foreground,icon 用 brand-500 */
|
|
627
|
+
const USER_ENTITY_TAG = [
|
|
628
|
+
'inline-flex items-center gap-1 shrink-0 align-middle',
|
|
629
|
+
'h-[26px] px-2 rounded-md',
|
|
630
|
+
'bg-brand-50 border border-brand-100',
|
|
631
|
+
'text-sm leading-[26px] text-foreground',
|
|
632
|
+
].join(' ');
|
|
633
|
+
|
|
634
|
+
/* ENTITY_TONE_PALETTE 已废弃:用户消息 TAG 现统一复用 ChatInput 的品牌色 chip 样式 */
|
|
635
|
+
|
|
636
|
+
/* ── 工具:归一化 ── */
|
|
637
|
+
function normalizeStep(step, index) {
|
|
638
|
+
return {
|
|
639
|
+
id: step?.id ?? `step-${index}`,
|
|
640
|
+
title: step?.title ?? '',
|
|
641
|
+
actionLabel: step?.actionLabel ?? '',
|
|
642
|
+
actionDetail: step?.actionDetail ?? '',
|
|
643
|
+
actionIconName: step?.actionIconName ?? 'code-square-01-stroked',
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/* 把任意输入归一化为「任务组」数组 */
|
|
648
|
+
function normalizeTaskGroups({ taskGroups, title, status, statusIconName, defaultExpanded, steps }) {
|
|
649
|
+
if (Array.isArray(taskGroups) && taskGroups.length > 0) {
|
|
650
|
+
return taskGroups.map((g, i) => ({
|
|
651
|
+
id: g?.id ?? `group-${i}`,
|
|
652
|
+
title: g?.title ?? '',
|
|
653
|
+
status: g?.status ?? 'completed',
|
|
654
|
+
statusIconName: g?.statusIconName ?? 'check-circle-stroked',
|
|
655
|
+
defaultExpanded: g?.defaultExpanded ?? true,
|
|
656
|
+
steps: Array.isArray(g?.steps) ? g.steps.map(normalizeStep) : [],
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
/* 旧 API 兜底:用顶层 title/steps/status 包装成单组 */
|
|
660
|
+
if (Array.isArray(steps) && steps.length > 0) {
|
|
661
|
+
return [
|
|
662
|
+
{
|
|
663
|
+
id: 'default-group',
|
|
664
|
+
title: title ?? '',
|
|
665
|
+
status: status ?? 'completed',
|
|
666
|
+
statusIconName: statusIconName ?? 'check-circle-stroked',
|
|
667
|
+
defaultExpanded: defaultExpanded ?? true,
|
|
668
|
+
steps: steps.map(normalizeStep),
|
|
669
|
+
},
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/* 老 type 别名 → 新 PNG icon 类型映射(保持旧 API 数据可用) */
|
|
676
|
+
const ARTIFACT_TYPE_ALIAS = {
|
|
677
|
+
web: 'webpage',
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
function normalizeResultArtifact(resultArtifact) {
|
|
681
|
+
if (!resultArtifact) return null;
|
|
682
|
+
const rawType = resultArtifact?.type ?? 'webpage';
|
|
683
|
+
const artifactType = ARTIFACT_TYPE_ALIAS[rawType] ?? rawType;
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
id: resultArtifact?.id ?? artifactType ?? 'artifact',
|
|
687
|
+
type: artifactType,
|
|
688
|
+
title: resultArtifact?.title ?? '',
|
|
689
|
+
meta: resultArtifact?.meta ?? '',
|
|
690
|
+
/* iconName 字段已废弃(保留只为向后兼容数据),实际渲染走 type → PNG */
|
|
691
|
+
iconName: resultArtifact?.iconName ?? null,
|
|
692
|
+
actionIconName: resultArtifact?.actionIconName ?? 'dots-horizontal-stroked',
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function normalizeResultArtifacts(resultArtifacts, resultArtifact) {
|
|
697
|
+
if (Array.isArray(resultArtifacts) && resultArtifacts.length > 0) {
|
|
698
|
+
return resultArtifacts.map(normalizeResultArtifact).filter(Boolean);
|
|
699
|
+
}
|
|
700
|
+
const normalizedSingle = normalizeResultArtifact(resultArtifact);
|
|
701
|
+
return normalizedSingle ? [normalizedSingle] : [];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function normalizeHumanConfirmNode(node, index) {
|
|
705
|
+
const mode = ['text-card', 'card-only', 'collapsed'].includes(node?.mode) ? node.mode : 'text-card';
|
|
706
|
+
return {
|
|
707
|
+
id: node?.id ?? `human-confirm-${index}`,
|
|
708
|
+
mode,
|
|
709
|
+
collapsed: node?.collapsed === true || mode === 'collapsed',
|
|
710
|
+
introText: node?.introText ?? '',
|
|
711
|
+
title: node?.title ?? '配置表单',
|
|
712
|
+
description: node?.description ?? '',
|
|
713
|
+
iconName: node?.iconName ?? 'sticker-square-stroked',
|
|
714
|
+
primaryActionLabel: node?.primaryActionLabel ?? '确认执行',
|
|
715
|
+
secondaryActionLabel: node?.secondaryActionLabel ?? '编辑',
|
|
716
|
+
onPrimaryAction: typeof node?.onPrimaryAction === 'function' ? node.onPrimaryAction : null,
|
|
717
|
+
onSecondaryAction: typeof node?.onSecondaryAction === 'function' ? node.onSecondaryAction : null,
|
|
718
|
+
onToggleCollapsed: typeof node?.onToggleCollapsed === 'function' ? node.onToggleCollapsed : null,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function normalizeHumanConfirmNodes(humanConfirmNodes) {
|
|
723
|
+
if (!Array.isArray(humanConfirmNodes) || humanConfirmNodes.length === 0) return [];
|
|
724
|
+
return humanConfirmNodes.map(normalizeHumanConfirmNode).filter(Boolean);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function normalizeAiHeader(input) {
|
|
728
|
+
if (!input) return null;
|
|
729
|
+
if (input === true) return { ...DEFAULT_CHAT_HEADER };
|
|
730
|
+
return {
|
|
731
|
+
name: input.name ?? DEFAULT_CHAT_HEADER.name,
|
|
732
|
+
avatarSrc: input.avatarSrc ?? DEFAULT_CHAT_HEADER.avatarSrc,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function normalizeThinkingProcess(input) {
|
|
737
|
+
if (!input) return null;
|
|
738
|
+
const state = ['thinking', 'completed'].includes(input.state) ? input.state : 'completed';
|
|
739
|
+
return {
|
|
740
|
+
state,
|
|
741
|
+
durationLabel: input.durationLabel ?? DEFAULT_CHAT_THINKING.durationLabel,
|
|
742
|
+
inProgressLabel: input.inProgressLabel ?? DEFAULT_CHAT_THINKING.inProgressLabel,
|
|
743
|
+
content: input.content ?? '',
|
|
744
|
+
defaultExpanded: input.defaultExpanded !== false,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function normalizeTaskPlan(input) {
|
|
749
|
+
if (!input) return null;
|
|
750
|
+
const tasks = Array.isArray(input.tasks)
|
|
751
|
+
? input.tasks.map((task, idx) => ({
|
|
752
|
+
id: task?.id ?? `task-${idx + 1}`,
|
|
753
|
+
title: task?.title ?? '',
|
|
754
|
+
items: Array.isArray(task?.items) ? task.items.filter(Boolean) : [],
|
|
755
|
+
}))
|
|
756
|
+
: [];
|
|
757
|
+
return {
|
|
758
|
+
iconName: input.iconName ?? DEFAULT_CHAT_PLAN.iconName,
|
|
759
|
+
title: input.title ?? DEFAULT_CHAT_PLAN.title,
|
|
760
|
+
taskCountLabel: input.taskCountLabel ?? `${tasks.length} 个任务`,
|
|
761
|
+
primaryActionLabel: input.primaryActionLabel ?? DEFAULT_CHAT_PLAN.primaryActionLabel,
|
|
762
|
+
secondaryActionLabel: input.secondaryActionLabel ?? DEFAULT_CHAT_PLAN.secondaryActionLabel,
|
|
763
|
+
defaultExpanded: input.defaultExpanded !== false,
|
|
764
|
+
/* defaultConfirmed=true 让历史消息里的卡片初始就处于禁用置灰态(按钮不可再点)
|
|
765
|
+
* 这里必须显式透传,否则 TaskPlanCard 拿不到 → 历史卡片可被反复点击 */
|
|
766
|
+
defaultConfirmed: input.defaultConfirmed === true,
|
|
767
|
+
tasks,
|
|
768
|
+
onPrimaryAction: typeof input.onPrimaryAction === 'function' ? input.onPrimaryAction : null,
|
|
769
|
+
onSecondaryAction: typeof input.onSecondaryAction === 'function' ? input.onSecondaryAction : null,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function normalizeFollowUpQuestions(input) {
|
|
774
|
+
if (!input) return null;
|
|
775
|
+
const items = Array.isArray(input)
|
|
776
|
+
? input
|
|
777
|
+
: Array.isArray(input?.items)
|
|
778
|
+
? input.items
|
|
779
|
+
: [];
|
|
780
|
+
if (items.length === 0) return null;
|
|
781
|
+
return {
|
|
782
|
+
items: items.filter(Boolean).map((item, idx) =>
|
|
783
|
+
typeof item === 'string'
|
|
784
|
+
? { id: `followup-${idx}`, label: item }
|
|
785
|
+
: { id: item?.id ?? `followup-${idx}`, label: item?.label ?? '' },
|
|
786
|
+
),
|
|
787
|
+
onSelect: typeof input?.onSelect === 'function' ? input.onSelect : null,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function normalizeMessageActions(input) {
|
|
792
|
+
if (!input) return null;
|
|
793
|
+
if (input === true) return { ...DEFAULT_CHAT_ACTIONS, historyMode: false };
|
|
794
|
+
return {
|
|
795
|
+
showCopy: input.showCopy !== false,
|
|
796
|
+
showQuote: input.showQuote !== false,
|
|
797
|
+
showLike: input.showLike !== false,
|
|
798
|
+
showDislike: input.showDislike !== false,
|
|
799
|
+
copyCount: typeof input.copyCount === 'number' ? input.copyCount : null,
|
|
800
|
+
historyMode: input.historyMode === true,
|
|
801
|
+
onCopy: typeof input?.onCopy === 'function' ? input.onCopy : null,
|
|
802
|
+
onQuote: typeof input?.onQuote === 'function' ? input.onQuote : null,
|
|
803
|
+
onLike: typeof input?.onLike === 'function' ? input.onLike : null,
|
|
804
|
+
onDislike: typeof input?.onDislike === 'function' ? input.onDislike : null,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function normalizeUserAttachments(input) {
|
|
809
|
+
if (!Array.isArray(input) || input.length === 0) return null;
|
|
810
|
+
return input.map((file, idx) => ({
|
|
811
|
+
id: file?.id ?? `att-${idx}`,
|
|
812
|
+
name: file?.name ?? '未命名文件',
|
|
813
|
+
size: typeof file?.size === 'number' ? file.size : null,
|
|
814
|
+
iconSrc: file?.iconSrc ?? null,
|
|
815
|
+
}));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function normalizeUserQuote(input) {
|
|
819
|
+
if (!input || typeof input !== 'object') return null;
|
|
820
|
+
const title = typeof input.title === 'string' ? input.title : '';
|
|
821
|
+
const description = typeof input.description === 'string' ? input.description : '';
|
|
822
|
+
if (!title && !description) return null;
|
|
823
|
+
return {
|
|
824
|
+
iconName: typeof input.iconName === 'string' ? input.iconName : 'type-01-stroked',
|
|
825
|
+
cornerIconName:
|
|
826
|
+
typeof input.cornerIconName === 'string' ? input.cornerIconName : 'corner-up-right-stroked',
|
|
827
|
+
title,
|
|
828
|
+
description,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function normalizeUserMessage(input) {
|
|
833
|
+
if (!Array.isArray(input) || input.length === 0) return null;
|
|
834
|
+
return input.map((token, idx) => {
|
|
835
|
+
if (token?.type === 'entity') {
|
|
836
|
+
return {
|
|
837
|
+
type: 'entity',
|
|
838
|
+
id: token.id ?? `entity-${idx}`,
|
|
839
|
+
icon: token.icon ?? 'message-chat-square-stroked',
|
|
840
|
+
label: token.label ?? '',
|
|
841
|
+
showChevron: token.showChevron !== false,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
type: 'text',
|
|
846
|
+
id: token?.id ?? `text-${idx}`,
|
|
847
|
+
value: token?.value ?? '',
|
|
848
|
+
};
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/* ── 工具:截断检测 ── */
|
|
853
|
+
function isTextTruncated(node) {
|
|
854
|
+
if (!node) return false;
|
|
855
|
+
return node.scrollWidth > node.clientWidth || node.scrollHeight > node.clientHeight;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function TruncationTooltipText({ label, detail, labelStyle, detailStyle }) {
|
|
859
|
+
const labelRef = useRef(null);
|
|
860
|
+
const detailRef = useRef(null);
|
|
861
|
+
const [showTooltip, setShowTooltip] = useState(false);
|
|
862
|
+
const tooltipText = [label, detail].filter(Boolean).join(' ');
|
|
863
|
+
|
|
864
|
+
useEffect(() => {
|
|
865
|
+
const update = () => {
|
|
866
|
+
setShowTooltip(isTextTruncated(labelRef.current) || isTextTruncated(detailRef.current));
|
|
867
|
+
};
|
|
868
|
+
update();
|
|
869
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
870
|
+
window.addEventListener('resize', update);
|
|
871
|
+
return () => window.removeEventListener('resize', update);
|
|
872
|
+
}
|
|
873
|
+
const observer = new ResizeObserver(update);
|
|
874
|
+
if (labelRef.current) observer.observe(labelRef.current);
|
|
875
|
+
if (detailRef.current) observer.observe(detailRef.current);
|
|
876
|
+
return () => observer.disconnect();
|
|
877
|
+
}, [label, detail]);
|
|
878
|
+
|
|
879
|
+
const content = (
|
|
880
|
+
<div className={ACTION_TEXT_GROUP}>
|
|
881
|
+
{label ? (
|
|
882
|
+
<span ref={labelRef} className={ACTION_LABEL} style={labelStyle}>
|
|
883
|
+
{label}
|
|
884
|
+
</span>
|
|
885
|
+
) : null}
|
|
886
|
+
{detail ? (
|
|
887
|
+
<span ref={detailRef} className={ACTION_DETAIL} style={detailStyle}>
|
|
888
|
+
{detail}
|
|
889
|
+
</span>
|
|
890
|
+
) : null}
|
|
891
|
+
</div>
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
if (!showTooltip || !tooltipText) {
|
|
895
|
+
return <div className={ACTION_TEXT_WRAP}>{content}</div>;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return (
|
|
899
|
+
<Tooltip content={tooltipText} placement="top" triggerClassName={ACTION_TEXT_WRAP}>
|
|
900
|
+
{content}
|
|
901
|
+
</Tooltip>
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function OverflowTooltipText({ text, textClassName, textStyle }) {
|
|
906
|
+
const textRef = useRef(null);
|
|
907
|
+
const [showTooltip, setShowTooltip] = useState(false);
|
|
908
|
+
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
const update = () => {
|
|
911
|
+
setShowTooltip(isTextTruncated(textRef.current));
|
|
912
|
+
};
|
|
913
|
+
update();
|
|
914
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
915
|
+
window.addEventListener('resize', update);
|
|
916
|
+
return () => window.removeEventListener('resize', update);
|
|
917
|
+
}
|
|
918
|
+
const observer = new ResizeObserver(update);
|
|
919
|
+
if (textRef.current) observer.observe(textRef.current);
|
|
920
|
+
return () => observer.disconnect();
|
|
921
|
+
}, [text]);
|
|
922
|
+
|
|
923
|
+
const content = (
|
|
924
|
+
<p ref={textRef} className={textClassName} style={textStyle}>
|
|
925
|
+
{text}
|
|
926
|
+
</p>
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
if (!showTooltip || !text) {
|
|
930
|
+
return <div className={RESULT_ARTIFACT_TITLE_WRAP}>{content}</div>;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return (
|
|
934
|
+
<Tooltip content={text} placement="top" triggerClassName={RESULT_ARTIFACT_TITLE_WRAP}>
|
|
935
|
+
{content}
|
|
936
|
+
</Tooltip>
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/* ── 标题状态指示(completed/processing)── */
|
|
941
|
+
function StatusIndicator({ status = 'completed', statusIconName = 'check-circle-stroked' }) {
|
|
942
|
+
if (status === 'processing') {
|
|
943
|
+
return (
|
|
944
|
+
<span className={STATUS_SPINNER_WRAP} aria-hidden="true">
|
|
945
|
+
<span className={STATUS_SPINNER_TRACK} />
|
|
946
|
+
<span
|
|
947
|
+
className={STATUS_SPINNER_ARC}
|
|
948
|
+
style={{
|
|
949
|
+
background:
|
|
950
|
+
'conic-gradient(from 200deg, transparent 0deg 180deg, var(--color-brand-500) 180deg, var(--color-cyan-300) 232deg, var(--color-blue-400) 286deg, var(--color-violet-300) 334deg, var(--color-purple-300) 360deg)',
|
|
951
|
+
WebkitMask:
|
|
952
|
+
'radial-gradient(farthest-side, transparent calc(100% - 2px), var(--color-black) calc(100% - 2px))',
|
|
953
|
+
mask: 'radial-gradient(farthest-side, transparent calc(100% - 2px), var(--color-black) calc(100% - 2px))',
|
|
954
|
+
}}
|
|
955
|
+
/>
|
|
956
|
+
</span>
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
return (
|
|
960
|
+
<Icon name={statusIconName} size="sm" color="var(--color-brand-500)" aria-hidden="true" />
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/* ── AI 头像区 ── */
|
|
965
|
+
function AIHeader({ aiHeader }) {
|
|
966
|
+
if (!aiHeader) return null;
|
|
967
|
+
return (
|
|
968
|
+
<div className={AI_HEADER} data-tfds-component="ChatMessage.Header">
|
|
969
|
+
<img className={AI_HEADER_AVATAR} src={aiHeader.avatarSrc} alt={aiHeader.name} />
|
|
970
|
+
<p className={AI_HEADER_NAME} style={{ color: 'var(--color-blueGrey-800)' }}>
|
|
971
|
+
{aiHeader.name}
|
|
972
|
+
</p>
|
|
973
|
+
</div>
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/* ── 请求中三圆点 loading ── */
|
|
978
|
+
function RequestingDots() {
|
|
979
|
+
return (
|
|
980
|
+
<div className={REQUESTING_DOTS_WRAP} aria-label="请求中">
|
|
981
|
+
<div className="flex items-center gap-[4px]">
|
|
982
|
+
<span
|
|
983
|
+
className={REQUESTING_DOT}
|
|
984
|
+
style={{
|
|
985
|
+
background: 'var(--color-blueGrey-600)',
|
|
986
|
+
opacity: 0.3,
|
|
987
|
+
animation: 'tfds-ai-dot-bounce 1.2s ease-in-out infinite',
|
|
988
|
+
}}
|
|
989
|
+
/>
|
|
990
|
+
<span
|
|
991
|
+
className={REQUESTING_DOT}
|
|
992
|
+
style={{
|
|
993
|
+
background: 'var(--color-blueGrey-600)',
|
|
994
|
+
opacity: 0.3,
|
|
995
|
+
animation: 'tfds-ai-dot-bounce 1.2s ease-in-out 0.15s infinite',
|
|
996
|
+
}}
|
|
997
|
+
/>
|
|
998
|
+
<span
|
|
999
|
+
className={REQUESTING_DOT}
|
|
1000
|
+
style={{
|
|
1001
|
+
background: 'var(--color-blueGrey-600)',
|
|
1002
|
+
opacity: 0.3,
|
|
1003
|
+
animation: 'tfds-ai-dot-bounce 1.2s ease-in-out 0.3s infinite',
|
|
1004
|
+
}}
|
|
1005
|
+
/>
|
|
1006
|
+
</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/* ── 深度思考块(思考中/完成)── */
|
|
1012
|
+
function ThinkingProcess({ thinkingProcess, tokenStyles }) {
|
|
1013
|
+
const [internalExpanded, setInternalExpanded] = useState(thinkingProcess.defaultExpanded);
|
|
1014
|
+
const isCompleted = thinkingProcess.state === 'completed';
|
|
1015
|
+
const showToggle = isCompleted && thinkingProcess.content;
|
|
1016
|
+
const showBody = isCompleted && internalExpanded && thinkingProcess.content;
|
|
1017
|
+
|
|
1018
|
+
const headerLabel = isCompleted ? thinkingProcess.durationLabel : thinkingProcess.inProgressLabel;
|
|
1019
|
+
|
|
1020
|
+
return (
|
|
1021
|
+
<div className={THINKING_BLOCK}>
|
|
1022
|
+
{showToggle ? (
|
|
1023
|
+
<button
|
|
1024
|
+
type="button"
|
|
1025
|
+
className={[THINKING_HEADER, THINKING_HEADER_INTERACTIVE].join(' ')}
|
|
1026
|
+
onClick={() => setInternalExpanded((v) => !v)}
|
|
1027
|
+
aria-expanded={internalExpanded}
|
|
1028
|
+
>
|
|
1029
|
+
<span>{headerLabel}</span>
|
|
1030
|
+
<Icon
|
|
1031
|
+
name={internalExpanded ? 'chevron-up-stroked' : 'chevron-down-stroked'}
|
|
1032
|
+
size="sm"
|
|
1033
|
+
color="currentColor"
|
|
1034
|
+
aria-hidden="true"
|
|
1035
|
+
/>
|
|
1036
|
+
</button>
|
|
1037
|
+
) : (
|
|
1038
|
+
<div className={THINKING_HEADER}>
|
|
1039
|
+
<span>{headerLabel}</span>
|
|
1040
|
+
</div>
|
|
1041
|
+
)}
|
|
1042
|
+
|
|
1043
|
+
{showBody ? (
|
|
1044
|
+
<div className={THINKING_BODY}>
|
|
1045
|
+
<div
|
|
1046
|
+
className={THINKING_RAIL}
|
|
1047
|
+
style={{ backgroundColor: 'var(--color-border-line-light)' }}
|
|
1048
|
+
aria-hidden="true"
|
|
1049
|
+
/>
|
|
1050
|
+
<p className={THINKING_TEXT} style={tokenStyles.muted}>
|
|
1051
|
+
{thinkingProcess.content}
|
|
1052
|
+
</p>
|
|
1053
|
+
</div>
|
|
1054
|
+
) : null}
|
|
1055
|
+
</div>
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/* ── 任务规划卡片 ── */
|
|
1060
|
+
function TaskPlanCard({ taskPlan, tokenStyles }) {
|
|
1061
|
+
const [internalExpanded, setInternalExpanded] = useState(taskPlan.defaultExpanded);
|
|
1062
|
+
/* 点击主按钮后进入"已确认"禁用态:内容半透明、按钮禁用
|
|
1063
|
+
* defaultConfirmed=true 可让历史消息里的卡片初始就处于禁用态 */
|
|
1064
|
+
const [confirmed, setConfirmed] = useState(taskPlan.defaultConfirmed === true);
|
|
1065
|
+
/* defaultConfirmed 变化时同步内部 state(防止外部从 false → true 时不生效,例如父组件追加新消息引起的重渲染) */
|
|
1066
|
+
useEffect(() => {
|
|
1067
|
+
if (taskPlan.defaultConfirmed === true) setConfirmed(true);
|
|
1068
|
+
}, [taskPlan.defaultConfirmed]);
|
|
1069
|
+
const handlePrimary = () => {
|
|
1070
|
+
if (typeof taskPlan.onPrimaryAction === 'function') taskPlan.onPrimaryAction();
|
|
1071
|
+
setConfirmed(true);
|
|
1072
|
+
};
|
|
1073
|
+
/* 次级按钮(取消)点击同样进入"已确认"禁用态:卡片置灰、按钮禁用 */
|
|
1074
|
+
const handleSecondary = () => {
|
|
1075
|
+
if (typeof taskPlan.onSecondaryAction === 'function') taskPlan.onSecondaryAction();
|
|
1076
|
+
setConfirmed(true);
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
return (
|
|
1080
|
+
<div
|
|
1081
|
+
className={TASK_PLAN_CARD}
|
|
1082
|
+
style={{
|
|
1083
|
+
backgroundColor: 'var(--color-fill)',
|
|
1084
|
+
borderColor: 'var(--color-border-line-light)',
|
|
1085
|
+
}}
|
|
1086
|
+
data-tfds-component="ChatMessage.TaskPlan"
|
|
1087
|
+
>
|
|
1088
|
+
<div className={TASK_PLAN_HEADER}>
|
|
1089
|
+
<div
|
|
1090
|
+
className={TASK_PLAN_HEADER_ICON_WRAP}
|
|
1091
|
+
style={tokenStyles.iconWrap}
|
|
1092
|
+
aria-hidden="true"
|
|
1093
|
+
>
|
|
1094
|
+
<Icon
|
|
1095
|
+
name={taskPlan.iconName}
|
|
1096
|
+
size={16}
|
|
1097
|
+
color="var(--color-foreground-secondary)"
|
|
1098
|
+
aria-hidden="true"
|
|
1099
|
+
/>
|
|
1100
|
+
</div>
|
|
1101
|
+
<p className={TASK_PLAN_HEADER_TITLE} style={tokenStyles.title}>
|
|
1102
|
+
{taskPlan.title}
|
|
1103
|
+
</p>
|
|
1104
|
+
{!internalExpanded && taskPlan.taskCountLabel ? (
|
|
1105
|
+
<span
|
|
1106
|
+
className={TASK_PLAN_COUNT_BADGE}
|
|
1107
|
+
style={{
|
|
1108
|
+
backgroundColor: 'rgba(255,255,255,0.6)',
|
|
1109
|
+
borderColor: 'var(--color-white)',
|
|
1110
|
+
color: 'var(--color-blueGrey-700)',
|
|
1111
|
+
}}
|
|
1112
|
+
>
|
|
1113
|
+
{taskPlan.taskCountLabel}
|
|
1114
|
+
</span>
|
|
1115
|
+
) : null}
|
|
1116
|
+
<button
|
|
1117
|
+
type="button"
|
|
1118
|
+
className={TASK_PLAN_TOGGLE}
|
|
1119
|
+
onClick={() => setInternalExpanded((v) => !v)}
|
|
1120
|
+
aria-expanded={internalExpanded}
|
|
1121
|
+
aria-label={internalExpanded ? '收起任务规划' : '展开任务规划'}
|
|
1122
|
+
style={tokenStyles.muted}
|
|
1123
|
+
data-tfds-component="ChatMessage.TaskPlanToggle"
|
|
1124
|
+
>
|
|
1125
|
+
<Icon
|
|
1126
|
+
name={internalExpanded ? 'chevron-up-stroked' : 'chevron-down-stroked'}
|
|
1127
|
+
size="sm"
|
|
1128
|
+
color="var(--color-blueGrey-600)"
|
|
1129
|
+
aria-hidden="true"
|
|
1130
|
+
/>
|
|
1131
|
+
</button>
|
|
1132
|
+
</div>
|
|
1133
|
+
|
|
1134
|
+
{internalExpanded ? (
|
|
1135
|
+
<>
|
|
1136
|
+
<div
|
|
1137
|
+
className={[TASK_PLAN_BODY, confirmed ? 'opacity-60' : ''].filter(Boolean).join(' ')}
|
|
1138
|
+
style={{
|
|
1139
|
+
backgroundColor: 'rgba(255,255,255,0.6)',
|
|
1140
|
+
borderColor: 'var(--color-white)',
|
|
1141
|
+
}}
|
|
1142
|
+
>
|
|
1143
|
+
<div className={TASK_PLAN_LIST}>
|
|
1144
|
+
{taskPlan.tasks.map((task, idx) => (
|
|
1145
|
+
<div key={task.id} className={TASK_PLAN_TASK_ROW}>
|
|
1146
|
+
<div className={TASK_PLAN_TASK_HEADER}>
|
|
1147
|
+
<span
|
|
1148
|
+
className={TASK_PLAN_TASK_BADGE}
|
|
1149
|
+
style={{
|
|
1150
|
+
background: 'linear-gradient(180deg, rgba(240,240,255,0.2) 0%, rgba(136,112,255,0.2) 100%)',
|
|
1151
|
+
borderColor: 'var(--color-violet-200)',
|
|
1152
|
+
color: 'var(--color-violet-500)',
|
|
1153
|
+
}}
|
|
1154
|
+
aria-hidden="true"
|
|
1155
|
+
>
|
|
1156
|
+
{idx + 1}
|
|
1157
|
+
</span>
|
|
1158
|
+
<p className={TASK_PLAN_TASK_TITLE} style={tokenStyles.title}>
|
|
1159
|
+
{task.title}
|
|
1160
|
+
</p>
|
|
1161
|
+
</div>
|
|
1162
|
+
{task.items.map((itemText, itemIdx) => (
|
|
1163
|
+
<div key={`${task.id}-item-${itemIdx}`} className={TASK_PLAN_ITEM_ROW}>
|
|
1164
|
+
<div className={TASK_PLAN_ITEM_DOT_WRAP} aria-hidden="true">
|
|
1165
|
+
<span
|
|
1166
|
+
className={TASK_PLAN_ITEM_DOT}
|
|
1167
|
+
style={{ borderColor: 'var(--color-blueGrey-400)' }}
|
|
1168
|
+
/>
|
|
1169
|
+
</div>
|
|
1170
|
+
<p
|
|
1171
|
+
className={TASK_PLAN_ITEM_TEXT}
|
|
1172
|
+
style={{ color: 'var(--color-blueGrey-700)' }}
|
|
1173
|
+
>
|
|
1174
|
+
{itemText}
|
|
1175
|
+
</p>
|
|
1176
|
+
</div>
|
|
1177
|
+
))}
|
|
1178
|
+
</div>
|
|
1179
|
+
))}
|
|
1180
|
+
</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
{taskPlan.primaryActionLabel || taskPlan.secondaryActionLabel ? (
|
|
1183
|
+
<div className={TASK_PLAN_FOOTER}>
|
|
1184
|
+
{taskPlan.secondaryActionLabel ? (
|
|
1185
|
+
<button
|
|
1186
|
+
type="button"
|
|
1187
|
+
className={HUMAN_CONFIRM_SECONDARY_BUTTON}
|
|
1188
|
+
onClick={handleSecondary}
|
|
1189
|
+
disabled={confirmed}
|
|
1190
|
+
data-tfds-component="ChatMessage.TaskPlanAction"
|
|
1191
|
+
>
|
|
1192
|
+
{taskPlan.secondaryActionLabel}
|
|
1193
|
+
</button>
|
|
1194
|
+
) : null}
|
|
1195
|
+
{taskPlan.primaryActionLabel ? (
|
|
1196
|
+
<button
|
|
1197
|
+
type="button"
|
|
1198
|
+
className={HUMAN_CONFIRM_PRIMARY_BUTTON}
|
|
1199
|
+
onClick={handlePrimary}
|
|
1200
|
+
disabled={confirmed}
|
|
1201
|
+
data-tfds-component="ChatMessage.TaskPlanAction"
|
|
1202
|
+
>
|
|
1203
|
+
{taskPlan.primaryActionLabel}
|
|
1204
|
+
</button>
|
|
1205
|
+
) : null}
|
|
1206
|
+
</div>
|
|
1207
|
+
) : null}
|
|
1208
|
+
</>
|
|
1209
|
+
) : null}
|
|
1210
|
+
</div>
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/* ── 追问按钮组 ── */
|
|
1215
|
+
function FollowUpQuestions({ followUpQuestions, tokenStyles }) {
|
|
1216
|
+
return (
|
|
1217
|
+
<div className={FOLLOW_UP_GROUP} role="group" aria-label="建议追问">
|
|
1218
|
+
{followUpQuestions.items.map((item) => (
|
|
1219
|
+
<button
|
|
1220
|
+
key={item.id}
|
|
1221
|
+
type="button"
|
|
1222
|
+
className={FOLLOW_UP_BUTTON}
|
|
1223
|
+
style={{
|
|
1224
|
+
color: 'var(--color-blueGrey-800)',
|
|
1225
|
+
...tokenStyles.title,
|
|
1226
|
+
}}
|
|
1227
|
+
onClick={
|
|
1228
|
+
followUpQuestions.onSelect
|
|
1229
|
+
? () => followUpQuestions.onSelect(item)
|
|
1230
|
+
: undefined
|
|
1231
|
+
}
|
|
1232
|
+
data-tfds-component="ChatMessage.FollowUp"
|
|
1233
|
+
>
|
|
1234
|
+
{item.label}
|
|
1235
|
+
</button>
|
|
1236
|
+
))}
|
|
1237
|
+
</div>
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/* ── 消息操作组(支持 historyMode:默认隐藏 + 父级 hover 显示,但占位保留)── */
|
|
1242
|
+
function MessageActions({ messageActions, timestamp, layout = 'left', tokenStyles }) {
|
|
1243
|
+
const items = [];
|
|
1244
|
+
const historyClass = messageActions.historyMode ? MESSAGE_ACTIONS_HISTORY_HIDDEN : '';
|
|
1245
|
+
const groupClass = [MESSAGE_ACTIONS_GROUP, historyClass].filter(Boolean).join(' ');
|
|
1246
|
+
|
|
1247
|
+
if (layout === 'right' && timestamp) {
|
|
1248
|
+
items.push(
|
|
1249
|
+
<span
|
|
1250
|
+
key="ts-left"
|
|
1251
|
+
className={MESSAGE_ACTION_TIMESTAMP}
|
|
1252
|
+
style={tokenStyles.muted}
|
|
1253
|
+
>
|
|
1254
|
+
{timestamp}
|
|
1255
|
+
</span>,
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (messageActions.showCopy) {
|
|
1260
|
+
items.push(
|
|
1261
|
+
<button
|
|
1262
|
+
key="copy"
|
|
1263
|
+
type="button"
|
|
1264
|
+
className={MESSAGE_ACTION_BUTTON}
|
|
1265
|
+
aria-label="复制"
|
|
1266
|
+
onClick={messageActions.onCopy || undefined}
|
|
1267
|
+
style={tokenStyles.muted}
|
|
1268
|
+
>
|
|
1269
|
+
<Icon
|
|
1270
|
+
name="copy-06-stroked"
|
|
1271
|
+
size="sm"
|
|
1272
|
+
color="var(--color-blueGrey-600)"
|
|
1273
|
+
aria-hidden="true"
|
|
1274
|
+
/>
|
|
1275
|
+
</button>,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (messageActions.showQuote) {
|
|
1279
|
+
items.push(
|
|
1280
|
+
<button
|
|
1281
|
+
key="quote"
|
|
1282
|
+
type="button"
|
|
1283
|
+
className={MESSAGE_ACTION_BUTTON}
|
|
1284
|
+
aria-label="引用"
|
|
1285
|
+
onClick={messageActions.onQuote || undefined}
|
|
1286
|
+
style={tokenStyles.muted}
|
|
1287
|
+
>
|
|
1288
|
+
<Icon
|
|
1289
|
+
name="ai-quote"
|
|
1290
|
+
size="sm"
|
|
1291
|
+
color="var(--color-blueGrey-600)"
|
|
1292
|
+
aria-hidden="true"
|
|
1293
|
+
/>
|
|
1294
|
+
</button>,
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (messageActions.showLike) {
|
|
1298
|
+
items.push(
|
|
1299
|
+
<button
|
|
1300
|
+
key="like"
|
|
1301
|
+
type="button"
|
|
1302
|
+
className={MESSAGE_ACTION_BUTTON}
|
|
1303
|
+
aria-label="赞"
|
|
1304
|
+
onClick={messageActions.onLike || undefined}
|
|
1305
|
+
style={tokenStyles.muted}
|
|
1306
|
+
>
|
|
1307
|
+
<Icon
|
|
1308
|
+
name="thumbs-up-stroked"
|
|
1309
|
+
size="sm"
|
|
1310
|
+
color="var(--color-blueGrey-600)"
|
|
1311
|
+
aria-hidden="true"
|
|
1312
|
+
/>
|
|
1313
|
+
</button>,
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
if (messageActions.showDislike) {
|
|
1317
|
+
items.push(
|
|
1318
|
+
<button
|
|
1319
|
+
key="dislike"
|
|
1320
|
+
type="button"
|
|
1321
|
+
className={MESSAGE_ACTION_BUTTON}
|
|
1322
|
+
aria-label="踩"
|
|
1323
|
+
onClick={messageActions.onDislike || undefined}
|
|
1324
|
+
style={tokenStyles.muted}
|
|
1325
|
+
>
|
|
1326
|
+
<Icon
|
|
1327
|
+
name="thumbs-down-stroked"
|
|
1328
|
+
size="sm"
|
|
1329
|
+
color="var(--color-blueGrey-600)"
|
|
1330
|
+
aria-hidden="true"
|
|
1331
|
+
/>
|
|
1332
|
+
</button>,
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (layout === 'left' && timestamp) {
|
|
1337
|
+
items.push(
|
|
1338
|
+
<span
|
|
1339
|
+
key="ts-right"
|
|
1340
|
+
className={MESSAGE_ACTION_TIMESTAMP}
|
|
1341
|
+
style={tokenStyles.muted}
|
|
1342
|
+
>
|
|
1343
|
+
{timestamp}
|
|
1344
|
+
</span>,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (items.length === 0) return null;
|
|
1349
|
+
|
|
1350
|
+
return <div className={groupClass} data-tfds-component="ChatMessage.Actions">{items}</div>;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/* ── 用户附件卡(参考 ChatInput 附件卡样式,180px 文件卡)── */
|
|
1354
|
+
function UserAttachmentRow({ attachments }) {
|
|
1355
|
+
return (
|
|
1356
|
+
<div className={USER_ATTACHMENT_ROW} data-tfds-component="ChatMessage.Attachments">
|
|
1357
|
+
{attachments.map((file) => (
|
|
1358
|
+
<span key={file.id} className={USER_ATTACHMENT_CARD}>
|
|
1359
|
+
<span className={USER_ATTACHMENT_ICON_WRAP} aria-hidden="true">
|
|
1360
|
+
<img
|
|
1361
|
+
src={getUserAttachmentIcon(file)}
|
|
1362
|
+
alt=""
|
|
1363
|
+
className="block w-8 h-[34px] object-contain"
|
|
1364
|
+
/>
|
|
1365
|
+
</span>
|
|
1366
|
+
<span className={USER_ATTACHMENT_TEXT_GROUP}>
|
|
1367
|
+
<span className={USER_ATTACHMENT_NAME} title={file.name}>
|
|
1368
|
+
{file.name}
|
|
1369
|
+
</span>
|
|
1370
|
+
{file.size != null ? (
|
|
1371
|
+
<span className={USER_ATTACHMENT_SIZE}>
|
|
1372
|
+
{formatUserAttachmentSize(file.size)}
|
|
1373
|
+
</span>
|
|
1374
|
+
) : null}
|
|
1375
|
+
</span>
|
|
1376
|
+
</span>
|
|
1377
|
+
))}
|
|
1378
|
+
</div>
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/* ── 产物图标(atom):PNG 渲染,type 取自 file-type-assets 共享 lookup ──
|
|
1383
|
+
* 为什么单独抽出来:让产物图标可作为独立子组件在预览体系中枚举 / 复用
|
|
1384
|
+
*/
|
|
1385
|
+
export function ArtifactIcon({ type = 'webpage', size = 32, className = '', alt = '' }) {
|
|
1386
|
+
const src = getFileTypeIcon({ iconType: type });
|
|
1387
|
+
return (
|
|
1388
|
+
<img
|
|
1389
|
+
src={src}
|
|
1390
|
+
alt={alt}
|
|
1391
|
+
aria-hidden={alt ? undefined : 'true'}
|
|
1392
|
+
className={['block object-contain', className].filter(Boolean).join(' ')}
|
|
1393
|
+
style={{ width: size, height: size }}
|
|
1394
|
+
/>
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/* ── 产物卡片(molecule):32px ArtifactIcon + 标题 / 描述 + 右侧操作按钮 ──
|
|
1399
|
+
* 独立 export:预览或外部组件可单卡复用
|
|
1400
|
+
* 数据兼容旧 API:传入对象会先经 normalizeResultArtifact
|
|
1401
|
+
*/
|
|
1402
|
+
export function ResultArtifactCard({ artifact, className = '' }) {
|
|
1403
|
+
const a = normalizeResultArtifact(artifact);
|
|
1404
|
+
if (!a) return null;
|
|
1405
|
+
return (
|
|
1406
|
+
<div
|
|
1407
|
+
className={[RESULT_ARTIFACT_CARD, className].filter(Boolean).join(' ')}
|
|
1408
|
+
style={{ borderColor: 'var(--color-border-line-light)' }}
|
|
1409
|
+
>
|
|
1410
|
+
<div className={RESULT_ARTIFACT_ICON} aria-hidden="true">
|
|
1411
|
+
<ArtifactIcon type={a.type} size={32} />
|
|
1412
|
+
</div>
|
|
1413
|
+
<div className={RESULT_ARTIFACT_TEXT_GROUP}>
|
|
1414
|
+
<OverflowTooltipText
|
|
1415
|
+
text={a.title}
|
|
1416
|
+
textClassName={RESULT_ARTIFACT_TITLE}
|
|
1417
|
+
textStyle={{ color: 'var(--color-blueGrey-800)' }}
|
|
1418
|
+
/>
|
|
1419
|
+
{a.meta ? (
|
|
1420
|
+
<p className={RESULT_ARTIFACT_META} style={{ color: 'var(--color-blueGrey-600)' }}>
|
|
1421
|
+
{a.meta}
|
|
1422
|
+
</p>
|
|
1423
|
+
) : null}
|
|
1424
|
+
</div>
|
|
1425
|
+
<button
|
|
1426
|
+
type="button"
|
|
1427
|
+
className={RESULT_ARTIFACT_ACTION}
|
|
1428
|
+
aria-label={`${a.title || '产物'}更多操作`}
|
|
1429
|
+
style={{ color: 'var(--color-blueGrey-600)' }}
|
|
1430
|
+
>
|
|
1431
|
+
<Icon
|
|
1432
|
+
name={a.actionIconName}
|
|
1433
|
+
size={16}
|
|
1434
|
+
color="var(--color-blueGrey-600)"
|
|
1435
|
+
aria-hidden="true"
|
|
1436
|
+
/>
|
|
1437
|
+
</button>
|
|
1438
|
+
</div>
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/* ── 产物组(organism):纵向排列多张产物卡片 ── */
|
|
1443
|
+
export function ResultArtifactGroup({ artifacts, className = '' }) {
|
|
1444
|
+
const list = normalizeResultArtifacts(artifacts);
|
|
1445
|
+
if (!list.length) return null;
|
|
1446
|
+
return (
|
|
1447
|
+
<div
|
|
1448
|
+
className={['flex w-full min-w-0 flex-col items-stretch gap-2', className]
|
|
1449
|
+
.filter(Boolean)
|
|
1450
|
+
.join(' ')}
|
|
1451
|
+
>
|
|
1452
|
+
{list.map((a) => (
|
|
1453
|
+
<ResultArtifactCard key={a.id} artifact={a} />
|
|
1454
|
+
))}
|
|
1455
|
+
</div>
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/* ── 用户引用条(紫色划词条 / quote bar)── */
|
|
1460
|
+
function UserQuote({ quote }) {
|
|
1461
|
+
return (
|
|
1462
|
+
<div className={USER_QUOTE_ROW}>
|
|
1463
|
+
<div
|
|
1464
|
+
className={USER_QUOTE_CARD}
|
|
1465
|
+
style={{
|
|
1466
|
+
/* 紫色渐变背景细微调,参考 Figma:violet-50 + violet-100 边框 */
|
|
1467
|
+
background: 'var(--color-violet-50)',
|
|
1468
|
+
borderColor: 'var(--color-violet-100)',
|
|
1469
|
+
}}
|
|
1470
|
+
>
|
|
1471
|
+
{/* 左侧 32x32 紫色 icon 容器(淡紫渐变 + violet-200 边框)*/}
|
|
1472
|
+
<div
|
|
1473
|
+
className={USER_QUOTE_ICON_WRAP}
|
|
1474
|
+
style={{
|
|
1475
|
+
background:
|
|
1476
|
+
'linear-gradient(180deg, rgba(240,240,255,0.35) 0%, rgba(136,112,255,0.18) 100%)',
|
|
1477
|
+
borderColor: 'var(--color-violet-200)',
|
|
1478
|
+
}}
|
|
1479
|
+
aria-hidden="true"
|
|
1480
|
+
>
|
|
1481
|
+
<Icon name={quote.iconName} size={16} color="var(--color-violet-600)" />
|
|
1482
|
+
</div>
|
|
1483
|
+
{/* 右侧标题 + 描述 */}
|
|
1484
|
+
<div className={USER_QUOTE_BODY}>
|
|
1485
|
+
<div className={USER_QUOTE_TITLE_ROW}>
|
|
1486
|
+
{quote.cornerIconName ? (
|
|
1487
|
+
<Icon
|
|
1488
|
+
name={quote.cornerIconName}
|
|
1489
|
+
size={12}
|
|
1490
|
+
color="var(--color-foreground-secondary)"
|
|
1491
|
+
aria-hidden="true"
|
|
1492
|
+
/>
|
|
1493
|
+
) : null}
|
|
1494
|
+
<span className={USER_QUOTE_TITLE}>{quote.title}</span>
|
|
1495
|
+
</div>
|
|
1496
|
+
{quote.description ? (
|
|
1497
|
+
<p className={USER_QUOTE_DESC}>{quote.description}</p>
|
|
1498
|
+
) : null}
|
|
1499
|
+
</div>
|
|
1500
|
+
</div>
|
|
1501
|
+
</div>
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/* ── 任务进行中徽章 ── */
|
|
1506
|
+
function TaskBadge({ taskBadge }) {
|
|
1507
|
+
return (
|
|
1508
|
+
<div
|
|
1509
|
+
className={TASK_BADGE}
|
|
1510
|
+
style={{
|
|
1511
|
+
backgroundColor: 'var(--color-indigo-50)',
|
|
1512
|
+
borderColor: 'var(--color-indigo-100)',
|
|
1513
|
+
color: 'var(--color-indigo-400)',
|
|
1514
|
+
}}
|
|
1515
|
+
>
|
|
1516
|
+
<Icon name="star-04-stroked" size="sm" color="var(--color-indigo-400)" aria-hidden="true" />
|
|
1517
|
+
<span>{taskBadge}</span>
|
|
1518
|
+
</div>
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/* ── 用户气泡富文本 token 渲染 ── */
|
|
1523
|
+
function UserMessageContent({ tokens }) {
|
|
1524
|
+
return (
|
|
1525
|
+
<div className={USER_BUBBLE_CONTENT} data-tfds-component="ChatMessage.UserContent">
|
|
1526
|
+
{tokens.map((token) => {
|
|
1527
|
+
if (token.type === 'entity') {
|
|
1528
|
+
return (
|
|
1529
|
+
<span key={token.id} className={USER_ENTITY_TAG}>
|
|
1530
|
+
{token.icon ? (
|
|
1531
|
+
<span className="shrink-0 flex text-brand-500">
|
|
1532
|
+
<Icon name={token.icon} size={14} color="currentColor" aria-hidden="true" />
|
|
1533
|
+
</span>
|
|
1534
|
+
) : null}
|
|
1535
|
+
<span className="truncate max-w-[220px]">{token.label}</span>
|
|
1536
|
+
{token.showChevron ? (
|
|
1537
|
+
<Icon
|
|
1538
|
+
name="chevron-down-stroked"
|
|
1539
|
+
size="sm"
|
|
1540
|
+
color="var(--color-brand-500)"
|
|
1541
|
+
aria-hidden="true"
|
|
1542
|
+
/>
|
|
1543
|
+
) : null}
|
|
1544
|
+
</span>
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
return (
|
|
1548
|
+
<span
|
|
1549
|
+
key={token.id}
|
|
1550
|
+
className={USER_BUBBLE_TEXT}
|
|
1551
|
+
style={{ color: 'var(--color-blueGrey-800)' }}
|
|
1552
|
+
>
|
|
1553
|
+
{token.value}
|
|
1554
|
+
</span>
|
|
1555
|
+
);
|
|
1556
|
+
})}
|
|
1557
|
+
</div>
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/* ── 人工确认节点 ── */
|
|
1562
|
+
function HumanConfirmNode({ node, tokenStyles }) {
|
|
1563
|
+
/* 内部自管折叠态(兼容外部受控:node.collapsed + node.onToggleCollapsed 都传时走外部) */
|
|
1564
|
+
const isControlled = typeof node.collapsed === 'boolean' && typeof node.onToggleCollapsed === 'function';
|
|
1565
|
+
const [internalCollapsed, setInternalCollapsed] = useState(node.collapsed === true);
|
|
1566
|
+
const isCollapsed = isControlled ? node.collapsed : internalCollapsed;
|
|
1567
|
+
const handleToggle = isControlled
|
|
1568
|
+
? node.onToggleCollapsed
|
|
1569
|
+
: () => setInternalCollapsed((v) => !v);
|
|
1570
|
+
|
|
1571
|
+
/* 点击主按钮后进入"已确认"禁用态:内容半透明、按钮禁用 */
|
|
1572
|
+
const [confirmed, setConfirmed] = useState(false);
|
|
1573
|
+
const handlePrimary = () => {
|
|
1574
|
+
if (typeof node.onPrimaryAction === 'function') node.onPrimaryAction();
|
|
1575
|
+
setConfirmed(true);
|
|
1576
|
+
};
|
|
1577
|
+
const handleSecondary = () => {
|
|
1578
|
+
if (typeof node.onSecondaryAction === 'function') node.onSecondaryAction();
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
const showIntroText = node.mode === 'text-card' && node.introText && !isCollapsed;
|
|
1582
|
+
const showCardBody = !isCollapsed;
|
|
1583
|
+
|
|
1584
|
+
const header = (
|
|
1585
|
+
<div className={HUMAN_CONFIRM_HEADER}>
|
|
1586
|
+
<div className={HUMAN_CONFIRM_ICON_WRAP} style={tokenStyles.iconWrap} aria-hidden="true">
|
|
1587
|
+
<Icon name={node.iconName} size={16} color="var(--color-foreground-secondary)" aria-hidden="true" />
|
|
1588
|
+
</div>
|
|
1589
|
+
<p className={HUMAN_CONFIRM_TITLE} style={tokenStyles.title}>
|
|
1590
|
+
{node.title}
|
|
1591
|
+
</p>
|
|
1592
|
+
<button
|
|
1593
|
+
type="button"
|
|
1594
|
+
className={HUMAN_CONFIRM_TOGGLE}
|
|
1595
|
+
style={tokenStyles.muted}
|
|
1596
|
+
onClick={handleToggle}
|
|
1597
|
+
aria-label={isCollapsed ? '展开人工确认节点' : '收起人工确认节点'}
|
|
1598
|
+
aria-expanded={!isCollapsed}
|
|
1599
|
+
data-tfds-component="ChatMessage.ConfirmToggle"
|
|
1600
|
+
>
|
|
1601
|
+
<Icon
|
|
1602
|
+
name={isCollapsed ? 'chevron-down-stroked' : 'chevron-up-stroked'}
|
|
1603
|
+
size="sm"
|
|
1604
|
+
color="currentColor"
|
|
1605
|
+
aria-hidden="true"
|
|
1606
|
+
/>
|
|
1607
|
+
</button>
|
|
1608
|
+
</div>
|
|
1609
|
+
);
|
|
1610
|
+
|
|
1611
|
+
if (isCollapsed) {
|
|
1612
|
+
return (
|
|
1613
|
+
<div className={HUMAN_CONFIRM_COLLAPSED} style={tokenStyles.humanConfirmCard}>
|
|
1614
|
+
{header}
|
|
1615
|
+
</div>
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
return (
|
|
1620
|
+
<div className={HUMAN_CONFIRM_STACK} data-tfds-component="ChatMessage.Confirm">
|
|
1621
|
+
{showIntroText ? (
|
|
1622
|
+
<p className={HUMAN_CONFIRM_INTRO} style={tokenStyles.title}>
|
|
1623
|
+
{node.introText}
|
|
1624
|
+
</p>
|
|
1625
|
+
) : null}
|
|
1626
|
+
<div className={HUMAN_CONFIRM_CARD} style={tokenStyles.humanConfirmCard}>
|
|
1627
|
+
{header}
|
|
1628
|
+
{showCardBody ? (
|
|
1629
|
+
<div
|
|
1630
|
+
className={[HUMAN_CONFIRM_BODY, confirmed ? 'opacity-60' : ''].filter(Boolean).join(' ')}
|
|
1631
|
+
style={tokenStyles.humanConfirmBody}
|
|
1632
|
+
>
|
|
1633
|
+
{node.description ? (
|
|
1634
|
+
<p className={HUMAN_CONFIRM_DESCRIPTION} style={tokenStyles.title}>
|
|
1635
|
+
{node.description}
|
|
1636
|
+
</p>
|
|
1637
|
+
) : null}
|
|
1638
|
+
</div>
|
|
1639
|
+
) : null}
|
|
1640
|
+
<div className={HUMAN_CONFIRM_ACTIONS}>
|
|
1641
|
+
<button
|
|
1642
|
+
type="button"
|
|
1643
|
+
className={HUMAN_CONFIRM_SECONDARY_BUTTON}
|
|
1644
|
+
onClick={handleSecondary}
|
|
1645
|
+
disabled={confirmed}
|
|
1646
|
+
data-tfds-component="ChatMessage.ConfirmAction"
|
|
1647
|
+
>
|
|
1648
|
+
{node.secondaryActionLabel}
|
|
1649
|
+
</button>
|
|
1650
|
+
<button
|
|
1651
|
+
type="button"
|
|
1652
|
+
className={HUMAN_CONFIRM_PRIMARY_BUTTON}
|
|
1653
|
+
onClick={handlePrimary}
|
|
1654
|
+
disabled={confirmed}
|
|
1655
|
+
data-tfds-component="ChatMessage.ConfirmAction"
|
|
1656
|
+
>
|
|
1657
|
+
{node.primaryActionLabel}
|
|
1658
|
+
</button>
|
|
1659
|
+
</div>
|
|
1660
|
+
</div>
|
|
1661
|
+
</div>
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/* ── 执行流·任务组 子组件 ──
|
|
1666
|
+
* 一个任务组 = 标题栏(status icon + title + chevron 整行可点击折叠)+ 子步骤列表
|
|
1667
|
+
* processing 状态下只有最后一个 step 的卡片显示扫光动效,前面的 step 视觉上视为已完成
|
|
1668
|
+
* 自动折叠规则(业界主流:流式期间不折叠,全部完成后统一折叠):
|
|
1669
|
+
* · 流式仍在进行(isStreamingActive=true)→ 所有已出现的组保持展开(避免滚动跳闪)
|
|
1670
|
+
* · 流式结束(isStreamingActive=false)→ completed 组统一自动折叠
|
|
1671
|
+
* · 用户手动点击切换后锁定,后续状态变化不再覆盖
|
|
1672
|
+
*/
|
|
1673
|
+
function ExecutionGroup({ group, tokenStyles, isStreamingActive = false }) {
|
|
1674
|
+
/* 默认展开条件:流式中 或 自身处于 processing;其余都默认折叠(含历史消息) */
|
|
1675
|
+
const autoExpanded = isStreamingActive || group.status === 'processing';
|
|
1676
|
+
const [internalExpanded, setInternalExpanded] = useState(autoExpanded);
|
|
1677
|
+
const userToggledRef = useRef(false);
|
|
1678
|
+
const handleToggle = useCallback(() => {
|
|
1679
|
+
userToggledRef.current = true;
|
|
1680
|
+
setInternalExpanded((v) => !v);
|
|
1681
|
+
}, []);
|
|
1682
|
+
/* 自动调整:用户没手动操作过才生效;按「流式信号 + 自身状态」联合决定 */
|
|
1683
|
+
useEffect(() => {
|
|
1684
|
+
if (userToggledRef.current) return;
|
|
1685
|
+
setInternalExpanded(isStreamingActive || group.status === 'processing');
|
|
1686
|
+
}, [group.status, isStreamingActive]);
|
|
1687
|
+
const expanded = internalExpanded;
|
|
1688
|
+
const steps = group.steps;
|
|
1689
|
+
const showSteps = expanded && steps.length > 0;
|
|
1690
|
+
const showHeader = Boolean(group.title);
|
|
1691
|
+
const railStyle = { ...tokenStyles.rail, bottom: '0px' };
|
|
1692
|
+
|
|
1693
|
+
if (!showHeader && !showSteps) return null;
|
|
1694
|
+
|
|
1695
|
+
return (
|
|
1696
|
+
<div className={BODY} data-tfds-component="ChatMessage.ExecutionGroup">
|
|
1697
|
+
{showSteps ? <div className={GLOBAL_RAIL} style={railStyle} aria-hidden="true" /> : null}
|
|
1698
|
+
|
|
1699
|
+
{showHeader ? (
|
|
1700
|
+
<div
|
|
1701
|
+
className={HEADER}
|
|
1702
|
+
role={steps.length > 0 ? 'button' : undefined}
|
|
1703
|
+
tabIndex={steps.length > 0 ? 0 : undefined}
|
|
1704
|
+
aria-expanded={steps.length > 0 ? expanded : undefined}
|
|
1705
|
+
onClick={steps.length > 0 ? handleToggle : undefined}
|
|
1706
|
+
onKeyDown={steps.length > 0 ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleToggle(); } } : undefined}
|
|
1707
|
+
data-tfds-component="ChatMessage.ExecutionHeader"
|
|
1708
|
+
>
|
|
1709
|
+
<div className={HEADER_ICON_WRAP}>
|
|
1710
|
+
<StatusIndicator status={group.status} statusIconName={group.statusIconName} />
|
|
1711
|
+
</div>
|
|
1712
|
+
<p className={HEADER_TITLE}>
|
|
1713
|
+
{group.title}
|
|
1714
|
+
</p>
|
|
1715
|
+
{steps.length > 0 ? (
|
|
1716
|
+
<span className={TOGGLE} aria-hidden="true" style={tokenStyles.muted}>
|
|
1717
|
+
<Icon
|
|
1718
|
+
name="chevron-up-stroked"
|
|
1719
|
+
size="sm"
|
|
1720
|
+
color="var(--color-blueGrey-600)"
|
|
1721
|
+
className={!expanded ? 'rotate-180' : undefined}
|
|
1722
|
+
/>
|
|
1723
|
+
</span>
|
|
1724
|
+
) : null}
|
|
1725
|
+
</div>
|
|
1726
|
+
) : null}
|
|
1727
|
+
|
|
1728
|
+
{showSteps ? (
|
|
1729
|
+
<div className="flex w-full min-w-0 max-w-full flex-col items-start">
|
|
1730
|
+
{steps.map((step, idx) => {
|
|
1731
|
+
/* processing 组只有最后一条 step 显示扫光,前面的算已完成 */
|
|
1732
|
+
const isLastStep = idx === steps.length - 1;
|
|
1733
|
+
const isProcessingCard = group.status === 'processing' && isLastStep;
|
|
1734
|
+
const hasCard = Boolean(step.actionLabel || step.actionDetail);
|
|
1735
|
+
return (
|
|
1736
|
+
<div key={step.id} className={STEP_ROW}>
|
|
1737
|
+
<div className={STEP_RAIL_WRAP} aria-hidden="true" />
|
|
1738
|
+
<div className={STEP_BODY}>
|
|
1739
|
+
<p className={STEP_TITLE} style={tokenStyles.title}>{step.title}</p>
|
|
1740
|
+
{hasCard ? (
|
|
1741
|
+
<div
|
|
1742
|
+
className={[ACTION_CARD, isProcessingCard ? ACTION_CARD_SKELETON : ''].filter(Boolean).join(' ')}
|
|
1743
|
+
style={{
|
|
1744
|
+
...tokenStyles.card,
|
|
1745
|
+
...(isProcessingCard ? tokenStyles.processingCard : {}),
|
|
1746
|
+
}}
|
|
1747
|
+
data-tfds-component="ChatMessage.ExecutionAction"
|
|
1748
|
+
>
|
|
1749
|
+
<div className={ACTION_ICON_WRAP} style={tokenStyles.iconWrap}>
|
|
1750
|
+
<Icon name={step.actionIconName} size={14} color="var(--color-blueGrey-600)" aria-hidden="true" />
|
|
1751
|
+
</div>
|
|
1752
|
+
<TruncationTooltipText
|
|
1753
|
+
label={step.actionLabel}
|
|
1754
|
+
detail={step.actionDetail}
|
|
1755
|
+
labelStyle={tokenStyles.title}
|
|
1756
|
+
detailStyle={tokenStyles.muted}
|
|
1757
|
+
/>
|
|
1758
|
+
</div>
|
|
1759
|
+
) : null}
|
|
1760
|
+
</div>
|
|
1761
|
+
</div>
|
|
1762
|
+
);
|
|
1763
|
+
})}
|
|
1764
|
+
</div>
|
|
1765
|
+
) : null}
|
|
1766
|
+
</div>
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
export default function ChatMessage({
|
|
1771
|
+
/* 角色与基础 */
|
|
1772
|
+
role = 'ai',
|
|
1773
|
+
className = '',
|
|
1774
|
+
style,
|
|
1775
|
+
timestamp = '',
|
|
1776
|
+
|
|
1777
|
+
/* AI 头像(原 aiHeader) */
|
|
1778
|
+
header = null,
|
|
1779
|
+
|
|
1780
|
+
/* 深度思考(原 thinkingProcess) */
|
|
1781
|
+
thinking = null,
|
|
1782
|
+
|
|
1783
|
+
/* 引导文案:常用于 plan/confirms 上方的"我开始规划啦..." */
|
|
1784
|
+
leadText = '',
|
|
1785
|
+
|
|
1786
|
+
/* 卡片回复·任务规划(原 taskPlan) */
|
|
1787
|
+
plan = null,
|
|
1788
|
+
|
|
1789
|
+
/* 执行流:标题 + 状态 + 步骤 + 任务组 */
|
|
1790
|
+
title = DEFAULT_CHAT_TITLE,
|
|
1791
|
+
status = 'completed',
|
|
1792
|
+
statusIconName = 'check-circle-stroked',
|
|
1793
|
+
steps = DEFAULT_CHAT_STEPS,
|
|
1794
|
+
taskGroups = null,
|
|
1795
|
+
|
|
1796
|
+
/* 结果区:文本 + 产物卡 + 人工确认 */
|
|
1797
|
+
resultText = '',
|
|
1798
|
+
resultArtifact,
|
|
1799
|
+
resultArtifacts,
|
|
1800
|
+
confirms = null,
|
|
1801
|
+
|
|
1802
|
+
/* 任务进行中徽章 */
|
|
1803
|
+
taskBadge = null,
|
|
1804
|
+
|
|
1805
|
+
/* 追问(原 followUpQuestions) */
|
|
1806
|
+
followUps = null,
|
|
1807
|
+
|
|
1808
|
+
/* 操作栏(原 messageActions) */
|
|
1809
|
+
actions = null,
|
|
1810
|
+
|
|
1811
|
+
/* 用户气泡(role='user')*/
|
|
1812
|
+
userContent = null,
|
|
1813
|
+
userAttachments = null,
|
|
1814
|
+
userQuote = null,
|
|
1815
|
+
userBubbleTone = 'auto',
|
|
1816
|
+
|
|
1817
|
+
/* 折叠态兼容标记(实际折叠由子 ExecutionGroup 自管) */
|
|
1818
|
+
defaultExpanded = true,
|
|
1819
|
+
expanded,
|
|
1820
|
+
onExpandedChange,
|
|
1821
|
+
continuedBelow = false,
|
|
1822
|
+
}) {
|
|
1823
|
+
/* defaultExpanded / expanded / onExpandedChange 现在仅作为旧 API 兼容标记,
|
|
1824
|
+
真正的折叠状态由每个 ExecutionGroup 子组件自管 */
|
|
1825
|
+
void expanded;
|
|
1826
|
+
void onExpandedChange;
|
|
1827
|
+
|
|
1828
|
+
const tokenStyles = {
|
|
1829
|
+
title: { color: 'var(--color-blueGrey-800)' },
|
|
1830
|
+
muted: { color: 'var(--color-blueGrey-600)' },
|
|
1831
|
+
rail: { backgroundColor: 'var(--color-border-line-light)' },
|
|
1832
|
+
card: {
|
|
1833
|
+
borderColor: 'var(--color-border-line-light)',
|
|
1834
|
+
backgroundColor: 'var(--color-fill)',
|
|
1835
|
+
},
|
|
1836
|
+
iconWrap: {
|
|
1837
|
+
borderColor: 'var(--color-border-line-light)',
|
|
1838
|
+
backgroundImage: 'var(--tfds-ai-execution-icon-gradient)',
|
|
1839
|
+
},
|
|
1840
|
+
humanConfirmCard: {
|
|
1841
|
+
borderColor: 'var(--color-border-default)',
|
|
1842
|
+
backgroundColor: 'var(--color-fill)',
|
|
1843
|
+
},
|
|
1844
|
+
humanConfirmBody: {
|
|
1845
|
+
borderColor: 'var(--color-white)',
|
|
1846
|
+
backgroundColor: 'var(--color-card-secondary)',
|
|
1847
|
+
},
|
|
1848
|
+
processingCard: {
|
|
1849
|
+
backgroundColor: 'var(--color-fill)',
|
|
1850
|
+
backgroundImage: 'var(--tfds-ai-execution-processing-shimmer)',
|
|
1851
|
+
},
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
const normalizedAiHeader = normalizeAiHeader(header);
|
|
1855
|
+
const normalizedThinking = normalizeThinkingProcess(thinking);
|
|
1856
|
+
const normalizedTaskPlan = normalizeTaskPlan(plan);
|
|
1857
|
+
const normalizedFollowUp = normalizeFollowUpQuestions(followUps);
|
|
1858
|
+
const normalizedMessageActions = normalizeMessageActions(actions);
|
|
1859
|
+
const normalizedUserMessage = normalizeUserMessage(userContent);
|
|
1860
|
+
const normalizedUserAttachments = normalizeUserAttachments(userAttachments);
|
|
1861
|
+
const normalizedUserQuote = normalizeUserQuote(userQuote);
|
|
1862
|
+
const rootRef = useRef(null);
|
|
1863
|
+
const [autoUserBubbleTone, setAutoUserBubbleTone] = useState(DEFAULT_USER_BUBBLE_TONE);
|
|
1864
|
+
useIsomorphicLayoutEffect(() => {
|
|
1865
|
+
if (userBubbleTone !== 'auto') return;
|
|
1866
|
+
const cleanupFns = [];
|
|
1867
|
+
const updateAutoTone = () => {
|
|
1868
|
+
const nextTone = resolveAutoUserBubbleTone(rootRef.current);
|
|
1869
|
+
setAutoUserBubbleTone((currentTone) => (currentTone === nextTone ? currentTone : nextTone));
|
|
1870
|
+
};
|
|
1871
|
+
const scheduleAutoToneUpdate = () => {
|
|
1872
|
+
updateAutoTone();
|
|
1873
|
+
if (typeof window === 'undefined') return;
|
|
1874
|
+
const rafId = window.requestAnimationFrame(updateAutoTone);
|
|
1875
|
+
const timeoutId = window.setTimeout(updateAutoTone, 240);
|
|
1876
|
+
cleanupFns.push(() => window.cancelAnimationFrame(rafId));
|
|
1877
|
+
cleanupFns.push(() => window.clearTimeout(timeoutId));
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
scheduleAutoToneUpdate();
|
|
1881
|
+
|
|
1882
|
+
const ancestorNodes = getBackgroundAncestorNodes(rootRef.current);
|
|
1883
|
+
|
|
1884
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
1885
|
+
const observer = new MutationObserver(scheduleAutoToneUpdate);
|
|
1886
|
+
ancestorNodes.forEach((node) => {
|
|
1887
|
+
observer.observe(node, { attributes: true, attributeFilter: ['class', 'style'] });
|
|
1888
|
+
});
|
|
1889
|
+
cleanupFns.push(() => observer.disconnect());
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
ancestorNodes.forEach((node) => {
|
|
1893
|
+
const handleTransitionEnd = (event) => {
|
|
1894
|
+
if (!event.propertyName || event.propertyName.includes('background')) {
|
|
1895
|
+
updateAutoTone();
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
node.addEventListener('transitionend', handleTransitionEnd);
|
|
1899
|
+
cleanupFns.push(() => node.removeEventListener('transitionend', handleTransitionEnd));
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
return () => cleanupFns.forEach((cleanup) => cleanup());
|
|
1903
|
+
}, [userBubbleTone]);
|
|
1904
|
+
const resolvedUserBubbleTone = userBubbleTone === 'auto'
|
|
1905
|
+
? autoUserBubbleTone
|
|
1906
|
+
: Object.prototype.hasOwnProperty.call(USER_BUBBLE_TONE_CLASS, userBubbleTone)
|
|
1907
|
+
? userBubbleTone
|
|
1908
|
+
: DEFAULT_USER_BUBBLE_TONE;
|
|
1909
|
+
|
|
1910
|
+
/* 待用户处理的卡片(任务规划 / 配置表单)状态:未处理时不显示操作栏;
|
|
1911
|
+
* 历史消息(plan.defaultConfirmed=true 或 confirms[i].defaultConfirmed=true)
|
|
1912
|
+
* 视为已处理,初始就显示操作栏占位 */
|
|
1913
|
+
const initialCardActioned =
|
|
1914
|
+
Boolean(plan?.defaultConfirmed) ||
|
|
1915
|
+
(Array.isArray(confirms) && confirms.some((n) => n?.defaultConfirmed));
|
|
1916
|
+
const [cardActioned, setCardActioned] = useState(initialCardActioned);
|
|
1917
|
+
/* defaultConfirmed 后续切到 true 时同步内部 state(防止外部异步标记不生效) */
|
|
1918
|
+
useEffect(() => {
|
|
1919
|
+
if (initialCardActioned) setCardActioned(true);
|
|
1920
|
+
}, [initialCardActioned]);
|
|
1921
|
+
const wrapPrimaryAction = (originalFn) => () => {
|
|
1922
|
+
if (typeof originalFn === 'function') originalFn();
|
|
1923
|
+
setCardActioned(true);
|
|
1924
|
+
};
|
|
1925
|
+
/* 取消按钮也算作"已处理":消息变为历史消息后操作栏占位需要保留 */
|
|
1926
|
+
const wrapSecondaryAction = (originalFn) => () => {
|
|
1927
|
+
if (typeof originalFn === 'function') originalFn();
|
|
1928
|
+
setCardActioned(true);
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
/* role='user' 分支:用户气泡(按 8% 容器宽度做左侧缩进,500px 容器=40px 缩进)*/
|
|
1932
|
+
if (role === 'user') {
|
|
1933
|
+
return (
|
|
1934
|
+
<div ref={rootRef} className={[ROOT, className].filter(Boolean).join(' ')} style={style} data-tfds-component="ChatMessage">
|
|
1935
|
+
<div className={USER_BUBBLE_WRAP} style={{ paddingLeft: '8%' }}>
|
|
1936
|
+
{/* 附件区:与气泡分离的独立行,右对齐,多个附件可换行 */}
|
|
1937
|
+
{normalizedUserAttachments ? (
|
|
1938
|
+
<UserAttachmentRow attachments={normalizedUserAttachments} />
|
|
1939
|
+
) : null}
|
|
1940
|
+
{/* 紫色划词条 / 引用:位于附件下方、气泡上方 */}
|
|
1941
|
+
{normalizedUserQuote ? <UserQuote quote={normalizedUserQuote} /> : null}
|
|
1942
|
+
{/* 文本气泡:仅当有文本(含 entity 混排)时才渲染 */}
|
|
1943
|
+
{normalizedUserMessage ? (
|
|
1944
|
+
<div className={`${USER_BUBBLE} ${USER_BUBBLE_TONE_CLASS[resolvedUserBubbleTone]}`} data-tfds-component="ChatMessage.Bubble">
|
|
1945
|
+
<UserMessageContent tokens={normalizedUserMessage} />
|
|
1946
|
+
</div>
|
|
1947
|
+
) : null}
|
|
1948
|
+
{normalizedMessageActions || timestamp ? (
|
|
1949
|
+
<MessageActions
|
|
1950
|
+
messageActions={
|
|
1951
|
+
normalizedMessageActions || {
|
|
1952
|
+
showCopy: false,
|
|
1953
|
+
showQuote: false,
|
|
1954
|
+
showLike: false,
|
|
1955
|
+
showDislike: false,
|
|
1956
|
+
historyMode: false,
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
timestamp={timestamp}
|
|
1960
|
+
layout="right"
|
|
1961
|
+
tokenStyles={tokenStyles}
|
|
1962
|
+
/>
|
|
1963
|
+
) : null}
|
|
1964
|
+
</div>
|
|
1965
|
+
</div>
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/* role='ai' 简化分支:requesting / thinking 状态只显示 AI 头像 + loading 占位 */
|
|
1970
|
+
if (status === 'requesting' || status === 'thinking') {
|
|
1971
|
+
return (
|
|
1972
|
+
<div className={[ROOT, className].filter(Boolean).join(' ')} style={style} data-tfds-component="ChatMessage">
|
|
1973
|
+
<div className="flex w-full min-w-0 flex-col items-start gap-2">
|
|
1974
|
+
{normalizedAiHeader ? <AIHeader aiHeader={normalizedAiHeader} /> : null}
|
|
1975
|
+
{status === 'requesting' ? (
|
|
1976
|
+
<RequestingDots />
|
|
1977
|
+
) : (
|
|
1978
|
+
<p className={THINKING_INLINE_LABEL}>
|
|
1979
|
+
{normalizedThinking?.inProgressLabel || '深度思考中 ...'}
|
|
1980
|
+
</p>
|
|
1981
|
+
)}
|
|
1982
|
+
</div>
|
|
1983
|
+
</div>
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/* role='ai' 完整分支 */
|
|
1988
|
+
const normalizedTaskGroups = normalizeTaskGroups({
|
|
1989
|
+
taskGroups,
|
|
1990
|
+
title,
|
|
1991
|
+
status,
|
|
1992
|
+
statusIconName,
|
|
1993
|
+
defaultExpanded,
|
|
1994
|
+
steps,
|
|
1995
|
+
});
|
|
1996
|
+
const normalizedHumanConfirmNodes = normalizeHumanConfirmNodes(confirms);
|
|
1997
|
+
const normalizedResultArtifacts = normalizeResultArtifacts(resultArtifacts, resultArtifact);
|
|
1998
|
+
const hasHumanConfirmResult = normalizedHumanConfirmNodes.length > 0;
|
|
1999
|
+
const hasResultContent =
|
|
2000
|
+
Boolean(resultText) || normalizedResultArtifacts.length > 0 || hasHumanConfirmResult;
|
|
2001
|
+
|
|
2002
|
+
/* 是否含待处理的交互卡片(任务规划 / 配置表单):未处理状态下不显示操作栏 */
|
|
2003
|
+
const hasInteractiveCard = Boolean(normalizedTaskPlan) || hasHumanConfirmResult;
|
|
2004
|
+
const trackedTaskPlan = normalizedTaskPlan
|
|
2005
|
+
? {
|
|
2006
|
+
...normalizedTaskPlan,
|
|
2007
|
+
onPrimaryAction: wrapPrimaryAction(normalizedTaskPlan.onPrimaryAction),
|
|
2008
|
+
onSecondaryAction: wrapSecondaryAction(normalizedTaskPlan.onSecondaryAction),
|
|
2009
|
+
}
|
|
2010
|
+
: null;
|
|
2011
|
+
const trackedHumanConfirmNodes = normalizedHumanConfirmNodes.map((node) => ({
|
|
2012
|
+
...node,
|
|
2013
|
+
onPrimaryAction: wrapPrimaryAction(node.onPrimaryAction),
|
|
2014
|
+
onSecondaryAction: wrapSecondaryAction(node.onSecondaryAction),
|
|
2015
|
+
}));
|
|
2016
|
+
const showMessageActions = Boolean(normalizedMessageActions) && (!hasInteractiveCard || cardActioned);
|
|
2017
|
+
|
|
2018
|
+
return (
|
|
2019
|
+
<div className={[ROOT, className].filter(Boolean).join(' ')} style={style} data-tfds-component="ChatMessage">
|
|
2020
|
+
<div className="flex w-full min-w-0 flex-col items-start gap-2">
|
|
2021
|
+
{normalizedAiHeader ? <AIHeader aiHeader={normalizedAiHeader} /> : null}
|
|
2022
|
+
|
|
2023
|
+
{normalizedThinking ? (
|
|
2024
|
+
<ThinkingProcess thinkingProcess={normalizedThinking} tokenStyles={tokenStyles} />
|
|
2025
|
+
) : null}
|
|
2026
|
+
|
|
2027
|
+
{leadText ? (
|
|
2028
|
+
<p className={RESULT_TEXT} style={tokenStyles.title}>
|
|
2029
|
+
{leadText}
|
|
2030
|
+
</p>
|
|
2031
|
+
) : null}
|
|
2032
|
+
|
|
2033
|
+
{trackedTaskPlan ? (
|
|
2034
|
+
<TaskPlanCard taskPlan={trackedTaskPlan} tokenStyles={tokenStyles} />
|
|
2035
|
+
) : null}
|
|
2036
|
+
|
|
2037
|
+
{(() => {
|
|
2038
|
+
/* 流式态判定:只要还有 group 处于 processing,就视为执行流仍在进行
|
|
2039
|
+
* 流式期间所有已出现的组保持展开(避免逐组折叠造成滚动跳闪)
|
|
2040
|
+
* 全部完成后再统一折叠(业界主流做法:Cursor / Claude Code / ChatGPT thinking) */
|
|
2041
|
+
const isStreamingActive = normalizedTaskGroups.some((g) => g.status === 'processing');
|
|
2042
|
+
return normalizedTaskGroups.map((group) => (
|
|
2043
|
+
<ExecutionGroup
|
|
2044
|
+
key={group.id}
|
|
2045
|
+
group={group}
|
|
2046
|
+
tokenStyles={tokenStyles}
|
|
2047
|
+
isStreamingActive={isStreamingActive}
|
|
2048
|
+
/>
|
|
2049
|
+
));
|
|
2050
|
+
})()}
|
|
2051
|
+
|
|
2052
|
+
{hasResultContent ? (
|
|
2053
|
+
<div className={RESULT_ROW}>
|
|
2054
|
+
<div className={RESULT_NODE}>
|
|
2055
|
+
{resultText ? (
|
|
2056
|
+
<p className={RESULT_TEXT} style={tokenStyles.title}>
|
|
2057
|
+
{resultText}
|
|
2058
|
+
</p>
|
|
2059
|
+
) : null}
|
|
2060
|
+
|
|
2061
|
+
{hasHumanConfirmResult ? (
|
|
2062
|
+
<div className={HUMAN_CONFIRM_SECTION}>
|
|
2063
|
+
{trackedHumanConfirmNodes.map((node) => (
|
|
2064
|
+
<HumanConfirmNode key={node.id} node={node} tokenStyles={tokenStyles} />
|
|
2065
|
+
))}
|
|
2066
|
+
</div>
|
|
2067
|
+
) : null}
|
|
2068
|
+
|
|
2069
|
+
{normalizedResultArtifacts.map((artifact) => (
|
|
2070
|
+
<div
|
|
2071
|
+
key={artifact.id}
|
|
2072
|
+
className={RESULT_ARTIFACT_CARD}
|
|
2073
|
+
style={{ borderColor: 'var(--color-border-line-light)' }}
|
|
2074
|
+
>
|
|
2075
|
+
<div className={RESULT_ARTIFACT_ICON} aria-hidden="true">
|
|
2076
|
+
<ArtifactIcon type={artifact.type} size={32} />
|
|
2077
|
+
</div>
|
|
2078
|
+
|
|
2079
|
+
<div className={RESULT_ARTIFACT_TEXT_GROUP}>
|
|
2080
|
+
<OverflowTooltipText
|
|
2081
|
+
text={artifact.title}
|
|
2082
|
+
textClassName={RESULT_ARTIFACT_TITLE}
|
|
2083
|
+
textStyle={tokenStyles.title}
|
|
2084
|
+
/>
|
|
2085
|
+
{artifact.meta ? (
|
|
2086
|
+
<p className={RESULT_ARTIFACT_META} style={tokenStyles.muted}>
|
|
2087
|
+
{artifact.meta}
|
|
2088
|
+
</p>
|
|
2089
|
+
) : null}
|
|
2090
|
+
</div>
|
|
2091
|
+
|
|
2092
|
+
<button
|
|
2093
|
+
type="button"
|
|
2094
|
+
className={RESULT_ARTIFACT_ACTION}
|
|
2095
|
+
aria-label={`${artifact.title || '产物'}更多操作`}
|
|
2096
|
+
style={tokenStyles.muted}
|
|
2097
|
+
data-tfds-component="ChatMessage.ArtifactAction"
|
|
2098
|
+
>
|
|
2099
|
+
<Icon
|
|
2100
|
+
name={artifact.actionIconName}
|
|
2101
|
+
size={16}
|
|
2102
|
+
color="var(--color-blueGrey-600)"
|
|
2103
|
+
aria-hidden="true"
|
|
2104
|
+
/>
|
|
2105
|
+
</button>
|
|
2106
|
+
</div>
|
|
2107
|
+
))}
|
|
2108
|
+
</div>
|
|
2109
|
+
</div>
|
|
2110
|
+
) : null}
|
|
2111
|
+
|
|
2112
|
+
{taskBadge ? <TaskBadge taskBadge={taskBadge} /> : null}
|
|
2113
|
+
|
|
2114
|
+
{showMessageActions ? (
|
|
2115
|
+
<MessageActions
|
|
2116
|
+
messageActions={normalizedMessageActions}
|
|
2117
|
+
timestamp={timestamp}
|
|
2118
|
+
layout="left"
|
|
2119
|
+
tokenStyles={tokenStyles}
|
|
2120
|
+
/>
|
|
2121
|
+
) : null}
|
|
2122
|
+
|
|
2123
|
+
{normalizedFollowUp ? (
|
|
2124
|
+
<FollowUpQuestions followUpQuestions={normalizedFollowUp} tokenStyles={tokenStyles} />
|
|
2125
|
+
) : null}
|
|
2126
|
+
</div>
|
|
2127
|
+
</div>
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/* ────────────────────────────────────────────────────────────
|
|
2132
|
+
* useStreamingTaskGroups — 执行流·流式输出 hook(业务层调用)
|
|
2133
|
+
*
|
|
2134
|
+
* 把一份「最终态」taskGroups 按节奏逐步推出,模拟 AI 实时执行:
|
|
2135
|
+
* 1. 第 1 组首个 step 立即出现,组 status='processing'(带扫光)
|
|
2136
|
+
* 2. 每过 intervalMs,向当前组追加下一个 step
|
|
2137
|
+
* 3. 当前组所有 step 都出齐后,把该组改为 'completed',
|
|
2138
|
+
* 下一 tick 推下一组首个 step(仍 processing)
|
|
2139
|
+
* 4. 全部推完 → 末组转 'completed',停止计时器
|
|
2140
|
+
*
|
|
2141
|
+
* 注意:传入数据中 group.status 会被 hook 接管,调用方传啥都行(建议传完整数据)。
|
|
2142
|
+
*
|
|
2143
|
+
* @param {Array} fullTaskGroups — 完整任务组数据(含所有 group/steps)
|
|
2144
|
+
* @param {object} [options]
|
|
2145
|
+
* @param {number} [options.intervalMs=600] — 每条 step 出现的间隔(ms)
|
|
2146
|
+
* @param {boolean} [options.enabled=true] — 是否启用流式(false 直接返回完整数据)
|
|
2147
|
+
* @returns {Array} 当前可见的 taskGroups 切片(结构与入参一致,可直接传给 ChatMessage)
|
|
2148
|
+
*
|
|
2149
|
+
* @example
|
|
2150
|
+
* function MyExecutionMessage({ taskGroups }) {
|
|
2151
|
+
* const streamed = useStreamingTaskGroups(taskGroups, { intervalMs: 600 });
|
|
2152
|
+
* return <ChatMessage header taskGroups={streamed} />;
|
|
2153
|
+
* }
|
|
2154
|
+
*/
|
|
2155
|
+
export function useStreamingTaskGroups(fullTaskGroups, options = {}) {
|
|
2156
|
+
const { intervalMs = 600, enabled = true } = options;
|
|
2157
|
+
const groups = Array.isArray(fullTaskGroups) ? fullTaskGroups : [];
|
|
2158
|
+
const totalSteps = groups.reduce((sum, g) => sum + (g?.steps?.length ?? 0), 0);
|
|
2159
|
+
|
|
2160
|
+
/* pointer:当前进度指针 [groupIdx, stepIdx],finished 标记全部播放完毕 */
|
|
2161
|
+
const [pointer, setPointer] = useState({ groupIdx: 0, stepIdx: 0, finished: false });
|
|
2162
|
+
|
|
2163
|
+
useEffect(() => {
|
|
2164
|
+
if (!enabled || groups.length === 0 || totalSteps === 0) {
|
|
2165
|
+
return undefined;
|
|
2166
|
+
}
|
|
2167
|
+
/* 重置:从第 1 组首个 step 开始 */
|
|
2168
|
+
setPointer({ groupIdx: 0, stepIdx: 0, finished: false });
|
|
2169
|
+
|
|
2170
|
+
const timer = setInterval(() => {
|
|
2171
|
+
setPointer((prev) => {
|
|
2172
|
+
if (prev.finished) return prev;
|
|
2173
|
+
const { groupIdx, stepIdx } = prev;
|
|
2174
|
+
const stepCount = groups[groupIdx]?.steps?.length ?? 0;
|
|
2175
|
+
/* 当前组还有下一个 step → 步进 */
|
|
2176
|
+
if (stepIdx + 1 < stepCount) {
|
|
2177
|
+
return { groupIdx, stepIdx: stepIdx + 1, finished: false };
|
|
2178
|
+
}
|
|
2179
|
+
/* 当前组完成 → 切到下一组的第 1 个 step */
|
|
2180
|
+
if (groupIdx + 1 < groups.length) {
|
|
2181
|
+
return { groupIdx: groupIdx + 1, stepIdx: 0, finished: false };
|
|
2182
|
+
}
|
|
2183
|
+
/* 已是最后一组的最后一步 → 标记完成(最后一组也会被切为 completed)*/
|
|
2184
|
+
return { groupIdx, stepIdx, finished: true };
|
|
2185
|
+
});
|
|
2186
|
+
}, intervalMs);
|
|
2187
|
+
|
|
2188
|
+
return () => clearInterval(timer);
|
|
2189
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
2190
|
+
}, [enabled, intervalMs, totalSteps]);
|
|
2191
|
+
|
|
2192
|
+
/* 关闭流式 → 直接返回原数组(不裁剪、不接管 status) */
|
|
2193
|
+
if (!enabled) return groups;
|
|
2194
|
+
if (groups.length === 0) return groups;
|
|
2195
|
+
|
|
2196
|
+
const { groupIdx, stepIdx, finished } = pointer;
|
|
2197
|
+
|
|
2198
|
+
/* 输出:已完成组完整保留 / 当前组裁切 + processing / 未到达组隐藏 */
|
|
2199
|
+
return groups
|
|
2200
|
+
.map((g, i) => {
|
|
2201
|
+
if (i < groupIdx) {
|
|
2202
|
+
return { ...g, status: 'completed' };
|
|
2203
|
+
}
|
|
2204
|
+
if (i === groupIdx) {
|
|
2205
|
+
const visibleSteps = (g.steps ?? []).slice(0, stepIdx + 1);
|
|
2206
|
+
return {
|
|
2207
|
+
...g,
|
|
2208
|
+
status: finished ? 'completed' : 'processing',
|
|
2209
|
+
steps: visibleSteps,
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
return null;
|
|
2213
|
+
})
|
|
2214
|
+
.filter(Boolean);
|
|
2215
|
+
}
|