@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.
Files changed (176) hide show
  1. package/AI_READ_FIRST.md +131 -0
  2. package/LICENSE +21 -0
  3. package/README.md +353 -0
  4. package/package.json +67 -0
  5. package/scripts/check-tfds-contract.mjs +334 -0
  6. package/scripts/check-tfds-integration.mjs +263 -0
  7. package/scripts/postinstall-cursor-skill.mjs +382 -0
  8. package/scripts/setup.mjs +520 -0
  9. package/skills/tfds/CHECKLIST.md +205 -0
  10. package/skills/tfds/COMMON_FAILURES.md +238 -0
  11. package/skills/tfds/DESIGN_PRINCIPLES.md +477 -0
  12. package/skills/tfds/GLOBAL_DESIGN_RULES.md +636 -0
  13. package/skills/tfds/LAYOUT_RECIPES.md +140 -0
  14. package/skills/tfds/LAYOUT_RULES.md +1355 -0
  15. package/skills/tfds/PAGE_ARCHETYPES.md +201 -0
  16. package/skills/tfds/SKILL.md +188 -0
  17. package/skills/tfds/components.index.json +7305 -0
  18. package/skills/tfds/components.summary.json +1809 -0
  19. package/src/_b_end_runtime/components/AiSuggestionShared.jsx +166 -0
  20. package/src/_b_end_runtime/components/Avatar.jsx +325 -0
  21. package/src/_b_end_runtime/components/Avatar.tokens.js +76 -0
  22. package/src/_b_end_runtime/components/AvatarGridPreview.jsx +56 -0
  23. package/src/_b_end_runtime/components/AvatarGroup.jsx +80 -0
  24. package/src/_b_end_runtime/components/AvatarGroup.tokens.js +28 -0
  25. package/src/_b_end_runtime/components/Button.jsx +144 -0
  26. package/src/_b_end_runtime/components/Button.tokens.js +90 -0
  27. package/src/_b_end_runtime/components/Card.jsx +460 -0
  28. package/src/_b_end_runtime/components/Card.tokens.js +124 -0
  29. package/src/_b_end_runtime/components/CardPreview.jsx +51 -0
  30. package/src/_b_end_runtime/components/ChatBubble.jsx +384 -0
  31. package/src/_b_end_runtime/components/ChatBubble.tokens.js +60 -0
  32. package/src/_b_end_runtime/components/ChatBubblePreview.jsx +129 -0
  33. package/src/_b_end_runtime/components/ChatInput.jsx +1399 -0
  34. package/src/_b_end_runtime/components/ChatInput.tokens.js +75 -0
  35. package/src/_b_end_runtime/components/ChatMessage.jsx +2215 -0
  36. package/src/_b_end_runtime/components/ChatMessage.tokens.js +257 -0
  37. package/src/_b_end_runtime/components/ChatMessagePreview.jsx +388 -0
  38. package/src/_b_end_runtime/components/Checkbox.jsx +317 -0
  39. package/src/_b_end_runtime/components/Checkbox.tokens.js +59 -0
  40. package/src/_b_end_runtime/components/ConversationList.jsx +1264 -0
  41. package/src/_b_end_runtime/components/ConversationList.tokens.js +135 -0
  42. package/src/_b_end_runtime/components/ConversationListPreview.jsx +108 -0
  43. package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +324 -0
  44. package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +69 -0
  45. package/src/_b_end_runtime/components/DatePicker.jsx +739 -0
  46. package/src/_b_end_runtime/components/DatePicker.tokens.js +99 -0
  47. package/src/_b_end_runtime/components/Empty.jsx +141 -0
  48. package/src/_b_end_runtime/components/Empty.tokens.js +40 -0
  49. package/src/_b_end_runtime/components/Form.jsx +609 -0
  50. package/src/_b_end_runtime/components/Form.tokens.js +77 -0
  51. package/src/_b_end_runtime/components/FormFieldStack.jsx +123 -0
  52. package/src/_b_end_runtime/components/FormFieldStack.tokens.js +12 -0
  53. package/src/_b_end_runtime/components/FormTitle.jsx +119 -0
  54. package/src/_b_end_runtime/components/FormTitle.tokens.js +87 -0
  55. package/src/_b_end_runtime/components/FullScreenPage.jsx +97 -0
  56. package/src/_b_end_runtime/components/FullScreenPage.tokens.js +19 -0
  57. package/src/_b_end_runtime/components/Icon.jsx +172 -0
  58. package/src/_b_end_runtime/components/Icon.tokens.js +26 -0
  59. package/src/_b_end_runtime/components/IconGridPreview.jsx +277 -0
  60. package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +620 -0
  61. package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +71 -0
  62. package/src/_b_end_runtime/components/InfoDisplayPanelPreview.jsx +133 -0
  63. package/src/_b_end_runtime/components/Input.jsx +258 -0
  64. package/src/_b_end_runtime/components/Input.tokens.js +68 -0
  65. package/src/_b_end_runtime/components/InputNumber.jsx +242 -0
  66. package/src/_b_end_runtime/components/InputNumber.tokens.js +55 -0
  67. package/src/_b_end_runtime/components/Modal.jsx +155 -0
  68. package/src/_b_end_runtime/components/Modal.tokens.js +73 -0
  69. package/src/_b_end_runtime/components/NavBar.jsx +842 -0
  70. package/src/_b_end_runtime/components/NavBar.tokens.js +97 -0
  71. package/src/_b_end_runtime/components/NavBarPreview.jsx +11 -0
  72. package/src/_b_end_runtime/components/Radio.jsx +227 -0
  73. package/src/_b_end_runtime/components/Radio.tokens.js +59 -0
  74. package/src/_b_end_runtime/components/Select.jsx +766 -0
  75. package/src/_b_end_runtime/components/Select.tokens.js +99 -0
  76. package/src/_b_end_runtime/components/Sheet.jsx +132 -0
  77. package/src/_b_end_runtime/components/Sheet.tokens.js +61 -0
  78. package/src/_b_end_runtime/components/Slider.jsx +346 -0
  79. package/src/_b_end_runtime/components/Slider.tokens.js +47 -0
  80. package/src/_b_end_runtime/components/Switch.jsx +124 -0
  81. package/src/_b_end_runtime/components/Switch.tokens.js +38 -0
  82. package/src/_b_end_runtime/components/Table.jsx +1338 -0
  83. package/src/_b_end_runtime/components/Table.tokens.js +147 -0
  84. package/src/_b_end_runtime/components/TablePreview.jsx +599 -0
  85. package/src/_b_end_runtime/components/Tabs.jsx +149 -0
  86. package/src/_b_end_runtime/components/Tabs.tokens.js +102 -0
  87. package/src/_b_end_runtime/components/Tag.jsx +199 -0
  88. package/src/_b_end_runtime/components/Tag.tokens.js +171 -0
  89. package/src/_b_end_runtime/components/TagBar.jsx +1134 -0
  90. package/src/_b_end_runtime/components/TagBar.tokens.js +75 -0
  91. package/src/_b_end_runtime/components/TagGridPreview.jsx +23 -0
  92. package/src/_b_end_runtime/components/TagInput.jsx +382 -0
  93. package/src/_b_end_runtime/components/TagInput.tokens.js +52 -0
  94. package/src/_b_end_runtime/components/TextArea.jsx +363 -0
  95. package/src/_b_end_runtime/components/TextArea.tokens.js +65 -0
  96. package/src/_b_end_runtime/components/TimePicker.jsx +444 -0
  97. package/src/_b_end_runtime/components/TimePicker.tokens.js +77 -0
  98. package/src/_b_end_runtime/components/Toast.jsx +120 -0
  99. package/src/_b_end_runtime/components/Toast.tokens.js +146 -0
  100. package/src/_b_end_runtime/components/Tooltip.jsx +282 -0
  101. package/src/_b_end_runtime/components/Tooltip.tokens.js +48 -0
  102. package/src/_b_end_runtime/components/TooltipPreview.jsx +50 -0
  103. package/src/_b_end_runtime/components/Upload.jsx +455 -0
  104. package/src/_b_end_runtime/components/Upload.tokens.js +47 -0
  105. package/src/_b_end_runtime/components/avatar-assets/avatar-default.png +0 -0
  106. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-1.png +0 -0
  107. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-2.png +0 -0
  108. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-3.png +0 -0
  109. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-4.png +0 -0
  110. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-5.png +0 -0
  111. package/src/_b_end_runtime/components/empty-assets/administrator-1.svg +40 -0
  112. package/src/_b_end_runtime/components/empty-assets/administrator-2.svg +33 -0
  113. package/src/_b_end_runtime/components/empty-assets/construction.svg +33 -0
  114. package/src/_b_end_runtime/components/empty-assets/failure.svg +49 -0
  115. package/src/_b_end_runtime/components/empty-assets/idle.svg +34 -0
  116. package/src/_b_end_runtime/components/empty-assets/no-access.svg +36 -0
  117. package/src/_b_end_runtime/components/empty-assets/no-content.svg +77 -0
  118. package/src/_b_end_runtime/components/empty-assets/no-result.svg +61 -0
  119. package/src/_b_end_runtime/components/empty-assets/not-found.svg +46 -0
  120. package/src/_b_end_runtime/components/empty-assets/success.svg +38 -0
  121. package/src/_b_end_runtime/components/file-type-assets/batch-report.png +0 -0
  122. package/src/_b_end_runtime/components/file-type-assets/catcat.svg +21 -0
  123. package/src/_b_end_runtime/components/file-type-assets/code.png +0 -0
  124. package/src/_b_end_runtime/components/file-type-assets/conversation.png +0 -0
  125. package/src/_b_end_runtime/components/file-type-assets/document.png +0 -0
  126. package/src/_b_end_runtime/components/file-type-assets/feishu-card.png +0 -0
  127. package/src/_b_end_runtime/components/file-type-assets/feishu-sheet.png +0 -0
  128. package/src/_b_end_runtime/components/file-type-assets/feishu.png +0 -0
  129. package/src/_b_end_runtime/components/file-type-assets/image.png +0 -0
  130. package/src/_b_end_runtime/components/file-type-assets/index.js +105 -0
  131. package/src/_b_end_runtime/components/file-type-assets/knowledge.png +0 -0
  132. package/src/_b_end_runtime/components/file-type-assets/pdf.png +0 -0
  133. package/src/_b_end_runtime/components/file-type-assets/pe.png +0 -0
  134. package/src/_b_end_runtime/components/file-type-assets/strategy.png +0 -0
  135. package/src/_b_end_runtime/components/file-type-assets/table.png +0 -0
  136. package/src/_b_end_runtime/components/file-type-assets/webpage.png +0 -0
  137. package/src/_b_end_runtime/components/file-type-assets/xmind.png +0 -0
  138. package/src/_b_end_runtime/components/icons/icon-data.js +12496 -0
  139. package/src/_b_end_runtime/components/nav-bar-assets/bytehi-logo-mark.svg +21 -0
  140. package/src/_b_end_runtime/components/table-assets/avatar.png +0 -0
  141. package/src/_b_end_runtime/components/table-assets/button.png +0 -0
  142. package/src/_b_end_runtime/components/table-assets/icon-chevron-down.png +0 -0
  143. package/src/_b_end_runtime/components/table-cell-assets/avatar.png +0 -0
  144. package/src/_b_end_runtime/components/table-cell-assets/button.png +0 -0
  145. package/src/_b_end_runtime/components/table-cell-assets/checkbox.png +0 -0
  146. package/src/_b_end_runtime/components/table-cell-assets/icon-chevron-right.png +0 -0
  147. package/src/_b_end_runtime/components/table-cell-assets/icon.png +0 -0
  148. package/src/_b_end_runtime/components/table-cell-assets/semi-icons-handle.png +0 -0
  149. package/src/_b_end_runtime/components/table-cell-assets/semi-icons-tree-triangle-right.png +0 -0
  150. package/src/_b_end_runtime/components/table-cell-assets/switch.png +0 -0
  151. package/src/_b_end_runtime/components/tagShared.js +3 -0
  152. package/src/_b_end_runtime/components/team-avatar-assets/chengcheng-murphy.png +0 -0
  153. package/src/_b_end_runtime/components/team-avatar-assets/duan-ran.png +0 -0
  154. package/src/_b_end_runtime/components/team-avatar-assets/guo-zhezhi.png +0 -0
  155. package/src/_b_end_runtime/components/team-avatar-assets/li-siru.png +0 -0
  156. package/src/_b_end_runtime/components/team-avatar-assets/liu-delin.png +0 -0
  157. package/src/_b_end_runtime/components.js +3499 -0
  158. package/src/_b_end_runtime/index.js +9 -0
  159. package/src/_b_end_runtime/page-patterns/BasePageFramePattern.jsx +395 -0
  160. package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +989 -0
  161. package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +281 -0
  162. package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +380 -0
  163. package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +392 -0
  164. package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +590 -0
  165. package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +237 -0
  166. package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +189 -0
  167. package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +594 -0
  168. package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +87 -0
  169. package/src/_b_end_runtime/page-patterns/pageListShared.jsx +177 -0
  170. package/src/_b_end_runtime/patterns.js +428 -0
  171. package/src/_b_end_runtime/preview-registry.jsx +4719 -0
  172. package/src/_b_end_runtime/teamMembers.js +56 -0
  173. package/src/_b_end_runtime/tokens.js +500 -0
  174. package/src/index.d.ts +1073 -0
  175. package/src/index.js +52 -0
  176. package/theme.css +350 -0
