@tfdesign/b-end 1.0.13 → 1.0.15

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 (35) hide show
  1. package/package.json +1 -1
  2. package/skills/tfds/CHECKLIST.md +5 -0
  3. package/skills/tfds/COMMON_FAILURES.md +48 -0
  4. package/skills/tfds/DESIGN_PRINCIPLES.md +5 -0
  5. package/skills/tfds/GLOBAL_DESIGN_RULES.md +31 -0
  6. package/skills/tfds/LAYOUT_RULES.md +31 -0
  7. package/skills/tfds/components.index.json +75 -27
  8. package/skills/tfds/components.summary.json +13 -13
  9. package/src/_b_end_runtime/components/Card.jsx +151 -13
  10. package/src/_b_end_runtime/components/Card.tokens.js +27 -3
  11. package/src/_b_end_runtime/components/CardPreview.jsx +11 -3
  12. package/src/_b_end_runtime/components/ChatMessage.jsx +59 -1
  13. package/src/_b_end_runtime/components/ConversationList.jsx +68 -68
  14. package/src/_b_end_runtime/components/ConversationList.tokens.js +5 -3
  15. package/src/_b_end_runtime/components/FullScreenPage.jsx +1 -0
  16. package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +13 -15
  17. package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +2 -0
  18. package/src/_b_end_runtime/components/Modal.jsx +1 -0
  19. package/src/_b_end_runtime/components/Sheet.jsx +1 -0
  20. package/src/_b_end_runtime/components/Table.jsx +7 -0
  21. package/src/_b_end_runtime/components/Tabs.jsx +46 -3
  22. package/src/_b_end_runtime/components/Tabs.tokens.js +3 -0
  23. package/src/_b_end_runtime/components/TagBar.jsx +2 -0
  24. package/src/_b_end_runtime/components/Toast.jsx +1 -0
  25. package/src/_b_end_runtime/components/Upload.jsx +1 -0
  26. package/src/_b_end_runtime/components.js +24 -11
  27. package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +548 -135
  28. package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +1 -1
  29. package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +6 -6
  30. package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +66 -5
  31. package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +50 -17
  32. package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +28 -78
  33. package/src/_b_end_runtime/patterns.js +32 -21
  34. package/src/_b_end_runtime/preview-registry.jsx +20 -4
  35. package/src/index.d.ts +4 -2
@@ -10,6 +10,7 @@ import ChatMessage, {
10
10
  DEFAULT_CHAT_TASK_GROUPS,
11
11
  DEFAULT_CHAT_PLAN,
12
12
  DEFAULT_CHAT_THINKING,
13
+ DEFAULT_CHAT_FORM_CONFIRM,
13
14
  useStreamingTaskGroups,
14
15
  } from '../components/ChatMessage';
15
16
 
