@tfdesign/b-end 1.0.14 → 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 +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/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 +18 -2
- package/src/index.d.ts +2 -0
|
@@ -57,6 +57,14 @@ const WORKSPACE_THREAD_CONTENT = {
|
|
|
57
57
|
{ id: 'pending-1-a2', kind: 'agent', text: '我已备注您正在到店等位,并同步门店优先处理;若核销仍失败,会为您申请等位补偿或原路退款。', time: ts('14:07', '02') },
|
|
58
58
|
{ id: 'pending-1-u5', kind: 'user', text: '好的,麻烦快一点,我这边还在等叫号。', time: ts('14:07', '18') },
|
|
59
59
|
{ id: 'pending-1-a3', kind: 'agent', text: '没问题,我会持续跟进西单店反馈。若 5 分钟内未解决,我会直接帮您升级商家值班人员处理。', time: ts('14:07', '36') },
|
|
60
|
+
{ id: 'pending-1-u6', kind: 'user', text: '门店刚刚还是说系统里没刷出来,我这边还要继续等吗?', time: ts('14:08', '04') },
|
|
61
|
+
{ id: 'pending-1-a4', kind: 'agent', text: '我已再次催门店值班经理人工核验,建议您先保留当前排队小票,我这边同步加急处理中。', time: ts('14:08', '22') },
|
|
62
|
+
{ id: 'pending-1-u7', kind: 'user', text: '如果最后还是不行,退款会退多久?', time: ts('14:08', '46') },
|
|
63
|
+
{ id: 'pending-1-a5', kind: 'agent', text: '若最终无法核销,我会优先为您走原路退款,通常 1-3 个工作日到账,具体以支付渠道为准。', time: ts('14:09', '05') },
|
|
64
|
+
{ id: 'pending-1-u8', kind: 'user', text: '那补偿大概多久能确认下来?', time: ts('14:09', '28') },
|
|
65
|
+
{ id: 'pending-1-a6', kind: 'agent', text: '若门店确认是核销异常导致等待,我会直接在本会话同步补偿方案,通常当场或当天内即可确认。', time: ts('14:09', '47') },
|
|
66
|
+
{ id: 'pending-1-u9', kind: 'user', text: '好,那你先按这个方案帮我备注,我这边继续等等。', time: ts('14:10', '11') },
|
|
67
|
+
{ id: 'pending-1-a7', kind: 'agent', text: '好的,已补充备注“到店等待中,优先核销;若失败按补偿/退款方案处理”,有进展我马上告诉您。', time: ts('14:10', '34') },
|
|
60
68
|
],
|
|
61
69
|
},
|
|
62
70
|
'pending-2': {
|
|
@@ -75,6 +83,16 @@ const WORKSPACE_THREAD_CONTENT = {
|
|
|
75
83
|
{ id: 'pending-2-a2', kind: 'agent', text: '我会帮您向商家发起加急催发,并要求在 2 小时内反馈是否能按时寄出。', time: ts('12:50', '01') },
|
|
76
84
|
{ id: 'pending-2-u3', kind: 'user', text: '如果今天发不了可以直接退款吗?', time: ts('12:51', '16') },
|
|
77
85
|
{ id: 'pending-2-a3', kind: 'agent', text: '可以。如果商家确认今日无法发出,我会协助您走未按约发货退款流程,并同步保留催发记录。', time: ts('12:51', '45') },
|
|
86
|
+
{ id: 'pending-2-u4', kind: 'user', text: '商家那边现在有新的回复吗?', time: ts('12:52', '08') },
|
|
87
|
+
{ id: 'pending-2-a4', kind: 'agent', text: '刚收到仓库侧反馈,当前包裹已完成分拣,正在等待承运商揽收并回传物流单号。', time: ts('12:52', '34') },
|
|
88
|
+
{ id: 'pending-2-u5', kind: 'user', text: '如果两小时内还是发不出来,就直接帮我走退款吧。', time: ts('12:53', '01') },
|
|
89
|
+
{ id: 'pending-2-a5', kind: 'agent', text: '可以,我已备注您的优先诉求:若超时未揽收,将按未按约发货为您优先发起退款申请。', time: ts('12:53', '26') },
|
|
90
|
+
{ id: 'pending-2-u6', kind: 'user', text: '我明早出门前一定要确认,不然就来不及用了。', time: ts('12:53', '52') },
|
|
91
|
+
{ id: 'pending-2-a6', kind: 'agent', text: '理解,我会在今晚 18 点前再次核对是否已揽收,并把结果同步给您,避免您空等。', time: ts('12:54', '18') },
|
|
92
|
+
{ id: 'pending-2-u7', kind: 'user', text: '能帮我备注优先联系仓库吗?', time: ts('12:54', '41') },
|
|
93
|
+
{ id: 'pending-2-a7', kind: 'agent', text: '已经备注“优先联系仓库核对实际出库进度”,并补充了您明早急用的场景说明。', time: ts('12:55', '03') },
|
|
94
|
+
{ id: 'pending-2-u8', kind: 'user', text: '好的,那我等你消息。', time: ts('12:55', '26') },
|
|
95
|
+
{ id: 'pending-2-a8', kind: 'agent', text: '好的,有新进展我会第一时间在本会话回复您,不需要您重复催问。', time: ts('12:55', '44') },
|
|
78
96
|
],
|
|
79
97
|
},
|
|
80
98
|
'pending-3': {
|
|
@@ -93,6 +111,17 @@ const WORKSPACE_THREAD_CONTENT = {
|
|
|
93
111
|
{ id: 'pending-3-a2', kind: 'agent', text: '我会先为您登记权益补偿,若原券无法恢复,会补发一张同面额可用券到您的账号。', time: ts('12:37', '01') },
|
|
94
112
|
{ id: 'pending-3-u3', kind: 'user', text: '那我今天还能下单吗?', time: ts('12:38', '12') },
|
|
95
113
|
{ id: 'pending-3-a3', kind: 'agent', text: '可以,我会在登记中备注“活动期内核销异常”,补偿券会按当前活动权益处理。', time: ts('12:38', '40') },
|
|
114
|
+
{ id: 'pending-3-u4', kind: 'user', text: '补发券一般多久会到账号里?', time: ts('12:39', '02') },
|
|
115
|
+
{ id: 'pending-3-a4', kind: 'agent', text: '若走补发方案,通常会在核验完成后当天内到账;若活动高峰期,最晚不超过 24 小时。', time: ts('12:39', '27') },
|
|
116
|
+
{ id: 'pending-3-u5', kind: 'user', text: '如果我今天就想下单,还有别的处理办法吗?', time: ts('12:39', '51') },
|
|
117
|
+
{ id: 'pending-3-a5', kind: 'agent', text: '我可以先为您登记“活动期核销异常”标记,优先争取按照原活动权益补发同等券后再下单使用。', time: ts('12:40', '18') },
|
|
118
|
+
{ id: 'pending-3-u6', kind: 'user', text: '这次活动今晚就结束,我怕到时候补偿也变少。', time: ts('12:40', '44') },
|
|
119
|
+
{ id: 'pending-3-a6', kind: 'agent', text: '已理解,我会在登记里注明“按活动结束前权益补偿”,避免后续按普通券面额降级处理。', time: ts('12:41', '10') },
|
|
120
|
+
{ id: 'pending-3-u7', kind: 'user', text: '如果原券恢复了,是不是我就直接还能用原来的?', time: ts('12:41', '36') },
|
|
121
|
+
{ id: 'pending-3-a7', kind: 'agent', text: '是的,若系统先恢复原券状态,您可以直接继续使用原券;补发流程会自动撤销,不会重复占用权益。', time: ts('12:42', '04') },
|
|
122
|
+
{ id: 'pending-3-u8', kind: 'user', text: '好,那我先等等你这边结果。', time: ts('12:42', '29') },
|
|
123
|
+
{ id: 'pending-3-a8', kind: 'agent', text: '好的,我会持续盯进度,有恢复结果或补发方案落地会第一时间同步您。', time: ts('12:42', '52') },
|
|
124
|
+
{ id: 'pending-3-a9', kind: 'agent', text: '如果 30 分钟内仍未恢复,我会直接升级活动运营侧加急核查。', time: ts('12:43', '16') },
|
|
96
125
|
],
|
|
97
126
|
},
|
|
98
127
|
'managed-1': {
|
|
@@ -181,8 +210,8 @@ function getConversationItems(sections) {
|
|
|
181
210
|
return (sections || []).flatMap((section) => section.items || []);
|
|
182
211
|
}
|
|
183
212
|
|
|
184
|
-
function findConversationItem(itemId) {
|
|
185
|
-
return getConversationItems(
|
|
213
|
+
function findConversationItem(itemId, sections = WORKSPACE_CONVERSATION_SECTIONS) {
|
|
214
|
+
return getConversationItems(sections).find((item) => item.id === itemId);
|
|
186
215
|
}
|
|
187
216
|
|
|
188
217
|
function getPrimaryTagLabel(item, fallback = '') {
|
|
@@ -198,6 +227,7 @@ function buildThreadContext(item) {
|
|
|
198
227
|
return {
|
|
199
228
|
id: resolvedItem?.id || DEFAULT_CONVERSATION_ID,
|
|
200
229
|
title: resolvedItem?.title || '当前会话',
|
|
230
|
+
tags: Array.isArray(resolvedItem?.tags) ? resolvedItem.tags : [],
|
|
201
231
|
tagLabel: content.tagLabel || getPrimaryTagLabel(resolvedItem),
|
|
202
232
|
userId: resolvedItem?.orderId || '未知会话',
|
|
203
233
|
channel: content.channel || '抖音',
|
|
@@ -209,6 +239,33 @@ function buildThreadContext(item) {
|
|
|
209
239
|
};
|
|
210
240
|
}
|
|
211
241
|
|
|
242
|
+
function buildConversationCardPreviewMessages(messages = []) {
|
|
243
|
+
return messages
|
|
244
|
+
.filter((message) => message?.kind !== 'system')
|
|
245
|
+
.map((message) => ({
|
|
246
|
+
id: `${message.id}-preview`,
|
|
247
|
+
role: message.kind === 'user'
|
|
248
|
+
? 'user'
|
|
249
|
+
: (message.kind === 'bot' ? 'bot' : 'agent'),
|
|
250
|
+
text: message.text,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildWorkspaceConversationSections(sections = WORKSPACE_CONVERSATION_SECTIONS) {
|
|
255
|
+
return sections.map((section) => ({
|
|
256
|
+
...section,
|
|
257
|
+
items: (section.items || []).map((item) => {
|
|
258
|
+
const threadContent = WORKSPACE_THREAD_CONTENT[item.id] || {};
|
|
259
|
+
return {
|
|
260
|
+
...item,
|
|
261
|
+
messages: buildConversationCardPreviewMessages(threadContent.messages || []),
|
|
262
|
+
status: 'replied',
|
|
263
|
+
draftText: '',
|
|
264
|
+
};
|
|
265
|
+
}),
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
212
269
|
/**
|
|
213
270
|
* CustomerServiceWorkspaceFramePattern — 客服工作台框架
|
|
214
271
|
*
|
|
@@ -225,9 +282,13 @@ export default function CustomerServiceWorkspaceFramePattern() {
|
|
|
225
282
|
const [mainPanelWidth, setMainPanelWidth] = useState(0);
|
|
226
283
|
const [infoPanelWidth, setInfoPanelWidth] = useState(INFO_PANEL_DEFAULT_WIDTH);
|
|
227
284
|
const [activeConversationId, setActiveConversationId] = useState(DEFAULT_CONVERSATION_ID);
|
|
285
|
+
const workspaceSections = useMemo(
|
|
286
|
+
() => buildWorkspaceConversationSections(WORKSPACE_CONVERSATION_SECTIONS),
|
|
287
|
+
[],
|
|
288
|
+
);
|
|
228
289
|
const activeConversationItem = useMemo(
|
|
229
|
-
() => findConversationItem(activeConversationId) || findConversationItem(DEFAULT_CONVERSATION_ID),
|
|
230
|
-
[activeConversationId],
|
|
290
|
+
() => findConversationItem(activeConversationId, workspaceSections) || findConversationItem(DEFAULT_CONVERSATION_ID, workspaceSections),
|
|
291
|
+
[activeConversationId, workspaceSections],
|
|
231
292
|
);
|
|
232
293
|
const activeThread = useMemo(
|
|
233
294
|
() => buildThreadContext(activeConversationItem),
|
|
@@ -323,7 +384,7 @@ export default function CustomerServiceWorkspaceFramePattern() {
|
|
|
323
384
|
const leftPanel = (
|
|
324
385
|
<ConversationList
|
|
325
386
|
title="会话列表"
|
|
326
|
-
sections={
|
|
387
|
+
sections={workspaceSections}
|
|
327
388
|
defaultActiveTab="all"
|
|
328
389
|
activeItemId={activeThread.id}
|
|
329
390
|
onItemClick={(item) => setActiveConversationId(item.id)}
|
|
@@ -142,6 +142,26 @@ function groupConversation(items) {
|
|
|
142
142
|
}, []);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
function isPrimaryStatusTag(tag) {
|
|
146
|
+
return ['red', 'green', 'orange', 'blue'].includes(tag?.variant);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function sortThreadHeaderTags(tags) {
|
|
150
|
+
if (!Array.isArray(tags) || tags.length <= 1) return Array.isArray(tags) ? tags : [];
|
|
151
|
+
|
|
152
|
+
const statusTags = [];
|
|
153
|
+
const secondaryTags = [];
|
|
154
|
+
tags.forEach((tag) => {
|
|
155
|
+
if (isPrimaryStatusTag(tag)) {
|
|
156
|
+
statusTags.push(tag);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
secondaryTags.push(tag);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return [...statusTags, ...secondaryTags];
|
|
163
|
+
}
|
|
164
|
+
|
|
145
165
|
/**
|
|
146
166
|
* @param {{
|
|
147
167
|
* asCard?: boolean,
|
|
@@ -154,6 +174,7 @@ function groupConversation(items) {
|
|
|
154
174
|
* thread?: {
|
|
155
175
|
* id?: string,
|
|
156
176
|
* title?: string,
|
|
177
|
+
* tags?: Array<{ label:string, variant?:string, iconName?:string }>,
|
|
157
178
|
* tagLabel?: string,
|
|
158
179
|
* userId?: string,
|
|
159
180
|
* channel?: string,
|
|
@@ -302,6 +323,10 @@ function IMThreadHeader({ thread }) {
|
|
|
302
323
|
const [overflowOpen, setOverflowOpen] = useState(false);
|
|
303
324
|
const [overflowPosition, setOverflowPosition] = useState({ top: 0, right: 0 });
|
|
304
325
|
const hiddenActions = THREAD_ACTIONS.slice(visibleActionCount);
|
|
326
|
+
const rawHeaderTags = Array.isArray(thread.tags) && thread.tags.length > 0
|
|
327
|
+
? thread.tags
|
|
328
|
+
: (thread.tagLabel ? [{ label: thread.tagLabel, variant: 'white' }] : []);
|
|
329
|
+
const headerTags = sortThreadHeaderTags(rawHeaderTags);
|
|
305
330
|
|
|
306
331
|
useEffect(() => {
|
|
307
332
|
const headerEl = headerRef.current;
|
|
@@ -405,11 +430,20 @@ function IMThreadHeader({ thread }) {
|
|
|
405
430
|
<h2 className="m-0 truncate text-sm leading-5 text-foreground [font-weight:var(--font-semibold)]">
|
|
406
431
|
{thread.title}
|
|
407
432
|
</h2>
|
|
408
|
-
{
|
|
409
|
-
<Tag
|
|
410
|
-
{
|
|
433
|
+
{headerTags.map((tag, index) => (
|
|
434
|
+
<Tag
|
|
435
|
+
key={`${tag.label}-${index}`}
|
|
436
|
+
variant={tag.variant || 'grey'}
|
|
437
|
+
size="m"
|
|
438
|
+
radius="md"
|
|
439
|
+
fontWeight="regular"
|
|
440
|
+
showIcon={Boolean(tag.iconName)}
|
|
441
|
+
iconName={tag.iconName}
|
|
442
|
+
className="shrink-0"
|
|
443
|
+
>
|
|
444
|
+
{tag.label}
|
|
411
445
|
</Tag>
|
|
412
|
-
)
|
|
446
|
+
))}
|
|
413
447
|
</div>
|
|
414
448
|
|
|
415
449
|
<div className="flex min-w-0 items-center gap-2 text-xs leading-4 text-foreground-muted">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import Avatar from '../components/Avatar';
|
|
3
3
|
import Button from '../components/Button';
|
|
4
|
+
import Card from '../components/Card';
|
|
4
5
|
import Icon from '../components/Icon';
|
|
5
6
|
import Tabs from '../components/Tabs';
|
|
6
7
|
import Tag from '../components/Tag';
|
|
@@ -317,7 +318,7 @@ export default function TabTopBarListPage({ onCreateQA }) {
|
|
|
317
318
|
style={{ gap: '12px' }}
|
|
318
319
|
>
|
|
319
320
|
{activeTab.data.map((item) => (
|
|
320
|
-
<
|
|
321
|
+
<KnowledgeInfoCard
|
|
321
322
|
key={item.id}
|
|
322
323
|
item={item}
|
|
323
324
|
typeLabel={activeTab.typeLabel}
|
|
@@ -340,86 +341,33 @@ export default function TabTopBarListPage({ onCreateQA }) {
|
|
|
340
341
|
);
|
|
341
342
|
}
|
|
342
343
|
|
|
343
|
-
/* ──
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
function
|
|
344
|
+
/* ── 知识列表卡片 ──
|
|
345
|
+
* 页面示例3的列表卡片必须引用 Card type="info3"。
|
|
346
|
+
* 选中/未选中/hover 三态由 Card 信息卡片3统一承载,页面只负责业务数据映射。 */
|
|
347
|
+
function KnowledgeInfoCard({ item, typeLabel, selected, onClick }) {
|
|
347
348
|
const creator = getTeamMemberByName(item.creator) || getTeamMemberByIndex(0);
|
|
348
|
-
const baseBg = selected
|
|
349
|
-
? 'var(--color-brand-50, #ECFFF9)'
|
|
350
|
-
: 'var(--color-blueGrey-50, #F4F7FB)';
|
|
351
|
-
const hoverBg = selected
|
|
352
|
-
? 'var(--color-brand-50, #ECFFF9)'
|
|
353
|
-
: 'var(--color-blueGrey-100, #E4EAF2)';
|
|
354
349
|
|
|
355
350
|
return (
|
|
356
|
-
<
|
|
357
|
-
type="
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
<p
|
|
377
|
-
className="[font-weight:var(--font-semibold)] text-blueGrey-900 m-0"
|
|
378
|
-
style={{
|
|
379
|
-
fontSize: '16px',
|
|
380
|
-
lineHeight: '24px',
|
|
381
|
-
display: '-webkit-box',
|
|
382
|
-
WebkitLineClamp: 2,
|
|
383
|
-
WebkitBoxOrient: 'vertical',
|
|
384
|
-
overflow: 'hidden',
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
387
|
-
{item.question}
|
|
388
|
-
</p>
|
|
389
|
-
<Tag variant={STATUS_TAG_VARIANT[item.status] || 'grey'} style={{ flexShrink: 0 }}>
|
|
390
|
-
{item.status}
|
|
391
|
-
</Tag>
|
|
392
|
-
</div>
|
|
393
|
-
|
|
394
|
-
<p
|
|
395
|
-
className="text-blueGrey-600 m-0"
|
|
396
|
-
style={{
|
|
397
|
-
fontSize: '14px',
|
|
398
|
-
lineHeight: '22px',
|
|
399
|
-
display: '-webkit-box',
|
|
400
|
-
WebkitLineClamp: 2,
|
|
401
|
-
WebkitBoxOrient: 'vertical',
|
|
402
|
-
overflow: 'hidden',
|
|
403
|
-
}}
|
|
404
|
-
>
|
|
405
|
-
{item.answer}
|
|
406
|
-
</p>
|
|
407
|
-
|
|
408
|
-
<div className="flex items-center text-blueGrey-500" style={{ fontSize: '12px', gap: '12px' }}>
|
|
409
|
-
<span className="inline-flex items-center" style={{ gap: '4px' }}>
|
|
410
|
-
<Icon name="tag-01-stroked" size="xs" />
|
|
411
|
-
{typeLabel}
|
|
412
|
-
</span>
|
|
413
|
-
<span className="inline-flex items-center" style={{ gap: '4px' }}>
|
|
414
|
-
<Icon name="clock-stroked" size="xs" />
|
|
415
|
-
{item.updatedAt}
|
|
416
|
-
</span>
|
|
417
|
-
<span className="inline-flex items-center" style={{ gap: '4px' }}>
|
|
418
|
-
<Avatar size="xxs" type="image" src={creator?.avatarSrc} alt={`${item.creator}头像`} />
|
|
419
|
-
{item.creator}
|
|
420
|
-
</span>
|
|
421
|
-
</div>
|
|
422
|
-
</button>
|
|
351
|
+
<Card
|
|
352
|
+
type="info3"
|
|
353
|
+
color="grey"
|
|
354
|
+
title={item.question}
|
|
355
|
+
description={item.answer}
|
|
356
|
+
status={item.status}
|
|
357
|
+
statusVariant={STATUS_TAG_VARIANT[item.status] || 'grey'}
|
|
358
|
+
selected={selected}
|
|
359
|
+
onAction={onClick}
|
|
360
|
+
actionAriaLabel={`查看${item.question}详情`}
|
|
361
|
+
metaItems={[
|
|
362
|
+
{ iconName: 'tag-01-stroked', value: typeLabel },
|
|
363
|
+
{ iconName: 'clock-stroked', value: item.updatedAt },
|
|
364
|
+
{
|
|
365
|
+
avatarSrc: creator?.avatarSrc,
|
|
366
|
+
avatarAlt: `${item.creator}头像`,
|
|
367
|
+
value: item.creator,
|
|
368
|
+
},
|
|
369
|
+
]}
|
|
370
|
+
/>
|
|
423
371
|
);
|
|
424
372
|
}
|
|
425
373
|
|
|
@@ -49,8 +49,9 @@ export const PATTERNS = [
|
|
|
49
49
|
'【可见圆角】最终可见的 6 个角统一为 16px:左侧板块左上 / 左下两个角露出;右侧主白卡四角露出;左侧板块右上 / 右下两个角必须被右侧主白卡完整覆盖。主面板覆盖量必须大于圆角半径(推荐 32px = 2 × 16px),防止主面板左上 / 左下圆角切角处露出左侧板块的右边界或描边,避免出现“残缺一块”的视觉。',
|
|
50
50
|
'【框架级能力不可删减】当 AI 生成页面使用客服工作台框架时,必须完整保留框架级交互能力:左侧列表 / 卡片容器宽度拖拽、左侧与右侧主白卡之间的覆盖式拖拽、右侧 InfoDisplayPanel 整体宽度拖拽、InfoDisplayPanel 内部栏间拖拽、响应式最小宽度保护、自动折叠、动态分栏和上下文同步。禁止只复刻截图外观却删除拖拽条、键盘微调、宽度下限、自动适配或 tab 拆分逻辑。',
|
|
51
51
|
'【左右拖拽】左侧区域与右侧主白卡之间必须提供纵向拖拽热区,默认热区 8px;拖拽热区覆盖在主面板左边缘上,不占用布局宽度、不制造中间空隙;拖动时调整左侧区域宽度并让右侧主面板 `flex-1 min-w-0` 自动占满剩余空间;左侧默认宽度 432px(400px 可视会话列表 + 32px 主面板覆盖区);拖拽最小宽度优先由左侧业务组件最小可用宽度决定,例如 ConversationList 默认列表纯头像锁定 88px 时,左侧板块最小宽度为 `max(100px 框架兜底, 88px + 32px 覆盖区) = 120px`;ConversationList 卡片列表最小内容宽度为 333px,左侧板块最小宽度为 365px;右侧纯白主容器最小宽度 380px,左侧最大可拖宽度需按 `工作区宽度 + 32px 覆盖量 - 380px` 动态计算;拖拽条需支持键盘左右方向键微调。',
|
|
52
|
-
'【左侧插槽】左侧半透明容器默认承载平台业务组件 `ConversationList`,用于会话 / 工单 / 托管队列切换;左侧内容区必须扣除右侧 32px 覆盖安全区,避免被主白卡裁切;`ConversationList` 展开态四周内容间距固定 16px。嵌入时 `ConversationList` 必须设置 `resizable={false}`、保留 `collapsible` 与 `autoCollapseOnNarrow`、`style={{ width: "100%", height: "100%" }}`,并在外层框架接入 `onLayoutWidthRequest` 与 `onVariantChange`:默认列表传 `leftContentMinWidth={88}`,卡片列表切换为 `leftContentMinWidth={333}`。框架统一管理宽度拖拽,但拖拽下限跟随子组件能力;点击会话列表左上角收起按钮时,外层左侧半透明容器必须同步吸附缩窄到 120px,不允许只把 ConversationList
|
|
53
|
-
'【左侧选中项 = 右侧上下文源】客服工作台框架内左侧 `ConversationList` 默认必须存在一个当前选中项;它不是单纯的导航高亮,而是整个右侧主白卡的上下文源。切换左侧会话 / 工单 / 托管项时,右侧纯白容器里的 IM 对话区和 InfoDisplayPanel 信息区都必须同步切换到同一个当前处理对象,禁止出现“左侧已切到 B 会话,但右侧聊天仍是 A、信息区还是 C”的上下文错位。预览实现必须以 `activeConversationId`
|
|
52
|
+
'【左侧插槽】左侧半透明容器默认承载平台业务组件 `ConversationList`,用于会话 / 工单 / 托管队列切换;左侧内容区必须扣除右侧 32px 覆盖安全区,避免被主白卡裁切;`ConversationList` 展开态四周内容间距固定 16px。嵌入时 `ConversationList` 必须设置 `resizable={false}`、保留 `collapsible` 与 `autoCollapseOnNarrow`、`style={{ width: "100%", height: "100%" }}`,并在外层框架接入 `onLayoutWidthRequest` 与 `onVariantChange`:默认列表传 `leftContentMinWidth={88}`,卡片列表切换为 `leftContentMinWidth={333}`。框架统一管理宽度拖拽,但拖拽下限跟随子组件能力;点击会话列表左上角收起按钮时,外层左侧半透明容器必须同步吸附缩窄到 120px,不允许只把 ConversationList 缩到头像列而外层仍保留大片空白。收起后的纯头像列表项高度必须固定为 68px,包含 `h-[68px] min-h-[68px] shrink-0` 级别约束;选中态头像胶囊也不得被父级 flex 均分或可用高度不足压扁。',
|
|
53
|
+
'【左侧选中项 = 右侧上下文源】客服工作台框架内左侧 `ConversationList` 默认必须存在一个当前选中项;它不是单纯的导航高亮,而是整个右侧主白卡的上下文源。切换左侧会话 / 工单 / 托管项时,右侧纯白容器里的 IM 对话区和 InfoDisplayPanel 信息区都必须同步切换到同一个当前处理对象,禁止出现“左侧已切到 B 会话,但右侧聊天仍是 A、信息区还是 C”的上下文错位。预览实现必须以 `activeConversationId` 作为单一事实源:左侧列表头像、标题、单号、标签数组与右侧 IM Header 的头像、标题、标签数组、会话 ID、渠道、沟通时长和消息脚本必须一一对应,不能只更新高亮而复用固定聊天剧本。右侧标题右侧的标签必须复用左侧当前选中 item.tags 的 label / variant / iconName,禁止只传单个 tagLabel 或统一渲染成白色标签;标签排序必须遵循状态优先:red / green / orange / blue 这类带颜色的会话状态标签默认排在最左侧,grey 辅助标签排在其后;灰色辅助标签 mock 文案必须控制在 4 个字以内。',
|
|
54
|
+
'【左侧卡片消息流 = 右侧完整气泡流缩小版】当 `ConversationList variant="card"` 展示选中会话卡片时,卡片内的消息区不是“只看几条摘要”的精简预览,而是右侧 IMConversationPattern 当前会话完整气泡消息流的缩小版窗口。若右侧聊天区存在 20 条可展示气泡消息,左侧卡片消息区也必须通过固定高度 + `overflow-y-auto` 的内部滚动查看这同一批 20 条气泡;禁止按条数截断、禁止只保留最后几条。左侧每条气泡的文案内容、发送方角色、气泡颜色语义必须与右侧聊天区对应消息完全一致:用户消息统一使用右侧用户气泡的浅灰语义,bot 回复统一使用 AI 渐变语义,人工客服回复统一使用右侧客服气泡的浅青语义。允许因为卡片更小而降低字号、压缩内边距、隐藏头像/时间或继续省略 system 提示,但禁止改写文案、调换角色、改颜色,或让左侧显示右侧不存在的消息。',
|
|
54
55
|
'【左侧卡片列表】`ConversationList variant="card"` 嵌入左栏时,卡片列数必须基于左侧内容区实际宽度动态判断:`<=580px` 为 1 列,`>580px` 为 2 列,`>950px` 为 3 列;外层拖拽变宽 / 变窄时卡片栅格必须实时跟随,不能固定按默认 400px 宽度生成单列。',
|
|
55
56
|
'【右侧主白卡默认内容】右侧主白卡默认采用左右分栏:左侧嵌入 IMConversationPattern 作为当前会话处理区,右侧嵌入 InfoDisplayPanel 作为用户信息、历史工单、工单日志、视频信息等辅助信息区;外层主白卡只负责容器、裁切、间距和分栏,不再额外套大白卡。',
|
|
56
57
|
'【右侧布局规则】右侧主白卡内默认 `p-4 + gap-4`,左侧 IM 对话区使用 `flex-1 min-w-0` 吃剩余空间,右侧 InfoDisplayPanel 为独立信息栏;框架 mainMinWidth 建议不低于 720px,避免 IM 对话与信息面板互相挤压。IMConversationPattern 和 InfoDisplayPanel 都必须 `height: 100%` 填满主白卡高度,并由各自组件自管内部滚动。',
|
|
@@ -92,7 +93,7 @@ export default function CustomerServicePage() {
|
|
|
92
93
|
/* 该模板本身贴浏览器边缘展示(无外圈圆角),预览容器无需再额外 padding */
|
|
93
94
|
previewPadding: 0,
|
|
94
95
|
components: [
|
|
95
|
-
'nav-bar', 'form-title', 'tag-bar', 'tabs', 'button', 'icon', 'input', 'select',
|
|
96
|
+
'nav-bar', 'form-title', 'tag-bar', 'tabs', 'button', 'icon', 'input', 'select', 'card',
|
|
96
97
|
'date-picker', 'tag-input', 'checkbox', 'table', 'tag', 'modal', 'sheet', 'empty', 'full-screen-page',
|
|
97
98
|
],
|
|
98
99
|
rules: [
|
|
@@ -117,9 +118,10 @@ export default function CustomerServicePage() {
|
|
|
117
118
|
'【全宽撑满铁律(左右大空白专项)】**白卡必须撑满浏览器全宽,main 的 padding 只能是 p-4(16px)**:⛔ 严禁用 `p-2` / `px-6` / `px-8` 当 main 外框间距(常见误区:把白卡内部 padding p-6 错用到 main);⛔ 严禁在 main 内套 `<div className="max-w-* mx-auto">` 居中容器——这是左右大空白的主因;⛔ 严禁根容器 / main 加 `max-w-screen-xl` / `max-w-7xl` 等收窄值;白卡 div 自身不加 `mx-*` 外边距,靠 `flex-1` 或 `shrink-0 w-[Npx]` 控制宽度。三层间距金字塔:浏览器边→白卡边缘 16px(main p-4)、白卡间距 8px(gap-2)、白卡内 padding 24px(p-6)。详见 LAYOUT_RULES §1.2 / §6.1。',
|
|
118
119
|
'【白卡纯白无描边铁律】**最外层大白卡容器必须是"纯白底 + 12px 圆角 + 无 border + 无 shadow"**:⛔ 严禁在背景为 `var(--color-surface)` 的最外层 div / Card 上加 `border` / `outline` / `ring` / `borderColor` / `border-border-default` 等任何描边;白卡的层级感**唯一靠"灰底 vs 白卡的明度差 + 12px 圆角"**承担,加 border 会让明度差被划线覆盖、视觉权重变硬、气息感丢失。✅ **保留**:白卡**内部**所有 TFDS 组件自带的描边(Input / Select / TagInput / Tag / Tabs line / Table 行线 / 内嵌 grey Card 等——这些是控件功能边界,不是卡片层级边界)。判别口诀:背景色 `--color-surface`(白)→ **禁止** border;背景色 `--color-blueGrey-200`(灰,页面外框)→ **允许** border。详见 GLOBAL_DESIGN_RULES § 5.5。',
|
|
119
120
|
'【Copilot详情页补充】当页面既不是纯入口页,也不是纯消息流页,而是“左侧/中部展示上下文结果,右侧或顶部继续让 AI 协助处理”的混合协作场景,可使用 `CopilotPagePattern`。它适用于“带上下文结果面板的 AI 协同详情页”,但仍不应被误写成纯白卡列表管理页。',
|
|
120
|
-
'【Tabs 硬约束】主工作区内凡多块互斥内容的面板切换**必须**用基础组件 `<Tabs />`;**卡片内**小模块切换、**内容区顶部**布局级切换**默认优先** `variant="segment"`(分段器);所有 4 种 Tabs 变体在白卡/内容区/表单分段/筛选维度/Playground 面板内默认尺寸统一为 **SM**(省略 size 或 `size="sm"`),⛔ 禁止内容区默认 `size="md"` / `size="lg"`;只有平台顶部 header / 页面级顶导 Tabs 可按场景使用 MD/LG
|
|
121
|
+
'【Tabs 硬约束】主工作区内凡多块互斥内容的面板切换**必须**用基础组件 `<Tabs />`;**卡片内**小模块切换、**内容区顶部**布局级切换**默认优先** `variant="segment"`(分段器);所有 4 种 Tabs 变体在白卡/内容区/表单分段/筛选维度/Playground 面板内默认尺寸统一为 **SM**(省略 size 或 `size="sm"`),⛔ 禁止内容区默认 `size="md"` / `size="lg"`;只有平台顶部 header / 页面级顶导 Tabs 可按场景使用 MD/LG。Tabs 默认内置横向滚动,父级必须 `min-w-0 max-w-full`,禁止 `w-fit` / `min-w-max` / 固定大宽度导致 Tabs 撑破卡片、侧栏或页面;禁止 `Button` 排一行或 `Tag` 冒充切换器',
|
|
121
122
|
'【模板独立】每个"列表示例"对应一个独立 .jsx 文件(如 VariableManagementPage / McpManagementPage),业务接入直接复制单文件即可',
|
|
122
123
|
'【示例映射(选型入口)】页面示例0=StrategyListPage(树形可展开列表+版本子行+分页);页面示例1=VariableManagementPage(单白卡:标题/筛选/表格);页面示例2=McpManagementPage(双白卡:左侧可拖拽辅助大卡 + 右侧弹性主白卡列表,是“横向大卡默认支持宽度拖拽”的标准示例);页面示例3=TabTopBarListPage(TopBar 胶囊场景切换 + 一级 Tab + 操作组;白卡内“列表+详情面板”联动);页面示例4=NoAccessPage(空状态+主行动:申请权限);页面示例5=ConstructionPage(空状态:功能建设中)',
|
|
124
|
+
'【页面示例3·信息卡片3】TabTopBarListPage 的左侧知识列表条目必须引用业务组件 `Card type="info3"`(信息卡片3),不得在页面内手写重复卡片样式。信息卡片3适用于知识库知识列表、规则列表、策略列表等密集信息场景,结构固定为“标题 + 右上状态 Tag + 描述 + 底部元信息”,圆角固定 12px(rounded-lg)。页面示例3只负责传入 question/answer/status/typeLabel/updatedAt/creator 与 selected/onAction;页面示例3的列表卡片处于白色工作区内,必须显式使用 `color="grey"` 灰色卡片样式;信息卡片3的背景色、描边和 hover 规则必须与其它 Card 分类一致,统一遵守 `color=white/grey` 的背景反衬规则;整卡选中 UI 由 Card 的 `selected` 统一控制,语义是“原本同一条灰色描边变为绿色描边”,禁止新增 ring / outline / 外扩描边,也不允许改写卡片背景色。点击已选卡片收起详情、点击其它卡片切换详情的交互状态仍由页面模板维护。',
|
|
123
125
|
'【关键词→示例】“策略管理/策略列表/版本子行/引用次数/渠道”→示例0;“变量管理/字段变量/流程引用”→示例1;“MCP/工具管理/分类树筛选/左侧标签树”→示例2;“知识库/QA对/文档知识/案例库/右侧详情面板/TopBar胶囊Tab”→示例3;“无权限/申请权限/无访问权限”→示例4;“建设中/敬请期待/开发中”→示例5',
|
|
124
126
|
'【NavBar】通过 navItems 自定义菜单项(id / label / iconName),通过 selectedItemId 高亮当前菜单;菜单 id 与右侧模板一一对应',
|
|
125
127
|
'【列表页结构】标题栏 → 筛选栏 → 表格,垂直 16px 间距,与白卡四边各 24px 边距',
|
|
@@ -340,14 +342,14 @@ export default function ChatHomePage() {
|
|
|
340
342
|
id: 'chat-conversation-page',
|
|
341
343
|
name: 'AI 对话页',
|
|
342
344
|
description:
|
|
343
|
-
'独立的 AI 助手会话详情页:顶导栏(返回 + 标题 + 操作按钮组)+ 800px 居中消息流 + 底部吸底 ChatInput。所有消息(用户 / AI)一律用 ChatMessage 渲染(用户走 role="user",AI 走 header=true)。支持 phase 切换:默认
|
|
345
|
+
'独立的 AI 助手会话详情页:顶导栏(返回 + 标题 + 操作按钮组)+ 800px 居中消息流 + 底部吸底 ChatInput。所有消息(用户 / AI)一律用 ChatMessage 渲染(用户走 role="user",AI 走 header=true)。支持 phase 切换:默认 welcome 阶段直接展示新对话页(CATCAT 头像 + 推荐 chip);点「新会话」后也回到该欢迎屏。若外部显式传入 initialMessages,则可直接打开已有会话。底部输入框真实发送后自动编排完整 AI 输出链路:用户气泡 → AI 深度思考 → 人工澄清确认卡片 → AI 自动执行流加载/完成 → AI 结论 + 最终产物 + 追问;追问继续追加在同一会话流,并自动继承上一轮账号、环境、Mock、结论和产物上下文。',
|
|
344
346
|
keywords: [
|
|
345
347
|
'AI 对话页', 'AI 会话页', 'AI 对话详情页', 'AI 会话详情', 'AI 助手会话',
|
|
346
348
|
'任务协作页', '资料整理页', '策略分析页', '内容生成追问页', '多轮追问页',
|
|
347
349
|
'消息流', '对话流', '消息列表', '会话面板',
|
|
348
350
|
'吸底 ChatInput', '底部输入', '800px 居中消息流',
|
|
349
351
|
'欢迎屏 CATCAT', '推荐 chip', '新会话',
|
|
350
|
-
'流式执行', '任务规划卡', '执行流消息', '产物卡 + 追问',
|
|
352
|
+
'流式执行', '自动编排', '人工澄清确认卡片', '任务规划卡', '执行流消息', '产物卡 + 追问',
|
|
351
353
|
// 错误信号词
|
|
352
354
|
'围绕单一任务持续协作', '持续推进', '消息为主',
|
|
353
355
|
'手搓 AI 对话页', '自制消息流', 'div 模拟聊天',
|
|
@@ -368,24 +370,29 @@ export default function ChatHomePage() {
|
|
|
368
370
|
'【消息组件统一】所有消息(用户 / AI)一律用 ChatMessage 渲染。用户消息 role="user" + userContent / userAttachments;AI 消息 header=true + 任意组合 thinking / leadText / plan / taskGroups / resultText / resultArtifacts / followUps。禁止用 ChatBubble 渲染 AI 回复',
|
|
369
371
|
'【消息间距】flex-col gap-2(8px),符合现代 AI 对话的紧凑节奏',
|
|
370
372
|
'【操作栏 historyMode】只有最后一条消息 historyMode=false(常显),其余消息 historyMode=true(hover 才显示,占位高度始终保留);待用户处理的交互卡片(plan / confirms)未操作时整条消息不显示操作栏,处理后才变成历史消息走 historyMode 规则',
|
|
371
|
-
'【流式执行流】用 useStreamingTaskGroups(taskGroups, { intervalMs: 600 }) 把最终态 taskGroups 转成"按 600ms 节奏一步步推出"的切片:当前组 status=processing 带扫光,每过一格推一个 step,本组 step 出齐后切到下一组首步;流式期间整条 AI 消息不显示操作栏(actions=null
|
|
372
|
-
'
|
|
373
|
-
'
|
|
374
|
-
'
|
|
375
|
-
'
|
|
373
|
+
'【流式执行流】用 useStreamingTaskGroups(taskGroups, { intervalMs: 600 }) 把最终态 taskGroups 转成"按 600ms 节奏一步步推出"的切片:当前组 status=processing 带扫光,每过一格推一个 step,本组 step 出齐后切到下一组首步;流式期间整条 AI 消息不显示操作栏(actions=null),完成后同一条消息更新为 stream=false + taskGroups 全部 completed + resultText / resultArtifacts / followUps。',
|
|
374
|
+
'【自动编排链路(强约束)】底部 ChatInput 或 followUps 发送任意有效文本后,必须立即追加右侧 user 气泡,并自动按时间推进:1) AI 深度思考中 2) AI 深度思考完成并说明已理解上下文 3) 若出现卡片型节点则按“统一人工闸门”规则判断是否暂停 4) 展示 AI 自动执行流加载状态(ai-flow stream=true)5) 执行流加载完成(stream=false + completed)6) 展示 AI 结论、最终产物 resultArtifacts 和下一组追问 followUps。禁止只回复一条静态短文本。',
|
|
375
|
+
'【统一人工闸门(强约束)】任务规划卡、澄清确认卡片、配置表单等卡片型节点,只要右下角存在可操作按钮,尤其是黑色主按钮(如“确认执行 / 确认提交 / 确认并继续 / 开始执行任务”),就必须暂停后续模拟链路;只有用户真实点击该主按钮后,才能恢复并继续执行下一段流程。不得在按钮未点击前自动跳过到执行流或最终结果。',
|
|
376
|
+
'【内容型卡片不暂停】如果卡片型节点仅用于展示内容,没有右下角可操作按钮,或没有可触发继续执行的主按钮,则该节点视为 AI 自动输出内容的一部分,不需要停下来等待人工确认,应继续自动推进后续链路。',
|
|
377
|
+
'【人工澄清确认卡片】AI 对话页的人工澄清确认必须复用 `ChatMessage` 的 `confirms` 能力,默认使用 `mode="form-card"`(澄清确认卡片2)样式:灰色外层确认容器 + 白色表单面板 + 业务场景 / 处理渠道 / 补充说明三段纵向表单 + 右下角黑色主按钮。不得在本模板中改回 option-card 单选列表或手写 Card/div 模拟澄清卡片;当 confirms 卡片存在右下角主按钮时,必须进入人工闸门暂停;当 confirms 仅展示信息且无按钮时,可自动继续后续执行。',
|
|
378
|
+
'【同会话上下文继承规则】同一会话内用户继续追问时,系统规则必须自动带上上一轮上下文:账号 account、环境 environment、Mock 数据说明 mock、上一轮结论 conclusion、上一轮产物 artifacts;这些上下文不需要用户重复输入,也不能因为点击 followUps 或底部继续输入而丢失。实现上建议维护 conversationContextRef / conversationContext 状态,每轮完成时回写本轮结论和产物,下一轮 thinking 与执行 payload 从该上下文读取。',
|
|
379
|
+
'【追问交互】followUps 字符串数组在父组件 wrap 成 { items, onSelect: onSend } 形式,点击 chip 必须等价于立即发送该文本:先追加一条右侧 user 气泡,再进入同一套自动编排链路(深度思考 → 卡片节点按是否有按钮决定暂停/自动继续 → 执行流 → 结论/产物/追问);不要只回填输入框等待用户二次发送,不要新开会话或替换历史消息。',
|
|
380
|
+
'【自动滚底与最新锚点】用 ResizeObserver 监听消息列表 firstElementChild 的高度变化,并在消息列表底部放置 latestAnchorRef;任何高度增长、阶段推进、流式 step、折叠展开都必须 `scrollIntoView({ block: "end", behavior: "smooth" })` 或 scrollTo bottom,保证 AI 输出过程中最新 thinking / confirm / execution / result 始终出现在可视会话流区域。',
|
|
376
381
|
'【新建会话页 = 空对话唯一标准态】只要当前会话没有消息(初次进入空会话、外部传入 `messages=[]`、或用户点击「新会话」清空消息),都必须展示统一的新建会话页:中部居中展示 CATCAT 头像(66px 渐变描边圆 + 蓝色投影 + 32px catcat.svg)+「OLA AI」标题 + 欢迎语 + 3 条推荐 chip(白底 0.6 透明 + inset 1px 白边 + 右箭头);底部 `ChatInput` 仍 shrink-0 吸底,placeholder 固定为"需要我为你做什么…";禁止出现空白消息区、默认 mock 对话残留或与图示不一致的空状态。',
|
|
377
|
-
'【关键词路由】① 含「整理 / 分析 / 生成 / 梳理 / 输出 / 汇总」→
|
|
382
|
+
'【关键词路由】① 含「整理 / 分析 / 生成 / 梳理 / 输出 / 汇总」→ 自动编排任务链路 ② 命中 followUps 文案 → 自动编排追问链路,并输出对应结论/产物/下一组追问 ③ 含「停止 / 取消」→ AI 短答「已停止当前任务,需要时再叫我。」并停止后续定时器 ④ 其他有效输入 → 仍进入自动编排链路,基于通用 mock 输出结果,避免对话页退化成普通问答短答。',
|
|
378
383
|
'【任务规划卡 defaultConfirmed】历史消息(mock 中的已处理卡片)通过 plan.defaultConfirmed=true 让卡片初始就处于禁用置灰态;TaskPlanCard 内部用 useState(plan.defaultConfirmed === true) 初始化 confirmed,并用 useEffect 同步 props 变化',
|
|
379
384
|
],
|
|
380
|
-
code: `// 见 ChatConversationPattern.jsx —— 完整可交互的会话页骨架(含欢迎屏 / 流式执行 /
|
|
385
|
+
code: `// 见 ChatConversationPattern.jsx —— 完整可交互的会话页骨架(含欢迎屏 / 自动编排 / 流式执行 / 追问上下文继承)
|
|
381
386
|
// 关键拼装(全部消息走 ChatMessage,无 ChatBubble):
|
|
382
387
|
// 用户消息: <ChatMessage role="user" userContent={[{type:"text",value:"..."}, {type:"entity",label:"..."}]} userAttachments={[...]} />
|
|
383
|
-
// AI
|
|
384
|
-
//
|
|
388
|
+
// AI 思考: <ChatMessage header thinking={{state:"thinking"|"completed", ...}} />
|
|
389
|
+
// 澄清确认: <ChatMessage header leadText="请确认..." confirms={[...DEFAULT_CHAT_FORM_CONFIRM]} /> // mode="form-card"(澄清确认卡片2)
|
|
390
|
+
// AI 流式: <StreamingChatMessage taskGroups={DEFAULT_CHAT_TASK_GROUPS} /> // 内部 useStreamingTaskGroups,stream=true 时展示加载态
|
|
385
391
|
// AI 完整态: <ChatMessage header taskGroups={...} resultText={...} resultArtifacts={...} followUps={{items, onSelect:onSend}} actions={{historyMode:false}} />
|
|
386
|
-
// 底部输入: <ChatInput onSend={handleSend} /> // handleSend
|
|
392
|
+
// 底部输入: <ChatInput onSend={handleSend} /> // handleSend 路由:用户气泡 → thinking → confirms → ai-flow → resultArtifacts → followUps
|
|
393
|
+
// 上下文继承: conversationContextRef 每轮回写 account/environment/mock/conclusion/artifacts,下一轮追问自动读取
|
|
387
394
|
// phase 切换: 'chat'(默认)<-> 'welcome'(点新会话)
|
|
388
|
-
// 自动滚底: ResizeObserver
|
|
395
|
+
// 自动滚底: ResizeObserver + latestAnchorRef,任意增长 / 阶段推进 / 流式输出 → scrollIntoView bottom smooth`,
|
|
389
396
|
},
|
|
390
397
|
{
|
|
391
398
|
id: 'im-conversation-page',
|
|
@@ -1171,13 +1171,14 @@ export const PREVIEW_REGISTRY = {
|
|
|
1171
1171
|
{ id: 'product', label: '商品卡片' },
|
|
1172
1172
|
{ id: 'info', label: '信息卡片1' },
|
|
1173
1173
|
{ id: 'info2', label: '信息卡片2' },
|
|
1174
|
+
{ id: 'info3', label: '信息卡片3' },
|
|
1174
1175
|
],
|
|
1175
1176
|
default: 'default',
|
|
1176
1177
|
},
|
|
1177
1178
|
],
|
|
1178
1179
|
|
|
1179
1180
|
mapProps: (cv, enums) => {
|
|
1180
|
-
const type = ['product', 'info', 'info2'].includes(cv.category) ? cv.category : 'data';
|
|
1181
|
+
const type = ['product', 'info', 'info2', 'info3'].includes(cv.category) ? cv.category : 'data';
|
|
1181
1182
|
return {
|
|
1182
1183
|
type,
|
|
1183
1184
|
color: enums.color || 'white',
|
|
@@ -1216,6 +1217,9 @@ export const PREVIEW_REGISTRY = {
|
|
|
1216
1217
|
if (category === 'info2') {
|
|
1217
1218
|
return props.filter((prop) => ['color', 'animatedTone'].includes(prop.name));
|
|
1218
1219
|
}
|
|
1220
|
+
if (category === 'info3') {
|
|
1221
|
+
return props.filter((prop) => prop.name === 'color');
|
|
1222
|
+
}
|
|
1219
1223
|
const dataVisibleNames = ['color', 'dataIconVisible'];
|
|
1220
1224
|
if ((enumValues.dataIconVisible || 'hidden') === 'visible') {
|
|
1221
1225
|
dataVisibleNames.push('dataIconStyle');
|
|
@@ -1228,7 +1232,7 @@ export const PREVIEW_REGISTRY = {
|
|
|
1228
1232
|
generateUsage: (enums, cv = {}) => {
|
|
1229
1233
|
const lines = [`import Card from './components/Card';`];
|
|
1230
1234
|
const color = enums.color || 'white';
|
|
1231
|
-
const type = ['product', 'info', 'info2'].includes(cv.category) ? cv.category : 'data';
|
|
1235
|
+
const type = ['product', 'info', 'info2', 'info3'].includes(cv.category) ? cv.category : 'data';
|
|
1232
1236
|
lines.push('');
|
|
1233
1237
|
|
|
1234
1238
|
if (type === 'data') {
|
|
@@ -1251,6 +1255,7 @@ export const PREVIEW_REGISTRY = {
|
|
|
1251
1255
|
if (type === 'product') lines.push(' type="product"');
|
|
1252
1256
|
if (type === 'info') lines.push(' type="info"');
|
|
1253
1257
|
if (type === 'info2') lines.push(' type="info2"');
|
|
1258
|
+
if (type === 'info3') lines.push(' type="info3"');
|
|
1254
1259
|
if (color !== 'white') lines.push(` color="${color}"`);
|
|
1255
1260
|
if (type === 'product') {
|
|
1256
1261
|
lines.push(' title="海底捞门店通用双人套餐"');
|
|
@@ -1281,6 +1286,17 @@ export const PREVIEW_REGISTRY = {
|
|
|
1281
1286
|
lines.push(' animatedBadge="AI 推荐"');
|
|
1282
1287
|
lines.push(' animatedActionText="立即体验"');
|
|
1283
1288
|
lines.push(' onAction={() => {}}');
|
|
1289
|
+
} else if (type === 'info3') {
|
|
1290
|
+
lines.push(' title="抖音生服订单已签收后还能申请退款吗?"');
|
|
1291
|
+
lines.push(' description="已签收订单仍可在 7 天无理由保障期内发起退款,需提供商品照片或服务异常说明,平台介入后通常 24h 内出结果。"');
|
|
1292
|
+
lines.push(' status="生效"');
|
|
1293
|
+
lines.push(' metaItems={[');
|
|
1294
|
+
lines.push(' { iconName: "tag-01-stroked", value: "QA" },');
|
|
1295
|
+
lines.push(' { iconName: "clock-stroked", value: "2026-04-21 10:30" },');
|
|
1296
|
+
lines.push(' { value: "李思儒" },');
|
|
1297
|
+
lines.push(' ]}');
|
|
1298
|
+
lines.push(' actionAriaLabel="查看知识详情"');
|
|
1299
|
+
lines.push(' onAction={() => {}}');
|
|
1284
1300
|
} else {
|
|
1285
1301
|
lines.push(' title="更新用户资料"');
|
|
1286
1302
|
lines.push(' description="更新用户个人资料信息,支持修改昵称、头像、简介等。"');
|
package/src/index.d.ts
CHANGED
|
@@ -452,6 +452,8 @@ export interface TabsProps extends TfdsCommonProps {
|
|
|
452
452
|
defaultIndex?: number;
|
|
453
453
|
/** function, default: null */
|
|
454
454
|
onChange?: (...args: any[]) => any;
|
|
455
|
+
/** enum<auto | visible>, default: "auto" */
|
|
456
|
+
overflow?: "auto" | "visible";
|
|
455
457
|
}
|
|
456
458
|
export const Tabs: React.FC<TabsProps>;
|
|
457
459
|
|