@@ -0,0 +1,989 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import Button from '../components/Button';
3
+ import Icon from '../components/Icon';
4
+ import ChatInput from '../components/ChatInput';
5
+ import catcatSvg from '../components/file-type-assets/catcat.svg';
6
+ import ChatMessage, {
7
+ DEFAULT_CHAT_FOLLOW_UPS,
8
+ DEFAULT_CHAT_RESULT,
9
+ DEFAULT_CHAT_RESULT_ARTIFACTS,
10
+ DEFAULT_CHAT_TASK_GROUPS,
11
+ DEFAULT_CHAT_PLAN,
12
+ DEFAULT_CHAT_THINKING,
13
+ useStreamingTaskGroups,
14
+ } from '../components/ChatMessage';
15
+
16
+ /**
17
+ * ChatConversationPattern — B 端 AI 对话页 / OLA AI 会话详情
18
+ *
19
+ * 布局:
20
+ * - 外框:灰底会话工作区(不带左侧 NavBar),业务页面根容器不额外加整页圆角或描边
21
+ * - 顶导栏 shrink-0 → 中部消息区 flex-1 内滚自动滚底 → 底部 ChatInput shrink-0 吸底
22
+ * - 800px 居中对话流
23
+ *
24
+ * 状态:
25
+ * - phase = 'chat' → 默认进入,展示端到端 mock 对话(覆盖全部 ChatMessage 子组件)
26
+ * - phase = 'welcome' → 点「新会话」清空回到欢迎屏(Hero + 居中 ChatInput + 推荐 prompt)
27
+ *
28
+ * 关键词路由(输入框发送文本时):
29
+ * - 含「整理 / 分析 / 生成 / 梳理」 → 走完整任务规划链路:
30
+ * 1. AI 引导文本 + 任务规划卡(待用户处理)
31
+ * 2. 用户点「开始执行任务」→ 卡片置灰 + 追加用户消息「开始执行任务」
32
+ * 3. AI 流式执行流(每 600ms 推一步,用 useStreamingTaskGroups)
33
+ * 4. 全部跑完 → 追加 AI 短答 + 产物组 + 追问按钮组
34
+ * - 含「停止 / 取消」 → 短答「已停止当前任务」
35
+ * - 其他 → 短答从备选句池随机一句
36
+ *
37
+ * 操作栏 historyMode:
38
+ * - 仅最后一条消息 historyMode=false(操作栏常显)
39
+ * - 其余历史消息 historyMode=true(hover 父消息时才显示,占位高度始终保留)
40
+ */
41
+
42
+ const STREAM_INTERVAL = 600;
43
+
44
+ /* ── 端到端 mock 对话 ── */
45
+ function buildInitMessages() {
46
+ return [
47
+ /* m1 用户:富文本 + tag + 附件 */
48
+ {
49
+ id: 'm1',
50
+ kind: 'user',
51
+ timestamp: '14:02',
52
+ userContent: [
53
+ { type: 'text', value: '帮我整理 ' },
54
+ {
55
+ type: 'entity',
56
+ icon: 'message-chat-square-stroked',
57
+ label: '智能会话:社交私信',
58
+ showChevron: true,
59
+ },
60
+ { type: 'text', value: ' 场景近 7 天的人工解决率,做一份分析报告' },
61
+ ],
62
+ userAttachments: [
63
+ { id: 'att-1', name: '抖音电商售后政策汇编.pdf', size: 327680 },
64
+ ],
65
+ },
66
+
67
+ /* m2 AI(合并:深度思考 + 引导文本 + 任务规划卡,已被用户处理 → 禁用置灰态) */
68
+ {
69
+ id: 'm2',
70
+ kind: 'ai-task-plan',
71
+ timestamp: '14:02',
72
+ thinking: {
73
+ ...DEFAULT_CHAT_THINKING,
74
+ state: 'completed',
75
+ defaultExpanded: false,
76
+ },
77
+ leadText: '好的,我先做一份任务规划,请稍后...',
78
+ planConfirmed: true,
79
+ },
80
+
81
+ /* m3 用户:开始执行任务 */
82
+ {
83
+ id: 'm3',
84
+ kind: 'user',
85
+ timestamp: '14:03',
86
+ userContent: [{ type: 'text', value: '开始执行任务' }],
87
+ },
88
+
89
+ /* m4 AI:完整执行流消息(执行流 + 总结文本 + 产物组 + 追问) */
90
+ {
91
+ id: 'm4',
92
+ kind: 'ai-flow',
93
+ timestamp: '14:08',
94
+ taskGroups: DEFAULT_CHAT_TASK_GROUPS.map((g) => ({ ...g, status: 'completed' })),
95
+ resultText: DEFAULT_CHAT_RESULT,
96
+ resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS,
97
+ followUps: DEFAULT_CHAT_FOLLOW_UPS,
98
+ },
99
+ ];
100
+ }
101
+
102
+ /* ── 关键词 → 回复路由 ── */
103
+ const TASK_KEYWORDS = ['整理', '分析', '生成', '梳理', '输出', '汇总'];
104
+ const STOP_KEYWORDS = ['停止', '取消'];
105
+ const GENERIC_REPLIES = [
106
+ '收到,已记下来。',
107
+ '明白了,我马上处理。',
108
+ '没问题,需要我同步给团队吗?',
109
+ '好的,我先按这个思路推进。',
110
+ ];
111
+
112
+ const FOLLOW_UP_REPLIES = {
113
+ 批量测试当前策略: {
114
+ resultText:
115
+ '已基于当前策略版本完成一轮模拟批测:覆盖 240 条近 7 天售后会话,重点校验退货退款、换货、拒收、运费归属 4 类路径。通过 226 条,需人工复核 14 条,风险主要集中在「签收超时但存在物流异常」和「凭证缺失但用户强诉求」两个边界场景。建议先修正升级口径,再进入 10% 灰度。',
116
+ resultArtifacts: [
117
+ {
118
+ id: 'policy-batch-report',
119
+ type: 'batch-report',
120
+ title: '当前策略批量测试报告',
121
+ meta: '批测 · 240 条样本',
122
+ },
123
+ {
124
+ id: 'policy-risk-table',
125
+ type: 'table',
126
+ title: '未通过与需复核样本明细',
127
+ meta: '表格 · 14 条高风险',
128
+ },
129
+ ],
130
+ followUps: ['查看未通过样本', '生成策略修复建议', '同步到飞书文档'],
131
+ },
132
+ 批量测试当前节点: {
133
+ resultText:
134
+ '已围绕当前节点做节点级模拟测试:共回放 96 条命中样本,覆盖入参缺失、类目白名单、签收时效、凭证有效性、运费判责 5 个分支。当前节点通过率 92.7%,失败样本多出现在「凭证材料为空但节点仍继续下钻」的路径,建议补一个必填校验和兜底转人工分支。',
135
+ resultArtifacts: [
136
+ {
137
+ id: 'node-batch-report',
138
+ type: 'batch-report',
139
+ title: '当前节点回放测试报告',
140
+ meta: '批测 · 96 条样本',
141
+ },
142
+ {
143
+ id: 'node-coverage-sheet',
144
+ type: 'table',
145
+ title: '节点分支覆盖矩阵',
146
+ meta: '表格 · 5 个分支',
147
+ },
148
+ ],
149
+ followUps: ['定位失败分支', '补充节点断言', '生成回归测试集'],
150
+ },
151
+ '开启 AB 实验': {
152
+ resultText:
153
+ '已生成 AB 实验草案:A 组保持当前售后策略,B 组启用优化后的凭证校验和升级口径,建议先以 10% 流量灰度 24 小时。主指标看人工解决率和一次答复命中率,护栏指标看客诉率、转人工率、平均处理时长;若护栏波动超过 3%,自动暂停 B 组放量。',
154
+ resultArtifacts: [
155
+ {
156
+ id: 'ab-strategy',
157
+ type: 'strategy',
158
+ title: '售后策略 AB 实验方案',
159
+ meta: '策略 · 10% 灰度',
160
+ },
161
+ {
162
+ id: 'ab-metric-sheet',
163
+ type: 'table',
164
+ title: 'AB 实验指标看板配置',
165
+ meta: '表格 · 7 个指标',
166
+ },
167
+ ],
168
+ followUps: ['生成实验配置', '补充护栏指标', '查看放量计划'],
169
+ },
170
+ };
171
+
172
+ const PROMPT_SUGGESTIONS = [
173
+ '整理抖音电商售后退换货政策',
174
+ '分析智能会话场景近 7 天解决率',
175
+ '生成 AB 实验方案',
176
+ ];
177
+
178
+ function nowHHmm() {
179
+ const d = new Date();
180
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
181
+ }
182
+
183
+ function routeReply(text) {
184
+ if (FOLLOW_UP_REPLIES[text]) {
185
+ return { type: 'follow-up-result', ...FOLLOW_UP_REPLIES[text] };
186
+ }
187
+ if (STOP_KEYWORDS.some((k) => text.includes(k))) {
188
+ return { type: 'short', text: '已停止当前任务,需要时再叫我。' };
189
+ }
190
+ if (TASK_KEYWORDS.some((k) => text.includes(k))) {
191
+ return { type: 'task' };
192
+ }
193
+ return { type: 'short', text: GENERIC_REPLIES[Math.floor(Math.random() * GENERIC_REPLIES.length)] };
194
+ }
195
+
196
+ /* ── 单条消息 → ChatMessage props 映射 ──
197
+ * isLatest:是否为列表中最后一条(决定 historyMode 是否启用)
198
+ * handlers:所有交互回调集合
199
+ * · onPlanConfirm(msgId) — 任务规划卡「开始执行任务」点击
200
+ * · onPlanCancel(msgId) — 任务规划卡「取消」点击
201
+ * · onFollowUpSelect(text) — 追问 chip 点击(默认作为新一轮对话发出)
202
+ */
203
+ function messageToChatProps(msg, isLatest, handlers = {}) {
204
+ const { onPlanConfirm, onPlanCancel, onFollowUpSelect } = handlers;
205
+ const baseProps = {
206
+ className: msg.className || '',
207
+ };
208
+ const baseActions = {
209
+ showCopy: true,
210
+ showQuote: true,
211
+ showLike: true,
212
+ showDislike: true,
213
+ historyMode: !isLatest,
214
+ };
215
+
216
+ /* 把字符串数组包装成 { items, onSelect } 形式,让点击 chip 直接发起会话 */
217
+ const wrapFollowUps = (raw) => {
218
+ if (!raw) return null;
219
+ const items = Array.isArray(raw) ? raw : raw.items;
220
+ if (!Array.isArray(items) || items.length === 0) return null;
221
+ return {
222
+ items,
223
+ onSelect: (item) => {
224
+ const label = typeof item === 'string' ? item : item?.label;
225
+ if (label && onFollowUpSelect) onFollowUpSelect(label);
226
+ },
227
+ };
228
+ };
229
+
230
+ if (msg.kind === 'user') {
231
+ return {
232
+ ...baseProps,
233
+ role: 'user',
234
+ timestamp: msg.timestamp,
235
+ userContent: msg.userContent,
236
+ userAttachments: msg.userAttachments,
237
+ actions: baseActions,
238
+ };
239
+ }
240
+
241
+ if (msg.kind === 'ai-text') {
242
+ return {
243
+ ...baseProps,
244
+ role: 'ai',
245
+ header: true,
246
+ title: '',
247
+ steps: null,
248
+ resultText: msg.resultText,
249
+ timestamp: msg.timestamp,
250
+ actions: baseActions,
251
+ };
252
+ }
253
+
254
+ if (msg.kind === 'ai-thinking') {
255
+ return {
256
+ ...baseProps,
257
+ role: 'ai',
258
+ header: true,
259
+ title: '',
260
+ steps: null,
261
+ thinking: msg.thinking,
262
+ resultText: msg.resultText,
263
+ timestamp: msg.timestamp,
264
+ actions: baseActions,
265
+ };
266
+ }
267
+
268
+ if (msg.kind === 'ai-task-plan') {
269
+ /* 任务规划卡:未操作时操作栏自动隐藏(ChatMessage 内部 showMessageActions 兜底),
270
+ * 处理后变为历史消息按 historyMode 走;defaultConfirmed=true 让历史卡片直接禁用置灰
271
+ * 可选叠加 thinking,让深度思考 + 引导文本 + 卡片合并到同一条 AI 消息中 */
272
+ return {
273
+ ...baseProps,
274
+ role: 'ai',
275
+ header: true,
276
+ title: '',
277
+ steps: null,
278
+ thinking: msg.thinking || null,
279
+ leadText: msg.leadText,
280
+ plan: {
281
+ ...DEFAULT_CHAT_PLAN,
282
+ defaultConfirmed: msg.planConfirmed === true,
283
+ onPrimaryAction: () => onPlanConfirm && onPlanConfirm(msg.id),
284
+ onSecondaryAction: () => onPlanCancel && onPlanCancel(msg.id),
285
+ },
286
+ timestamp: msg.timestamp,
287
+ actions: baseActions,
288
+ };
289
+ }
290
+
291
+ if (msg.kind === 'ai-flow') {
292
+ /* 完整执行流消息:执行流 + 总结文本 + 产物组 + 追问 都可以同时存在;
293
+ * 流式播放期间(stream=true)整条消息不显示操作栏,播完后转为最新消息显示 */
294
+ return {
295
+ ...baseProps,
296
+ role: 'ai',
297
+ header: true,
298
+ title: '',
299
+ steps: null,
300
+ taskGroups: msg.taskGroups,
301
+ resultText: msg.resultText,
302
+ resultArtifacts: msg.resultArtifacts,
303
+ followUps: wrapFollowUps(msg.followUps),
304
+ taskBadge: msg.taskBadge,
305
+ timestamp: msg.timestamp,
306
+ actions: msg.stream === true ? null : baseActions,
307
+ };
308
+ }
309
+
310
+ /* ai-result:兼容旧消息形态(短答文本 + 产物组 + 追问,独立成一条) */
311
+ return {
312
+ ...baseProps,
313
+ role: 'ai',
314
+ header: true,
315
+ title: '',
316
+ steps: null,
317
+ resultText: msg.resultText,
318
+ resultArtifacts: msg.resultArtifacts,
319
+ followUps: wrapFollowUps(msg.followUps),
320
+ taskBadge: msg.taskBadge,
321
+ timestamp: msg.timestamp,
322
+ actions: baseActions,
323
+ };
324
+ }
325
+
326
+ export default function ChatConversationPattern({
327
+ title = '抖音电商客服售后政策梳理',
328
+ }) {
329
+ const [phase, setPhase] = useState('chat'); // 'chat' | 'welcome'
330
+ const [messages, setMessages] = useState(buildInitMessages);
331
+ /* ── ChatInput 受控状态机 ──
332
+ * inputView:'default' | 'replying' | 'busy'
333
+ * · default → 静止 / 失焦 / AI 完成回复(含 replying & busy 完成)
334
+ * · replying → AI 短答中(短文本回复 600ms)
335
+ * · busy → AI 任务执行中(流式跑 taskGroups)
336
+ * prefillText / prefillSeed:欢迎页推荐 chip 点击时把文本回填到输入框,等用户主动点发送 */
337
+ const [inputView, setInputView] = useState('default');
338
+ const [prefillText, setPrefillText] = useState('');
339
+ const [prefillSeed, setPrefillSeed] = useState(0);
340
+ const replyTimerRef = useRef(null);
341
+ const busyTimerRef = useRef(null);
342
+ const scrollRef = useRef(null);
343
+ const idSeedRef = useRef(100);
344
+ const nextId = useCallback((prefix = 'm') => `${prefix}${++idSeedRef.current}`, []);
345
+
346
+ /* chip 回填到输入框(覆盖当前草稿 + 自动聚焦) */
347
+ const prefillInput = useCallback((text) => {
348
+ setPrefillText(text || '');
349
+ setPrefillSeed((s) => s + 1);
350
+ }, []);
351
+
352
+ /* 卸载时清掉计时器,避免内存泄漏 / setState on unmounted */
353
+ useEffect(
354
+ () => () => {
355
+ if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
356
+ if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
357
+ },
358
+ [],
359
+ );
360
+
361
+ /* 自动滚到底:用 ResizeObserver 监听消息容器内容高度变化
362
+ * 覆盖所有场景:新消息追加、流式 step 逐步推出、ChatMessage 内部折叠展开 */
363
+ useEffect(() => {
364
+ if (phase !== 'chat') return undefined;
365
+ const el = scrollRef.current;
366
+ if (!el) return undefined;
367
+ const inner = el.firstElementChild;
368
+ if (!inner) return undefined;
369
+ const ro = new ResizeObserver(() => {
370
+ el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
371
+ });
372
+ ro.observe(inner);
373
+ return () => ro.disconnect();
374
+ }, [phase]);
375
+
376
+ /* 重置回欢迎状态 */
377
+ const handleNewSession = useCallback(() => {
378
+ setMessages([]);
379
+ setPhase('welcome');
380
+ }, []);
381
+
382
+ /* ── 取消链路:用户在任务规划卡里点「取消」 ──
383
+ * 1. 标记该任务规划卡为「已确认」(卡片置灰)
384
+ * 2. 追加用户消息「取消」
385
+ * 3. ChatInput 进 replying(短答态)+ 600ms 后追加 AI 寒暄 + 回 default */
386
+ const handlePlanCancel = useCallback(
387
+ (planMsgId) => {
388
+ setMessages((prev) =>
389
+ prev.map((m) => (m.id === planMsgId ? { ...m, planConfirmed: true } : m)),
390
+ );
391
+ setMessages((prev) => [
392
+ ...prev,
393
+ {
394
+ id: nextId('u'),
395
+ kind: 'user',
396
+ timestamp: nowHHmm(),
397
+ userContent: [{ type: 'text', value: '取消' }],
398
+ },
399
+ ]);
400
+ setInputView('replying');
401
+ if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
402
+ replyTimerRef.current = setTimeout(() => {
403
+ setMessages((prev) => [
404
+ ...prev,
405
+ {
406
+ id: nextId('a'),
407
+ kind: 'ai-text',
408
+ timestamp: nowHHmm(),
409
+ resultText: '好的,已取消任务。还有其他问题随时可以找我~',
410
+ },
411
+ ]);
412
+ setInputView('default');
413
+ }, 600);
414
+ },
415
+ [nextId],
416
+ );
417
+
418
+ /* ── 任务规划链路:用户在任务规划卡里点「开始执行任务」 ── */
419
+ const handlePlanConfirm = useCallback(
420
+ (planMsgId) => {
421
+ /* 1) 标记该任务规划卡为「已确认」(持久化到消息数据,重渲染时也是禁用态) */
422
+ setMessages((prev) =>
423
+ prev.map((m) => (m.id === planMsgId ? { ...m, planConfirmed: true } : m)),
424
+ );
425
+
426
+ const flowMsgId = nextId('flow');
427
+
428
+ /* 2) 立即追加:用户消息「开始执行任务」+ AI 流式执行流(带 stream 标记,由 StreamingChatMessage 接管) */
429
+ setMessages((prev) => [
430
+ ...prev,
431
+ {
432
+ id: nextId('u'),
433
+ kind: 'user',
434
+ timestamp: nowHHmm(),
435
+ userContent: [{ type: 'text', value: '开始执行任务' }],
436
+ },
437
+ {
438
+ id: flowMsgId,
439
+ kind: 'ai-flow',
440
+ timestamp: nowHHmm(),
441
+ taskGroups: DEFAULT_CHAT_TASK_GROUPS,
442
+ stream: true,
443
+ },
444
+ ]);
445
+
446
+ /* 3) ChatInput 进 busy(任务执行态,编辑器锁定,仅显示状态文案 + 停止按钮) */
447
+ setInputView('busy');
448
+
449
+ /* 4) 估算流式总时长:(总 step 数 + 1 buffer) × 600ms,跑完后追加结果消息 + 输入框回 default */
450
+ const totalSteps = DEFAULT_CHAT_TASK_GROUPS.reduce(
451
+ (sum, g) => sum + (g.steps?.length ?? 0),
452
+ 0,
453
+ );
454
+ const totalMs = (totalSteps + 1) * STREAM_INTERVAL;
455
+ if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
456
+ busyTimerRef.current = setTimeout(() => {
457
+ /* 流式播完 → 更新同一条消息:取消 stream 标记 + 所有组转 completed +
458
+ * 补上总结文本 + 产物组 + 追问。整条消息此时变成「完整 AI 回复」一条到底 */
459
+ setMessages((prev) =>
460
+ prev.map((m) =>
461
+ m.id === flowMsgId
462
+ ? {
463
+ ...m,
464
+ stream: false,
465
+ taskGroups: DEFAULT_CHAT_TASK_GROUPS.map((g) => ({
466
+ ...g,
467
+ status: 'completed',
468
+ })),
469
+ resultText: DEFAULT_CHAT_RESULT,
470
+ resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS,
471
+ followUps: DEFAULT_CHAT_FOLLOW_UPS,
472
+ timestamp: nowHHmm(),
473
+ }
474
+ : m,
475
+ ),
476
+ );
477
+ setInputView('default');
478
+ }, totalMs);
479
+ },
480
+ [nextId],
481
+ );
482
+
483
+ /* ── 用户输入框发送 ──
484
+ * 状态机:default → replying → 600ms 后追加 AI 回复 → default
485
+ * 任务关键词分支也走 replying 态(正在规划中),规划卡推出后回 default 等用户处理卡片 */
486
+ const handleSend = useCallback(
487
+ (text, ctx = {}) => {
488
+ const trimmed = (text || '').trim();
489
+ if (!trimmed) return;
490
+
491
+ /* 欢迎屏发送第一条消息 → 切到 chat 阶段 */
492
+ if (phase === 'welcome') setPhase('chat');
493
+
494
+ const userTime = nowHHmm();
495
+
496
+ /* 优先用 ctx.segments 还原 chip + 文本混排结构(保留 entity tag 渲染)
497
+ * 若拿不到 segments(旧调用方)则回退到一段纯 text token */
498
+ const userContent = Array.isArray(ctx.segments) && ctx.segments.length > 0
499
+ ? ctx.segments
500
+ : [{ type: 'text', value: trimmed }];
501
+
502
+ /* 1) 立即追加用户气泡 + ChatInput 进 replying(短答态,编辑器锁定) */
503
+ setMessages((prev) => [
504
+ ...prev,
505
+ {
506
+ id: nextId('u'),
507
+ kind: 'user',
508
+ timestamp: userTime,
509
+ userContent,
510
+ className: ctx.source === 'follow-up' ? 'tfds-followup-user-pop' : '',
511
+ },
512
+ ]);
513
+ setInputView('replying');
514
+
515
+ const reply = routeReply(trimmed);
516
+ if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
517
+
518
+ /* 2) 追问结果:点击 follow-up chip 等价于立即发送,并模拟生成对应的新答案 */
519
+ if (reply.type === 'follow-up-result') {
520
+ replyTimerRef.current = setTimeout(() => {
521
+ setMessages((prev) => [
522
+ ...prev,
523
+ {
524
+ id: nextId('a'),
525
+ kind: 'ai-result',
526
+ timestamp: nowHHmm(),
527
+ resultText: reply.resultText,
528
+ resultArtifacts: reply.resultArtifacts,
529
+ followUps: reply.followUps,
530
+ className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
531
+ },
532
+ ]);
533
+ setInputView('default');
534
+ }, 600);
535
+ return;
536
+ }
537
+
538
+ /* 3) 任务流:AI 推规划卡(待用户处理);规划卡出现后输入框回 default 让用户操作卡片 */
539
+ if (reply.type === 'task') {
540
+ replyTimerRef.current = setTimeout(() => {
541
+ setMessages((prev) => [
542
+ ...prev,
543
+ {
544
+ id: nextId('plan'),
545
+ kind: 'ai-task-plan',
546
+ timestamp: nowHHmm(),
547
+ leadText: '我开始规划啦,请稍后...',
548
+ planConfirmed: false,
549
+ },
550
+ ]);
551
+ setInputView('default');
552
+ }, 600);
553
+ return;
554
+ }
555
+
556
+ /* 4) 短答:600ms 后追加 AI 短答 + 输入框回 default */
557
+ replyTimerRef.current = setTimeout(() => {
558
+ setMessages((prev) => [
559
+ ...prev,
560
+ {
561
+ id: nextId('a'),
562
+ kind: 'ai-text',
563
+ timestamp: nowHHmm(),
564
+ resultText: reply.text,
565
+ className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
566
+ },
567
+ ]);
568
+ setInputView('default');
569
+ }, 600);
570
+ },
571
+ [phase, nextId],
572
+ );
573
+
574
+ const handleFollowUpSend = useCallback(
575
+ (text) => {
576
+ handleSend(text, { source: 'follow-up' });
577
+ },
578
+ [handleSend],
579
+ );
580
+
581
+ /* ── 用户主动停止:replying / busy 期间点 ChatInput 的停止按钮 ──
582
+ * 立即清掉计时器 + 输入框回 default + 追加 AI 短答提示 */
583
+ const handleStop = useCallback(() => {
584
+ if (replyTimerRef.current) {
585
+ clearTimeout(replyTimerRef.current);
586
+ replyTimerRef.current = null;
587
+ }
588
+ if (busyTimerRef.current) {
589
+ clearTimeout(busyTimerRef.current);
590
+ busyTimerRef.current = null;
591
+ }
592
+ setInputView('default');
593
+ setMessages((prev) => [
594
+ ...prev,
595
+ {
596
+ id: nextId('a'),
597
+ kind: 'ai-text',
598
+ timestamp: nowHHmm(),
599
+ resultText: '已停止当前任务,需要时再叫我。',
600
+ },
601
+ ]);
602
+ }, [nextId]);
603
+
604
+ /* 新会话:清空消息 + 切欢迎屏 + 重置输入框状态 */
605
+ const handleNewSessionFull = useCallback(() => {
606
+ if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
607
+ if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
608
+ setInputView('default');
609
+ handleNewSession();
610
+ }, [handleNewSession]);
611
+
612
+ const lastIdx = messages.length - 1;
613
+
614
+ /* 当前阶段标题:欢迎屏标题简化为「新会话」 */
615
+ const displayTitle = phase === 'welcome' ? '新会话' : title;
616
+
617
+ return (
618
+ <div
619
+ className="flex h-full w-full min-h-0 flex-col overflow-hidden"
620
+ style={{
621
+ flex: '1 0 auto',
622
+ alignSelf: 'stretch',
623
+ background: 'var(--color-blueGrey-200, #F2F4F7)',
624
+ borderRadius: 'inherit',
625
+ border: '1px solid var(--color-border-default, #E4E7EC)',
626
+ }}
627
+ >
628
+ <TopBar title={displayTitle} onNewSession={handleNewSessionFull} disableNewSession={phase === 'welcome'} />
629
+
630
+ {phase === 'chat' ? (
631
+ <ChatPhase
632
+ scrollRef={scrollRef}
633
+ messages={messages}
634
+ lastIdx={lastIdx}
635
+ handlers={{
636
+ onPlanConfirm: handlePlanConfirm,
637
+ onPlanCancel: handlePlanCancel,
638
+ onFollowUpSelect: handleFollowUpSend,
639
+ }}
640
+ onSend={handleSend}
641
+ onStop={handleStop}
642
+ inputView={inputView}
643
+ prefillText={prefillText}
644
+ prefillSeed={prefillSeed}
645
+ />
646
+ ) : (
647
+ <WelcomePhase
648
+ onSend={handleSend}
649
+ onStop={handleStop}
650
+ onPrefill={prefillInput}
651
+ inputView={inputView}
652
+ prefillText={prefillText}
653
+ prefillSeed={prefillSeed}
654
+ />
655
+ )}
656
+ </div>
657
+ );
658
+ }
659
+
660
+ /* ============================================================
661
+ * ChatPhase — 对话阶段:消息流 + 底部 ChatInput
662
+ * ============================================================ */
663
+ function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inputView, prefillText, prefillSeed }) {
664
+ /* 上下 40px 渐隐遮罩:让消息进入 / 离开滚动视口时柔和过渡,不硬切
665
+ * 顶部渐隐仅在「可向上滚」时启用,回到第一条时取消,避免首条被遮淡 */
666
+ const [atTop, setAtTop] = useState(true);
667
+ useEffect(() => {
668
+ const el = scrollRef.current;
669
+ if (!el) return undefined;
670
+ const update = () => setAtTop(el.scrollTop <= 4);
671
+ update();
672
+ el.addEventListener('scroll', update, { passive: true });
673
+ return () => el.removeEventListener('scroll', update);
674
+ }, [scrollRef, messages.length]);
675
+ const FADE_MASK = atTop
676
+ ? 'linear-gradient(to bottom, #000 0, #000 calc(100% - 40px), transparent 100%)'
677
+ : 'linear-gradient(to bottom, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%)';
678
+ return (
679
+ <>
680
+ <style>
681
+ {`
682
+ @keyframes tfds-followup-user-pop {
683
+ 0% { opacity: 0; transform: translate(28px, -8px) scale(0.98); }
684
+ 100% { opacity: 1; transform: translate(0, 0) scale(1); }
685
+ }
686
+ @keyframes tfds-followup-ai-pop {
687
+ 0% { opacity: 0; transform: translateY(8px); }
688
+ 100% { opacity: 1; transform: translateY(0); }
689
+ }
690
+ .tfds-followup-user-pop {
691
+ animation: tfds-followup-user-pop 240ms cubic-bezier(0.22, 1, 0.36, 1) both;
692
+ transform-origin: right center;
693
+ }
694
+ .tfds-followup-ai-pop {
695
+ animation: tfds-followup-ai-pop 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
696
+ }
697
+ @media (prefers-reduced-motion: reduce) {
698
+ .tfds-followup-user-pop,
699
+ .tfds-followup-ai-pop {
700
+ animation: none;
701
+ }
702
+ }
703
+ `}
704
+ </style>
705
+
706
+ {/* 消息区:自适应剩余高度 + 内滚 + 上下渐隐 */}
707
+ <div
708
+ ref={scrollRef}
709
+ className="w-full flex-1 min-h-0 overflow-y-auto"
710
+ style={{
711
+ padding: '8px 0',
712
+ maskImage: FADE_MASK,
713
+ WebkitMaskImage: FADE_MASK,
714
+ }}
715
+ >
716
+ <div
717
+ className="mx-auto flex w-full flex-col gap-2"
718
+ /* 底部 60px padding:避免最后一条消息贴住底部 ChatInput */
719
+ style={{ width: '800px', maxWidth: '100%', padding: '0 20px 60px' }}
720
+ >
721
+ {messages.map((m, idx) => {
722
+ const isLatest = idx === lastIdx;
723
+ const props = messageToChatProps(m, isLatest, handlers);
724
+ /* 流式执行流走子组件:挂载即按 600ms 节奏推 step;流式期间不传操作栏 */
725
+ if (m.kind === 'ai-flow' && m.stream === true) {
726
+ return <StreamingChatMessage key={m.id} {...props} taskGroups={m.taskGroups} />;
727
+ }
728
+ return <ChatMessage key={m.id} {...props} />;
729
+ })}
730
+ </div>
731
+ </div>
732
+
733
+ {/* 底部 ChatInput:shrink-0 吸底 */}
734
+ <div className="w-full shrink-0" style={{ padding: '12px 0 20px' }}>
735
+ <div
736
+ className="mx-auto w-full"
737
+ style={{ width: '800px', maxWidth: '100%', padding: '0 8px' }}
738
+ >
739
+ {/* ChatInput 受控状态机:variant 由父组件 inputView 决定(default/replying/busy 切换)
740
+ * autoReplyOnSend=false 关掉内部"发送即跳 replying"自动行为,统一由父组件根据消息链路调度
741
+ * prefillSeed 变化时把 prefillText 覆盖到编辑器并聚焦(用于欢迎页推荐 chip 回填) */}
742
+ <ChatInput
743
+ variant={inputView}
744
+ autoReplyOnSend={false}
745
+ prefillText={prefillText}
746
+ prefillSeed={prefillSeed}
747
+ placeholder="继续对话,或输入「整理 / 分析 / 生成」触发任务流..."
748
+ onSend={onSend}
749
+ onStop={onStop}
750
+ />
751
+ </div>
752
+ </div>
753
+ </>
754
+ );
755
+ }
756
+
757
+ /* ============================================================
758
+ * WelcomePhase — 欢迎阶段:CATCAT 头像 + OLA AI 标题 + 欢迎语 + 推荐 chip
759
+ * 复用 CopilotPagePattern 的欢迎态视觉,ChatInput 仍底部吸底
760
+ * ============================================================ */
761
+ function WelcomePhase({ onSend, onStop, onPrefill, inputView, prefillText, prefillSeed }) {
762
+ return (
763
+ <>
764
+ {/* 中部 hero:自适应剩余高度,居中展示头像/标题/欢迎语/chips */}
765
+ <div className="flex flex-1 min-h-0 w-full items-center justify-center overflow-y-auto">
766
+ <div
767
+ className="flex w-full flex-col items-center justify-center gap-6"
768
+ style={{ width: '440px', maxWidth: '100%', padding: '40px 24px' }}
769
+ >
770
+ {/* AI 头像 + 标题 */}
771
+ <div className="flex flex-col items-center gap-3">
772
+ {/* 头像:渐变描边圆 + 蓝色投影 + CATCAT */}
773
+ <div style={{ position: 'relative', width: '66px', height: '66px', flexShrink: 0 }}>
774
+ <div
775
+ style={{
776
+ position: 'absolute',
777
+ inset: '0',
778
+ borderRadius: '50%',
779
+ padding: '0.5px',
780
+ background:
781
+ 'linear-gradient(135deg, rgba(63,226,213,0.6) 0%, rgba(64,147,224,0.6) 35%, rgba(122,97,250,0.6) 65%, rgba(214,130,235,0.6) 100%)',
782
+ }}
783
+ >
784
+ <div
785
+ style={{
786
+ width: '100%',
787
+ height: '100%',
788
+ borderRadius: '50%',
789
+ background: [
790
+ 'linear-gradient(42deg, #FFF 11.61%, rgba(255,255,255,0.00) 37.84%)',
791
+ 'linear-gradient(74deg, rgba(63,226,213,0.15) 12.18%, rgba(64,147,224,0.15) 39.9%, rgba(122,97,250,0.15) 63.86%, rgba(214,130,235,0.15) 86.38%)',
792
+ '#FFF',
793
+ ].join(', '),
794
+ boxShadow: '0px 8px 15px rgba(180,218,244,0.50)',
795
+ }}
796
+ />
797
+ </div>
798
+ <img
799
+ src={catcatSvg}
800
+ alt="OLA AI"
801
+ style={{
802
+ position: 'absolute',
803
+ width: '32px',
804
+ height: '32px',
805
+ top: '50%',
806
+ left: '50%',
807
+ transform: 'translate(-50%, -50%)',
808
+ zIndex: 1,
809
+ }}
810
+ />
811
+ </div>
812
+ <span
813
+ className="whitespace-nowrap [font-weight:var(--font-semibold)]"
814
+ style={{
815
+ fontSize: '18px',
816
+ lineHeight: '24px',
817
+ letterSpacing: '-0.03em',
818
+ color: 'var(--foreground, #0F1C35)',
819
+ }}
820
+ >
821
+ OLA AI
822
+ </span>
823
+ </div>
824
+
825
+ {/* 欢迎语 */}
826
+ <p
827
+ className="text-sm text-center leading-5 m-0"
828
+ style={{ color: 'var(--foreground-muted, rgba(15,28,53,0.6))', width: '100%' }}
829
+ >
830
+ 您好!我是您的智能小助手 ✨ ~ 我能帮您整理资料、分析数据、规划任务、生成报告等~
831
+ </p>
832
+
833
+ {/* 快捷建议 chips(垂直堆叠 + 白底内阴影 + 右箭头,复用 Copilot 风格)
834
+ * 行为约定:点击只回填到输入框(覆盖草稿)+ 自动聚焦,等用户主动点发送 */}
835
+ <div className="flex flex-col gap-2 w-full">
836
+ {PROMPT_SUGGESTIONS.map((text) => (
837
+ <button
838
+ key={text}
839
+ type="button"
840
+ onClick={() => onPrefill(text)}
841
+ className="flex items-center justify-between w-full cursor-pointer border-0 text-left"
842
+ style={{
843
+ padding: '10px 12px',
844
+ borderRadius: '8px',
845
+ background: 'rgba(255,255,255,0.6)',
846
+ boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.9)',
847
+ fontSize: '12px',
848
+ lineHeight: '16px',
849
+ color: 'var(--foreground, #0F1C35)',
850
+ fontFamily: 'inherit',
851
+ transition: 'background 150ms ease',
852
+ }}
853
+ onMouseEnter={(e) => {
854
+ e.currentTarget.style.background = 'rgba(255,255,255,0.85)';
855
+ }}
856
+ onMouseLeave={(e) => {
857
+ e.currentTarget.style.background = 'rgba(255,255,255,0.6)';
858
+ }}
859
+ >
860
+ <span>{text}</span>
861
+ <Icon name="arrow-narrow-right-stroked" size={12} />
862
+ </button>
863
+ ))}
864
+ </div>
865
+ </div>
866
+ </div>
867
+
868
+ {/* 底部 ChatInput:shrink-0 吸底,与 chat 阶段保持一致 */}
869
+ <div className="w-full shrink-0" style={{ padding: '12px 0 20px' }}>
870
+ <div
871
+ className="mx-auto w-full"
872
+ style={{ width: '800px', maxWidth: '100%', padding: '0 8px' }}
873
+ >
874
+ <ChatInput
875
+ variant={inputView}
876
+ autoReplyOnSend={false}
877
+ prefillText={prefillText}
878
+ prefillSeed={prefillSeed}
879
+ placeholder="需要我为你做什么…"
880
+ onSend={onSend}
881
+ onStop={onStop}
882
+ />
883
+ </div>
884
+ </div>
885
+ </>
886
+ );
887
+ }
888
+
889
+ /* ============================================================
890
+ * StreamingChatMessage — 把执行流的 taskGroups 接到 useStreamingTaskGroups
891
+ * 挂载即开始按 600ms 节奏一条一条刷出;最新一条不显示操作栏
892
+ * ============================================================ */
893
+ function StreamingChatMessage({ taskGroups, ...rest }) {
894
+ const streamed = useStreamingTaskGroups(taskGroups, { intervalMs: STREAM_INTERVAL });
895
+ return <ChatMessage {...rest} taskGroups={streamed} />;
896
+ }
897
+
898
+ /* ============================================================
899
+ * TopBar — 顶导栏(参考 Figma OLA3.0 / 993:74300 独立任务导航)
900
+ * 三段式:左 187px 品牌 + 入口 / 中 flex-1 当前会话 / 右 分享
901
+ * · 左:CATCAT logo + 「OLA」品牌字 + 新建会话 / 历史记录两个 ghost 图标按钮
902
+ * · 中:32×32 渐变描边的会话图标(装饰,非交互)+ 当前会话标题
903
+ * · 右:带文字的 outline 「分享」按钮
904
+ * onNewSession 绑定到左侧 message-plus-square 按钮(即 Figma 中的「新建任务」入口)
905
+ * ============================================================ */
906
+ function TopBar({ title, onNewSession, disableNewSession }) {
907
+ return (
908
+ <div
909
+ className="flex items-center shrink-0 w-full"
910
+ style={{ padding: '16px 24px', gap: '24px' }}
911
+ >
912
+ {/* 左:品牌 + 入口(固定 187px,与 Figma 对齐) */}
913
+ <div className="flex shrink-0 items-center justify-between" style={{ width: '187px' }}>
914
+ <div className="flex items-center gap-2">
915
+ <img
916
+ src={catcatSvg}
917
+ alt="OLA"
918
+ style={{ width: '28px', height: '28px', flexShrink: 0 }}
919
+ />
920
+ <span
921
+ style={{
922
+ fontFamily: '"Arial Black", "PingFang SC", sans-serif',
923
+ fontSize: '14px',
924
+ lineHeight: '20px',
925
+ fontWeight: 900,
926
+ color: 'var(--foreground, #222727)',
927
+ }}
928
+ >
929
+ OLA
930
+ </span>
931
+ </div>
932
+ <div className="flex shrink-0 items-center">
933
+ <Button
934
+ variant="ghost-black"
935
+ iconOnly
936
+ icon={<Icon name="message-plus-square-stroked" />}
937
+ onClick={onNewSession}
938
+ disabled={disableNewSession}
939
+ aria-label="新建会话"
940
+ />
941
+ <Button
942
+ variant="ghost-black"
943
+ iconOnly
944
+ icon={<Icon name="clock-rewind-stroked" />}
945
+ aria-label="历史记录"
946
+ />
947
+ </div>
948
+ </div>
949
+
950
+ {/* 中:会话图标 + 标题(自适应剩余宽,标题超长截断) */}
951
+ <div
952
+ className="flex flex-1 min-w-0 items-center"
953
+ style={{ gap: '12px', padding: '0 12px' }}
954
+ >
955
+ <div
956
+ className="flex shrink-0 items-center justify-center"
957
+ style={{
958
+ width: '32px',
959
+ height: '32px',
960
+ borderRadius: '8px',
961
+ border: '0.5px solid var(--color-border-default, rgba(34,39,39,0.09))',
962
+ background:
963
+ 'linear-gradient(180deg, rgba(200,214,210,0.09) 0%, rgba(52,59,57,0.09) 100%)',
964
+ color: 'var(--foreground, rgba(34,39,39,0.8))',
965
+ }}
966
+ >
967
+ <Icon name="message-circle-02-stroked" />
968
+ </div>
969
+ <h1
970
+ className="m-0 truncate [font-weight:var(--font-semibold)]"
971
+ style={{
972
+ fontSize: '16px',
973
+ lineHeight: '22px',
974
+ color: 'var(--foreground, rgba(34,39,39,0.8))',
975
+ }}
976
+ >
977
+ {title}
978
+ </h1>
979
+ </div>
980
+
981
+ {/* 右:分享 */}
982
+ <div className="flex shrink-0 items-center">
983
+ <Button variant="outline-black" icon={<Icon name="share-07-stroked" />}>
984
+ 分享
985
+ </Button>
986
+ </div>
987
+ </div>
988
+ );
989
+ }