@tfdesign/b-end 1.0.11 → 1.0.12

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.
@@ -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
- /* TASK_PLAN_PRIMARY_BUTTON 已废弃,统一复用 HUMAN_CONFIRM_PRIMARY_BUTTON / HUMAN_CONFIRM_SECONDARY_BUTTON */
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
- <button
1186
- type="button"
1187
- className={HUMAN_CONFIRM_SECONDARY_BUTTON}
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
- </button>
1278
+ </Button>
1194
1279
  ) : null}
1195
1280
  {taskPlan.primaryActionLabel ? (
1196
- <button
1197
- type="button"
1198
- className={HUMAN_CONFIRM_PRIMARY_BUTTON}
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
- </button>
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(false);
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
- if (typeof node.onPrimaryAction === 'function') node.onPrimaryAction();
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={HUMAN_CONFIRM_HEADER}>
1586
- <div className={HUMAN_CONFIRM_ICON_WRAP} style={tokenStyles.iconWrap} aria-hidden="true">
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={HUMAN_CONFIRM_TITLE} style={tokenStyles.title}>
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 className={HUMAN_CONFIRM_CARD} style={tokenStyles.humanConfirmCard}>
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={[HUMAN_CONFIRM_BODY, confirmed ? 'opacity-60' : ''].filter(Boolean).join(' ')}
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
- {node.description ? (
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
- <button
1642
- type="button"
1643
- className={HUMAN_CONFIRM_SECONDARY_BUTTON}
1644
- onClick={handleSecondary}
1645
- disabled={confirmed}
1646
- data-tfds-component="ChatMessage.ConfirmAction"
1647
- >
1648
- {node.secondaryActionLabel}
1649
- </button>
1650
- <button
1651
- type="button"
1652
- className={HUMAN_CONFIRM_PRIMARY_BUTTON}
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
- </button>
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
- <div className={METRIC_ITEM} key={item.id || `${item.iconName}-${index}`} title={item.label}>
239
- <Icon name={item.iconName} size={14} className="text-foreground" aria-hidden="true" />
240
- <p className={METRIC_TEXT}>{item.value}</p>
241
- </div>
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 档)' },