@tfdesign/b-end 1.0.14 → 1.0.16
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/package.json +2 -2
- package/skills/tfds/CHECKLIST.md +5 -0
- package/skills/tfds/COMMON_FAILURES.md +48 -0
- package/skills/tfds/DESIGN_PRINCIPLES.md +5 -0
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +31 -0
- package/skills/tfds/LAYOUT_RULES.md +32 -1
- package/skills/tfds/components.index.json +61 -19
- package/skills/tfds/components.summary.json +10 -10
- package/src/_b_end_runtime/components/Card.jsx +147 -11
- package/src/_b_end_runtime/components/Card.tokens.js +26 -4
- package/src/_b_end_runtime/components/CardPreview.jsx +11 -3
- package/src/_b_end_runtime/components/ChatMessage.jsx +59 -1
- package/src/_b_end_runtime/components/ConversationList.jsx +15 -10
- package/src/_b_end_runtime/components/ConversationList.tokens.js +5 -3
- package/src/_b_end_runtime/components/Table.jsx +54 -23
- package/src/_b_end_runtime/components/Tabs.jsx +46 -3
- package/src/_b_end_runtime/components/Tabs.tokens.js +3 -0
- package/src/_b_end_runtime/components.js +18 -7
- package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +516 -115
- package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +66 -5
- package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +38 -4
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +26 -78
- package/src/_b_end_runtime/patterns.js +24 -17
- package/src/_b_end_runtime/preview-registry.jsx +19 -2
- package/src/index.d.ts +2 -0
|
@@ -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
|
|
|
@@ -41,64 +42,20 @@ import ChatMessage, {
|
|
|
41
42
|
*/
|
|
42
43
|
|
|
43
44
|
const STREAM_INTERVAL = 600;
|
|
45
|
+
const AUTO_FLOW_TIMINGS = {
|
|
46
|
+
thinkingDone: 900,
|
|
47
|
+
confirmShow: 1400,
|
|
48
|
+
confirmDone: 2400,
|
|
49
|
+
flowStart: 3000,
|
|
50
|
+
};
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
timestamp: '14:02',
|
|
53
|
-
userContent: [
|
|
54
|
-
{ type: 'text', value: '帮我整理 ' },
|
|
55
|
-
{
|
|
56
|
-
type: 'entity',
|
|
57
|
-
icon: 'message-chat-square-stroked',
|
|
58
|
-
label: '智能会话:社交私信',
|
|
59
|
-
showChevron: true,
|
|
60
|
-
},
|
|
61
|
-
{ type: 'text', value: ' 场景近 7 天的人工解决率,做一份分析报告' },
|
|
62
|
-
],
|
|
63
|
-
userAttachments: [
|
|
64
|
-
{ id: 'att-1', name: '抖音电商售后政策汇编.pdf', size: 327680 },
|
|
65
|
-
],
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
/* m2 AI(合并:深度思考 + 引导文本 + 任务规划卡,已被用户处理 → 禁用置灰态) */
|
|
69
|
-
{
|
|
70
|
-
id: 'm2',
|
|
71
|
-
kind: 'ai-task-plan',
|
|
72
|
-
timestamp: '14:02',
|
|
73
|
-
thinking: {
|
|
74
|
-
...DEFAULT_CHAT_THINKING,
|
|
75
|
-
state: 'completed',
|
|
76
|
-
defaultExpanded: false,
|
|
77
|
-
},
|
|
78
|
-
leadText: '好的,我先做一份任务规划,请稍后...',
|
|
79
|
-
planConfirmed: true,
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
/* m3 用户:开始执行任务 */
|
|
83
|
-
{
|
|
84
|
-
id: 'm3',
|
|
85
|
-
kind: 'user',
|
|
86
|
-
timestamp: '14:03',
|
|
87
|
-
userContent: [{ type: 'text', value: '开始执行任务' }],
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
/* m4 AI:完整执行流消息(执行流 + 总结文本 + 产物组 + 追问) */
|
|
91
|
-
{
|
|
92
|
-
id: 'm4',
|
|
93
|
-
kind: 'ai-flow',
|
|
94
|
-
timestamp: '14:08',
|
|
95
|
-
taskGroups: DEFAULT_CHAT_TASK_GROUPS.map((g) => ({ ...g, status: 'completed' })),
|
|
96
|
-
resultText: DEFAULT_CHAT_RESULT,
|
|
97
|
-
resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS,
|
|
98
|
-
followUps: DEFAULT_CHAT_FOLLOW_UPS,
|
|
99
|
-
},
|
|
100
|
-
];
|
|
101
|
-
}
|
|
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
|
+
};
|
|
102
59
|
|
|
103
60
|
/* ── 关键词 → 回复路由 ── */
|
|
104
61
|
const TASK_KEYWORDS = ['整理', '分析', '生成', '梳理', '输出', '汇总'];
|
|
@@ -194,6 +151,108 @@ function routeReply(text) {
|
|
|
194
151
|
return { type: 'short', text: GENERIC_REPLIES[Math.floor(Math.random() * GENERIC_REPLIES.length)] };
|
|
195
152
|
}
|
|
196
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
|
+
|
|
197
256
|
/* ── 单条消息 → ChatMessage props 映射 ──
|
|
198
257
|
* isLatest:是否为列表中最后一条(决定 historyMode 是否启用)
|
|
199
258
|
* handlers:所有交互回调集合
|
|
@@ -202,7 +261,15 @@ function routeReply(text) {
|
|
|
202
261
|
* · onFollowUpSelect(text) — 追问 chip 点击(默认作为新一轮对话发出)
|
|
203
262
|
*/
|
|
204
263
|
function messageToChatProps(msg, isLatest, handlers = {}) {
|
|
205
|
-
const {
|
|
264
|
+
const {
|
|
265
|
+
onPlanConfirm,
|
|
266
|
+
onPlanCancel,
|
|
267
|
+
onFollowUpSelect,
|
|
268
|
+
onConfirmPrimary,
|
|
269
|
+
onConfirmSecondary,
|
|
270
|
+
onConfirmOptionChange,
|
|
271
|
+
onConfirmFormChange,
|
|
272
|
+
} = handlers;
|
|
206
273
|
const baseProps = {
|
|
207
274
|
className: msg.className || '',
|
|
208
275
|
};
|
|
@@ -262,6 +329,41 @@ function messageToChatProps(msg, isLatest, handlers = {}) {
|
|
|
262
329
|
thinking: msg.thinking,
|
|
263
330
|
resultText: msg.resultText,
|
|
264
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,
|
|
265
367
|
actions: baseActions,
|
|
266
368
|
};
|
|
267
369
|
}
|
|
@@ -329,7 +431,7 @@ export default function ChatConversationPattern({
|
|
|
329
431
|
initialMessages,
|
|
330
432
|
}) {
|
|
331
433
|
const resolvedInitialMessagesRef = useRef(
|
|
332
|
-
Array.isArray(initialMessages) ? initialMessages :
|
|
434
|
+
Array.isArray(initialMessages) ? initialMessages : [],
|
|
333
435
|
);
|
|
334
436
|
const [messages, setMessages] = useState(() => resolvedInitialMessagesRef.current);
|
|
335
437
|
const [phase, setPhase] = useState(() => (
|
|
@@ -346,10 +448,62 @@ export default function ChatConversationPattern({
|
|
|
346
448
|
const [prefillSeed, setPrefillSeed] = useState(0);
|
|
347
449
|
const replyTimerRef = useRef(null);
|
|
348
450
|
const busyTimerRef = useRef(null);
|
|
451
|
+
const flowTimersRef = useRef([]);
|
|
452
|
+
const pendingFlowResumeRef = useRef(null);
|
|
349
453
|
const scrollRef = useRef(null);
|
|
454
|
+
const latestAnchorRef = useRef(null);
|
|
455
|
+
const conversationContextRef = useRef({ ...DEFAULT_CONVERSATION_CONTEXT });
|
|
350
456
|
const idSeedRef = useRef(100);
|
|
351
457
|
const nextId = useCallback((prefix = 'm') => `${prefix}${++idSeedRef.current}`, []);
|
|
352
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
|
+
|
|
353
507
|
/* chip 回填到输入框(覆盖当前草稿 + 自动聚焦) */
|
|
354
508
|
const prefillInput = useCallback((text) => {
|
|
355
509
|
setPrefillText(text || '');
|
|
@@ -361,8 +515,10 @@ export default function ChatConversationPattern({
|
|
|
361
515
|
() => () => {
|
|
362
516
|
if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
|
|
363
517
|
if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
|
|
518
|
+
clearFlowTimers();
|
|
519
|
+
clearPendingFlowResume();
|
|
364
520
|
},
|
|
365
|
-
[],
|
|
521
|
+
[clearFlowTimers, clearPendingFlowResume],
|
|
366
522
|
);
|
|
367
523
|
|
|
368
524
|
/* 自动滚到底:用 ResizeObserver 监听消息容器内容高度变化
|
|
@@ -374,11 +530,11 @@ export default function ChatConversationPattern({
|
|
|
374
530
|
const inner = el.firstElementChild;
|
|
375
531
|
if (!inner) return undefined;
|
|
376
532
|
const ro = new ResizeObserver(() => {
|
|
377
|
-
|
|
533
|
+
scrollToLatest();
|
|
378
534
|
});
|
|
379
535
|
ro.observe(inner);
|
|
380
536
|
return () => ro.disconnect();
|
|
381
|
-
}, [phase, messages.length]);
|
|
537
|
+
}, [phase, messages.length, scrollToLatest]);
|
|
382
538
|
|
|
383
539
|
/* 重置回欢迎状态 */
|
|
384
540
|
const handleNewSession = useCallback(() => {
|
|
@@ -388,12 +544,11 @@ export default function ChatConversationPattern({
|
|
|
388
544
|
setPrefillSeed((s) => s + 1);
|
|
389
545
|
}, []);
|
|
390
546
|
|
|
391
|
-
/* ──
|
|
392
|
-
*
|
|
393
|
-
* 2. 追加用户消息「取消」
|
|
394
|
-
* 3. ChatInput 进 replying(短答态)+ 600ms 后追加 AI 寒暄 + 回 default */
|
|
547
|
+
/* ── 取消链路:任务规划卡次按钮 ──
|
|
548
|
+
* 在自动编排有人为闸门等待时,取消会清空 pending resume 并暂停后续链路。 */
|
|
395
549
|
const handlePlanCancel = useCallback(
|
|
396
550
|
(planMsgId) => {
|
|
551
|
+
clearPendingFlowResume();
|
|
397
552
|
setMessages((prev) =>
|
|
398
553
|
prev.map((m) => (m.id === planMsgId ? { ...m, planConfirmed: true } : m)),
|
|
399
554
|
);
|
|
@@ -421,17 +576,137 @@ export default function ChatConversationPattern({
|
|
|
421
576
|
setInputView('default');
|
|
422
577
|
}, 600);
|
|
423
578
|
},
|
|
424
|
-
[nextId],
|
|
579
|
+
[nextId, clearPendingFlowResume],
|
|
580
|
+
);
|
|
581
|
+
|
|
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],
|
|
425
614
|
);
|
|
426
615
|
|
|
427
|
-
|
|
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
|
+
/* ── 任务规划卡主按钮:有人为闸门时用于恢复自动链路;无闸门时保留旧兜底链路 ── */
|
|
428
688
|
const handlePlanConfirm = useCallback(
|
|
429
689
|
(planMsgId) => {
|
|
690
|
+
const hasPendingResume = typeof pendingFlowResumeRef.current === 'function';
|
|
430
691
|
/* 1) 标记该任务规划卡为「已确认」(持久化到消息数据,重渲染时也是禁用态) */
|
|
431
692
|
setMessages((prev) =>
|
|
432
693
|
prev.map((m) => (m.id === planMsgId ? { ...m, planConfirmed: true } : m)),
|
|
433
694
|
);
|
|
434
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
|
+
|
|
435
710
|
const flowMsgId = nextId('flow');
|
|
436
711
|
|
|
437
712
|
/* 2) 立即追加:用户消息「开始执行任务」+ AI 流式执行流(带 stream 标记,由 StreamingChatMessage 接管) */
|
|
@@ -486,12 +761,145 @@ export default function ChatConversationPattern({
|
|
|
486
761
|
setInputView('default');
|
|
487
762
|
}, totalMs);
|
|
488
763
|
},
|
|
489
|
-
[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],
|
|
490
898
|
);
|
|
491
899
|
|
|
492
900
|
/* ── 用户输入框发送 ──
|
|
493
|
-
* 状态机:default → replying →
|
|
494
|
-
*
|
|
901
|
+
* 状态机:default → replying → AI 深度思考 → 人工澄清确认 → busy 执行流 → 结论/产物/追问 → default
|
|
902
|
+
* 底部输入与 followUps 点击都进入同一条会话流,并自动继承上一轮上下文。 */
|
|
495
903
|
const handleSend = useCallback(
|
|
496
904
|
(text, ctx = {}) => {
|
|
497
905
|
const trimmed = (text || '').trim();
|
|
@@ -520,64 +928,35 @@ export default function ChatConversationPattern({
|
|
|
520
928
|
},
|
|
521
929
|
]);
|
|
522
930
|
setInputView('replying');
|
|
931
|
+
scheduleScrollToLatest();
|
|
523
932
|
|
|
524
933
|
const reply = routeReply(trimmed);
|
|
525
934
|
if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
|
|
935
|
+
if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
|
|
936
|
+
clearFlowTimers();
|
|
937
|
+
clearPendingFlowResume();
|
|
526
938
|
|
|
527
|
-
|
|
528
|
-
if (reply.type === 'follow-up-result') {
|
|
939
|
+
if (reply.type === 'short' && STOP_KEYWORDS.some((keyword) => trimmed.includes(keyword))) {
|
|
529
940
|
replyTimerRef.current = setTimeout(() => {
|
|
530
941
|
setMessages((prev) => [
|
|
531
942
|
...prev,
|
|
532
943
|
{
|
|
533
944
|
id: nextId('a'),
|
|
534
|
-
kind: 'ai-
|
|
945
|
+
kind: 'ai-text',
|
|
535
946
|
timestamp: nowHHmm(),
|
|
536
|
-
resultText: reply.
|
|
537
|
-
resultArtifacts: reply.resultArtifacts,
|
|
538
|
-
followUps: reply.followUps,
|
|
947
|
+
resultText: reply.text,
|
|
539
948
|
className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
|
|
540
949
|
},
|
|
541
950
|
]);
|
|
542
951
|
setInputView('default');
|
|
952
|
+
scheduleScrollToLatest();
|
|
543
953
|
}, 600);
|
|
544
954
|
return;
|
|
545
955
|
}
|
|
546
956
|
|
|
547
|
-
|
|
548
|
-
if (reply.type === 'task') {
|
|
549
|
-
replyTimerRef.current = setTimeout(() => {
|
|
550
|
-
setMessages((prev) => [
|
|
551
|
-
...prev,
|
|
552
|
-
{
|
|
553
|
-
id: nextId('plan'),
|
|
554
|
-
kind: 'ai-task-plan',
|
|
555
|
-
timestamp: nowHHmm(),
|
|
556
|
-
leadText: '我开始规划啦,请稍后...',
|
|
557
|
-
planConfirmed: false,
|
|
558
|
-
},
|
|
559
|
-
]);
|
|
560
|
-
setInputView('default');
|
|
561
|
-
}, 600);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/* 4) 短答:600ms 后追加 AI 短答 + 输入框回 default */
|
|
566
|
-
replyTimerRef.current = setTimeout(() => {
|
|
567
|
-
setMessages((prev) => [
|
|
568
|
-
...prev,
|
|
569
|
-
{
|
|
570
|
-
id: nextId('a'),
|
|
571
|
-
kind: 'ai-text',
|
|
572
|
-
timestamp: nowHHmm(),
|
|
573
|
-
resultText: reply.text,
|
|
574
|
-
className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
|
|
575
|
-
},
|
|
576
|
-
]);
|
|
577
|
-
setInputView('default');
|
|
578
|
-
}, 600);
|
|
957
|
+
startAutoOrchestratedFlow(trimmed, reply, ctx);
|
|
579
958
|
},
|
|
580
|
-
[phase, nextId],
|
|
959
|
+
[phase, nextId, clearFlowTimers, clearPendingFlowResume, scheduleScrollToLatest, startAutoOrchestratedFlow],
|
|
581
960
|
);
|
|
582
961
|
|
|
583
962
|
const handleFollowUpSend = useCallback(
|
|
@@ -598,6 +977,8 @@ export default function ChatConversationPattern({
|
|
|
598
977
|
clearTimeout(busyTimerRef.current);
|
|
599
978
|
busyTimerRef.current = null;
|
|
600
979
|
}
|
|
980
|
+
clearFlowTimers();
|
|
981
|
+
clearPendingFlowResume();
|
|
601
982
|
setInputView('default');
|
|
602
983
|
setMessages((prev) => [
|
|
603
984
|
...prev,
|
|
@@ -608,15 +989,18 @@ export default function ChatConversationPattern({
|
|
|
608
989
|
resultText: '已停止当前任务,需要时再叫我。',
|
|
609
990
|
},
|
|
610
991
|
]);
|
|
611
|
-
}, [nextId]);
|
|
992
|
+
}, [nextId, clearFlowTimers, clearPendingFlowResume]);
|
|
612
993
|
|
|
613
994
|
/* 新会话:清空消息 + 切欢迎屏 + 重置输入框状态 */
|
|
614
995
|
const handleNewSessionFull = useCallback(() => {
|
|
615
996
|
if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
|
|
616
997
|
if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
|
|
998
|
+
clearFlowTimers();
|
|
999
|
+
clearPendingFlowResume();
|
|
1000
|
+
conversationContextRef.current = { ...DEFAULT_CONVERSATION_CONTEXT };
|
|
617
1001
|
setInputView('default');
|
|
618
1002
|
handleNewSession();
|
|
619
|
-
}, [handleNewSession]);
|
|
1003
|
+
}, [handleNewSession, clearFlowTimers, clearPendingFlowResume]);
|
|
620
1004
|
|
|
621
1005
|
const lastIdx = messages.length - 1;
|
|
622
1006
|
const isNewConversationState = phase === 'welcome' || messages.length === 0;
|
|
@@ -649,11 +1033,16 @@ export default function ChatConversationPattern({
|
|
|
649
1033
|
) : (
|
|
650
1034
|
<ChatPhase
|
|
651
1035
|
scrollRef={scrollRef}
|
|
1036
|
+
latestAnchorRef={latestAnchorRef}
|
|
652
1037
|
messages={messages}
|
|
653
1038
|
lastIdx={lastIdx}
|
|
654
1039
|
handlers={{
|
|
655
1040
|
onPlanConfirm: handlePlanConfirm,
|
|
656
1041
|
onPlanCancel: handlePlanCancel,
|
|
1042
|
+
onConfirmPrimary: handleConfirmPrimary,
|
|
1043
|
+
onConfirmSecondary: handleConfirmSecondary,
|
|
1044
|
+
onConfirmOptionChange: handleConfirmOptionChange,
|
|
1045
|
+
onConfirmFormChange: handleConfirmFormChange,
|
|
657
1046
|
onFollowUpSelect: handleFollowUpSend,
|
|
658
1047
|
}}
|
|
659
1048
|
onSend={handleSend}
|
|
@@ -670,7 +1059,18 @@ export default function ChatConversationPattern({
|
|
|
670
1059
|
/* ============================================================
|
|
671
1060
|
* ChatPhase — 对话阶段:消息流 + 底部 ChatInput
|
|
672
1061
|
* ============================================================ */
|
|
673
|
-
function ChatPhase({
|
|
1062
|
+
function ChatPhase({
|
|
1063
|
+
scrollRef,
|
|
1064
|
+
latestAnchorRef,
|
|
1065
|
+
messages,
|
|
1066
|
+
lastIdx,
|
|
1067
|
+
handlers,
|
|
1068
|
+
onSend,
|
|
1069
|
+
onStop,
|
|
1070
|
+
inputView,
|
|
1071
|
+
prefillText,
|
|
1072
|
+
prefillSeed,
|
|
1073
|
+
}) {
|
|
674
1074
|
/* 上下 40px 渐隐遮罩:让消息进入 / 离开滚动视口时柔和过渡,不硬切
|
|
675
1075
|
* 顶部渐隐仅在「可向上滚」时启用,回到第一条时取消,避免首条被遮淡 */
|
|
676
1076
|
const [atTop, setAtTop] = useState(true);
|
|
@@ -725,8 +1125,8 @@ function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inp
|
|
|
725
1125
|
>
|
|
726
1126
|
<div
|
|
727
1127
|
className="mx-auto flex w-full flex-col gap-2"
|
|
728
|
-
/* 底部
|
|
729
|
-
style={{ width: '800px', maxWidth: '100%', padding: '0 20px
|
|
1128
|
+
/* 底部 80px padding:让最后一条会话卡片再上移 20px,避免被底部渐隐遮挡 */
|
|
1129
|
+
style={{ width: '800px', maxWidth: '100%', padding: '0 20px 80px' }}
|
|
730
1130
|
>
|
|
731
1131
|
{messages.map((m, idx) => {
|
|
732
1132
|
const isLatest = idx === lastIdx;
|
|
@@ -737,6 +1137,7 @@ function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inp
|
|
|
737
1137
|
}
|
|
738
1138
|
return <ChatMessage key={m.id} {...props} />;
|
|
739
1139
|
})}
|
|
1140
|
+
<div ref={latestAnchorRef} aria-hidden="true" style={{ height: '1px' }} />
|
|
740
1141
|
</div>
|
|
741
1142
|
</div>
|
|
742
1143
|
|