@@ -22,8 +23,9 @@ import ChatMessage, {
22
23
  * - 800px 居中对话流
23
24
  *
24
25
  * 状态:
25
- * - phase = 'chat' 默认进入,展示端到端 mock 对话(覆盖全部 ChatMessage 子组件)
26
- * - phase = 'welcome' 点「新会话」清空回到欢迎屏(Hero + 居中 ChatInput + 推荐 prompt)
26
+ * - 空对话 / 新建会话态 当消息为空时,必须展示新建会话页面(Hero + 居中 ChatInput + 推荐 prompt)
27
+ * - phase = 'chat' 有消息时展示消息流
28
+ * - phase = 'welcome' → 点「新会话」后进入空对话 / 新建会话态
27
29
  *
28
30
  * 关键词路由(输入框发送文本时):
29
31
  * - 含「整理 / 分析 / 生成 / 梳理」 → 走完整任务规划链路:
@@ -40,64 +42,20 @@ import ChatMessage, {
40
42
  */
41
43
 
42
44
  const STREAM_INTERVAL = 600;
45
+ const AUTO_FLOW_TIMINGS = {
46
+ thinkingDone: 900,
47
+ confirmShow: 1400,
48
+ confirmDone: 2400,
49
+ flowStart: 3000,
50
+ };
43
51
 
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
- }
52
+ const DEFAULT_CONVERSATION_CONTEXT = {
53
+ account: '抖音电商客服账号 · after_sales_prod',
54
+ environment: '生产环境 / OLA Workflow v13',
55
+ mock: '近 7 天售后会话样本 240 条',
56
+ conclusion: '上一轮已完成售后政策口径梳理,并沉淀可继续追问的结论摘要。',
57
+ artifacts: DEFAULT_CHAT_RESULT_ARTIFACTS.map((item) => item.title),
58
+ };
101
59
 
102
60
  /* ── 关键词 → 回复路由 ── */
103
61
  const TASK_KEYWORDS = ['整理', '分析', '生成', '梳理', '输出', '汇总'];
@@ -193,6 +151,108 @@ function routeReply(text) {
193
151
  return { type: 'short', text: GENERIC_REPLIES[Math.floor(Math.random() * GENERIC_REPLIES.length)] };
194
152
  }
195
153
 
154
+ function buildThinkingContent(text, context) {
155
+ const artifacts = context.artifacts?.length ? context.artifacts.join('、') : '暂无历史产物';
156
+ return [
157
+ `用户本轮输入:${text}`,
158
+ `系统自动带入上一轮账号:${context.account}`,
159
+ `系统自动带入上一轮环境:${context.environment}`,
160
+ `系统自动带入上一轮 Mock:${context.mock}`,
161
+ `系统自动带入上一轮结论:${context.conclusion || '暂无历史结论'}`,
162
+ `系统自动带入上一轮产物:${artifacts}`,
163
+ '基于上述连续会话上下文,先确认关键执行口径,再进入自动执行流。',
164
+ ].join('\n');
165
+ }
166
+
167
+ function buildClarificationConfirms(context) {
168
+ return DEFAULT_CHAT_FORM_CONFIRM.map((confirm) => ({
169
+ ...confirm,
170
+ id: 'auto-clarify-confirm',
171
+ title: '人工澄清确认',
172
+ primaryActionLabel: '确认并继续',
173
+ secondaryActionLabel: '',
174
+ formItems: [
175
+ {
176
+ id: 'scene',
177
+ label: '业务场景',
178
+ type: 'select',
179
+ placeholder: '请选择业务场景',
180
+ defaultValue: 'after-sales-policy',
181
+ options: [
182
+ { value: 'after-sales-policy', label: '售后政策整理' },
183
+ { value: 'conversation-analysis', label: '会话场景分析' },
184
+ { value: 'ab-experiment', label: 'AB 实验方案' },
185
+ ],
186
+ fullWidth: true,
187
+ },
188
+ {
189
+ id: 'channel',
190
+ label: '处理渠道',
191
+ type: 'select',
192
+ placeholder: '请选择处理渠道',
193
+ defaultValue: 'current-context',
194
+ options: [
195
+ {
196
+ value: 'current-context',
197
+ label: `使用当前账号、${context.environment} 与 ${context.mock}`,
198
+ },
199
+ { value: 'refresh-mock', label: '刷新 Mock 数据后再执行' },
200
+ { value: 'manual-review', label: '先转人工复核上下文再执行' },
201
+ ],
202
+ fullWidth: true,
203
+ },
204
+ {
205
+ id: 'remark',
206
+ label: '补充说明',
207
+ type: 'input',
208
+ placeholder: '请输入补充说明(可选)',
209
+ fullWidth: true,
210
+ },
211
+ ],
212
+ }));
213
+ }
214
+
215
+ function buildAutoFlowPayload(text, reply, context) {
216
+ if (reply.type === 'follow-up-result') {
217
+ return {
218
+ resultText: `${reply.resultText}\n\n本轮追问已自动继承上一轮账号、环境、Mock、结论和产物上下文,未新开会话。`,
219
+ resultArtifacts: reply.resultArtifacts,
220
+ followUps: reply.followUps,
221
+ };
222
+ }
223
+
224
+ if (reply.type === 'task') {
225
+ return {
226
+ resultText: DEFAULT_CHAT_RESULT,
227
+ resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS,
228
+ followUps: DEFAULT_CHAT_FOLLOW_UPS,
229
+ };
230
+ }
231
+
232
+ return {
233
+ resultText:
234
+ `${reply.text} 已结合当前会话上下文继续处理「${text}」:账号为 ${context.account},环境为 ${context.environment},Mock 使用 ${context.mock}。如需深入推进,可继续选择下方追问或直接在底部输入补充要求。`,
235
+ resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS.slice(0, 2),
236
+ followUps: ['继续细化结论', '补充一轮 Mock 验证', '生成可交付文档'],
237
+ };
238
+ }
239
+
240
+ function getFlowTotalMs() {
241
+ const totalSteps = DEFAULT_CHAT_TASK_GROUPS.reduce(
242
+ (sum, group) => sum + (group.steps?.length ?? 0),
243
+ 0,
244
+ );
245
+ return (totalSteps + 1) * STREAM_INTERVAL;
246
+ }
247
+
248
+ function hasActionablePrimaryButton(label) {
249
+ return typeof label === 'string' && label.trim().length > 0;
250
+ }
251
+
252
+ function confirmsRequireHumanGate(confirms) {
253
+ return Array.isArray(confirms) && confirms.some((confirm) => hasActionablePrimaryButton(confirm?.primaryActionLabel));
254
+ }
255
+
196
256
  /* ── 单条消息 → ChatMessage props 映射 ──
197
257
  * isLatest:是否为列表中最后一条(决定 historyMode 是否启用)
198
258
  * handlers:所有交互回调集合
@@ -201,7 +261,15 @@ function routeReply(text) {
201
261
  * · onFollowUpSelect(text) — 追问 chip 点击(默认作为新一轮对话发出)
202
262
  */
203
263
  function messageToChatProps(msg, isLatest, handlers = {}) {
204
- const { onPlanConfirm, onPlanCancel, onFollowUpSelect } = handlers;
264
+ const {
265
+ onPlanConfirm,
266
+ onPlanCancel,
267
+ onFollowUpSelect,
268
+ onConfirmPrimary,
269
+ onConfirmSecondary,
270
+ onConfirmOptionChange,
271
+ onConfirmFormChange,
272
+ } = handlers;
205
273
  const baseProps = {
206
274
  className: msg.className || '',
207
275
  };
@@ -261,6 +329,41 @@ function messageToChatProps(msg, isLatest, handlers = {}) {
261
329
  thinking: msg.thinking,
262
330
  resultText: msg.resultText,
263
331
  timestamp: msg.timestamp,
332
+ actions: msg.thinking?.state === 'thinking' ? null : baseActions,
333
+ };
334
+ }
335
+
336
+ if (msg.kind === 'ai-confirm') {
337
+ return {
338
+ ...baseProps,
339
+ role: 'ai',
340
+ header: true,
341
+ title: '',
342
+ steps: null,
343
+ leadText: msg.leadText,
344
+ confirms: Array.isArray(msg.confirms)
345
+ ? msg.confirms.map((confirm) => ({
346
+ ...confirm,
347
+ onOptionChange: (value, option) => onConfirmOptionChange && onConfirmOptionChange(
348
+ msg.id,
349
+ confirm.id,
350
+ value,
351
+ option,
352
+ ),
353
+ onFormChange: (formValues, meta) => onConfirmFormChange && onConfirmFormChange(
354
+ msg.id,
355
+ confirm.id,
356
+ formValues,
357
+ meta,
358
+ ),
359
+ onPrimaryAction: (payload) => onConfirmPrimary && onConfirmPrimary(msg.id, confirm.id, {
360
+ ...payload,
361
+ primaryActionLabel: confirm.primaryActionLabel,
362
+ }),
363
+ onSecondaryAction: () => onConfirmSecondary && onConfirmSecondary(msg.id, confirm.id),
364
+ }))
365
+ : msg.confirms,
366
+ timestamp: msg.timestamp,
264
367
  actions: baseActions,
265
368
  };
266
369
  }
@@ -325,9 +428,15 @@ function messageToChatProps(msg, isLatest, handlers = {}) {
325
428
 
326
429
  export default function ChatConversationPattern({
327
430
  title = '抖音电商客服售后政策梳理',
431
+ initialMessages,
328
432
  }) {
329
- const [phase, setPhase] = useState('chat'); // 'chat' | 'welcome'
330
- const [messages, setMessages] = useState(buildInitMessages);
433
+ const resolvedInitialMessagesRef = useRef(
434
+ Array.isArray(initialMessages) ? initialMessages : [],
435
+ );
436
+ const [messages, setMessages] = useState(() => resolvedInitialMessagesRef.current);
437
+ const [phase, setPhase] = useState(() => (
438
+ resolvedInitialMessagesRef.current.length === 0 ? 'welcome' : 'chat'
439
+ )); // 'chat' | 'welcome'
331
440
  /* ── ChatInput 受控状态机 ──
332
441
  * inputView:'default' | 'replying' | 'busy'
333
442
  * · default → 静止 / 失焦 / AI 完成回复(含 replying & busy 完成)
@@ -339,10 +448,62 @@ export default function ChatConversationPattern({
339
448
  const [prefillSeed, setPrefillSeed] = useState(0);
340
449
  const replyTimerRef = useRef(null);
341
450
  const busyTimerRef = useRef(null);
451
+ const flowTimersRef = useRef([]);
452
+ const pendingFlowResumeRef = useRef(null);
342
453
  const scrollRef = useRef(null);
454
+ const latestAnchorRef = useRef(null);
455
+ const conversationContextRef = useRef({ ...DEFAULT_CONVERSATION_CONTEXT });
343
456
  const idSeedRef = useRef(100);
344
457
  const nextId = useCallback((prefix = 'm') => `${prefix}${++idSeedRef.current}`, []);
345
458
 
459
+ const scrollToLatest = useCallback((behavior = 'smooth') => {
460
+ if (latestAnchorRef.current?.scrollIntoView) {
461
+ latestAnchorRef.current.scrollIntoView({ block: 'end', behavior });
462
+ return;
463
+ }
464
+ const el = scrollRef.current;
465
+ if (el) el.scrollTo({ top: el.scrollHeight, behavior });
466
+ }, []);
467
+
468
+ const scheduleScrollToLatest = useCallback((behavior = 'smooth') => {
469
+ if (typeof window === 'undefined') return;
470
+ window.requestAnimationFrame(() => scrollToLatest(behavior));
471
+ }, [scrollToLatest]);
472
+
473
+ const clearFlowTimers = useCallback(() => {
474
+ flowTimersRef.current.forEach((timer) => clearTimeout(timer));
475
+ flowTimersRef.current = [];
476
+ }, []);
477
+
478
+ const clearPendingFlowResume = useCallback(() => {
479
+ pendingFlowResumeRef.current = null;
480
+ }, []);
481
+
482
+ const pauseForHumanGate = useCallback((resume) => {
483
+ pendingFlowResumeRef.current = resume;
484
+ setInputView('default');
485
+ scheduleScrollToLatest();
486
+ }, [scheduleScrollToLatest]);
487
+
488
+ const scheduleFlowTimer = useCallback((callback, delay) => {
489
+ const timer = setTimeout(() => {
490
+ flowTimersRef.current = flowTimersRef.current.filter((item) => item !== timer);
491
+ callback();
492
+ scheduleScrollToLatest();
493
+ }, delay);
494
+ flowTimersRef.current.push(timer);
495
+ return timer;
496
+ }, [scheduleScrollToLatest]);
497
+
498
+ const resumePendingFlow = useCallback((delay = 240) => {
499
+ const resume = pendingFlowResumeRef.current;
500
+ pendingFlowResumeRef.current = null;
501
+ if (typeof resume !== 'function') return;
502
+ scheduleFlowTimer(() => {
503
+ resume();
504
+ }, delay);
505
+ }, [scheduleFlowTimer]);
506
+
346
507
  /* chip 回填到输入框(覆盖当前草稿 + 自动聚焦) */
347
508
  const prefillInput = useCallback((text) => {
348
509
  setPrefillText(text || '');
@@ -354,37 +515,40 @@ export default function ChatConversationPattern({
354
515
  () => () => {
355
516
  if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
356
517
  if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
518
+ clearFlowTimers();
519
+ clearPendingFlowResume();
357
520
  },
358
- [],
521
+ [clearFlowTimers, clearPendingFlowResume],
359
522
  );
360
523
 
361
524
  /* 自动滚到底:用 ResizeObserver 监听消息容器内容高度变化
362
525
  * 覆盖所有场景:新消息追加、流式 step 逐步推出、ChatMessage 内部折叠展开 */
363
526
  useEffect(() => {
364
- if (phase !== 'chat') return undefined;
527
+ if (phase !== 'chat' || messages.length === 0) return undefined;
365
528
  const el = scrollRef.current;
366
529
  if (!el) return undefined;
367
530
  const inner = el.firstElementChild;
368
531
  if (!inner) return undefined;
369
532
  const ro = new ResizeObserver(() => {
370
- el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
533
+ scrollToLatest();
371
534
  });
372
535
  ro.observe(inner);
373
536
  return () => ro.disconnect();
374
- }, [phase]);
537
+ }, [phase, messages.length, scrollToLatest]);
375
538
 
376
539
  /* 重置回欢迎状态 */
377
540
  const handleNewSession = useCallback(() => {
378
541
  setMessages([]);
379
542
  setPhase('welcome');
543
+ setPrefillText('');
544
+ setPrefillSeed((s) => s + 1);
380
545
  }, []);
381
546
 
382
- /* ── 取消链路:用户在任务规划卡里点「取消」 ──
383
- * 1. 标记该任务规划卡为「已确认」(卡片置灰)
384
- * 2. 追加用户消息「取消」
385
- * 3. ChatInput 进 replying(短答态)+ 600ms 后追加 AI 寒暄 + 回 default */
547
+ /* ── 取消链路:任务规划卡次按钮 ──
548
+ * 在自动编排有人为闸门等待时,取消会清空 pending resume 并暂停后续链路。 */
386
549
  const handlePlanCancel = useCallback(
387
550
  (planMsgId) => {
551
+ clearPendingFlowResume();
388
552
  setMessages((prev) =>
389
553
  prev.map((m) => (m.id === planMsgId ? { ...m, planConfirmed: true } : m)),
390
554
  );
@@ -412,17 +576,137 @@ export default function ChatConversationPattern({
412
576
  setInputView('default');
413
577
  }, 600);
414
578
  },
415
- [nextId],
579
+ [nextId, clearPendingFlowResume],
416
580
  );
417
581
 
418
- /* ── 任务规划链路:用户在任务规划卡里点「开始执行任务」 ── */
582
+ const handleConfirmPrimary = useCallback(
583
+ (messageId, confirmId, payload = {}) => {
584
+ const isActionable = hasActionablePrimaryButton(payload.primaryActionLabel);
585
+ const selectedLabel = payload.option?.label ?? '';
586
+ setMessages((prev) =>
587
+ prev.map((message) =>
588
+ message.id === messageId
589
+ ? {
590
+ ...message,
591
+ confirms: Array.isArray(message.confirms)
592
+ ? message.confirms.map((confirm) => (
593
+ confirm.id === confirmId
594
+ ? {
595
+ ...confirm,
596
+ defaultConfirmed: true,
597
+ selectedValue: payload.value ?? confirm.selectedValue,
598
+ defaultSelectedValue: payload.value ?? confirm.defaultSelectedValue,
599
+ selectedLabel: selectedLabel || confirm.selectedLabel,
600
+ formValues: payload.formValues ?? confirm.formValues,
601
+ }
602
+ : confirm
603
+ ))
604
+ : message.confirms,
605
+ leadText: isActionable ? '已确认,继续执行后续流程。' : message.leadText,
606
+ timestamp: nowHHmm(),
607
+ }
608
+ : message,
609
+ ),
610
+ );
611
+ if (isActionable) resumePendingFlow();
612
+ },
613
+ [resumePendingFlow],
614
+ );
615
+
616
+ const handleConfirmOptionChange = useCallback((messageId, confirmId, value, option) => {
617
+ setMessages((prev) =>
618
+ prev.map((message) =>
619
+ message.id === messageId
620
+ ? {
621
+ ...message,
622
+ confirms: Array.isArray(message.confirms)
623
+ ? message.confirms.map((confirm) => (
624
+ confirm.id === confirmId
625
+ ? {
626
+ ...confirm,
627
+ selectedValue: value,
628
+ defaultSelectedValue: value,
629
+ selectedLabel: option?.label ?? confirm.selectedLabel,
630
+ }
631
+ : confirm
632
+ ))
633
+ : message.confirms,
634
+ }
635
+ : message,
636
+ ),
637
+ );
638
+ }, []);
639
+
640
+ const handleConfirmFormChange = useCallback((messageId, confirmId, formValues, meta = {}) => {
641
+ setMessages((prev) =>
642
+ prev.map((message) =>
643
+ message.id === messageId
644
+ ? {
645
+ ...message,
646
+ confirms: Array.isArray(message.confirms)
647
+ ? message.confirms.map((confirm) => (
648
+ confirm.id === confirmId
649
+ ? {
650
+ ...confirm,
651
+ formValues,
652
+ lastChangedFieldId: meta.fieldId,
653
+ }
654
+ : confirm
655
+ ))
656
+ : message.confirms,
657
+ }
658
+ : message,
659
+ ),
660
+ );
661
+ }, []);
662
+
663
+ const handleConfirmSecondary = useCallback(
664
+ (messageId, confirmId) => {
665
+ setMessages((prev) =>
666
+ prev.map((message) =>
667
+ message.id === messageId
668
+ ? {
669
+ ...message,
670
+ confirms: Array.isArray(message.confirms)
671
+ ? message.confirms.map((confirm) => (
672
+ confirm.id === confirmId ? { ...confirm, defaultConfirmed: true } : confirm
673
+ ))
674
+ : message.confirms,
675
+ leadText: '已取消当前确认,流程暂停。',
676
+ timestamp: nowHHmm(),
677
+ }
678
+ : message,
679
+ ),
680
+ );
681
+ clearPendingFlowResume();
682
+ setInputView('default');
683
+ },
684
+ [clearPendingFlowResume],
685
+ );
686
+
687
+ /* ── 任务规划卡主按钮:有人为闸门时用于恢复自动链路;无闸门时保留旧兜底链路 ── */
419
688
  const handlePlanConfirm = useCallback(
420
689
  (planMsgId) => {
690
+ const hasPendingResume = typeof pendingFlowResumeRef.current === 'function';
421
691
  /* 1) 标记该任务规划卡为「已确认」(持久化到消息数据,重渲染时也是禁用态) */
422
692
  setMessages((prev) =>
423
693
  prev.map((m) => (m.id === planMsgId ? { ...m, planConfirmed: true } : m)),
424
694
  );
425
695
 
696
+ if (hasPendingResume) {
697
+ setMessages((prev) => [
698
+ ...prev,
699
+ {
700
+ id: nextId('u'),
701
+ kind: 'user',
702
+ timestamp: nowHHmm(),
703
+ userContent: [{ type: 'text', value: '开始执行任务' }],
704
+ },
705
+ ]);
706
+ resumePendingFlow();
707
+ return;
708
+ }
709
+
426
710
  const flowMsgId = nextId('flow');
427
711
 
428
712
  /* 2) 立即追加:用户消息「开始执行任务」+ AI 流式执行流(带 stream 标记,由 StreamingChatMessage 接管) */
@@ -477,12 +761,145 @@ export default function ChatConversationPattern({
477
761
  setInputView('default');
478
762
  }, totalMs);
479
763
  },
480
- [nextId],
764
+ [nextId, resumePendingFlow],
765
+ );
766
+
767
+ const startAutoOrchestratedFlow = useCallback(
768
+ (text, reply, ctx = {}) => {
769
+ const contextSnapshot = { ...conversationContextRef.current };
770
+ const thinkingMsgId = nextId('think');
771
+ const confirmMsgId = nextId('confirm');
772
+ const flowMsgId = nextId('flow');
773
+ const payload = buildAutoFlowPayload(text, reply, contextSnapshot);
774
+ const clarificationConfirms = buildClarificationConfirms(contextSnapshot);
775
+
776
+ const finishExecutionFlow = () => {
777
+ setMessages((prev) =>
778
+ prev.map((message) =>
779
+ message.id === flowMsgId
780
+ ? {
781
+ ...message,
782
+ stream: false,
783
+ taskGroups: DEFAULT_CHAT_TASK_GROUPS.map((group) => ({
784
+ ...group,
785
+ status: 'completed',
786
+ })),
787
+ resultText: payload.resultText,
788
+ resultArtifacts: payload.resultArtifacts,
789
+ followUps: payload.followUps,
790
+ timestamp: nowHHmm(),
791
+ }
792
+ : message,
793
+ ),
794
+ );
795
+ conversationContextRef.current = {
796
+ ...contextSnapshot,
797
+ conclusion: payload.resultText,
798
+ artifacts: payload.resultArtifacts.map((artifact) => artifact.title),
799
+ lastUserIntent: text,
800
+ };
801
+ setInputView('default');
802
+ };
803
+
804
+ const startExecutionFlow = () => {
805
+ setInputView('busy');
806
+ setMessages((prev) => [
807
+ ...prev,
808
+ {
809
+ id: flowMsgId,
810
+ kind: 'ai-flow',
811
+ timestamp: nowHHmm(),
812
+ taskGroups: DEFAULT_CHAT_TASK_GROUPS,
813
+ stream: true,
814
+ },
815
+ ]);
816
+ scheduleFlowTimer(finishExecutionFlow, getFlowTotalMs());
817
+ };
818
+
819
+ const continueAfterConfirm = () => {
820
+ setMessages((prev) =>
821
+ prev.map((message) =>
822
+ message.id === confirmMsgId
823
+ ? {
824
+ ...message,
825
+ confirms: Array.isArray(message.confirms)
826
+ ? message.confirms.map((confirm) => ({
827
+ ...confirm,
828
+ defaultConfirmed: true,
829
+ }))
830
+ : message.confirms,
831
+ leadText: '已确认澄清信息,开始自动执行。',
832
+ timestamp: nowHHmm(),
833
+ }
834
+ : message,
835
+ ),
836
+ );
837
+ scheduleFlowTimer(startExecutionFlow, 320);
838
+ };
839
+
840
+ setMessages((prev) => [
841
+ ...prev,
842
+ {
843
+ id: thinkingMsgId,
844
+ kind: 'ai-thinking',
845
+ timestamp: nowHHmm(),
846
+ thinking: {
847
+ ...DEFAULT_CHAT_THINKING,
848
+ state: 'thinking',
849
+ inProgressLabel: '深度思考中 ...',
850
+ defaultExpanded: false,
851
+ },
852
+ className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
853
+ },
854
+ ]);
855
+
856
+ scheduleFlowTimer(() => {
857
+ setMessages((prev) =>
858
+ prev.map((message) =>
859
+ message.id === thinkingMsgId
860
+ ? {
861
+ ...message,
862
+ thinking: {
863
+ ...DEFAULT_CHAT_THINKING,
864
+ state: 'completed',
865
+ durationLabel: '深度思考(用时 3.20 秒)',
866
+ content: buildThinkingContent(text, contextSnapshot),
867
+ defaultExpanded: false,
868
+ },
869
+ resultText: '已完成上下文理解,接下来需要先确认本轮执行口径。',
870
+ timestamp: nowHHmm(),
871
+ }
872
+ : message,
873
+ ),
874
+ );
875
+ }, AUTO_FLOW_TIMINGS.thinkingDone);
876
+
877
+ scheduleFlowTimer(() => {
878
+ setMessages((prev) => [
879
+ ...prev,
880
+ {
881
+ id: confirmMsgId,
882
+ kind: 'ai-confirm',
883
+ timestamp: nowHHmm(),
884
+ leadText: '请确认以下人工澄清信息;若卡片右下角存在可操作主按钮,则点击后再继续执行。',
885
+ confirms: clarificationConfirms,
886
+ },
887
+ ]);
888
+
889
+ if (confirmsRequireHumanGate(clarificationConfirms)) {
890
+ pauseForHumanGate(continueAfterConfirm);
891
+ return;
892
+ }
893
+
894
+ scheduleFlowTimer(continueAfterConfirm, 320);
895
+ }, AUTO_FLOW_TIMINGS.confirmShow);
896
+ },
897
+ [nextId, pauseForHumanGate, scheduleFlowTimer],
481
898
  );
482
899
 
483
900
  /* ── 用户输入框发送 ──
484
- * 状态机:default → replying → 600ms 后追加 AI 回复 → default
485
- * 任务关键词分支也走 replying 态(正在规划中),规划卡推出后回 default 等用户处理卡片 */
901
+ * 状态机:default → replying → AI 深度思考 人工澄清确认busy 执行流 → 结论/产物/追问 → default
902
+ * 底部输入与 followUps 点击都进入同一条会话流,并自动继承上一轮上下文。 */
486
903
  const handleSend = useCallback(
487
904
  (text, ctx = {}) => {
488
905
  const trimmed = (text || '').trim();
@@ -511,64 +928,35 @@ export default function ChatConversationPattern({
511
928
  },
512
929
  ]);
513
930
  setInputView('replying');
931
+ scheduleScrollToLatest();
514
932
 
515
933
  const reply = routeReply(trimmed);
516
934
  if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
935
+ if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
936
+ clearFlowTimers();
937
+ clearPendingFlowResume();
517
938
 
518
- /* 2) 追问结果:点击 follow-up chip 等价于立即发送,并模拟生成对应的新答案 */
519
- if (reply.type === 'follow-up-result') {
939
+ if (reply.type === 'short' && STOP_KEYWORDS.some((keyword) => trimmed.includes(keyword))) {
520
940
  replyTimerRef.current = setTimeout(() => {
521
941
  setMessages((prev) => [
522
942
  ...prev,
523
943
  {
524
944
  id: nextId('a'),
525
- kind: 'ai-result',
945
+ kind: 'ai-text',
526
946
  timestamp: nowHHmm(),
527
- resultText: reply.resultText,
528
- resultArtifacts: reply.resultArtifacts,
529
- followUps: reply.followUps,
947
+ resultText: reply.text,
530
948
  className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
531
949
  },
532
950
  ]);
533
951
  setInputView('default');
952
+ scheduleScrollToLatest();
534
953
  }, 600);
535
954
  return;
536
955
  }
537
956
 
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);
957
+ startAutoOrchestratedFlow(trimmed, reply, ctx);
570
958
  },
571
- [phase, nextId],
959
+ [phase, nextId, clearFlowTimers, clearPendingFlowResume, scheduleScrollToLatest, startAutoOrchestratedFlow],
572
960
  );
573
961
 
574
962
  const handleFollowUpSend = useCallback(
@@ -589,6 +977,8 @@ export default function ChatConversationPattern({
589
977
  clearTimeout(busyTimerRef.current);
590
978
  busyTimerRef.current = null;
591
979
  }
980
+ clearFlowTimers();
981
+ clearPendingFlowResume();
592
982
  setInputView('default');
593
983
  setMessages((prev) => [
594
984
  ...prev,
@@ -599,20 +989,24 @@ export default function ChatConversationPattern({
599
989
  resultText: '已停止当前任务,需要时再叫我。',
600
990
  },
601
991
  ]);
602
- }, [nextId]);
992
+ }, [nextId, clearFlowTimers, clearPendingFlowResume]);
603
993
 
604
994
  /* 新会话:清空消息 + 切欢迎屏 + 重置输入框状态 */
605
995
  const handleNewSessionFull = useCallback(() => {
606
996
  if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
607
997
  if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
998
+ clearFlowTimers();
999
+ clearPendingFlowResume();
1000
+ conversationContextRef.current = { ...DEFAULT_CONVERSATION_CONTEXT };
608
1001
  setInputView('default');
609
1002
  handleNewSession();
610
- }, [handleNewSession]);
1003
+ }, [handleNewSession, clearFlowTimers, clearPendingFlowResume]);
611
1004
 
