@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.
- package/package.json +1 -1
- 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 +31 -0
- package/skills/tfds/components.index.json +75 -27
- package/skills/tfds/components.summary.json +13 -13
- package/src/_b_end_runtime/components/Card.jsx +151 -13
- package/src/_b_end_runtime/components/Card.tokens.js +27 -3
- 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 +68 -68
- package/src/_b_end_runtime/components/ConversationList.tokens.js +5 -3
- package/src/_b_end_runtime/components/FullScreenPage.jsx +1 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +13 -15
- package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +2 -0
- package/src/_b_end_runtime/components/Modal.jsx +1 -0
- package/src/_b_end_runtime/components/Sheet.jsx +1 -0
- package/src/_b_end_runtime/components/Table.jsx +7 -0
- 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/TagBar.jsx +2 -0
- package/src/_b_end_runtime/components/Toast.jsx +1 -0
- package/src/_b_end_runtime/components/Upload.jsx +1 -0
- package/src/_b_end_runtime/components.js +24 -11
- package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +548 -135
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +1 -1
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +6 -6
- package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +66 -5
- package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +50 -17
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +28 -78
- package/src/_b_end_runtime/patterns.js +32 -21
- package/src/_b_end_runtime/preview-registry.jsx +20 -4
- 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
|
-
* -
|
|
26
|
-
* - phase = '
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 {
|
|
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
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 →
|
|
485
|
-
*
|
|
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
|
-
|
|
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-
|
|
945
|
+
kind: 'ai-text',
|
|
526
946
|
timestamp: nowHHmm(),
|
|
527
|
-
resultText: reply.
|
|
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
|
-
|
|
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 =
|
|
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={
|
|
1022
|
+
<TopBar title={displayTitle} onNewSession={handleNewSessionFull} disableNewSession={isNewConversationState} />
|
|
629
1023
|
|
|
630
|
-
{
|
|
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({
|
|
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
|
-
/* 底部
|
|
719
|
-
style={{ width: '800px', maxWidth: '100%', padding: '0 20px
|
|
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
|
-
*
|
|
759
|
-
*
|
|
1169
|
+
* NewConversationPhase — AI 对话页的唯一空会话 / 新建会话页面
|
|
1170
|
+
* 当 messages 为空时,必须展示该页面;复用 Copilot welcome 视觉,ChatInput 仍底部吸底
|
|
760
1171
|
* ============================================================ */
|
|
761
|
-
function
|
|
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>
|