@tfdesign/b-end 1.0.11 → 1.0.13
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/README.md +23 -25
- package/package.json +1 -1
- package/skills/tfds/components.index.json +271 -67
- package/skills/tfds/components.summary.json +101 -62
- package/src/_b_end_runtime/components/ChatMessage.jsx +210 -61
- package/src/_b_end_runtime/components/ChatMessage.tokens.js +30 -0
- package/src/_b_end_runtime/components/ChatMessagePreview.jsx +14 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +30 -6
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +5 -0
- package/src/_b_end_runtime/components/Filter.jsx +390 -0
- package/src/_b_end_runtime/components/Filter.tokens.js +98 -0
- package/src/_b_end_runtime/components/Input.jsx +3 -1
- package/src/_b_end_runtime/components/Modal.jsx +10 -3
- package/src/_b_end_runtime/components/Radio.jsx +174 -4
- package/src/_b_end_runtime/components/Radio.tokens.js +22 -0
- package/src/_b_end_runtime/components.js +124 -13
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +35 -26
- package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +14 -1
- package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +19 -12
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +14 -1
- package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +15 -2
- package/src/_b_end_runtime/page-patterns/pageListShared.jsx +54 -36
- package/src/_b_end_runtime/patterns.js +33 -21
- package/src/_b_end_runtime/preview-registry.jsx +180 -8
- package/src/index.d.ts +30 -1
- package/src/index.js +2 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 行业对齐:assistant-ui Message / Vercel AI SDK Message / Microsoft Copilot ChatMessage。
|
|
5
5
|
* 与 ChatBubble(仅气泡)、ChatInput(输入器)共同组成 Chat 命名空间。
|
|
6
|
-
* 样式依赖:入口 CSS 须 @import "@
|
|
6
|
+
* 样式依赖:入口 CSS 须 @import "@tfdesign/b-end/theme.css"(提供 --color-border-default、--color-blueGrey-*、--tfds-ai-execution-* 等);缺失时 var() 无效,追问/执行流等易出现深黑描边。
|
|
7
7
|
* 一个 ChatMessage = 一条会话消息,按 role 渲染:
|
|
8
8
|
* · role='ai' AI 输出消息(默认):header / thinking / plan / 执行流(title+steps/taskGroups)
|
|
9
9
|
* / 结果(resultText+resultArtifacts+confirms) / taskBadge / actions / followUps
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* @prop {string} [statusIconName='check-circle-stroked'] — 标题左侧状态图标名(completed 时使用)
|
|
42
42
|
* @prop {Array|null} [steps=DEFAULT_CHAT_STEPS] — 执行步骤数组
|
|
43
43
|
* @prop {Array|null} [taskGroups=null] — 多任务组(每组可独立折叠 + 流式输出)
|
|
44
|
-
* @prop {Array|null} [confirms=null] — 人工确认节点数组(原 humanConfirmNodes)
|
|
44
|
+
* @prop {Array|null} [confirms=null] — 人工确认节点数组(原 humanConfirmNodes),支持 mode='text-card'|'card-only'|'option-card'|'form-card'(option-card = 澄清确认卡片1,form-card = 澄清确认卡片2)
|
|
45
45
|
* @prop {string} [resultText=''] — 结果节点文案
|
|
46
46
|
* @prop {object|null} [resultArtifact=null] — 结果节点单个产物卡片(兼容旧 API)
|
|
47
47
|
* @prop {Array|null} [resultArtifacts=null] — 结果节点产物卡片数组
|
|
@@ -54,7 +54,10 @@
|
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
56
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
57
|
+
import Button from './Button';
|
|
57
58
|
import Icon from './Icon';
|
|
59
|
+
import RadioGroup, { Radio } from './Radio';
|
|
60
|
+
import Form from './Form';
|
|
58
61
|
import Tooltip from './Tooltip';
|
|
59
62
|
import catcatSvg from './file-type-assets/catcat.svg';
|
|
60
63
|
import { getFileTypeIcon } from './file-type-assets';
|
|
@@ -247,6 +250,77 @@ export const DEFAULT_CHAT_CONFIRMS = [
|
|
|
247
250
|
},
|
|
248
251
|
];
|
|
249
252
|
|
|
253
|
+
export const DEFAULT_CHAT_OPTION_CONFIRM = [
|
|
254
|
+
{
|
|
255
|
+
id: 'confirm-option-card',
|
|
256
|
+
mode: 'option-card',
|
|
257
|
+
title: '澄清确认',
|
|
258
|
+
iconName: 'clipboard-check-stroked',
|
|
259
|
+
primaryActionLabel: '确认',
|
|
260
|
+
secondaryActionLabel: '',
|
|
261
|
+
defaultSelectedValue: 'handoff-guide',
|
|
262
|
+
options: [
|
|
263
|
+
{
|
|
264
|
+
value: 'handoff-guide',
|
|
265
|
+
label: '方案1: 用户请求转人工但系统未触发转接,持续引导自助操作',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
value: 'handoff',
|
|
269
|
+
label: '方案2: 用户请求转人工',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
value: 'self-service',
|
|
273
|
+
label: '方案3: 持续引导自助操作',
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
export const DEFAULT_CHAT_FORM_CONFIRM = [
|
|
280
|
+
{
|
|
281
|
+
id: 'confirm-form-card',
|
|
282
|
+
mode: 'form-card',
|
|
283
|
+
title: '澄清确认',
|
|
284
|
+
iconName: 'clipboard-check-stroked',
|
|
285
|
+
primaryActionLabel: '确认',
|
|
286
|
+
secondaryActionLabel: '',
|
|
287
|
+
formItems: [
|
|
288
|
+
{
|
|
289
|
+
id: 'scene',
|
|
290
|
+
label: '业务场景',
|
|
291
|
+
type: 'select',
|
|
292
|
+
placeholder: '请选择业务场景',
|
|
293
|
+
defaultValue: 'after-sales',
|
|
294
|
+
options: [
|
|
295
|
+
{ value: 'after-sales', label: '售后咨询' },
|
|
296
|
+
{ value: 'pre-sales', label: '售前咨询' },
|
|
297
|
+
{ value: 'logistics', label: '物流问题' },
|
|
298
|
+
],
|
|
299
|
+
fullWidth: true,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'channel',
|
|
303
|
+
label: '处理渠道',
|
|
304
|
+
type: 'select',
|
|
305
|
+
placeholder: '请选择处理渠道',
|
|
306
|
+
defaultValue: 'self-service',
|
|
307
|
+
options: [
|
|
308
|
+
{ value: 'self-service', label: '自助引导' },
|
|
309
|
+
{ value: 'handoff', label: '转人工' },
|
|
310
|
+
],
|
|
311
|
+
fullWidth: true,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: 'remark',
|
|
315
|
+
label: '补充说明',
|
|
316
|
+
type: 'input',
|
|
317
|
+
placeholder: '请输入补充说明(可选)',
|
|
318
|
+
fullWidth: true,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
|
|
250
324
|
/* ── 默认示例数据:新增模块 ── */
|
|
251
325
|
export const DEFAULT_CHAT_HEADER = {
|
|
252
326
|
name: 'OLA AI',
|
|
@@ -407,29 +481,15 @@ const HUMAN_CONFIRM_TOGGLE = [
|
|
|
407
481
|
const HUMAN_CONFIRM_BODY = 'flex w-full min-w-0 flex-col items-stretch justify-center rounded-[12px] border border-border-default px-[19px] py-[15px]';
|
|
408
482
|
const HUMAN_CONFIRM_DESCRIPTION = 'w-full min-w-0 text-sm font-normal leading-5 tracking-[0] text-justify';
|
|
409
483
|
const HUMAN_CONFIRM_ACTIONS = 'flex w-full min-w-0 items-center justify-end gap-3';
|
|
410
|
-
const HUMAN_CONFIRM_SECONDARY_BUTTON = [
|
|
411
|
-
'inline-flex h-[36px] shrink-0 items-center justify-center rounded-md border',
|
|
412
|
-
'border-border-default bg-surface px-[11px] py-[5px]',
|
|
413
|
-
'text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0] text-foreground-secondary',
|
|
414
|
-
'cursor-pointer transition-all duration-150',
|
|
415
|
-
'[outline:2px_solid_transparent] outline-offset-2',
|
|
416
|
-
'hover:bg-blueGrey-50 hover:border-border-strong',
|
|
417
|
-
'active:bg-blueGrey-100 active:scale-[0.97]',
|
|
418
|
-
'focus-visible:outline-blueGrey-400',
|
|
419
|
-
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
420
|
-
].join(' ');
|
|
421
|
-
const HUMAN_CONFIRM_PRIMARY_BUTTON = [
|
|
422
|
-
'inline-flex h-[36px] shrink-0 items-center justify-center rounded-md border border-transparent',
|
|
423
|
-
'bg-blueGrey-800 px-3 py-[6px]',
|
|
424
|
-
'text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0] text-white',
|
|
425
|
-
'cursor-pointer transition-all duration-150',
|
|
426
|
-
'[outline:2px_solid_transparent] outline-offset-2',
|
|
427
|
-
'hover:bg-blueGrey-700',
|
|
428
|
-
'active:bg-blueGrey-800 active:scale-[0.97]',
|
|
429
|
-
'focus-visible:outline-blueGrey-400',
|
|
430
|
-
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
431
|
-
].join(' ');
|
|
432
484
|
const HUMAN_CONFIRM_COLLAPSED = 'flex w-full min-w-0 max-w-full items-center gap-2 rounded-[12px] border border-border-default px-4 py-3';
|
|
485
|
+
const HUMAN_CONFIRM_OPTION_CARD = HUMAN_CONFIRM_CARD;
|
|
486
|
+
const HUMAN_CONFIRM_OPTION_HEADER = HUMAN_CONFIRM_HEADER;
|
|
487
|
+
const HUMAN_CONFIRM_OPTION_ICON_WRAP = HUMAN_CONFIRM_ICON_WRAP;
|
|
488
|
+
const HUMAN_CONFIRM_OPTION_TITLE = HUMAN_CONFIRM_TITLE;
|
|
489
|
+
const HUMAN_CONFIRM_OPTION_BODY = HUMAN_CONFIRM_BODY;
|
|
490
|
+
const HUMAN_CONFIRM_OPTION_GROUP = 'w-full gap-2';
|
|
491
|
+
const HUMAN_CONFIRM_OPTION_RADIO = 'w-full min-w-0 !justify-start';
|
|
492
|
+
const HUMAN_CONFIRM_OPTION_ACTIONS = HUMAN_CONFIRM_ACTIONS;
|
|
433
493
|
|
|
434
494
|
/* ── 新增:AI 头像 / 深度思考 / 任务规划 / 追问 / 消息操作 / 任务徽章 / 用户气泡 类名 ── */
|
|
435
495
|
const AI_HEADER = 'flex w-full min-w-0 items-center gap-2';
|
|
@@ -487,7 +547,7 @@ const TASK_PLAN_ITEM_TEXT = [
|
|
|
487
547
|
'flex-1 min-w-0 text-sm font-normal leading-5 tracking-[0]',
|
|
488
548
|
].join(' ');
|
|
489
549
|
const TASK_PLAN_FOOTER = 'mt-3 flex w-full min-w-0 items-center justify-end gap-3';
|
|
490
|
-
/*
|
|
550
|
+
/* TaskPlan / HumanConfirm 的主次操作统一复用基础 Button */
|
|
491
551
|
|
|
492
552
|
const FOLLOW_UP_GROUP = 'flex w-full min-w-0 flex-col items-stretch gap-2';
|
|
493
553
|
const FOLLOW_UP_BUTTON = [
|
|
@@ -701,8 +761,28 @@ function normalizeResultArtifacts(resultArtifacts, resultArtifact) {
|
|
|
701
761
|
return normalizedSingle ? [normalizedSingle] : [];
|
|
702
762
|
}
|
|
703
763
|
|
|
764
|
+
function normalizeConfirmOption(option, index) {
|
|
765
|
+
if (typeof option === 'string') {
|
|
766
|
+
return {
|
|
767
|
+
value: option,
|
|
768
|
+
label: option,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const value = option?.value ?? option?.id ?? `option-${index}`;
|
|
772
|
+
return {
|
|
773
|
+
value,
|
|
774
|
+
label: option?.label ?? option?.title ?? String(value),
|
|
775
|
+
disabled: option?.disabled === true,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
704
779
|
function normalizeHumanConfirmNode(node, index) {
|
|
705
|
-
const mode = ['text-card', 'card-only', 'collapsed'].includes(node?.mode) ? node.mode : 'text-card';
|
|
780
|
+
const mode = ['text-card', 'card-only', 'option-card', 'form-card', 'collapsed'].includes(node?.mode) ? node.mode : 'text-card';
|
|
781
|
+
const options = Array.isArray(node?.options)
|
|
782
|
+
? node.options.map(normalizeConfirmOption).filter((option) => option.label)
|
|
783
|
+
: [];
|
|
784
|
+
const defaultSelectedValue = node?.defaultSelectedValue ?? node?.selectedValue ?? options[0]?.value;
|
|
785
|
+
const formItems = Array.isArray(node?.formItems) ? node.formItems : [];
|
|
706
786
|
return {
|
|
707
787
|
id: node?.id ?? `human-confirm-${index}`,
|
|
708
788
|
mode,
|
|
@@ -713,6 +793,11 @@ function normalizeHumanConfirmNode(node, index) {
|
|
|
713
793
|
iconName: node?.iconName ?? 'sticker-square-stroked',
|
|
714
794
|
primaryActionLabel: node?.primaryActionLabel ?? '确认执行',
|
|
715
795
|
secondaryActionLabel: node?.secondaryActionLabel ?? '编辑',
|
|
796
|
+
defaultConfirmed: node?.defaultConfirmed === true,
|
|
797
|
+
options,
|
|
798
|
+
defaultSelectedValue,
|
|
799
|
+
formItems,
|
|
800
|
+
onOptionChange: typeof node?.onOptionChange === 'function' ? node.onOptionChange : null,
|
|
716
801
|
onPrimaryAction: typeof node?.onPrimaryAction === 'function' ? node.onPrimaryAction : null,
|
|
717
802
|
onSecondaryAction: typeof node?.onSecondaryAction === 'function' ? node.onSecondaryAction : null,
|
|
718
803
|
onToggleCollapsed: typeof node?.onToggleCollapsed === 'function' ? node.onToggleCollapsed : null,
|
|
@@ -1182,26 +1267,26 @@ function TaskPlanCard({ taskPlan, tokenStyles }) {
|
|
|
1182
1267
|
{taskPlan.primaryActionLabel || taskPlan.secondaryActionLabel ? (
|
|
1183
1268
|
<div className={TASK_PLAN_FOOTER}>
|
|
1184
1269
|
{taskPlan.secondaryActionLabel ? (
|
|
1185
|
-
<
|
|
1186
|
-
|
|
1187
|
-
|
|
1270
|
+
<Button
|
|
1271
|
+
variant="outline-black"
|
|
1272
|
+
size="md"
|
|
1188
1273
|
onClick={handleSecondary}
|
|
1189
1274
|
disabled={confirmed}
|
|
1190
1275
|
data-tfds-component="ChatMessage.TaskPlanAction"
|
|
1191
1276
|
>
|
|
1192
1277
|
{taskPlan.secondaryActionLabel}
|
|
1193
|
-
</
|
|
1278
|
+
</Button>
|
|
1194
1279
|
) : null}
|
|
1195
1280
|
{taskPlan.primaryActionLabel ? (
|
|
1196
|
-
<
|
|
1197
|
-
|
|
1198
|
-
|
|
1281
|
+
<Button
|
|
1282
|
+
variant="primary"
|
|
1283
|
+
size="md"
|
|
1199
1284
|
onClick={handlePrimary}
|
|
1200
1285
|
disabled={confirmed}
|
|
1201
1286
|
data-tfds-component="ChatMessage.TaskPlanAction"
|
|
1202
1287
|
>
|
|
1203
1288
|
{taskPlan.primaryActionLabel}
|
|
1204
|
-
</
|
|
1289
|
+
</Button>
|
|
1205
1290
|
) : null}
|
|
1206
1291
|
</div>
|
|
1207
1292
|
) : null}
|
|
@@ -1569,24 +1654,49 @@ function HumanConfirmNode({ node, tokenStyles }) {
|
|
|
1569
1654
|
: () => setInternalCollapsed((v) => !v);
|
|
1570
1655
|
|
|
1571
1656
|
/* 点击主按钮后进入"已确认"禁用态:内容半透明、按钮禁用 */
|
|
1572
|
-
const [confirmed, setConfirmed] = useState(
|
|
1657
|
+
const [confirmed, setConfirmed] = useState(node.defaultConfirmed === true);
|
|
1658
|
+
const [selectedOptionValue, setSelectedOptionValue] = useState(node.defaultSelectedValue);
|
|
1659
|
+
useEffect(() => {
|
|
1660
|
+
if (node.defaultConfirmed === true) setConfirmed(true);
|
|
1661
|
+
}, [node.defaultConfirmed]);
|
|
1662
|
+
useEffect(() => {
|
|
1663
|
+
setSelectedOptionValue(node.defaultSelectedValue);
|
|
1664
|
+
}, [node.defaultSelectedValue]);
|
|
1573
1665
|
const handlePrimary = () => {
|
|
1574
|
-
|
|
1666
|
+
const selectedOption = node.options.find((option) => String(option.value) === String(selectedOptionValue)) ?? null;
|
|
1667
|
+
if (typeof node.onPrimaryAction === 'function') {
|
|
1668
|
+
node.onPrimaryAction({
|
|
1669
|
+
value: selectedOptionValue,
|
|
1670
|
+
option: selectedOption,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1575
1673
|
setConfirmed(true);
|
|
1576
1674
|
};
|
|
1577
1675
|
const handleSecondary = () => {
|
|
1578
1676
|
if (typeof node.onSecondaryAction === 'function') node.onSecondaryAction();
|
|
1579
1677
|
};
|
|
1678
|
+
const handleOptionChange = (nextValue) => {
|
|
1679
|
+
setSelectedOptionValue(nextValue);
|
|
1680
|
+
const selectedOption = node.options.find((option) => String(option.value) === String(nextValue)) ?? null;
|
|
1681
|
+
if (typeof node.onOptionChange === 'function') {
|
|
1682
|
+
node.onOptionChange(nextValue, selectedOption);
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1580
1685
|
|
|
1581
1686
|
const showIntroText = node.mode === 'text-card' && node.introText && !isCollapsed;
|
|
1582
1687
|
const showCardBody = !isCollapsed;
|
|
1688
|
+
const isOptionCard = node.mode === 'option-card';
|
|
1689
|
+
const isFormCard = node.mode === 'form-card';
|
|
1583
1690
|
|
|
1691
|
+
const headerClassName = isOptionCard ? HUMAN_CONFIRM_OPTION_HEADER : HUMAN_CONFIRM_HEADER;
|
|
1692
|
+
const iconWrapClassName = isOptionCard ? HUMAN_CONFIRM_OPTION_ICON_WRAP : HUMAN_CONFIRM_ICON_WRAP;
|
|
1693
|
+
const titleClassName = isOptionCard ? HUMAN_CONFIRM_OPTION_TITLE : HUMAN_CONFIRM_TITLE;
|
|
1584
1694
|
const header = (
|
|
1585
|
-
<div className={
|
|
1586
|
-
<div className={
|
|
1695
|
+
<div className={headerClassName}>
|
|
1696
|
+
<div className={iconWrapClassName} style={tokenStyles.iconWrap} aria-hidden="true">
|
|
1587
1697
|
<Icon name={node.iconName} size={16} color="var(--color-foreground-secondary)" aria-hidden="true" />
|
|
1588
1698
|
</div>
|
|
1589
|
-
<p className={
|
|
1699
|
+
<p className={titleClassName} style={tokenStyles.title}>
|
|
1590
1700
|
{node.title}
|
|
1591
1701
|
</p>
|
|
1592
1702
|
<button
|
|
@@ -1623,39 +1733,78 @@ function HumanConfirmNode({ node, tokenStyles }) {
|
|
|
1623
1733
|
{node.introText}
|
|
1624
1734
|
</p>
|
|
1625
1735
|
) : null}
|
|
1626
|
-
<div
|
|
1736
|
+
<div
|
|
1737
|
+
className={isOptionCard || isFormCard ? HUMAN_CONFIRM_OPTION_CARD : HUMAN_CONFIRM_CARD}
|
|
1738
|
+
style={tokenStyles.humanConfirmCard}
|
|
1739
|
+
>
|
|
1627
1740
|
{header}
|
|
1628
1741
|
{showCardBody ? (
|
|
1629
1742
|
<div
|
|
1630
|
-
className={[
|
|
1743
|
+
className={[
|
|
1744
|
+
isOptionCard || isFormCard ? HUMAN_CONFIRM_OPTION_BODY : HUMAN_CONFIRM_BODY,
|
|
1745
|
+
confirmed ? 'opacity-60' : '',
|
|
1746
|
+
].filter(Boolean).join(' ')}
|
|
1631
1747
|
style={tokenStyles.humanConfirmBody}
|
|
1632
1748
|
>
|
|
1633
|
-
{
|
|
1749
|
+
{isOptionCard ? (
|
|
1750
|
+
<RadioGroup
|
|
1751
|
+
variant="brand"
|
|
1752
|
+
styleType="pureCard"
|
|
1753
|
+
layout="vertical"
|
|
1754
|
+
value={selectedOptionValue}
|
|
1755
|
+
onChange={handleOptionChange}
|
|
1756
|
+
disabled={confirmed}
|
|
1757
|
+
className={HUMAN_CONFIRM_OPTION_GROUP}
|
|
1758
|
+
data-tfds-component="ChatMessage.OptionCardRadioGroup"
|
|
1759
|
+
>
|
|
1760
|
+
{node.options.map((option) => (
|
|
1761
|
+
<Radio
|
|
1762
|
+
key={String(option.value)}
|
|
1763
|
+
value={option.value}
|
|
1764
|
+
disabled={option.disabled}
|
|
1765
|
+
className={HUMAN_CONFIRM_OPTION_RADIO}
|
|
1766
|
+
>
|
|
1767
|
+
{option.label}
|
|
1768
|
+
</Radio>
|
|
1769
|
+
))}
|
|
1770
|
+
</RadioGroup>
|
|
1771
|
+
) : isFormCard ? (
|
|
1772
|
+
<Form
|
|
1773
|
+
items={node.formItems}
|
|
1774
|
+
layout="vertical"
|
|
1775
|
+
size="md"
|
|
1776
|
+
disabled={confirmed}
|
|
1777
|
+
className="w-full min-w-0 gap-3 pb-2"
|
|
1778
|
+
data-tfds-component="ChatMessage.FormCardForm"
|
|
1779
|
+
/>
|
|
1780
|
+
) : node.description ? (
|
|
1634
1781
|
<p className={HUMAN_CONFIRM_DESCRIPTION} style={tokenStyles.title}>
|
|
1635
1782
|
{node.description}
|
|
1636
1783
|
</p>
|
|
1637
1784
|
) : null}
|
|
1638
1785
|
</div>
|
|
1639
1786
|
) : null}
|
|
1640
|
-
<div className={HUMAN_CONFIRM_ACTIONS}>
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1787
|
+
<div className={isOptionCard || isFormCard ? HUMAN_CONFIRM_OPTION_ACTIONS : HUMAN_CONFIRM_ACTIONS}>
|
|
1788
|
+
{node.secondaryActionLabel ? (
|
|
1789
|
+
<Button
|
|
1790
|
+
variant="outline-black"
|
|
1791
|
+
size="md"
|
|
1792
|
+
onClick={handleSecondary}
|
|
1793
|
+
disabled={confirmed}
|
|
1794
|
+
data-tfds-component="ChatMessage.ConfirmAction"
|
|
1795
|
+
>
|
|
1796
|
+
{node.secondaryActionLabel}
|
|
1797
|
+
</Button>
|
|
1798
|
+
) : null}
|
|
1799
|
+
<Button
|
|
1800
|
+
variant="primary"
|
|
1801
|
+
size="md"
|
|
1653
1802
|
onClick={handlePrimary}
|
|
1654
|
-
disabled={confirmed}
|
|
1803
|
+
disabled={confirmed || (isOptionCard && node.options.length === 0)}
|
|
1655
1804
|
data-tfds-component="ChatMessage.ConfirmAction"
|
|
1656
1805
|
>
|
|
1657
1806
|
{node.primaryActionLabel}
|
|
1658
|
-
</
|
|
1807
|
+
</Button>
|
|
1659
1808
|
</div>
|
|
1660
1809
|
</div>
|
|
1661
1810
|
</div>
|
|
@@ -1918,13 +2067,13 @@ export default function ChatMessage({
|
|
|
1918
2067
|
useEffect(() => {
|
|
1919
2068
|
if (initialCardActioned) setCardActioned(true);
|
|
1920
2069
|
}, [initialCardActioned]);
|
|
1921
|
-
const wrapPrimaryAction = (originalFn) => () => {
|
|
1922
|
-
if (typeof originalFn === 'function') originalFn();
|
|
2070
|
+
const wrapPrimaryAction = (originalFn) => (...args) => {
|
|
2071
|
+
if (typeof originalFn === 'function') originalFn(...args);
|
|
1923
2072
|
setCardActioned(true);
|
|
1924
2073
|
};
|
|
1925
2074
|
/* 取消按钮也算作"已处理":消息变为历史消息后操作栏占位需要保留 */
|
|
1926
|
-
const wrapSecondaryAction = (originalFn) => () => {
|
|
1927
|
-
if (typeof originalFn === 'function') originalFn();
|
|
2075
|
+
const wrapSecondaryAction = (originalFn) => (...args) => {
|
|
2076
|
+
if (typeof originalFn === 'function') originalFn(...args);
|
|
1928
2077
|
setCardActioned(true);
|
|
1929
2078
|
};
|
|
1930
2079
|
|
|
@@ -67,6 +67,36 @@ export const CHAT_MESSAGE_TOKEN_MAP = {
|
|
|
67
67
|
{ label: '正文描边', cssProp: 'border-color', token: '--color-white', value: '#FFFFFF', state: 'body' },
|
|
68
68
|
{ label: '正文底色', cssProp: 'background', token: '--color-card-secondary', value: 'rgba(255,255,255,0.65)', state: 'body' },
|
|
69
69
|
],
|
|
70
|
+
澄清确认卡片1: [
|
|
71
|
+
{ label: '实现来源', cssProp: 'component', value: 'RadioGroup + Radio / styleType="pureCard" / variant="brand"', state: 'mode=option-card' },
|
|
72
|
+
{ label: '外框背景', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)' },
|
|
73
|
+
{ label: '外框圆角', cssProp: 'border-radius', value: '12px' },
|
|
74
|
+
{ label: '外框内边距', cssProp: 'padding', value: '12px 16px' },
|
|
75
|
+
{ label: '标题字号', cssProp: 'font-size', value: '14px' },
|
|
76
|
+
{ label: '标题行高', cssProp: 'line-height', value: '20px' },
|
|
77
|
+
{ label: '标题字重', cssProp: 'font-weight', token: '--font-semibold', value: '600(运行时使用 [font-weight:var(--font-semibold)])' },
|
|
78
|
+
{ label: '内容描边', cssProp: 'border-color', token: '--color-white', value: '#FFFFFF' },
|
|
79
|
+
{ label: '内容底色', cssProp: 'background', token: '--color-card-secondary', value: 'rgba(255,255,255,0.65)' },
|
|
80
|
+
{ label: '内容圆角', cssProp: 'border-radius', value: '12px' },
|
|
81
|
+
{ label: '内容内边距', cssProp: 'padding', value: '15px 19px' },
|
|
82
|
+
{ label: '选项间距', cssProp: 'gap', value: '8px', state: 'RadioGroup vertical' },
|
|
83
|
+
],
|
|
84
|
+
澄清确认卡片2: [
|
|
85
|
+
{ label: '实现来源', cssProp: 'component', value: 'Form + FormField / type="select" | "input" | ...', state: 'mode=form-card' },
|
|
86
|
+
{ label: '外框背景', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)' },
|
|
87
|
+
{ label: '外框圆角', cssProp: 'border-radius', value: '12px' },
|
|
88
|
+
{ label: '外框内边距', cssProp: 'padding', value: '12px 16px' },
|
|
89
|
+
{ label: '标题字号', cssProp: 'font-size', value: '14px' },
|
|
90
|
+
{ label: '标题行高', cssProp: 'line-height', value: '20px' },
|
|
91
|
+
{ label: '标题字重', cssProp: 'font-weight', token: '--font-semibold', value: '600(运行时使用 [font-weight:var(--font-semibold)])' },
|
|
92
|
+
{ label: '内容描边', cssProp: 'border-color', token: '--color-white', value: '#FFFFFF' },
|
|
93
|
+
{ label: '内容底色', cssProp: 'background', token: '--color-card-secondary', value: 'rgba(255,255,255,0.65)' },
|
|
94
|
+
{ label: '内容圆角', cssProp: 'border-radius', value: '12px' },
|
|
95
|
+
{ label: '内容内边距', cssProp: 'padding', value: '15px 19px' },
|
|
96
|
+
{ label: '表单布局', cssProp: 'layout', value: 'vertical', state: 'Form layout' },
|
|
97
|
+
{ label: '表单尺寸', cssProp: 'size', value: 'md', state: 'Form size' },
|
|
98
|
+
{ label: '字段间距', cssProp: 'gap', value: '12px', state: 'Form items vertical' },
|
|
99
|
+
],
|
|
70
100
|
确认按钮: [
|
|
71
101
|
{ label: '主背景', cssProp: 'background', token: '--color-neutral', value: '#343B3A', state: 'primary' },
|
|
72
102
|
{ label: '主文字', cssProp: 'color', token: '--color-white', value: '#FFFFFF', state: 'primary' },
|
|
@@ -6,6 +6,8 @@ import ChatMessage, {
|
|
|
6
6
|
DEFAULT_CHAT_ARTIFACT_GROUP,
|
|
7
7
|
DEFAULT_CHAT_CONFIRMS,
|
|
8
8
|
DEFAULT_CHAT_FOLLOW_UPS,
|
|
9
|
+
DEFAULT_CHAT_FORM_CONFIRM,
|
|
10
|
+
DEFAULT_CHAT_OPTION_CONFIRM,
|
|
9
11
|
DEFAULT_CHAT_PLAN,
|
|
10
12
|
DEFAULT_CHAT_RESULT,
|
|
11
13
|
DEFAULT_CHAT_RESULT_ARTIFACTS,
|
|
@@ -91,6 +93,18 @@ function getProps(subComponent, variant, options = {}) {
|
|
|
91
93
|
confirms: DEFAULT_CHAT_CONFIRMS.map(({ introText, ...rest }) => rest),
|
|
92
94
|
};
|
|
93
95
|
}
|
|
96
|
+
if (variant === 'option-card') {
|
|
97
|
+
return {
|
|
98
|
+
role: 'ai', title: '', steps: null,
|
|
99
|
+
confirms: DEFAULT_CHAT_OPTION_CONFIRM,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (variant === 'form-card') {
|
|
103
|
+
return {
|
|
104
|
+
role: 'ai', title: '', steps: null,
|
|
105
|
+
confirms: DEFAULT_CHAT_FORM_CONFIRM,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
94
108
|
return {
|
|
95
109
|
role: 'ai', title: '', steps: null,
|
|
96
110
|
plan: DEFAULT_CHAT_PLAN,
|
|
@@ -9,6 +9,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
9
9
|
import Icon from './Icon';
|
|
10
10
|
import Button from './Button';
|
|
11
11
|
import Tabs from './Tabs';
|
|
12
|
+
import Tooltip from './Tooltip';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_STATS = [
|
|
14
15
|
{ id: 'online-users', iconName: 'users-01-stroked', value: '12', label: '在线接待数' },
|
|
@@ -27,6 +28,9 @@ const DEFAULT_MODES = [
|
|
|
27
28
|
{ id: 'managed', label: '托管模式' },
|
|
28
29
|
];
|
|
29
30
|
|
|
31
|
+
const MAX_STATS = 4;
|
|
32
|
+
const MAX_TOOLS = 3;
|
|
33
|
+
|
|
30
34
|
// 覆盖深度必须大于 16px 圆角半径,避免主面板圆角切角处露出左板块右边界。
|
|
31
35
|
const PANEL_OVERLAP = 32;
|
|
32
36
|
|
|
@@ -79,6 +83,14 @@ function normalizeItems(items, fallback) {
|
|
|
79
83
|
return Array.isArray(items) && items.length > 0 ? items : fallback;
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
function getStatTooltip(item) {
|
|
87
|
+
return item.tooltip || item.description || item.label || item.value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getStatAriaLabel(item) {
|
|
91
|
+
return [item.label, item.value].filter(Boolean).join(' ');
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
function resolveMaxSideWidth(workspaceWidth, maxSideWidth, minSideWidth, mainMinWidth) {
|
|
83
95
|
const finiteMaxSideWidth = Number.isFinite(maxSideWidth) ? maxSideWidth : Number.POSITIVE_INFINITY;
|
|
84
96
|
if (workspaceWidth <= 0) {
|
|
@@ -116,8 +128,8 @@ export default function CustomerServiceWorkspaceFrame({
|
|
|
116
128
|
}) {
|
|
117
129
|
const workspaceRef = useRef(null);
|
|
118
130
|
const modeItems = useMemo(() => normalizeItems(modes, DEFAULT_MODES), [modes]);
|
|
119
|
-
const statItems = useMemo(() => normalizeItems(stats, DEFAULT_STATS), [stats]);
|
|
120
|
-
const toolItems = useMemo(() => normalizeItems(tools, DEFAULT_TOOLS), [tools]);
|
|
131
|
+
const statItems = useMemo(() => normalizeItems(stats, DEFAULT_STATS).slice(0, MAX_STATS), [stats]);
|
|
132
|
+
const toolItems = useMemo(() => normalizeItems(tools, DEFAULT_TOOLS).slice(0, MAX_TOOLS), [tools]);
|
|
121
133
|
const isControlled = typeof mode === 'string';
|
|
122
134
|
const [innerMode, setInnerMode] = useState(defaultMode);
|
|
123
135
|
const [workspaceWidth, setWorkspaceWidth] = useState(0);
|
|
@@ -235,10 +247,21 @@ export default function CustomerServiceWorkspaceFrame({
|
|
|
235
247
|
<div className={METRICS_SLOT}>
|
|
236
248
|
<div className={METRICS_CARD} data-tfds-component="CustomerServiceWorkspaceFrame.Metrics">
|
|
237
249
|
{statItems.map((item, index) => (
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
250
|
+
<Tooltip
|
|
251
|
+
key={item.id || `${item.iconName}-${index}`}
|
|
252
|
+
content={getStatTooltip(item)}
|
|
253
|
+
placement="top"
|
|
254
|
+
triggerClassName="inline-flex shrink-0"
|
|
255
|
+
>
|
|
256
|
+
<div
|
|
257
|
+
className={METRIC_ITEM}
|
|
258
|
+
title={item.label}
|
|
259
|
+
aria-label={getStatAriaLabel(item)}
|
|
260
|
+
>
|
|
261
|
+
<Icon name={item.iconName} size={14} className="text-foreground" aria-hidden="true" />
|
|
262
|
+
<p className={METRIC_TEXT}>{item.value}</p>
|
|
263
|
+
</div>
|
|
264
|
+
</Tooltip>
|
|
242
265
|
))}
|
|
243
266
|
{toolItems.length > 0 ? <span className={METRIC_DIVIDER} aria-hidden="true" /> : null}
|
|
244
267
|
{toolItems.length > 0 ? (
|
|
@@ -251,6 +274,7 @@ export default function CustomerServiceWorkspaceFrame({
|
|
|
251
274
|
size="sm"
|
|
252
275
|
icon={<Icon name={item.iconName} size={16} />}
|
|
253
276
|
iconOnly
|
|
277
|
+
tooltip={item.label}
|
|
254
278
|
aria-label={item.label}
|
|
255
279
|
title={item.label}
|
|
256
280
|
/>
|
|
@@ -24,6 +24,11 @@ export const CUSTOMER_SERVICE_WORKSPACE_FRAME_TOKEN_MAP = {
|
|
|
24
24
|
{ label: '在线点颜色', cssProp: 'background', token: '--color-green-500', value: '#3EB346' },
|
|
25
25
|
],
|
|
26
26
|
指标工具区: [
|
|
27
|
+
{ label: '数据展示上限', cssProp: 'count', value: '最多 4 类高优关注数据', state: 'stats' },
|
|
28
|
+
{ label: '数据展示形式', cssProp: 'structure', value: 'Icon + 数字值', state: 'stats' },
|
|
29
|
+
{ label: '数据说明', cssProp: 'component', value: 'Tooltip / hover + focus 展示文案解释', state: 'stats' },
|
|
30
|
+
{ label: '工具按钮上限', cssProp: 'count', value: '最多 3 个平台框架级按钮', state: 'tools' },
|
|
31
|
+
{ label: '工具按钮语义', cssProp: 'scope', value: '全局设置 / 平台公告 / 平台数据统计等', state: 'tools' },
|
|
27
32
|
{ label: '背景', cssProp: 'background', value: 'rgba(255,255,255,0.6)' },
|
|
28
33
|
{ label: '描边', cssProp: 'border-color', token: '--color-white', value: '#FFFFFF' },
|
|
29
34
|
{ label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px(Figma 6px,落到现有 md 档)' },
|