612
1005
  const lastIdx = messages.length - 1;
1006
+ const isNewConversationState = phase === 'welcome' || messages.length === 0;
613
1007
 
614
- /* 当前阶段标题:欢迎屏标题简化为「新会话」 */
615
- const displayTitle = phase === 'welcome' ? '新会话' : title;
1008
+ /* 当前阶段标题:空对话 / 新建会话态统一显示「新会话」 */
1009
+ const displayTitle = isNewConversationState ? '新会话' : title;
616
1010
 
617
1011
  return (
618
1012
  <div
@@ -625,16 +1019,30 @@ export default function ChatConversationPattern({
625
1019
  border: '1px solid var(--color-border-default, #E4E7EC)',
626
1020
  }}
627
1021
  >
628
- <TopBar title={displayTitle} onNewSession={handleNewSessionFull} disableNewSession={phase === 'welcome'} />
1022
+ <TopBar title={displayTitle} onNewSession={handleNewSessionFull} disableNewSession={isNewConversationState} />
629
1023
 
630
- {phase === 'chat' ? (
1024
+ {isNewConversationState ? (
1025
+ <NewConversationPhase
1026
+ onSend={handleSend}
1027
+ onStop={handleStop}
1028
+ onPrefill={prefillInput}
1029
+ inputView={inputView}
1030
+ prefillText={prefillText}
1031
+ prefillSeed={prefillSeed}
1032
+ />
1033
+ ) : (
631
1034
  <ChatPhase
632
1035
  scrollRef={scrollRef}
1036
+ latestAnchorRef={latestAnchorRef}
633
1037
  messages={messages}
634
1038
  lastIdx={lastIdx}
635
1039
  handlers={{
636
1040
  onPlanConfirm: handlePlanConfirm,
637
1041
  onPlanCancel: handlePlanCancel,
1042
+ onConfirmPrimary: handleConfirmPrimary,
1043
+ onConfirmSecondary: handleConfirmSecondary,
1044
+ onConfirmOptionChange: handleConfirmOptionChange,
1045
+ onConfirmFormChange: handleConfirmFormChange,
638
1046
  onFollowUpSelect: handleFollowUpSend,
639
1047
  }}
640
1048
  onSend={handleSend}
@@ -643,15 +1051,6 @@ export default function ChatConversationPattern({
643
1051
  prefillText={prefillText}
644
1052
  prefillSeed={prefillSeed}
645
1053
  />
646
- ) : (
647
- <WelcomePhase
648
- onSend={handleSend}
649
- onStop={handleStop}
650
- onPrefill={prefillInput}
651
- inputView={inputView}
652
- prefillText={prefillText}
653
- prefillSeed={prefillSeed}
654
- />
655
1054
  )}
656
1055
  </div>
657
1056
  );
@@ -660,7 +1059,18 @@ export default function ChatConversationPattern({
660
1059
  /* ============================================================
661
1060
  * ChatPhase — 对话阶段:消息流 + 底部 ChatInput
662
1061
  * ============================================================ */
663
- function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inputView, prefillText, prefillSeed }) {
1062
+ function ChatPhase({
1063
+ scrollRef,
1064
+ latestAnchorRef,
1065
+ messages,
1066
+ lastIdx,
1067
+ handlers,
1068
+ onSend,
1069
+ onStop,
1070
+ inputView,
1071
+ prefillText,
1072
+ prefillSeed,
1073
+ }) {
664
1074
  /* 上下 40px 渐隐遮罩:让消息进入 / 离开滚动视口时柔和过渡,不硬切
665
1075
  * 顶部渐隐仅在「可向上滚」时启用,回到第一条时取消,避免首条被遮淡 */
666
1076
  const [atTop, setAtTop] = useState(true);
@@ -715,8 +1125,8 @@ function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inp
715
1125
  >
716
1126
  <div
717
1127
  className="mx-auto flex w-full flex-col gap-2"
718
- /* 底部 60px padding:避免最后一条消息贴住底部 ChatInput */
719
- style={{ width: '800px', maxWidth: '100%', padding: '0 20px 60px' }}
1128
+ /* 底部 80px padding:让最后一条会话卡片再上移 20px,避免被底部渐隐遮挡 */
1129
+ style={{ width: '800px', maxWidth: '100%', padding: '0 20px 80px' }}
720
1130
  >
721
1131
  {messages.map((m, idx) => {
722
1132
  const isLatest = idx === lastIdx;
@@ -727,6 +1137,7 @@ function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inp
727
1137
  }
728
1138
  return <ChatMessage key={m.id} {...props} />;
729
1139
  })}
1140
+ <div ref={latestAnchorRef} aria-hidden="true" style={{ height: '1px' }} />
730
1141
  </div>
731
1142
  </div>
732
1143
 
@@ -755,10 +1166,10 @@ function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inp
755
1166
  }
756
1167
 
757
1168
  /* ============================================================
758
- * WelcomePhase欢迎阶段:CATCAT 头像 + OLA AI 标题 + 欢迎语 + 推荐 chip
759
- * 复用 CopilotPagePattern 的欢迎态视觉,ChatInput 仍底部吸底
1169
+ * NewConversationPhase — AI 对话页的唯一空会话 / 新建会话页面
1170
+ * messages 为空时,必须展示该页面;复用 Copilot welcome 视觉,ChatInput 仍底部吸底
760
1171
  * ============================================================ */
761
- function WelcomePhase({ onSend, onStop, onPrefill, inputView, prefillText, prefillSeed }) {
1172
+ function NewConversationPhase({ onSend, onStop, onPrefill, inputView, prefillText, prefillSeed }) {
762
1173
  return (
763
1174
  <>
764
1175
  {/* 中部 hero:自适应剩余高度,居中展示头像/标题/欢迎语/chips */}
@@ -936,12 +1347,14 @@ function TopBar({ title, onNewSession, disableNewSession }) {
936
1347
  icon={<Icon name="message-plus-square-stroked" />}
937
1348
  onClick={onNewSession}
938
1349
  disabled={disableNewSession}
1350
+ tooltip="新建会话"
939
1351
  aria-label="新建会话"
940
1352
  />
941
1353
  <Button
942
1354
  variant="ghost-black"
943
1355
  iconOnly
944
1356
  icon={<Icon name="clock-rewind-stroked" />}
1357
+ tooltip="历史记录"
945
1358
  aria-label="历史记录"
946
1359
  />
947
1360
  </div>