@tfdesign/b-end 1.0.4
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/AI_READ_FIRST.md +131 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/package.json +67 -0
- package/scripts/check-tfds-contract.mjs +334 -0
- package/scripts/check-tfds-integration.mjs +263 -0
- package/scripts/postinstall-cursor-skill.mjs +382 -0
- package/scripts/setup.mjs +520 -0
- package/skills/tfds/CHECKLIST.md +205 -0
- package/skills/tfds/COMMON_FAILURES.md +238 -0
- package/skills/tfds/DESIGN_PRINCIPLES.md +477 -0
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +636 -0
- package/skills/tfds/LAYOUT_RECIPES.md +140 -0
- package/skills/tfds/LAYOUT_RULES.md +1355 -0
- package/skills/tfds/PAGE_ARCHETYPES.md +201 -0
- package/skills/tfds/SKILL.md +188 -0
- package/skills/tfds/components.index.json +7305 -0
- package/skills/tfds/components.summary.json +1809 -0
- package/src/_b_end_runtime/components/AiSuggestionShared.jsx +166 -0
- package/src/_b_end_runtime/components/Avatar.jsx +325 -0
- package/src/_b_end_runtime/components/Avatar.tokens.js +76 -0
- package/src/_b_end_runtime/components/AvatarGridPreview.jsx +56 -0
- package/src/_b_end_runtime/components/AvatarGroup.jsx +80 -0
- package/src/_b_end_runtime/components/AvatarGroup.tokens.js +28 -0
- package/src/_b_end_runtime/components/Button.jsx +144 -0
- package/src/_b_end_runtime/components/Button.tokens.js +90 -0
- package/src/_b_end_runtime/components/Card.jsx +460 -0
- package/src/_b_end_runtime/components/Card.tokens.js +124 -0
- package/src/_b_end_runtime/components/CardPreview.jsx +51 -0
- package/src/_b_end_runtime/components/ChatBubble.jsx +384 -0
- package/src/_b_end_runtime/components/ChatBubble.tokens.js +60 -0
- package/src/_b_end_runtime/components/ChatBubblePreview.jsx +129 -0
- package/src/_b_end_runtime/components/ChatInput.jsx +1399 -0
- package/src/_b_end_runtime/components/ChatInput.tokens.js +75 -0
- package/src/_b_end_runtime/components/ChatMessage.jsx +2215 -0
- package/src/_b_end_runtime/components/ChatMessage.tokens.js +257 -0
- package/src/_b_end_runtime/components/ChatMessagePreview.jsx +388 -0
- package/src/_b_end_runtime/components/Checkbox.jsx +317 -0
- package/src/_b_end_runtime/components/Checkbox.tokens.js +59 -0
- package/src/_b_end_runtime/components/ConversationList.jsx +1264 -0
- package/src/_b_end_runtime/components/ConversationList.tokens.js +135 -0
- package/src/_b_end_runtime/components/ConversationListPreview.jsx +108 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +324 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +69 -0
- package/src/_b_end_runtime/components/DatePicker.jsx +739 -0
- package/src/_b_end_runtime/components/DatePicker.tokens.js +99 -0
- package/src/_b_end_runtime/components/Empty.jsx +141 -0
- package/src/_b_end_runtime/components/Empty.tokens.js +40 -0
- package/src/_b_end_runtime/components/Form.jsx +609 -0
- package/src/_b_end_runtime/components/Form.tokens.js +77 -0
- package/src/_b_end_runtime/components/FormFieldStack.jsx +123 -0
- package/src/_b_end_runtime/components/FormFieldStack.tokens.js +12 -0
- package/src/_b_end_runtime/components/FormTitle.jsx +119 -0
- package/src/_b_end_runtime/components/FormTitle.tokens.js +87 -0
- package/src/_b_end_runtime/components/FullScreenPage.jsx +97 -0
- package/src/_b_end_runtime/components/FullScreenPage.tokens.js +19 -0
- package/src/_b_end_runtime/components/Icon.jsx +172 -0
- package/src/_b_end_runtime/components/Icon.tokens.js +26 -0
- package/src/_b_end_runtime/components/IconGridPreview.jsx +277 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +620 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +71 -0
- package/src/_b_end_runtime/components/InfoDisplayPanelPreview.jsx +133 -0
- package/src/_b_end_runtime/components/Input.jsx +258 -0
- package/src/_b_end_runtime/components/Input.tokens.js +68 -0
- package/src/_b_end_runtime/components/InputNumber.jsx +242 -0
- package/src/_b_end_runtime/components/InputNumber.tokens.js +55 -0
- package/src/_b_end_runtime/components/Modal.jsx +155 -0
- package/src/_b_end_runtime/components/Modal.tokens.js +73 -0
- package/src/_b_end_runtime/components/NavBar.jsx +842 -0
- package/src/_b_end_runtime/components/NavBar.tokens.js +97 -0
- package/src/_b_end_runtime/components/NavBarPreview.jsx +11 -0
- package/src/_b_end_runtime/components/Radio.jsx +227 -0
- package/src/_b_end_runtime/components/Radio.tokens.js +59 -0
- package/src/_b_end_runtime/components/Select.jsx +766 -0
- package/src/_b_end_runtime/components/Select.tokens.js +99 -0
- package/src/_b_end_runtime/components/Sheet.jsx +132 -0
- package/src/_b_end_runtime/components/Sheet.tokens.js +61 -0
- package/src/_b_end_runtime/components/Slider.jsx +346 -0
- package/src/_b_end_runtime/components/Slider.tokens.js +47 -0
- package/src/_b_end_runtime/components/Switch.jsx +124 -0
- package/src/_b_end_runtime/components/Switch.tokens.js +38 -0
- package/src/_b_end_runtime/components/Table.jsx +1338 -0
- package/src/_b_end_runtime/components/Table.tokens.js +147 -0
- package/src/_b_end_runtime/components/TablePreview.jsx +599 -0
- package/src/_b_end_runtime/components/Tabs.jsx +149 -0
- package/src/_b_end_runtime/components/Tabs.tokens.js +102 -0
- package/src/_b_end_runtime/components/Tag.jsx +199 -0
- package/src/_b_end_runtime/components/Tag.tokens.js +171 -0
- package/src/_b_end_runtime/components/TagBar.jsx +1134 -0
- package/src/_b_end_runtime/components/TagBar.tokens.js +75 -0
- package/src/_b_end_runtime/components/TagGridPreview.jsx +23 -0
- package/src/_b_end_runtime/components/TagInput.jsx +382 -0
- package/src/_b_end_runtime/components/TagInput.tokens.js +52 -0
- package/src/_b_end_runtime/components/TextArea.jsx +363 -0
- package/src/_b_end_runtime/components/TextArea.tokens.js +65 -0
- package/src/_b_end_runtime/components/TimePicker.jsx +444 -0
- package/src/_b_end_runtime/components/TimePicker.tokens.js +77 -0
- package/src/_b_end_runtime/components/Toast.jsx +120 -0
- package/src/_b_end_runtime/components/Toast.tokens.js +146 -0
- package/src/_b_end_runtime/components/Tooltip.jsx +282 -0
- package/src/_b_end_runtime/components/Tooltip.tokens.js +48 -0
- package/src/_b_end_runtime/components/TooltipPreview.jsx +50 -0
- package/src/_b_end_runtime/components/Upload.jsx +455 -0
- package/src/_b_end_runtime/components/Upload.tokens.js +47 -0
- package/src/_b_end_runtime/components/avatar-assets/avatar-default.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-1.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-2.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-3.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-4.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-5.png +0 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-1.svg +40 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-2.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/construction.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/failure.svg +49 -0
- package/src/_b_end_runtime/components/empty-assets/idle.svg +34 -0
- package/src/_b_end_runtime/components/empty-assets/no-access.svg +36 -0
- package/src/_b_end_runtime/components/empty-assets/no-content.svg +77 -0
- package/src/_b_end_runtime/components/empty-assets/no-result.svg +61 -0
- package/src/_b_end_runtime/components/empty-assets/not-found.svg +46 -0
- package/src/_b_end_runtime/components/empty-assets/success.svg +38 -0
- package/src/_b_end_runtime/components/file-type-assets/batch-report.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/catcat.svg +21 -0
- package/src/_b_end_runtime/components/file-type-assets/code.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/conversation.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/document.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-card.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-sheet.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/image.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/index.js +105 -0
- package/src/_b_end_runtime/components/file-type-assets/knowledge.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pdf.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pe.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/strategy.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/table.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/webpage.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/xmind.png +0 -0
- package/src/_b_end_runtime/components/icons/icon-data.js +12496 -0
- package/src/_b_end_runtime/components/nav-bar-assets/bytehi-logo-mark.svg +21 -0
- package/src/_b_end_runtime/components/table-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-assets/icon-chevron-down.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/checkbox.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon-chevron-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-handle.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-tree-triangle-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/switch.png +0 -0
- package/src/_b_end_runtime/components/tagShared.js +3 -0
- package/src/_b_end_runtime/components/team-avatar-assets/chengcheng-murphy.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/duan-ran.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/guo-zhezhi.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/li-siru.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/liu-delin.png +0 -0
- package/src/_b_end_runtime/components.js +3499 -0
- package/src/_b_end_runtime/index.js +9 -0
- package/src/_b_end_runtime/page-patterns/BasePageFramePattern.jsx +395 -0
- package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +989 -0
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +281 -0
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +380 -0
- package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +392 -0
- package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +590 -0
- package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +237 -0
- package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +189 -0
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +594 -0
- package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +87 -0
- package/src/_b_end_runtime/page-patterns/pageListShared.jsx +177 -0
- package/src/_b_end_runtime/patterns.js +428 -0
- package/src/_b_end_runtime/preview-registry.jsx +4719 -0
- package/src/_b_end_runtime/teamMembers.js +56 -0
- package/src/_b_end_runtime/tokens.js +500 -0
- package/src/index.d.ts +1073 -0
- package/src/index.js +52 -0
- package/theme.css +350 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import Avatar from '../components/Avatar';
|
|
4
|
+
import Button from '../components/Button';
|
|
5
|
+
import ChatBubble from '../components/ChatBubble';
|
|
6
|
+
import ChatInput from '../components/ChatInput';
|
|
7
|
+
import Icon from '../components/Icon';
|
|
8
|
+
import Tag from '../components/Tag';
|
|
9
|
+
import Tooltip from '../components/Tooltip';
|
|
10
|
+
import { getTeamMemberByName } from '../teamMembers';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* IMConversationPattern — IM 对话片段(可交互版)
|
|
14
|
+
*
|
|
15
|
+
* 一个白卡里完整呈现"用户进线 → 机器人接待 → 转人工 → 客服跟进"的一组对话,
|
|
16
|
+
* 用于演示 IM / 即时通讯场景里三种角色的 ChatBubble 组合用法。顶部默认展示当前线程信息元素,
|
|
17
|
+
* 底部默认搭配组合输入框;输入框样式按场景判断:常规 IM 对话默认用 im-basic,若是 AI 协同型会话可切到 AI ChatInput。
|
|
18
|
+
*
|
|
19
|
+
* 角色 → 气泡映射(铁律):
|
|
20
|
+
* 用户 → ChatBubble variant="incoming" avatarType="image" incomingTone="grey"
|
|
21
|
+
* 机器人 → ChatBubble variant="outgoing" avatarType="ai" outgoingTone="ai"
|
|
22
|
+
* 人工 → ChatBubble variant="outgoing" avatarType="image" outgoingTone="default" receipt="read"
|
|
23
|
+
* 系统提示(接入 / 转人工)→ 居中小字,不是气泡
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const SCRIPT_DATE = '2026-04-16';
|
|
27
|
+
function ts(hhmm, ss = '00') {
|
|
28
|
+
return `${SCRIPT_DATE} ${hhmm}:${ss}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatCurrentTimestamp() {
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
34
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
35
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
36
|
+
return `${SCRIPT_DATE} ${hh}:${mm}:${ss}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SCRIPT = [
|
|
40
|
+
{ id: 's1', kind: 'system', text: '已接入AI智能客服', time: ts('14:01', '00') },
|
|
41
|
+
|
|
42
|
+
{ id: 'u1', kind: 'user', text: '我买了海底捞西单店的 2 人餐,到店以后店员说券码暂时核销不了,能帮我看下吗', time: ts('14:02', '12') },
|
|
43
|
+
{ id: 'b1', kind: 'bot', text: '您好,我是抖音生活服务助手,正在为您查询团购券状态…', time: ts('14:02', '25') },
|
|
44
|
+
{ id: 'b2', kind: 'bot', text: '请问可以提供下券码或订单号吗?支持直接复制粘贴。', time: ts('14:02', '38') },
|
|
45
|
+
{ id: 'b3', kind: 'bot', text: '如果不方便复制,也可以发券码截图,我会一起核对适用门店、使用时段和套餐权益。', time: ts('14:02', '52') },
|
|
46
|
+
|
|
47
|
+
// 用户连续两条:订单号 + 券码尾号
|
|
48
|
+
{ id: 'u2', kind: 'user', text: 'LS20260423-HDL998877', time: ts('14:03', '04') },
|
|
49
|
+
{ id: 'u3', kind: 'user', text: '券码尾号 8877', time: ts('14:03', '16') },
|
|
50
|
+
|
|
51
|
+
// AI连续两条:查券结果 + 规则解释
|
|
52
|
+
{ id: 'b4', kind: 'bot', text: '已查到您的订单 LS20260423-HDL998877,套餐为“海底捞火锅 2 人餐”,适用门店包含西单大悦城店。', time: ts('14:03', '28') },
|
|
53
|
+
{ id: 'b5', kind: 'bot', text: '当前券码状态显示“待核销”,使用时段仍有效。我先为您核对门店收银端同步状态。', time: ts('14:03', '44') },
|
|
54
|
+
|
|
55
|
+
// 到店核销异常(AI多轮)
|
|
56
|
+
{ id: 'u4', kind: 'user', text: '我现在已经在店里等位了,如果一直不能核销是不是只能退款?', time: ts('14:04', '03') },
|
|
57
|
+
{ id: 'b6', kind: 'bot', text: '先别着急,我会优先帮您确认门店核销链路,避免影响您到店用餐。', time: ts('14:04', '18') },
|
|
58
|
+
{ id: 'b7', kind: 'bot', text: '如果确认为门店端同步延迟,可由商家手动核验订单;如无法履约,再为您进入退款或补偿流程。', time: ts('14:04', '36') },
|
|
59
|
+
|
|
60
|
+
// 补偿诉求(用户单条 + AI单条)
|
|
61
|
+
{ id: 'u5', kind: 'user', text: '我朋友都到了,等太久体验很差,能不能给个补偿?', time: ts('14:04', '54') },
|
|
62
|
+
{ id: 'b8', kind: 'bot', text: '理解您的感受。若确认是核销异常导致等待,我们会协助门店给出等位补偿或套餐权益补救方案。', time: ts('14:05', '08') },
|
|
63
|
+
|
|
64
|
+
{ id: 'u6', kind: 'user', text: '那你先帮我联系门店吧,不行就转人工处理', time: ts('14:05', '26') },
|
|
65
|
+
{ id: 'b9', kind: 'bot', text: '好的,我已为您发起门店核销协同,预计 5 分钟内反馈。', time: ts('14:05', '42') },
|
|
66
|
+
{ id: 'b10', kind: 'bot', text: '如门店端仍无法确认,我会继续为您转接人工客服处理退款和补偿方案。', time: ts('14:05', '58') },
|
|
67
|
+
|
|
68
|
+
{ id: 'u7', kind: 'user', text: '转人工', time: ts('14:06', '12') },
|
|
69
|
+
{ id: 'b11', kind: 'bot', text: '好的,正在为您转接人工客服,请稍候…', time: ts('14:06', '20') },
|
|
70
|
+
|
|
71
|
+
{ id: 's2', kind: 'system', text: '已接入人工客服', time: ts('14:06', '32') },
|
|
72
|
+
|
|
73
|
+
// 人工连续两条:说明 + 方案
|
|
74
|
+
{ id: 'a1', kind: 'agent', text: '您好,我是人工客服小语,已看到您的海底捞团购订单。门店反馈当前收银端券码同步延迟,正在手动核验。', time: ts('14:06', '48') },
|
|
75
|
+
{ id: 'a2', kind: 'agent', text: '我已备注您正在到店等位,并同步门店优先处理;若核销仍失败,会为您申请等位补偿或原路退款。', time: ts('14:07', '02') },
|
|
76
|
+
|
|
77
|
+
{ id: 'u8', kind: 'user', text: '好的,麻烦快一点,我这边还在等叫号', time: ts('14:07', '18') },
|
|
78
|
+
{ id: 'a3', kind: 'agent', text: '没问题~我会持续跟进西单店反馈。若 5 分钟内未解决,我会直接帮您升级商家值班人员处理。', time: ts('14:07', '36') },
|
|
79
|
+
{ id: 'u9', kind: 'user', text: '明白了,谢谢', time: ts('14:08', '04') },
|
|
80
|
+
{ id: 'a4', kind: 'agent', text: '不客气,也感谢您的耐心等待,祝您用餐愉快~', time: ts('14:08', '12') },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const LEFT_USER_AVATAR = getTeamMemberByName('程程murphy');
|
|
84
|
+
const RIGHT_AGENT_AVATAR = getTeamMemberByName('段然');
|
|
85
|
+
const DEFAULT_THREAD = {
|
|
86
|
+
id: 'default-thread',
|
|
87
|
+
title: '用户不认可/不理解处罚',
|
|
88
|
+
tagLabel: '十万',
|
|
89
|
+
userId: '218401319',
|
|
90
|
+
channel: '抖音',
|
|
91
|
+
duration: '00:18:57',
|
|
92
|
+
userName: LEFT_USER_AVATAR?.name || '用户',
|
|
93
|
+
userAvatarSrc: LEFT_USER_AVATAR?.avatarSrc,
|
|
94
|
+
agentName: RIGHT_AGENT_AVATAR?.name || '客服',
|
|
95
|
+
agentAvatarSrc: RIGHT_AGENT_AVATAR?.avatarSrc,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const THREAD_ACTIONS = [
|
|
99
|
+
{ id: 'participants', label: '查看参与人', iconName: 'users-01-stroked' },
|
|
100
|
+
{ id: 'minimize', label: '收起会话', iconName: 'minus-circle-stroked' },
|
|
101
|
+
{ id: 'forward', label: '转发会话', iconName: 'share-06-stroked' },
|
|
102
|
+
{ id: 'resolve', label: '完成处理', iconName: 'check-circle-stroked' },
|
|
103
|
+
];
|
|
104
|
+
const THREAD_ACTION_BUTTON_SIZE = 36;
|
|
105
|
+
const THREAD_ACTION_GAP = 8;
|
|
106
|
+
const THREAD_ACTION_FULL_WIDTH =
|
|
107
|
+
THREAD_ACTIONS.length * THREAD_ACTION_BUTTON_SIZE
|
|
108
|
+
+ (THREAD_ACTIONS.length - 1) * THREAD_ACTION_GAP;
|
|
109
|
+
|
|
110
|
+
function getAttachmentSummary(attachments = []) {
|
|
111
|
+
if (!attachments.length) return '';
|
|
112
|
+
return attachments
|
|
113
|
+
.map((file) => file?.name || '图片')
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map((name) => `[图片] ${name}`)
|
|
116
|
+
.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function groupConversation(items) {
|
|
120
|
+
return items.reduce((groups, item) => {
|
|
121
|
+
if (item.kind === 'system') {
|
|
122
|
+
groups.push(item);
|
|
123
|
+
return groups;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const last = groups[groups.length - 1];
|
|
127
|
+
if (last && last.kind === item.kind && Array.isArray(last.messages)) {
|
|
128
|
+
last.messages.push(item.text);
|
|
129
|
+
last.timestamps.push(item.time);
|
|
130
|
+
last.ids.push(item.id);
|
|
131
|
+
return groups;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
groups.push({
|
|
135
|
+
id: item.id,
|
|
136
|
+
kind: item.kind,
|
|
137
|
+
messages: [item.text],
|
|
138
|
+
timestamps: [item.time],
|
|
139
|
+
ids: [item.id],
|
|
140
|
+
});
|
|
141
|
+
return groups;
|
|
142
|
+
}, []);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {{
|
|
147
|
+
* asCard?: boolean,
|
|
148
|
+
* maxWidth?: number | string | null,
|
|
149
|
+
* showComposer?: boolean,
|
|
150
|
+
* composerVariant?: 'auto' | 'im-basic' | 'default',
|
|
151
|
+
* composerPlaceholder?: string,
|
|
152
|
+
* onSend?: (text: string, ctx?: unknown) => void,
|
|
153
|
+
* showThreadHeader?: boolean,
|
|
154
|
+
* thread?: {
|
|
155
|
+
* id?: string,
|
|
156
|
+
* title?: string,
|
|
157
|
+
* tagLabel?: string,
|
|
158
|
+
* userId?: string,
|
|
159
|
+
* channel?: string,
|
|
160
|
+
* duration?: string,
|
|
161
|
+
* userName?: string,
|
|
162
|
+
* userAvatarSrc?: string,
|
|
163
|
+
* agentName?: string,
|
|
164
|
+
* agentAvatarSrc?: string,
|
|
165
|
+
* },
|
|
166
|
+
* messages?: Array<{ id:string, kind:'system'|'user'|'bot'|'agent', text:string, time:string }>,
|
|
167
|
+
* className?: string,
|
|
168
|
+
* style?: import('react').CSSProperties,
|
|
169
|
+
* }} props
|
|
170
|
+
*/
|
|
171
|
+
export default function IMConversationPattern({
|
|
172
|
+
asCard = false,
|
|
173
|
+
maxWidth = null,
|
|
174
|
+
showComposer = true,
|
|
175
|
+
composerVariant = 'auto',
|
|
176
|
+
composerPlaceholder,
|
|
177
|
+
onSend = () => {},
|
|
178
|
+
showThreadHeader = true,
|
|
179
|
+
thread,
|
|
180
|
+
messages,
|
|
181
|
+
className = '',
|
|
182
|
+
style,
|
|
183
|
+
} = {}) {
|
|
184
|
+
const [runtimeMessages, setRuntimeMessages] = useState([]);
|
|
185
|
+
const scrollRef = useRef(null);
|
|
186
|
+
const resolvedThread = useMemo(
|
|
187
|
+
() => ({
|
|
188
|
+
...DEFAULT_THREAD,
|
|
189
|
+
...(thread || {}),
|
|
190
|
+
}),
|
|
191
|
+
[thread],
|
|
192
|
+
);
|
|
193
|
+
const baseMessages = Array.isArray(messages) && messages.length > 0 ? messages : SCRIPT;
|
|
194
|
+
const groupedItems = useMemo(
|
|
195
|
+
() => groupConversation([...baseMessages, ...runtimeMessages]),
|
|
196
|
+
[baseMessages, runtimeMessages],
|
|
197
|
+
);
|
|
198
|
+
const resolvedComposerVariant =
|
|
199
|
+
composerVariant === 'auto'
|
|
200
|
+
? 'im-basic'
|
|
201
|
+
: composerVariant === 'default'
|
|
202
|
+
? 'default'
|
|
203
|
+
: 'im-basic';
|
|
204
|
+
const resolvedComposerPlaceholder =
|
|
205
|
+
composerPlaceholder
|
|
206
|
+
|| (resolvedComposerVariant === 'default' ? '继续协同处理当前会话…' : '请输入消息…');
|
|
207
|
+
|
|
208
|
+
const cardStyle = asCard
|
|
209
|
+
? {
|
|
210
|
+
background: 'var(--color-white, #FFFFFF)',
|
|
211
|
+
borderRadius: 'inherit',
|
|
212
|
+
border: '1px solid var(--color-border-default, #E4E7EC)',
|
|
213
|
+
boxSizing: 'border-box',
|
|
214
|
+
}
|
|
215
|
+
: {};
|
|
216
|
+
|
|
217
|
+
const maxWidthStyle =
|
|
218
|
+
maxWidth == null
|
|
219
|
+
? {}
|
|
220
|
+
: {
|
|
221
|
+
maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : String(maxWidth),
|
|
222
|
+
marginLeft: 'auto',
|
|
223
|
+
marginRight: 'auto',
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
setRuntimeMessages([]);
|
|
228
|
+
}, [resolvedThread.id]);
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const el = scrollRef.current;
|
|
232
|
+
if (!el) return undefined;
|
|
233
|
+
const frame = window.requestAnimationFrame(() => {
|
|
234
|
+
el.scrollTo({
|
|
235
|
+
top: el.scrollHeight,
|
|
236
|
+
behavior: runtimeMessages.length > 0 ? 'smooth' : 'auto',
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
return () => window.cancelAnimationFrame(frame);
|
|
240
|
+
}, [baseMessages, runtimeMessages.length]);
|
|
241
|
+
|
|
242
|
+
const handleSend = useCallback((text, ctx) => {
|
|
243
|
+
const content = (text || '').trim() || getAttachmentSummary(ctx?.attachments);
|
|
244
|
+
if (!content) return;
|
|
245
|
+
|
|
246
|
+
setRuntimeMessages((prev) => [
|
|
247
|
+
...prev,
|
|
248
|
+
{
|
|
249
|
+
id: `runtime-agent-${Date.now()}-${prev.length}`,
|
|
250
|
+
kind: 'agent',
|
|
251
|
+
text: content,
|
|
252
|
+
time: formatCurrentTimestamp(),
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
onSend?.(content, ctx);
|
|
256
|
+
}, [onSend]);
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div
|
|
260
|
+
className={['flex w-full min-w-0 flex-col overflow-hidden', className].filter(Boolean).join(' ')}
|
|
261
|
+
style={{
|
|
262
|
+
...maxWidthStyle,
|
|
263
|
+
...cardStyle,
|
|
264
|
+
boxSizing: 'border-box',
|
|
265
|
+
...style,
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
{showThreadHeader && <IMThreadHeader thread={resolvedThread} />}
|
|
269
|
+
|
|
270
|
+
<div
|
|
271
|
+
ref={scrollRef}
|
|
272
|
+
className="flex min-h-0 flex-1 min-w-0 flex-col overflow-y-auto overflow-x-hidden"
|
|
273
|
+
style={{ paddingBottom: showComposer ? '16px' : undefined }}
|
|
274
|
+
>
|
|
275
|
+
<div className="flex min-w-0 flex-col gap-5">
|
|
276
|
+
{groupedItems.map((item) => renderMessage(item, resolvedThread))}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
{showComposer && (
|
|
281
|
+
<div className="w-full shrink-0">
|
|
282
|
+
<ChatInput
|
|
283
|
+
variant={resolvedComposerVariant}
|
|
284
|
+
autoReplyOnSend={false}
|
|
285
|
+
placeholder={resolvedComposerPlaceholder}
|
|
286
|
+
acceptFiles={resolvedComposerVariant === 'im-basic' ? 'image/*' : '*'}
|
|
287
|
+
onSend={handleSend}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function IMThreadHeader({ thread }) {
|
|
296
|
+
const headerRef = useRef(null);
|
|
297
|
+
const infoRef = useRef(null);
|
|
298
|
+
const actionBarRef = useRef(null);
|
|
299
|
+
const moreButtonRef = useRef(null);
|
|
300
|
+
const [visibleActionCount, setVisibleActionCount] = useState(THREAD_ACTIONS.length);
|
|
301
|
+
const [actionBarWidth, setActionBarWidth] = useState(THREAD_ACTION_FULL_WIDTH);
|
|
302
|
+
const [overflowOpen, setOverflowOpen] = useState(false);
|
|
303
|
+
const [overflowPosition, setOverflowPosition] = useState({ top: 0, right: 0 });
|
|
304
|
+
const hiddenActions = THREAD_ACTIONS.slice(visibleActionCount);
|
|
305
|
+
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
const headerEl = headerRef.current;
|
|
308
|
+
const infoEl = infoRef.current;
|
|
309
|
+
if (!headerEl || !infoEl) return undefined;
|
|
310
|
+
|
|
311
|
+
const updateVisibleActions = () => {
|
|
312
|
+
const headerWidth = headerEl.getBoundingClientRect().width;
|
|
313
|
+
const infoWidth = Math.ceil(Math.max(
|
|
314
|
+
infoEl.getBoundingClientRect().width,
|
|
315
|
+
infoEl.scrollWidth,
|
|
316
|
+
));
|
|
317
|
+
const availableWidth = Math.max(
|
|
318
|
+
THREAD_ACTION_BUTTON_SIZE,
|
|
319
|
+
headerWidth - infoWidth - 16,
|
|
320
|
+
);
|
|
321
|
+
const nextActionBarWidth = Math.min(availableWidth, THREAD_ACTION_FULL_WIDTH);
|
|
322
|
+
setActionBarWidth(nextActionBarWidth);
|
|
323
|
+
|
|
324
|
+
if (nextActionBarWidth >= THREAD_ACTION_FULL_WIDTH) {
|
|
325
|
+
setVisibleActionCount(THREAD_ACTIONS.length);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let nextVisibleCount = 0;
|
|
330
|
+
for (let count = THREAD_ACTIONS.length - 1; count >= 0; count -= 1) {
|
|
331
|
+
const itemCount = count + 1; // visible actions + more button
|
|
332
|
+
const neededWidth =
|
|
333
|
+
itemCount * THREAD_ACTION_BUTTON_SIZE
|
|
334
|
+
+ Math.max(0, itemCount - 1) * THREAD_ACTION_GAP;
|
|
335
|
+
if (nextActionBarWidth >= neededWidth) {
|
|
336
|
+
nextVisibleCount = count;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
setVisibleActionCount(nextVisibleCount);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
updateVisibleActions();
|
|
344
|
+
const observer = new ResizeObserver(updateVisibleActions);
|
|
345
|
+
observer.observe(headerEl);
|
|
346
|
+
observer.observe(infoEl);
|
|
347
|
+
return () => observer.disconnect();
|
|
348
|
+
}, []);
|
|
349
|
+
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
if (visibleActionCount >= THREAD_ACTIONS.length) {
|
|
352
|
+
setOverflowOpen(false);
|
|
353
|
+
}
|
|
354
|
+
}, [visibleActionCount]);
|
|
355
|
+
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
if (!overflowOpen) return undefined;
|
|
358
|
+
|
|
359
|
+
const updatePosition = () => {
|
|
360
|
+
const rect = moreButtonRef.current?.getBoundingClientRect();
|
|
361
|
+
if (!rect) return;
|
|
362
|
+
setOverflowPosition({
|
|
363
|
+
top: rect.bottom + THREAD_ACTION_GAP,
|
|
364
|
+
right: Math.max(8, window.innerWidth - rect.right),
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
const handlePointerDown = (event) => {
|
|
368
|
+
if (moreButtonRef.current?.contains(event.target)) return;
|
|
369
|
+
if (event.target?.closest?.('[data-tfds-im-action-overflow-menu="true"]')) return;
|
|
370
|
+
setOverflowOpen(false);
|
|
371
|
+
};
|
|
372
|
+
const handleKeyDown = (event) => {
|
|
373
|
+
if (event.key === 'Escape') setOverflowOpen(false);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
updatePosition();
|
|
377
|
+
window.addEventListener('resize', updatePosition);
|
|
378
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
379
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
380
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
381
|
+
return () => {
|
|
382
|
+
window.removeEventListener('resize', updatePosition);
|
|
383
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
384
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
385
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
386
|
+
};
|
|
387
|
+
}, [overflowOpen]);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<div
|
|
391
|
+
ref={headerRef}
|
|
392
|
+
className="flex min-h-[64px] w-full shrink-0 items-center justify-between gap-4 py-3"
|
|
393
|
+
data-tfds-im-thread-header="true"
|
|
394
|
+
>
|
|
395
|
+
<div ref={infoRef} className="flex min-w-0 items-center gap-3">
|
|
396
|
+
<Avatar
|
|
397
|
+
size="s"
|
|
398
|
+
type="image"
|
|
399
|
+
src={thread.userAvatarSrc}
|
|
400
|
+
alt={`${thread.userName || '用户'}头像`}
|
|
401
|
+
/>
|
|
402
|
+
|
|
403
|
+
<div className="flex min-w-0 flex-col justify-center gap-1.5">
|
|
404
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
405
|
+
<h2 className="m-0 truncate text-sm leading-5 text-foreground [font-weight:var(--font-semibold)]">
|
|
406
|
+
{thread.title}
|
|
407
|
+
</h2>
|
|
408
|
+
{thread.tagLabel ? (
|
|
409
|
+
<Tag variant="white" size="m" radius="md" fontWeight="bold">
|
|
410
|
+
{thread.tagLabel}
|
|
411
|
+
</Tag>
|
|
412
|
+
) : null}
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<div className="flex min-w-0 items-center gap-2 text-xs leading-4 text-foreground-muted">
|
|
416
|
+
<MetaItem iconName="user-01-stroked" label={thread.userId} tooltip="会话ID" />
|
|
417
|
+
<MetaDivider />
|
|
418
|
+
<MetaItem iconName="phone-01-stroked" label={thread.channel} tooltip="沟通渠道" />
|
|
419
|
+
<MetaDivider />
|
|
420
|
+
<MetaItem iconName="clock-stroked" label={thread.duration} tooltip="沟通时长" />
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div
|
|
426
|
+
ref={actionBarRef}
|
|
427
|
+
className="relative flex min-w-9 shrink-0 items-center justify-end gap-2 overflow-visible"
|
|
428
|
+
style={{ width: `${actionBarWidth}px` }}
|
|
429
|
+
>
|
|
430
|
+
{THREAD_ACTIONS.slice(0, visibleActionCount).map((action) => (
|
|
431
|
+
<ThreadActionButton key={action.id} action={action} />
|
|
432
|
+
))}
|
|
433
|
+
{hiddenActions.length > 0 && (
|
|
434
|
+
<ThreadActionButton
|
|
435
|
+
ref={moreButtonRef}
|
|
436
|
+
action={{ id: 'more', label: '更多操作', iconName: 'dots-horizontal-stroked' }}
|
|
437
|
+
ariaExpanded={overflowOpen}
|
|
438
|
+
onClick={() => setOverflowOpen((open) => !open)}
|
|
439
|
+
/>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
{overflowOpen && hiddenActions.length > 0 && (
|
|
444
|
+
<ThreadActionOverflowMenu
|
|
445
|
+
actions={hiddenActions}
|
|
446
|
+
position={overflowPosition}
|
|
447
|
+
onClose={() => setOverflowOpen(false)}
|
|
448
|
+
/>
|
|
449
|
+
)}
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function ThreadActionButton({ action, ariaExpanded, onClick, ref }) {
|
|
455
|
+
return (
|
|
456
|
+
<Tooltip content={action.label} placement="top" triggerClassName="inline-flex shrink-0">
|
|
457
|
+
<Button
|
|
458
|
+
ref={ref}
|
|
459
|
+
type="button"
|
|
460
|
+
variant="ghost-black"
|
|
461
|
+
size="md"
|
|
462
|
+
iconOnly
|
|
463
|
+
aria-label={action.label}
|
|
464
|
+
aria-expanded={ariaExpanded}
|
|
465
|
+
onClick={onClick}
|
|
466
|
+
icon={<Icon name={action.iconName} size="sm" aria-hidden="true" />}
|
|
467
|
+
/>
|
|
468
|
+
</Tooltip>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function ThreadActionOverflowMenu({ actions, position, onClose }) {
|
|
473
|
+
if (typeof document === 'undefined') return null;
|
|
474
|
+
|
|
475
|
+
return createPortal(
|
|
476
|
+
<div
|
|
477
|
+
data-tfds-im-action-overflow-menu="true"
|
|
478
|
+
className="fixed z-50 flex min-w-[148px] flex-col gap-1 rounded-lg border border-border-default bg-surface p-1 shadow-lg"
|
|
479
|
+
style={{ top: `${position.top}px`, right: `${position.right}px` }}
|
|
480
|
+
>
|
|
481
|
+
{actions.map((action) => (
|
|
482
|
+
<Tooltip
|
|
483
|
+
key={action.id}
|
|
484
|
+
content={action.label}
|
|
485
|
+
placement="left"
|
|
486
|
+
triggerClassName="inline-flex w-full"
|
|
487
|
+
>
|
|
488
|
+
<Button
|
|
489
|
+
type="button"
|
|
490
|
+
variant="ghost-black"
|
|
491
|
+
size="md"
|
|
492
|
+
className="!w-full !justify-start !px-2"
|
|
493
|
+
icon={<Icon name={action.iconName} size="sm" aria-hidden="true" />}
|
|
494
|
+
onClick={onClose}
|
|
495
|
+
>
|
|
496
|
+
{action.label}
|
|
497
|
+
</Button>
|
|
498
|
+
</Tooltip>
|
|
499
|
+
))}
|
|
500
|
+
</div>,
|
|
501
|
+
document.body,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function MetaItem({ iconName, label, tooltip }) {
|
|
506
|
+
return (
|
|
507
|
+
<Tooltip content={tooltip} placement="top" triggerClassName="inline-flex min-w-0 shrink-0">
|
|
508
|
+
<span className="inline-flex min-w-0 shrink-0 items-center justify-center gap-1">
|
|
509
|
+
<Icon name={iconName} size="xs" aria-hidden="true" />
|
|
510
|
+
<span className="truncate">{label}</span>
|
|
511
|
+
</span>
|
|
512
|
+
</Tooltip>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function MetaDivider() {
|
|
517
|
+
return (
|
|
518
|
+
<span
|
|
519
|
+
aria-hidden="true"
|
|
520
|
+
className="h-2.5 w-px shrink-0 bg-border-default"
|
|
521
|
+
/>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function renderMessage(m, thread) {
|
|
526
|
+
if (m.kind === 'system') {
|
|
527
|
+
return <SystemNote key={m.id} text={m.text} time={m.time} />;
|
|
528
|
+
}
|
|
529
|
+
if (m.kind === 'user') {
|
|
530
|
+
return (
|
|
531
|
+
<ChatBubble
|
|
532
|
+
key={m.id}
|
|
533
|
+
variant="incoming"
|
|
534
|
+
avatarType="image"
|
|
535
|
+
avatarSrc={thread.userAvatarSrc}
|
|
536
|
+
avatarAlt={`${thread.userName || '用户'}头像`}
|
|
537
|
+
incomingTone="grey"
|
|
538
|
+
messages={m.messages}
|
|
539
|
+
timestamps={m.timestamps}
|
|
540
|
+
/>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
if (m.kind === 'bot') {
|
|
544
|
+
return (
|
|
545
|
+
<ChatBubble
|
|
546
|
+
key={m.id}
|
|
547
|
+
variant="outgoing"
|
|
548
|
+
avatarType="ai"
|
|
549
|
+
outgoingTone="ai"
|
|
550
|
+
messages={m.messages}
|
|
551
|
+
timestamps={m.timestamps}
|
|
552
|
+
/>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
return (
|
|
556
|
+
<ChatBubble
|
|
557
|
+
key={m.id}
|
|
558
|
+
variant="outgoing"
|
|
559
|
+
avatarType="image"
|
|
560
|
+
avatarSrc={thread.agentAvatarSrc}
|
|
561
|
+
avatarAlt={`${thread.agentName || '客服'}头像`}
|
|
562
|
+
outgoingTone="default"
|
|
563
|
+
receipt="read"
|
|
564
|
+
messages={m.messages}
|
|
565
|
+
timestamps={m.timestamps}
|
|
566
|
+
/>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function formatSystemTime(value) {
|
|
571
|
+
if (typeof value !== 'string') return value;
|
|
572
|
+
const parts = value.trim().split(' ');
|
|
573
|
+
const hhmmss = parts.length >= 2 ? parts[1] : parts[0];
|
|
574
|
+
if (!hhmmss) return value;
|
|
575
|
+
const hhmm = hhmmss.split(':').slice(0, 2).join(':');
|
|
576
|
+
return hhmm || value;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function SystemNote({ text, time }) {
|
|
580
|
+
return (
|
|
581
|
+
<div
|
|
582
|
+
className="flex w-full items-center justify-center gap-2 text-xs"
|
|
583
|
+
style={{ color: 'var(--foreground-muted, #667085)', padding: '4px 0' }}
|
|
584
|
+
>
|
|
585
|
+
<span>{formatSystemTime(time)}</span>
|
|
586
|
+
<span>·</span>
|
|
587
|
+
<span>{text}</span>
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
}
|