@tfdesign/b-end 1.0.16 → 1.0.18
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 +1 -1
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +123 -4
- package/skills/tfds/LAYOUT_RECIPES.md +5 -2
- package/skills/tfds/LAYOUT_RULES.md +52 -29
- package/skills/tfds/components.index.json +33 -9
- package/skills/tfds/components.summary.json +12 -10
- package/src/_b_end_runtime/components/Filter.jsx +60 -27
- package/src/_b_end_runtime/components/NavBar.jsx +1 -1
- package/src/_b_end_runtime/components/NavBar.tokens.js +1 -0
- package/src/_b_end_runtime/components/Select.jsx +18 -12
- package/src/_b_end_runtime/components/Select.tokens.js +6 -2
- package/src/_b_end_runtime/components/Table.jsx +7 -7
- package/src/_b_end_runtime/components.js +17 -9
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +882 -83
- package/src/_b_end_runtime/preview-registry.jsx +27 -9
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import Button from '../components/Button';
|
|
3
3
|
import Tabs from '../components/Tabs';
|
|
4
4
|
import ChatInput from '../components/ChatInput';
|
|
5
5
|
import Switch from '../components/Switch';
|
|
6
6
|
import Icon from '../components/Icon';
|
|
7
|
+
import ChatMessage, {
|
|
8
|
+
DEFAULT_CHAT_FORM_CONFIRM,
|
|
9
|
+
DEFAULT_CHAT_RESULT,
|
|
10
|
+
DEFAULT_CHAT_RESULT_ARTIFACTS,
|
|
11
|
+
DEFAULT_CHAT_TASK_GROUPS,
|
|
12
|
+
DEFAULT_CHAT_THINKING,
|
|
13
|
+
useStreamingTaskGroups,
|
|
14
|
+
} from '../components/ChatMessage';
|
|
7
15
|
import catcatSvg from '../components/file-type-assets/catcat.svg';
|
|
8
16
|
|
|
9
17
|
/**
|
|
@@ -35,8 +43,305 @@ const COPILOT_SUGGESTIONS = [
|
|
|
35
43
|
'基于批测报告分析优化方向和问题',
|
|
36
44
|
];
|
|
37
45
|
|
|
46
|
+
const STREAM_INTERVAL = 600;
|
|
47
|
+
const AUTO_FLOW_TIMINGS = {
|
|
48
|
+
thinkingDone: 900,
|
|
49
|
+
confirmShow: 1400,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const DEFAULT_CONVERSATION_CONTEXT = {
|
|
53
|
+
account: '抖音社区-社交-私信',
|
|
54
|
+
environment: '服务策略 / 线上策略 V8',
|
|
55
|
+
mock: '近 7 天私信会话样本 186 条',
|
|
56
|
+
conclusion: '上一轮已沉淀私信场景的服务策略问题清单,可继续追问优化方向。',
|
|
57
|
+
artifacts: DEFAULT_CHAT_RESULT_ARTIFACTS.map((item) => item.title),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const TASK_KEYWORDS = ['整理', '分析', '生成', '梳理', '输出', '汇总', '优化'];
|
|
61
|
+
const STOP_KEYWORDS = ['停止', '取消'];
|
|
62
|
+
const GENERIC_REPLIES = [
|
|
63
|
+
'收到,我会结合当前策略上下文继续处理。',
|
|
64
|
+
'明白了,我先按这个方向帮你梳理。',
|
|
65
|
+
'可以,我会基于当前私信场景继续补充分析。',
|
|
66
|
+
'没问题,我先整理成可执行的建议给你。',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const FOLLOW_UP_REPLIES = {
|
|
70
|
+
查看风险样本: {
|
|
71
|
+
resultText:
|
|
72
|
+
'已筛出 12 条高风险私信样本,主要集中在「用户情绪升级但意图识别偏弱」和「跨轮追问时上下文衔接不足」两类问题。建议优先补强情绪识别权重、补齐意图转人工闸门,并针对高投诉场景增加策略兜底。',
|
|
73
|
+
resultArtifacts: [
|
|
74
|
+
{ id: 'risk-samples', type: 'table', title: '高风险私信样本明细', meta: '表格 · 12 条' },
|
|
75
|
+
{ id: 'risk-summary', type: 'document', title: '问题样本归因摘要', meta: '文档 · 2 页' },
|
|
76
|
+
],
|
|
77
|
+
followUps: ['生成优化建议', '补充验证方案', '导出给运营复盘'],
|
|
78
|
+
},
|
|
79
|
+
生成优化建议: {
|
|
80
|
+
resultText:
|
|
81
|
+
'已生成一版服务策略优化建议:针对私信挽回、情绪安抚、转人工兜底和促成成交 4 条链路补充了策略节点,建议先在高风险词命中场景灰度 10% 验证一次答复命中率和人工接管率,再逐步放量。',
|
|
82
|
+
resultArtifacts: [
|
|
83
|
+
{ id: 'strategy-revamp', type: 'strategy', title: '服务策略优化建议', meta: '策略 · 4 条主链路' },
|
|
84
|
+
{ id: 'validation-plan', type: 'document', title: '灰度验证计划', meta: '文档 · 3 项指标' },
|
|
85
|
+
],
|
|
86
|
+
followUps: ['生成 AB 实验方案', '补充灰度指标', '同步到飞书文档'],
|
|
87
|
+
},
|
|
88
|
+
补充验证方案: {
|
|
89
|
+
resultText:
|
|
90
|
+
'我补充了一版验证方案:建议按照新客转化、老客挽回、风险升级 3 类会话拆分样本,分别观察策略命中率、追问完成率和转人工率,并补一组高情绪用户样本做极端场景回放。',
|
|
91
|
+
resultArtifacts: [
|
|
92
|
+
{ id: 'validation-sheet', type: 'table', title: '验证样本拆分矩阵', meta: '表格 · 3 类场景' },
|
|
93
|
+
{ id: 'replay-plan', type: 'batch-report', title: '极端场景回放计划', meta: '回放 · 28 条样本' },
|
|
94
|
+
],
|
|
95
|
+
followUps: ['生成批测任务', '查看高情绪样本', '补充策略兜底'],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function nowHHmm() {
|
|
100
|
+
const d = new Date();
|
|
101
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function routeReply(text) {
|
|
105
|
+
if (FOLLOW_UP_REPLIES[text]) {
|
|
106
|
+
return { type: 'follow-up-result', ...FOLLOW_UP_REPLIES[text] };
|
|
107
|
+
}
|
|
108
|
+
if (STOP_KEYWORDS.some((keyword) => text.includes(keyword))) {
|
|
109
|
+
return { type: 'short', text: '已停止当前任务,需要时再叫我。' };
|
|
110
|
+
}
|
|
111
|
+
if (TASK_KEYWORDS.some((keyword) => text.includes(keyword))) {
|
|
112
|
+
return { type: 'task' };
|
|
113
|
+
}
|
|
114
|
+
return { type: 'short', text: GENERIC_REPLIES[Math.floor(Math.random() * GENERIC_REPLIES.length)] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildThinkingContent(text, context) {
|
|
118
|
+
const artifacts = context.artifacts?.length ? context.artifacts.join('、') : '暂无历史产物';
|
|
119
|
+
return [
|
|
120
|
+
`用户本轮输入:${text}`,
|
|
121
|
+
`系统自动带入当前业务场景:${context.account}`,
|
|
122
|
+
`系统自动带入当前环境:${context.environment}`,
|
|
123
|
+
`系统自动带入样本范围:${context.mock}`,
|
|
124
|
+
`系统自动带入上一轮结论:${context.conclusion || '暂无历史结论'}`,
|
|
125
|
+
`系统自动带入上一轮产物:${artifacts}`,
|
|
126
|
+
'先完成上下文理解与执行口径确认,再进入自动执行流。',
|
|
127
|
+
].join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildClarificationConfirms(context) {
|
|
131
|
+
return DEFAULT_CHAT_FORM_CONFIRM.map((confirm) => ({
|
|
132
|
+
...confirm,
|
|
133
|
+
id: 'copilot-clarify-confirm',
|
|
134
|
+
title: '人工澄清确认',
|
|
135
|
+
primaryActionLabel: '确认并继续',
|
|
136
|
+
secondaryActionLabel: '',
|
|
137
|
+
formItems: [
|
|
138
|
+
{
|
|
139
|
+
id: 'scene',
|
|
140
|
+
label: '分析目标',
|
|
141
|
+
type: 'select',
|
|
142
|
+
placeholder: '请选择分析目标',
|
|
143
|
+
defaultValue: 'strategy-analysis',
|
|
144
|
+
options: [
|
|
145
|
+
{ value: 'strategy-analysis', label: '服务策略分析' },
|
|
146
|
+
{ value: 'conversation-review', label: '私信会话复盘' },
|
|
147
|
+
{ value: 'ab-plan', label: 'AB 实验方案' },
|
|
148
|
+
],
|
|
149
|
+
fullWidth: true,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'channel',
|
|
153
|
+
label: '上下文来源',
|
|
154
|
+
type: 'select',
|
|
155
|
+
placeholder: '请选择上下文来源',
|
|
156
|
+
defaultValue: 'current-context',
|
|
157
|
+
options: [
|
|
158
|
+
{
|
|
159
|
+
value: 'current-context',
|
|
160
|
+
label: `使用当前页面上下文、${context.environment} 与 ${context.mock}`,
|
|
161
|
+
},
|
|
162
|
+
{ value: 'refresh-mock', label: '刷新样本后继续分析' },
|
|
163
|
+
{ value: 'manual-review', label: '先人工复核关键信息' },
|
|
164
|
+
],
|
|
165
|
+
fullWidth: true,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'remark',
|
|
169
|
+
label: '补充说明',
|
|
170
|
+
type: 'input',
|
|
171
|
+
placeholder: '请输入补充说明(可选)',
|
|
172
|
+
fullWidth: true,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildAutoFlowPayload(text, reply, context) {
|
|
179
|
+
if (reply.type === 'follow-up-result') {
|
|
180
|
+
return {
|
|
181
|
+
resultText: `${reply.resultText}\n\n本轮追问已自动继承当前业务场景、页面环境、样本范围和上一轮结论,无需重新补充上下文。`,
|
|
182
|
+
resultArtifacts: reply.resultArtifacts,
|
|
183
|
+
followUps: reply.followUps,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (reply.type === 'task') {
|
|
188
|
+
return {
|
|
189
|
+
resultText:
|
|
190
|
+
`${DEFAULT_CHAT_RESULT}\n\n我已结合当前页面的「${context.account}」上下文继续执行,并会优先围绕服务策略、流程节点和批测风险给出建议。`,
|
|
191
|
+
resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS,
|
|
192
|
+
followUps: ['查看风险样本', '生成优化建议', '补充验证方案'],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
resultText:
|
|
198
|
+
`${reply.text} 已结合当前页面上下文继续处理「${text}」:当前场景为 ${context.account},环境为 ${context.environment},样本范围为 ${context.mock}。如果你需要更深入的结论,可以继续选择下方追问。`,
|
|
199
|
+
resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS.slice(0, 2),
|
|
200
|
+
followUps: ['继续细化结论', '补充一轮验证', '生成可交付文档'],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getFlowTotalMs() {
|
|
205
|
+
const totalSteps = DEFAULT_CHAT_TASK_GROUPS.reduce(
|
|
206
|
+
(sum, group) => sum + (group.steps?.length ?? 0),
|
|
207
|
+
0,
|
|
208
|
+
);
|
|
209
|
+
return (totalSteps + 1) * STREAM_INTERVAL;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function hasActionablePrimaryButton(label) {
|
|
213
|
+
return typeof label === 'string' && label.trim().length > 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function confirmsRequireHumanGate(confirms) {
|
|
217
|
+
return Array.isArray(confirms) && confirms.some((confirm) => hasActionablePrimaryButton(confirm?.primaryActionLabel));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function messageToChatProps(msg, isLatest, handlers = {}) {
|
|
221
|
+
const {
|
|
222
|
+
onFollowUpSelect,
|
|
223
|
+
onConfirmPrimary,
|
|
224
|
+
onConfirmSecondary,
|
|
225
|
+
onConfirmOptionChange,
|
|
226
|
+
onConfirmFormChange,
|
|
227
|
+
} = handlers;
|
|
228
|
+
const baseActions = {
|
|
229
|
+
showCopy: true,
|
|
230
|
+
showQuote: true,
|
|
231
|
+
showLike: true,
|
|
232
|
+
showDislike: true,
|
|
233
|
+
historyMode: !isLatest,
|
|
234
|
+
};
|
|
235
|
+
const baseProps = { className: msg.className || '' };
|
|
236
|
+
|
|
237
|
+
const wrapFollowUps = (raw) => {
|
|
238
|
+
if (!raw) return null;
|
|
239
|
+
const items = Array.isArray(raw) ? raw : raw.items;
|
|
240
|
+
if (!Array.isArray(items) || items.length === 0) return null;
|
|
241
|
+
return {
|
|
242
|
+
items,
|
|
243
|
+
onSelect: (item) => {
|
|
244
|
+
const label = typeof item === 'string' ? item : item?.label;
|
|
245
|
+
if (label && onFollowUpSelect) onFollowUpSelect(label);
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (msg.kind === 'user') {
|
|
251
|
+
return {
|
|
252
|
+
...baseProps,
|
|
253
|
+
role: 'user',
|
|
254
|
+
timestamp: msg.timestamp,
|
|
255
|
+
userContent: msg.userContent,
|
|
256
|
+
actions: baseActions,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (msg.kind === 'ai-text') {
|
|
261
|
+
return {
|
|
262
|
+
...baseProps,
|
|
263
|
+
role: 'ai',
|
|
264
|
+
header: true,
|
|
265
|
+
title: '',
|
|
266
|
+
steps: null,
|
|
267
|
+
resultText: msg.resultText,
|
|
268
|
+
timestamp: msg.timestamp,
|
|
269
|
+
actions: baseActions,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (msg.kind === 'ai-thinking') {
|
|
274
|
+
return {
|
|
275
|
+
...baseProps,
|
|
276
|
+
role: 'ai',
|
|
277
|
+
header: true,
|
|
278
|
+
title: '',
|
|
279
|
+
steps: null,
|
|
280
|
+
thinking: msg.thinking,
|
|
281
|
+
resultText: msg.resultText,
|
|
282
|
+
timestamp: msg.timestamp,
|
|
283
|
+
actions: msg.thinking?.state === 'thinking' ? null : baseActions,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (msg.kind === 'ai-confirm') {
|
|
288
|
+
return {
|
|
289
|
+
...baseProps,
|
|
290
|
+
role: 'ai',
|
|
291
|
+
header: true,
|
|
292
|
+
title: '',
|
|
293
|
+
steps: null,
|
|
294
|
+
leadText: msg.leadText,
|
|
295
|
+
confirms: Array.isArray(msg.confirms)
|
|
296
|
+
? msg.confirms.map((confirm) => ({
|
|
297
|
+
...confirm,
|
|
298
|
+
onOptionChange: (value, option) => onConfirmOptionChange && onConfirmOptionChange(
|
|
299
|
+
msg.id,
|
|
300
|
+
confirm.id,
|
|
301
|
+
value,
|
|
302
|
+
option,
|
|
303
|
+
),
|
|
304
|
+
onFormChange: (formValues, meta) => onConfirmFormChange && onConfirmFormChange(
|
|
305
|
+
msg.id,
|
|
306
|
+
confirm.id,
|
|
307
|
+
formValues,
|
|
308
|
+
meta,
|
|
309
|
+
),
|
|
310
|
+
onPrimaryAction: (payload) => onConfirmPrimary && onConfirmPrimary(msg.id, confirm.id, {
|
|
311
|
+
...payload,
|
|
312
|
+
primaryActionLabel: confirm.primaryActionLabel,
|
|
313
|
+
}),
|
|
314
|
+
onSecondaryAction: () => onConfirmSecondary && onConfirmSecondary(msg.id, confirm.id),
|
|
315
|
+
}))
|
|
316
|
+
: msg.confirms,
|
|
317
|
+
timestamp: msg.timestamp,
|
|
318
|
+
actions: baseActions,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
...baseProps,
|
|
324
|
+
role: 'ai',
|
|
325
|
+
header: true,
|
|
326
|
+
title: '',
|
|
327
|
+
steps: null,
|
|
328
|
+
taskGroups: msg.taskGroups,
|
|
329
|
+
resultText: msg.resultText,
|
|
330
|
+
resultArtifacts: msg.resultArtifacts,
|
|
331
|
+
followUps: wrapFollowUps(msg.followUps),
|
|
332
|
+
taskBadge: msg.taskBadge,
|
|
333
|
+
timestamp: msg.timestamp,
|
|
334
|
+
actions: msg.stream === true ? null : baseActions,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function StreamingChatMessage({ taskGroups, ...rest }) {
|
|
339
|
+
const streamed = useStreamingTaskGroups(taskGroups, { intervalMs: STREAM_INTERVAL });
|
|
340
|
+
return <ChatMessage {...rest} taskGroups={streamed} />;
|
|
341
|
+
}
|
|
342
|
+
|
|
38
343
|
export default function CopilotPagePattern() {
|
|
39
|
-
const [copilotOpen, setCopilotOpen] = useState(
|
|
344
|
+
const [copilotOpen, setCopilotOpen] = useState(true);
|
|
40
345
|
|
|
41
346
|
return (
|
|
42
347
|
<div
|
|
@@ -63,24 +368,24 @@ export default function CopilotPagePattern() {
|
|
|
63
368
|
/* ────────────────────────────────────────────
|
|
64
369
|
顶导栏
|
|
65
370
|
左:返回 + 标题 + OLA AI 触发按钮
|
|
66
|
-
|
|
371
|
+
中:一级页面 Tab(宽度不足时整组换到第二行左对齐)
|
|
67
372
|
右:版本 + 更多 + 次操作 + 主操作
|
|
68
373
|
──────────────────────────────────────────── */
|
|
69
374
|
function TopBar({ copilotOpen, onToggleCopilot }) {
|
|
70
375
|
return (
|
|
71
376
|
<div className="shrink-0 px-4 py-4">
|
|
72
|
-
<div className="relative flex min-h-[36px] items-center">
|
|
73
|
-
<div className="flex min-w-
|
|
377
|
+
<div className="relative flex min-h-[36px] min-w-0 flex-wrap items-center gap-x-4 gap-y-3">
|
|
378
|
+
<div className="order-1 flex min-w-[280px] flex-1 items-center gap-3 xl:order-none">
|
|
74
379
|
<TopBarLead copilotOpen={copilotOpen} onToggleCopilot={onToggleCopilot} />
|
|
75
380
|
</div>
|
|
76
381
|
|
|
77
|
-
<div className="pointer-events-none absolute inset-x-0
|
|
78
|
-
<div className="pointer-events-auto
|
|
382
|
+
<div className="order-3 flex w-full min-w-0 justify-start xl:pointer-events-none xl:absolute xl:inset-x-0 xl:order-none xl:w-auto xl:justify-center">
|
|
383
|
+
<div className="xl:pointer-events-auto">
|
|
79
384
|
<TopBarTabs />
|
|
80
385
|
</div>
|
|
81
386
|
</div>
|
|
82
387
|
|
|
83
|
-
<div className="ml-auto flex shrink-0 items-center gap-4
|
|
388
|
+
<div className="order-2 ml-auto flex shrink-0 items-center gap-4 xl:order-none">
|
|
84
389
|
<TopBarActions />
|
|
85
390
|
</div>
|
|
86
391
|
</div>
|
|
@@ -91,27 +396,25 @@ function TopBar({ copilotOpen, onToggleCopilot }) {
|
|
|
91
396
|
function TopBarLead({ copilotOpen, onToggleCopilot }) {
|
|
92
397
|
return (
|
|
93
398
|
<>
|
|
94
|
-
<Button variant="outline-black" iconOnly icon={<Icon name="arrow-left-stroked" />} tooltip="返回" aria-label="返回" />
|
|
95
|
-
|
|
96
399
|
<span
|
|
97
|
-
className="font-semibold text-base leading-[22px] whitespace-nowrap"
|
|
400
|
+
className="min-w-0 truncate font-semibold [font-weight:var(--font-semibold)] text-base leading-[22px] whitespace-nowrap"
|
|
98
401
|
style={{ color: 'var(--foreground, #0F1C35)' }}
|
|
99
402
|
>
|
|
100
|
-
|
|
403
|
+
抖音社区-社交-私信
|
|
101
404
|
</span>
|
|
102
405
|
|
|
103
406
|
{!copilotOpen && (
|
|
104
|
-
<
|
|
407
|
+
<Button
|
|
408
|
+
variant="outline-black"
|
|
409
|
+
radius="full"
|
|
105
410
|
onClick={onToggleCopilot}
|
|
106
|
-
className="shrink-0
|
|
411
|
+
className="shrink-0 border-transparent bg-[image:var(--gradient-ai-fill-2)] p-[1.5px] hover:bg-[image:var(--gradient-ai-fill-2)] active:bg-[image:var(--gradient-ai-fill-2)] focus-visible:outline-[var(--color-brand-200)]"
|
|
107
412
|
style={{
|
|
108
|
-
|
|
109
|
-
borderRadius: '999px',
|
|
110
|
-
background: 'linear-gradient(-45deg, rgba(255, 153, 248, 0.4) 0%, rgba(181, 131, 255, 0.4) 25%, rgba(114, 156, 255, 0.4) 48%, rgba(117, 218, 231, 0.4) 83%, rgba(115, 230, 204, 0.4) 100%)',
|
|
413
|
+
boxShadow: '0 0 0 1px rgba(255,255,255,0.6) inset',
|
|
111
414
|
}}
|
|
112
415
|
>
|
|
113
|
-
<
|
|
114
|
-
className="inline-flex items-center gap-2"
|
|
416
|
+
<span
|
|
417
|
+
className="inline-flex items-center gap-2 rounded-full"
|
|
115
418
|
style={{
|
|
116
419
|
height: '34px',
|
|
117
420
|
padding: '0 14px',
|
|
@@ -126,8 +429,8 @@ function TopBarLead({ copilotOpen, onToggleCopilot }) {
|
|
|
126
429
|
>
|
|
127
430
|
<img src={catcatSvg} alt="OLA AI" style={{ width: '20px', height: '20px', display: 'block' }} />
|
|
128
431
|
<span>OLA AI</span>
|
|
129
|
-
</
|
|
130
|
-
</
|
|
432
|
+
</span>
|
|
433
|
+
</Button>
|
|
131
434
|
)}
|
|
132
435
|
</>
|
|
133
436
|
);
|
|
@@ -140,23 +443,6 @@ function TopBarTabs() {
|
|
|
140
443
|
function TopBarActions() {
|
|
141
444
|
return (
|
|
142
445
|
<>
|
|
143
|
-
<div
|
|
144
|
-
className="inline-flex items-center gap-1 shrink-0"
|
|
145
|
-
style={{
|
|
146
|
-
height: '36px',
|
|
147
|
-
padding: '0 16px',
|
|
148
|
-
borderRadius: '999px',
|
|
149
|
-
background: 'var(--color-brand-50, #f0fdfa)',
|
|
150
|
-
border: '1px solid var(--color-brand-500, #5eead4)',
|
|
151
|
-
fontSize: '14px',
|
|
152
|
-
fontWeight: 600,
|
|
153
|
-
color: 'var(--color-brand-950, #065f46)',
|
|
154
|
-
}}
|
|
155
|
-
>
|
|
156
|
-
<span>V8 线上</span>
|
|
157
|
-
<Icon name="chevron-selector-vertical-stroked" size={16} />
|
|
158
|
-
</div>
|
|
159
|
-
|
|
160
446
|
<div className="flex items-center gap-2">
|
|
161
447
|
<Button variant="ghost-black" iconOnly icon={<Icon name="dots-horizontal-stroked" />} tooltip="更多" aria-label="更多" />
|
|
162
448
|
<Button variant="outline-black">次操作</Button>
|
|
@@ -173,57 +459,478 @@ function TopBarActions() {
|
|
|
173
459
|
下:ChatInput
|
|
174
460
|
──────────────────────────────────────────── */
|
|
175
461
|
function CopilotPanel({ onClose }) {
|
|
462
|
+
const [phase, setPhase] = useState('welcome');
|
|
463
|
+
const [messages, setMessages] = useState([]);
|
|
464
|
+
const [inputView, setInputView] = useState('default');
|
|
465
|
+
const replyTimerRef = useRef(null);
|
|
466
|
+
const busyTimerRef = useRef(null);
|
|
467
|
+
const flowTimersRef = useRef([]);
|
|
468
|
+
const pendingFlowResumeRef = useRef(null);
|
|
469
|
+
const scrollRef = useRef(null);
|
|
470
|
+
const latestAnchorRef = useRef(null);
|
|
471
|
+
const idSeedRef = useRef(100);
|
|
472
|
+
const conversationContextRef = useRef({ ...DEFAULT_CONVERSATION_CONTEXT });
|
|
473
|
+
const nextId = useCallback((prefix = 'm') => `${prefix}${++idSeedRef.current}`, []);
|
|
474
|
+
|
|
475
|
+
const clearFlowTimers = useCallback(() => {
|
|
476
|
+
flowTimersRef.current.forEach((timerId) => clearTimeout(timerId));
|
|
477
|
+
flowTimersRef.current = [];
|
|
478
|
+
}, []);
|
|
479
|
+
|
|
480
|
+
const scheduleFlowTimer = useCallback((callback, delay) => {
|
|
481
|
+
const timerId = setTimeout(() => {
|
|
482
|
+
flowTimersRef.current = flowTimersRef.current.filter((id) => id !== timerId);
|
|
483
|
+
callback();
|
|
484
|
+
}, delay);
|
|
485
|
+
flowTimersRef.current.push(timerId);
|
|
486
|
+
}, []);
|
|
487
|
+
|
|
488
|
+
const clearPendingFlowResume = useCallback(() => {
|
|
489
|
+
pendingFlowResumeRef.current = null;
|
|
490
|
+
}, []);
|
|
491
|
+
|
|
492
|
+
const pauseForHumanGate = useCallback((resume) => {
|
|
493
|
+
pendingFlowResumeRef.current = resume;
|
|
494
|
+
}, []);
|
|
495
|
+
|
|
496
|
+
const resumePendingFlow = useCallback(() => {
|
|
497
|
+
const resume = pendingFlowResumeRef.current;
|
|
498
|
+
pendingFlowResumeRef.current = null;
|
|
499
|
+
if (typeof resume === 'function') resume();
|
|
500
|
+
}, []);
|
|
501
|
+
|
|
502
|
+
const scrollToLatest = useCallback((behavior = 'smooth') => {
|
|
503
|
+
if (latestAnchorRef.current?.scrollIntoView) {
|
|
504
|
+
latestAnchorRef.current.scrollIntoView({ block: 'end', behavior });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const el = scrollRef.current;
|
|
508
|
+
if (el) el.scrollTo({ top: el.scrollHeight, behavior });
|
|
509
|
+
}, []);
|
|
510
|
+
|
|
511
|
+
useEffect(() => () => {
|
|
512
|
+
if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
|
|
513
|
+
if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
|
|
514
|
+
clearFlowTimers();
|
|
515
|
+
}, [clearFlowTimers]);
|
|
516
|
+
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
if (phase !== 'chat' || messages.length === 0) return undefined;
|
|
519
|
+
const el = scrollRef.current;
|
|
520
|
+
if (!el) return undefined;
|
|
521
|
+
const inner = el.firstElementChild;
|
|
522
|
+
if (!inner) return undefined;
|
|
523
|
+
const ro = new ResizeObserver(() => {
|
|
524
|
+
scrollToLatest();
|
|
525
|
+
});
|
|
526
|
+
ro.observe(inner);
|
|
527
|
+
return () => ro.disconnect();
|
|
528
|
+
}, [phase, messages.length, scrollToLatest]);
|
|
529
|
+
|
|
530
|
+
const handleNewSession = useCallback(() => {
|
|
531
|
+
if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
|
|
532
|
+
if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
|
|
533
|
+
clearFlowTimers();
|
|
534
|
+
clearPendingFlowResume();
|
|
535
|
+
conversationContextRef.current = { ...DEFAULT_CONVERSATION_CONTEXT };
|
|
536
|
+
setMessages([]);
|
|
537
|
+
setPhase('welcome');
|
|
538
|
+
setInputView('default');
|
|
539
|
+
}, [clearFlowTimers, clearPendingFlowResume]);
|
|
540
|
+
|
|
541
|
+
const handleConfirmPrimary = useCallback(
|
|
542
|
+
(messageId, confirmId, payload = {}) => {
|
|
543
|
+
const isActionable = hasActionablePrimaryButton(payload.primaryActionLabel);
|
|
544
|
+
const selectedLabel = payload.option?.label ?? '';
|
|
545
|
+
setMessages((prev) =>
|
|
546
|
+
prev.map((message) =>
|
|
547
|
+
message.id === messageId
|
|
548
|
+
? {
|
|
549
|
+
...message,
|
|
550
|
+
confirms: Array.isArray(message.confirms)
|
|
551
|
+
? message.confirms.map((confirm) => (
|
|
552
|
+
confirm.id === confirmId
|
|
553
|
+
? {
|
|
554
|
+
...confirm,
|
|
555
|
+
defaultConfirmed: true,
|
|
556
|
+
selectedValue: payload.value ?? confirm.selectedValue,
|
|
557
|
+
defaultSelectedValue: payload.value ?? confirm.defaultSelectedValue,
|
|
558
|
+
selectedLabel: selectedLabel || confirm.selectedLabel,
|
|
559
|
+
formValues: payload.formValues ?? confirm.formValues,
|
|
560
|
+
}
|
|
561
|
+
: confirm
|
|
562
|
+
))
|
|
563
|
+
: message.confirms,
|
|
564
|
+
leadText: isActionable ? '已确认澄清信息,继续执行后续流程。' : message.leadText,
|
|
565
|
+
timestamp: nowHHmm(),
|
|
566
|
+
}
|
|
567
|
+
: message,
|
|
568
|
+
),
|
|
569
|
+
);
|
|
570
|
+
if (isActionable) resumePendingFlow();
|
|
571
|
+
},
|
|
572
|
+
[resumePendingFlow],
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
const handleConfirmOptionChange = useCallback((messageId, confirmId, value, option) => {
|
|
576
|
+
setMessages((prev) =>
|
|
577
|
+
prev.map((message) =>
|
|
578
|
+
message.id === messageId
|
|
579
|
+
? {
|
|
580
|
+
...message,
|
|
581
|
+
confirms: Array.isArray(message.confirms)
|
|
582
|
+
? message.confirms.map((confirm) => (
|
|
583
|
+
confirm.id === confirmId
|
|
584
|
+
? {
|
|
585
|
+
...confirm,
|
|
586
|
+
selectedValue: value,
|
|
587
|
+
defaultSelectedValue: value,
|
|
588
|
+
selectedLabel: option?.label ?? confirm.selectedLabel,
|
|
589
|
+
}
|
|
590
|
+
: confirm
|
|
591
|
+
))
|
|
592
|
+
: message.confirms,
|
|
593
|
+
}
|
|
594
|
+
: message,
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
}, []);
|
|
598
|
+
|
|
599
|
+
const handleConfirmFormChange = useCallback((messageId, confirmId, formValues, meta = {}) => {
|
|
600
|
+
setMessages((prev) =>
|
|
601
|
+
prev.map((message) =>
|
|
602
|
+
message.id === messageId
|
|
603
|
+
? {
|
|
604
|
+
...message,
|
|
605
|
+
confirms: Array.isArray(message.confirms)
|
|
606
|
+
? message.confirms.map((confirm) => (
|
|
607
|
+
confirm.id === confirmId
|
|
608
|
+
? {
|
|
609
|
+
...confirm,
|
|
610
|
+
formValues,
|
|
611
|
+
lastChangedFieldId: meta.fieldId,
|
|
612
|
+
}
|
|
613
|
+
: confirm
|
|
614
|
+
))
|
|
615
|
+
: message.confirms,
|
|
616
|
+
}
|
|
617
|
+
: message,
|
|
618
|
+
),
|
|
619
|
+
);
|
|
620
|
+
}, []);
|
|
621
|
+
|
|
622
|
+
const handleConfirmSecondary = useCallback(
|
|
623
|
+
(messageId, confirmId) => {
|
|
624
|
+
setMessages((prev) =>
|
|
625
|
+
prev.map((message) =>
|
|
626
|
+
message.id === messageId
|
|
627
|
+
? {
|
|
628
|
+
...message,
|
|
629
|
+
confirms: Array.isArray(message.confirms)
|
|
630
|
+
? message.confirms.map((confirm) => (
|
|
631
|
+
confirm.id === confirmId ? { ...confirm, defaultConfirmed: true } : confirm
|
|
632
|
+
))
|
|
633
|
+
: message.confirms,
|
|
634
|
+
leadText: '已取消当前确认,流程暂停。',
|
|
635
|
+
timestamp: nowHHmm(),
|
|
636
|
+
}
|
|
637
|
+
: message,
|
|
638
|
+
),
|
|
639
|
+
);
|
|
640
|
+
clearPendingFlowResume();
|
|
641
|
+
setInputView('default');
|
|
642
|
+
},
|
|
643
|
+
[clearPendingFlowResume],
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
const startAutoOrchestratedFlow = useCallback(
|
|
647
|
+
(text, reply, ctx = {}) => {
|
|
648
|
+
const contextSnapshot = { ...conversationContextRef.current };
|
|
649
|
+
const thinkingMsgId = nextId('think');
|
|
650
|
+
const confirmMsgId = nextId('confirm');
|
|
651
|
+
const flowMsgId = nextId('flow');
|
|
652
|
+
const payload = buildAutoFlowPayload(text, reply, contextSnapshot);
|
|
653
|
+
const clarificationConfirms = buildClarificationConfirms(contextSnapshot);
|
|
654
|
+
|
|
655
|
+
const finishExecutionFlow = () => {
|
|
656
|
+
setMessages((prev) =>
|
|
657
|
+
prev.map((message) =>
|
|
658
|
+
message.id === flowMsgId
|
|
659
|
+
? {
|
|
660
|
+
...message,
|
|
661
|
+
stream: false,
|
|
662
|
+
taskGroups: DEFAULT_CHAT_TASK_GROUPS.map((group) => ({
|
|
663
|
+
...group,
|
|
664
|
+
status: 'completed',
|
|
665
|
+
})),
|
|
666
|
+
resultText: payload.resultText,
|
|
667
|
+
resultArtifacts: payload.resultArtifacts,
|
|
668
|
+
followUps: payload.followUps,
|
|
669
|
+
timestamp: nowHHmm(),
|
|
670
|
+
}
|
|
671
|
+
: message,
|
|
672
|
+
),
|
|
673
|
+
);
|
|
674
|
+
conversationContextRef.current = {
|
|
675
|
+
...contextSnapshot,
|
|
676
|
+
conclusion: payload.resultText,
|
|
677
|
+
artifacts: payload.resultArtifacts.map((artifact) => artifact.title),
|
|
678
|
+
lastUserIntent: text,
|
|
679
|
+
};
|
|
680
|
+
setInputView('default');
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const startExecutionFlow = () => {
|
|
684
|
+
setInputView('busy');
|
|
685
|
+
setMessages((prev) => [
|
|
686
|
+
...prev,
|
|
687
|
+
{
|
|
688
|
+
id: flowMsgId,
|
|
689
|
+
kind: 'ai-flow',
|
|
690
|
+
timestamp: nowHHmm(),
|
|
691
|
+
taskGroups: DEFAULT_CHAT_TASK_GROUPS,
|
|
692
|
+
stream: true,
|
|
693
|
+
},
|
|
694
|
+
]);
|
|
695
|
+
scheduleFlowTimer(finishExecutionFlow, getFlowTotalMs());
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const continueAfterConfirm = () => {
|
|
699
|
+
setMessages((prev) =>
|
|
700
|
+
prev.map((message) =>
|
|
701
|
+
message.id === confirmMsgId
|
|
702
|
+
? {
|
|
703
|
+
...message,
|
|
704
|
+
confirms: Array.isArray(message.confirms)
|
|
705
|
+
? message.confirms.map((confirm) => ({
|
|
706
|
+
...confirm,
|
|
707
|
+
defaultConfirmed: true,
|
|
708
|
+
}))
|
|
709
|
+
: message.confirms,
|
|
710
|
+
leadText: '已确认澄清信息,开始自动执行。',
|
|
711
|
+
timestamp: nowHHmm(),
|
|
712
|
+
}
|
|
713
|
+
: message,
|
|
714
|
+
),
|
|
715
|
+
);
|
|
716
|
+
scheduleFlowTimer(startExecutionFlow, 320);
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
setMessages((prev) => [
|
|
720
|
+
...prev,
|
|
721
|
+
{
|
|
722
|
+
id: thinkingMsgId,
|
|
723
|
+
kind: 'ai-thinking',
|
|
724
|
+
timestamp: nowHHmm(),
|
|
725
|
+
thinking: {
|
|
726
|
+
...DEFAULT_CHAT_THINKING,
|
|
727
|
+
state: 'thinking',
|
|
728
|
+
inProgressLabel: '深度思考中 ...',
|
|
729
|
+
defaultExpanded: false,
|
|
730
|
+
},
|
|
731
|
+
className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
|
|
732
|
+
},
|
|
733
|
+
]);
|
|
734
|
+
|
|
735
|
+
scheduleFlowTimer(() => {
|
|
736
|
+
setMessages((prev) =>
|
|
737
|
+
prev.map((message) =>
|
|
738
|
+
message.id === thinkingMsgId
|
|
739
|
+
? {
|
|
740
|
+
...message,
|
|
741
|
+
thinking: {
|
|
742
|
+
...DEFAULT_CHAT_THINKING,
|
|
743
|
+
state: 'completed',
|
|
744
|
+
durationLabel: '深度思考(用时 3.20 秒)',
|
|
745
|
+
content: buildThinkingContent(text, contextSnapshot),
|
|
746
|
+
defaultExpanded: false,
|
|
747
|
+
},
|
|
748
|
+
resultText: '已完成上下文理解,接下来需要先确认本轮执行口径。',
|
|
749
|
+
timestamp: nowHHmm(),
|
|
750
|
+
}
|
|
751
|
+
: message,
|
|
752
|
+
),
|
|
753
|
+
);
|
|
754
|
+
}, AUTO_FLOW_TIMINGS.thinkingDone);
|
|
755
|
+
|
|
756
|
+
scheduleFlowTimer(() => {
|
|
757
|
+
setMessages((prev) => [
|
|
758
|
+
...prev,
|
|
759
|
+
{
|
|
760
|
+
id: confirmMsgId,
|
|
761
|
+
kind: 'ai-confirm',
|
|
762
|
+
timestamp: nowHHmm(),
|
|
763
|
+
leadText: '请确认以下人工澄清信息;确认后我会继续自动执行策略分析和结果整理。',
|
|
764
|
+
confirms: clarificationConfirms,
|
|
765
|
+
},
|
|
766
|
+
]);
|
|
767
|
+
|
|
768
|
+
if (confirmsRequireHumanGate(clarificationConfirms)) {
|
|
769
|
+
pauseForHumanGate(continueAfterConfirm);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
scheduleFlowTimer(continueAfterConfirm, 320);
|
|
774
|
+
}, AUTO_FLOW_TIMINGS.confirmShow);
|
|
775
|
+
},
|
|
776
|
+
[nextId, pauseForHumanGate, scheduleFlowTimer],
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
const handleSend = useCallback(
|
|
780
|
+
(text, ctx = {}) => {
|
|
781
|
+
const trimmed = (text || '').trim();
|
|
782
|
+
if (!trimmed) return;
|
|
783
|
+
|
|
784
|
+
if (phase === 'welcome') setPhase('chat');
|
|
785
|
+
|
|
786
|
+
const userContent = Array.isArray(ctx.segments) && ctx.segments.length > 0
|
|
787
|
+
? ctx.segments
|
|
788
|
+
: [{ type: 'text', value: trimmed }];
|
|
789
|
+
|
|
790
|
+
setMessages((prev) => [
|
|
791
|
+
...prev,
|
|
792
|
+
{
|
|
793
|
+
id: nextId('u'),
|
|
794
|
+
kind: 'user',
|
|
795
|
+
timestamp: nowHHmm(),
|
|
796
|
+
userContent,
|
|
797
|
+
className: ctx.source === 'follow-up' ? 'tfds-followup-user-pop' : '',
|
|
798
|
+
},
|
|
799
|
+
]);
|
|
800
|
+
setInputView('replying');
|
|
801
|
+
|
|
802
|
+
const reply = routeReply(trimmed);
|
|
803
|
+
if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
|
|
804
|
+
if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
|
|
805
|
+
clearFlowTimers();
|
|
806
|
+
clearPendingFlowResume();
|
|
807
|
+
|
|
808
|
+
if (reply.type === 'short') {
|
|
809
|
+
replyTimerRef.current = setTimeout(() => {
|
|
810
|
+
setMessages((prev) => [
|
|
811
|
+
...prev,
|
|
812
|
+
{
|
|
813
|
+
id: nextId('a'),
|
|
814
|
+
kind: 'ai-text',
|
|
815
|
+
timestamp: nowHHmm(),
|
|
816
|
+
resultText: reply.text,
|
|
817
|
+
className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
|
|
818
|
+
},
|
|
819
|
+
]);
|
|
820
|
+
setInputView('default');
|
|
821
|
+
scrollToLatest();
|
|
822
|
+
}, 600);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
startAutoOrchestratedFlow(trimmed, reply, ctx);
|
|
827
|
+
scrollToLatest();
|
|
828
|
+
},
|
|
829
|
+
[phase, nextId, clearFlowTimers, clearPendingFlowResume, scrollToLatest, startAutoOrchestratedFlow],
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
const handleFollowUpSend = useCallback(
|
|
833
|
+
(text) => {
|
|
834
|
+
handleSend(text, { source: 'follow-up' });
|
|
835
|
+
},
|
|
836
|
+
[handleSend],
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
const handleStop = useCallback(() => {
|
|
840
|
+
if (replyTimerRef.current) {
|
|
841
|
+
clearTimeout(replyTimerRef.current);
|
|
842
|
+
replyTimerRef.current = null;
|
|
843
|
+
}
|
|
844
|
+
if (busyTimerRef.current) {
|
|
845
|
+
clearTimeout(busyTimerRef.current);
|
|
846
|
+
busyTimerRef.current = null;
|
|
847
|
+
}
|
|
848
|
+
clearFlowTimers();
|
|
849
|
+
clearPendingFlowResume();
|
|
850
|
+
setInputView('default');
|
|
851
|
+
setMessages((prev) => [
|
|
852
|
+
...prev,
|
|
853
|
+
{
|
|
854
|
+
id: nextId('a'),
|
|
855
|
+
kind: 'ai-text',
|
|
856
|
+
timestamp: nowHHmm(),
|
|
857
|
+
resultText: '已停止当前任务,需要时再叫我。',
|
|
858
|
+
},
|
|
859
|
+
]);
|
|
860
|
+
}, [nextId, clearFlowTimers, clearPendingFlowResume]);
|
|
861
|
+
|
|
862
|
+
const lastIdx = messages.length - 1;
|
|
863
|
+
|
|
176
864
|
return (
|
|
177
865
|
<div
|
|
178
|
-
className="flex h-full min-h-0 min-w-0 flex-col
|
|
866
|
+
className="flex h-full min-h-0 min-w-0 flex-col"
|
|
179
867
|
style={{
|
|
180
868
|
width: 'min(450px, 100%)',
|
|
181
869
|
}}
|
|
182
870
|
>
|
|
183
871
|
{/* 标题栏 */}
|
|
184
872
|
<div className="flex items-center shrink-0" style={{ height: '48px', paddingRight: '4px' }}>
|
|
185
|
-
<div className="flex items-center flex-1 min-w-0 gap-1"
|
|
186
|
-
<span
|
|
187
|
-
className="text-sm leading-5 pl-1"
|
|
188
|
-
style={{ color: 'var(--foreground-muted, rgba(15,28,53,0.6))' }}
|
|
189
|
-
>
|
|
190
|
-
新会话
|
|
191
|
-
</span>
|
|
192
|
-
</div>
|
|
873
|
+
<div className="flex items-center flex-1 min-w-0 gap-1" />
|
|
193
874
|
<div className="flex items-center gap-0.5 shrink-0">
|
|
194
|
-
<Button variant="ghost-black" iconOnly icon={<Icon name="message-plus-square-stroked" />} tooltip="新建会话" aria-label="新建会话" />
|
|
875
|
+
<Button variant="ghost-black" iconOnly icon={<Icon name="message-plus-square-stroked" />} tooltip="新建会话" aria-label="新建会话" onClick={handleNewSession} />
|
|
195
876
|
<Button variant="ghost-black" iconOnly icon={<Icon name="clock-stroked" />} tooltip="历史记录" aria-label="历史记录" />
|
|
196
877
|
<Button variant="ghost-black" iconOnly icon={<Icon name="layout-left-stroked" />} tooltip="收起面板" aria-label="收起面板" onClick={onClose} />
|
|
197
878
|
</div>
|
|
198
879
|
</div>
|
|
199
880
|
|
|
200
|
-
{
|
|
201
|
-
|
|
202
|
-
|
|
881
|
+
{phase === 'welcome' ? (
|
|
882
|
+
<CopilotWelcomePhase onSend={handleSend} inputView={inputView} onStop={handleStop} />
|
|
883
|
+
) : (
|
|
884
|
+
<CopilotChatPhase
|
|
885
|
+
scrollRef={scrollRef}
|
|
886
|
+
latestAnchorRef={latestAnchorRef}
|
|
887
|
+
messages={messages}
|
|
888
|
+
lastIdx={lastIdx}
|
|
889
|
+
inputView={inputView}
|
|
890
|
+
onSend={handleSend}
|
|
891
|
+
onStop={handleStop}
|
|
892
|
+
handlers={{
|
|
893
|
+
onFollowUpSelect: handleFollowUpSend,
|
|
894
|
+
onConfirmPrimary: handleConfirmPrimary,
|
|
895
|
+
onConfirmSecondary: handleConfirmSecondary,
|
|
896
|
+
onConfirmOptionChange: handleConfirmOptionChange,
|
|
897
|
+
onConfirmFormChange: handleConfirmFormChange,
|
|
898
|
+
}}
|
|
899
|
+
/>
|
|
900
|
+
)}
|
|
901
|
+
</div>
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function CopilotWelcomePhase({ onSend, onStop, inputView }) {
|
|
906
|
+
return (
|
|
907
|
+
<>
|
|
908
|
+
<div className="flex flex-1 min-h-0 flex-col items-center justify-center gap-6 overflow-y-auto" style={{ padding: '0 28px' }}>
|
|
203
909
|
<div className="flex flex-col items-center gap-3">
|
|
204
|
-
{/* 头像:渐变描边圆 + 蓝色投影 + CATCAT */}
|
|
205
910
|
<div style={{ position: 'relative', width: '66px', height: '66px', flexShrink: 0 }}>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
borderRadius: '50%',
|
|
211
|
-
padding: '0.5px',
|
|
212
|
-
background: 'linear-gradient(135deg, rgba(63,226,213,0.6) 0%, rgba(64,147,224,0.6) 35%, rgba(122,97,250,0.6) 65%, rgba(214,130,235,0.6) 100%)',
|
|
213
|
-
}}>
|
|
214
|
-
<div style={{
|
|
215
|
-
width: '100%',
|
|
216
|
-
height: '100%',
|
|
911
|
+
<div
|
|
912
|
+
style={{
|
|
913
|
+
position: 'absolute',
|
|
914
|
+
inset: '0',
|
|
217
915
|
borderRadius: '50%',
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
916
|
+
padding: '0.5px',
|
|
917
|
+
background: 'linear-gradient(135deg, rgba(63,226,213,0.6) 0%, rgba(64,147,224,0.6) 35%, rgba(122,97,250,0.6) 65%, rgba(214,130,235,0.6) 100%)',
|
|
918
|
+
}}
|
|
919
|
+
>
|
|
920
|
+
<div
|
|
921
|
+
style={{
|
|
922
|
+
width: '100%',
|
|
923
|
+
height: '100%',
|
|
924
|
+
borderRadius: '50%',
|
|
925
|
+
background: [
|
|
926
|
+
'linear-gradient(42deg, #FFF 11.61%, rgba(255,255,255,0.00) 37.84%)',
|
|
927
|
+
'linear-gradient(74deg, rgba(63,226,213,0.15) 12.18%, rgba(64,147,224,0.15) 39.9%, rgba(122,97,250,0.15) 63.86%, rgba(214,130,235,0.15) 86.38%)',
|
|
928
|
+
'#FFF',
|
|
929
|
+
].join(', '),
|
|
930
|
+
boxShadow: '0px 8px 15px rgba(180,218,244,0.50)',
|
|
931
|
+
}}
|
|
932
|
+
/>
|
|
225
933
|
</div>
|
|
226
|
-
{/* CATCAT 图标 */}
|
|
227
934
|
<img
|
|
228
935
|
src={catcatSvg}
|
|
229
936
|
alt="OLA AI"
|
|
@@ -239,28 +946,26 @@ function CopilotPanel({ onClose }) {
|
|
|
239
946
|
/>
|
|
240
947
|
</div>
|
|
241
948
|
<span
|
|
242
|
-
className="font-semibold
|
|
949
|
+
className="whitespace-nowrap [font-weight:var(--font-semibold)]"
|
|
243
950
|
style={{ fontSize: '18px', lineHeight: '24px', letterSpacing: '-0.03em', color: 'var(--foreground, #0F1C35)' }}
|
|
244
951
|
>
|
|
245
952
|
OLA AI
|
|
246
953
|
</span>
|
|
247
954
|
</div>
|
|
248
955
|
|
|
249
|
-
{/* 欢迎语 */}
|
|
250
956
|
<p
|
|
251
|
-
className="
|
|
957
|
+
className="m-0 text-center text-sm leading-5"
|
|
252
958
|
style={{ color: 'var(--foreground-muted, rgba(15,28,53,0.6))', width: '100%' }}
|
|
253
959
|
>
|
|
254
|
-
|
|
960
|
+
您好!我是您的智能小助手,我可以帮您分析私信会话问题、生成服务策略、补充验证方案和沉淀可交付结论。
|
|
255
961
|
</p>
|
|
256
962
|
|
|
257
|
-
|
|
258
|
-
<div className="flex flex-col gap-2 w-full">
|
|
963
|
+
<div className="flex w-full flex-col gap-2">
|
|
259
964
|
{COPILOT_SUGGESTIONS.map((text) => (
|
|
260
965
|
<button
|
|
261
966
|
key={text}
|
|
262
967
|
type="button"
|
|
263
|
-
className="flex items-center justify-between
|
|
968
|
+
className="flex w-full cursor-pointer items-center justify-between border-0 text-left"
|
|
264
969
|
style={{
|
|
265
970
|
padding: '10px 12px',
|
|
266
971
|
borderRadius: '8px',
|
|
@@ -272,8 +977,9 @@ function CopilotPanel({ onClose }) {
|
|
|
272
977
|
fontFamily: 'inherit',
|
|
273
978
|
transition: 'background 150ms ease',
|
|
274
979
|
}}
|
|
275
|
-
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.85)'; }}
|
|
276
|
-
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.6)'; }}
|
|
980
|
+
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.85)'; }}
|
|
981
|
+
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.6)'; }}
|
|
982
|
+
onClick={() => onSend(text, { source: 'suggestion' })}
|
|
277
983
|
>
|
|
278
984
|
<span>{text}</span>
|
|
279
985
|
<Icon name="arrow-narrow-right-stroked" size={12} />
|
|
@@ -282,11 +988,104 @@ function CopilotPanel({ onClose }) {
|
|
|
282
988
|
</div>
|
|
283
989
|
</div>
|
|
284
990
|
|
|
285
|
-
{/* ChatInput:父级 CopilotPanel 不裁剪,让 -bottom-2 氛围背景与炫彩投影能向外溢出 */}
|
|
286
991
|
<div className="shrink-0 pt-2">
|
|
287
|
-
<ChatInput
|
|
992
|
+
<ChatInput
|
|
993
|
+
variant={inputView}
|
|
994
|
+
autoReplyOnSend={false}
|
|
995
|
+
placeholder="需要我为你做什么"
|
|
996
|
+
onSend={onSend}
|
|
997
|
+
onStop={onStop}
|
|
998
|
+
/>
|
|
288
999
|
</div>
|
|
289
|
-
|
|
1000
|
+
</>
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function CopilotChatPhase({
|
|
1005
|
+
scrollRef,
|
|
1006
|
+
latestAnchorRef,
|
|
1007
|
+
messages,
|
|
1008
|
+
lastIdx,
|
|
1009
|
+
handlers,
|
|
1010
|
+
onSend,
|
|
1011
|
+
onStop,
|
|
1012
|
+
inputView,
|
|
1013
|
+
}) {
|
|
1014
|
+
const [atTop, setAtTop] = useState(true);
|
|
1015
|
+
|
|
1016
|
+
useEffect(() => {
|
|
1017
|
+
const el = scrollRef.current;
|
|
1018
|
+
if (!el) return undefined;
|
|
1019
|
+
const update = () => setAtTop(el.scrollTop <= 4);
|
|
1020
|
+
update();
|
|
1021
|
+
el.addEventListener('scroll', update, { passive: true });
|
|
1022
|
+
return () => el.removeEventListener('scroll', update);
|
|
1023
|
+
}, [scrollRef, messages.length]);
|
|
1024
|
+
|
|
1025
|
+
const FADE_MASK = atTop
|
|
1026
|
+
? 'linear-gradient(to bottom, #000 0, #000 calc(100% - 32px), transparent 100%)'
|
|
1027
|
+
: 'linear-gradient(to bottom, transparent 0, #000 32px, #000 calc(100% - 32px), transparent 100%)';
|
|
1028
|
+
|
|
1029
|
+
return (
|
|
1030
|
+
<>
|
|
1031
|
+
<style>
|
|
1032
|
+
{`
|
|
1033
|
+
@keyframes tfds-followup-user-pop {
|
|
1034
|
+
0% { opacity: 0; transform: translate(28px, -8px) scale(0.98); }
|
|
1035
|
+
100% { opacity: 1; transform: translate(0, 0) scale(1); }
|
|
1036
|
+
}
|
|
1037
|
+
@keyframes tfds-followup-ai-pop {
|
|
1038
|
+
0% { opacity: 0; transform: translateY(8px); }
|
|
1039
|
+
100% { opacity: 1; transform: translateY(0); }
|
|
1040
|
+
}
|
|
1041
|
+
.tfds-followup-user-pop {
|
|
1042
|
+
animation: tfds-followup-user-pop 240ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
1043
|
+
transform-origin: right center;
|
|
1044
|
+
}
|
|
1045
|
+
.tfds-followup-ai-pop {
|
|
1046
|
+
animation: tfds-followup-ai-pop 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
1047
|
+
}
|
|
1048
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1049
|
+
.tfds-followup-user-pop,
|
|
1050
|
+
.tfds-followup-ai-pop {
|
|
1051
|
+
animation: none;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
`}
|
|
1055
|
+
</style>
|
|
1056
|
+
|
|
1057
|
+
<div
|
|
1058
|
+
ref={scrollRef}
|
|
1059
|
+
className="w-full flex-1 min-h-0 overflow-y-auto"
|
|
1060
|
+
style={{
|
|
1061
|
+
padding: '8px 0',
|
|
1062
|
+
maskImage: FADE_MASK,
|
|
1063
|
+
WebkitMaskImage: FADE_MASK,
|
|
1064
|
+
}}
|
|
1065
|
+
>
|
|
1066
|
+
<div className="mx-auto flex w-full flex-col gap-2" style={{ maxWidth: '100%', padding: '0 4px 72px' }}>
|
|
1067
|
+
{messages.map((message, idx) => {
|
|
1068
|
+
const isLatest = idx === lastIdx;
|
|
1069
|
+
const props = messageToChatProps(message, isLatest, handlers);
|
|
1070
|
+
if (message.kind === 'ai-flow' && message.stream === true) {
|
|
1071
|
+
return <StreamingChatMessage key={message.id} {...props} taskGroups={message.taskGroups} />;
|
|
1072
|
+
}
|
|
1073
|
+
return <ChatMessage key={message.id} {...props} />;
|
|
1074
|
+
})}
|
|
1075
|
+
<div ref={latestAnchorRef} aria-hidden="true" style={{ height: '1px' }} />
|
|
1076
|
+
</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
|
|
1079
|
+
<div className="w-full shrink-0 pt-2">
|
|
1080
|
+
<ChatInput
|
|
1081
|
+
variant={inputView}
|
|
1082
|
+
autoReplyOnSend={false}
|
|
1083
|
+
placeholder="继续追问,或输入分析/生成/整理"
|
|
1084
|
+
onSend={onSend}
|
|
1085
|
+
onStop={onStop}
|
|
1086
|
+
/>
|
|
1087
|
+
</div>
|
|
1088
|
+
</>
|
|
290
1089
|
);
|
|
291
1090
|
}
|
|
292
1091
|
|