@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,1264 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import Avatar from './Avatar';
|
|
3
|
+
import Button from './Button';
|
|
4
|
+
import Icon from './Icon';
|
|
5
|
+
import Tag from './Tag';
|
|
6
|
+
import Tabs from './Tabs';
|
|
7
|
+
import Tooltip from './Tooltip';
|
|
8
|
+
import liSiruAvatar from './team-avatar-assets/li-siru.png';
|
|
9
|
+
import chengchengAvatar from './team-avatar-assets/chengcheng-murphy.png';
|
|
10
|
+
import guoZhezhiAvatar from './team-avatar-assets/guo-zhezhi.png';
|
|
11
|
+
import liuDelinAvatar from './team-avatar-assets/liu-delin.png';
|
|
12
|
+
import duanRanAvatar from './team-avatar-assets/duan-ran.png';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TABS = [
|
|
15
|
+
{ id: 'all', label: '全部' },
|
|
16
|
+
{ id: 'pending', label: '待干预', count: 3 },
|
|
17
|
+
{ id: 'managed', label: '托管中', count: 6 },
|
|
18
|
+
{ id: 'other', label: '其它' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const STATUS_TAG_BY_TAB = {
|
|
22
|
+
pending: { label: '待干预', variant: 'red' },
|
|
23
|
+
managed: { label: '托管中', variant: 'green' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/* ── 默认展开宽度(与平台预览、TOKEN_MAP 一致)── */
|
|
27
|
+
const DEFAULT_WIDTH = 400;
|
|
28
|
+
const CARD_MIN_WIDTH = 333;
|
|
29
|
+
const AVATAR_ONLY_WIDTH = 88;
|
|
30
|
+
const AVATAR_ONLY_THRESHOLD = 150;
|
|
31
|
+
const RESIZE_STEP = 16;
|
|
32
|
+
const CARD_TWO_COLUMN_THRESHOLD = 580;
|
|
33
|
+
const CARD_THREE_COLUMN_THRESHOLD = 950;
|
|
34
|
+
|
|
35
|
+
const DEFAULT_SECTIONS = [
|
|
36
|
+
{
|
|
37
|
+
id: 'pending',
|
|
38
|
+
title: '待干预',
|
|
39
|
+
count: 3,
|
|
40
|
+
items: [
|
|
41
|
+
{
|
|
42
|
+
id: 'pending-1',
|
|
43
|
+
title: '酒旅·仲裁退款',
|
|
44
|
+
userName: '小兔叽掉毛毛🐰',
|
|
45
|
+
orderId: '2918575148379876',
|
|
46
|
+
time: '13:32',
|
|
47
|
+
avatarSrc: liSiruAvatar,
|
|
48
|
+
tags: [
|
|
49
|
+
{ label: '异常监控提醒', variant: 'grey' },
|
|
50
|
+
{ label: '待干预', variant: 'red' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'pending-2',
|
|
55
|
+
title: '订单发货进度咨询',
|
|
56
|
+
userName: '糯叽叽的团子呀',
|
|
57
|
+
orderId: '2918575148391024',
|
|
58
|
+
time: '12:54',
|
|
59
|
+
avatarSrc: chengchengAvatar,
|
|
60
|
+
tags: [
|
|
61
|
+
{ label: '催4', variant: 'grey' },
|
|
62
|
+
{ label: '待干预', variant: 'red' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'pending-3',
|
|
67
|
+
title: '优惠券核销失败',
|
|
68
|
+
userName: '草莓气泡水🍓',
|
|
69
|
+
orderId: '2918575148403288',
|
|
70
|
+
time: '12:40',
|
|
71
|
+
avatarSrc: guoZhezhiAvatar,
|
|
72
|
+
tags: [
|
|
73
|
+
{ label: '退款执行错误', variant: 'grey' },
|
|
74
|
+
{ label: '待干预', variant: 'red' },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'managed',
|
|
81
|
+
title: '托管中',
|
|
82
|
+
count: 6,
|
|
83
|
+
items: [
|
|
84
|
+
{
|
|
85
|
+
id: 'managed-1',
|
|
86
|
+
title: '物流状态异常跟进',
|
|
87
|
+
userName: '软软想吃糖葫芦',
|
|
88
|
+
orderId: '2918575148472390',
|
|
89
|
+
time: '11:22',
|
|
90
|
+
avatarSrc: chengchengAvatar,
|
|
91
|
+
tags: [
|
|
92
|
+
{ label: '24时30分', variant: 'grey', iconName: 'alarm-clock-stroked' },
|
|
93
|
+
{ label: '托管中', variant: 'green' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'managed-2',
|
|
98
|
+
title: '退款方案确认',
|
|
99
|
+
userName: '月亮送我一束光🌙',
|
|
100
|
+
orderId: '2918575148421185',
|
|
101
|
+
time: '10:18',
|
|
102
|
+
avatarSrc: liSiruAvatar,
|
|
103
|
+
tags: [
|
|
104
|
+
{ label: '已挂起', variant: 'orange' },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'managed-3',
|
|
109
|
+
title: '售后凭证补充',
|
|
110
|
+
userName: '布丁小熊不吃苦',
|
|
111
|
+
orderId: '2918575148437702',
|
|
112
|
+
time: '09:46',
|
|
113
|
+
avatarSrc: liuDelinAvatar,
|
|
114
|
+
tags: [
|
|
115
|
+
{ label: '协商不成功', variant: 'grey' },
|
|
116
|
+
{ label: '托管中', variant: 'green' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'other',
|
|
123
|
+
title: '其它',
|
|
124
|
+
count: 20,
|
|
125
|
+
items: [
|
|
126
|
+
{
|
|
127
|
+
id: 'other-1',
|
|
128
|
+
title: '订单已完成回访',
|
|
129
|
+
userName: '星星被窝藏好啦✨',
|
|
130
|
+
orderId: '2918575148499712',
|
|
131
|
+
time: '昨天',
|
|
132
|
+
avatarSrc: duanRanAvatar,
|
|
133
|
+
tags: [
|
|
134
|
+
{ label: '已处理', variant: 'blue' },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'other-2',
|
|
139
|
+
title: '门店服务评价',
|
|
140
|
+
userName: '困困阿喵要睡觉',
|
|
141
|
+
orderId: '2918575148502149',
|
|
142
|
+
time: '周二',
|
|
143
|
+
avatarSrc: guoZhezhiAvatar,
|
|
144
|
+
tags: [
|
|
145
|
+
{ label: '已处理', variant: 'blue' },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const DEFAULT_CARD_DRAFT_TEXT = '祝您生活愉快~如果您觉得我的服务态度还可以的话,辛苦您点击左下角结束服务为本次服务点亮小星星哦~';
|
|
153
|
+
const DEFAULT_CARD_DRAFT_TEXTS = [
|
|
154
|
+
'好的,我这边先帮您发起内部复核,稍后把需要您补充的内容整理给您。',
|
|
155
|
+
'我先继续核对系统记录,确认完当前进展后第一时间同步您。',
|
|
156
|
+
'理解您的着急,我会持续帮您盯进度,有新结果会尽快回复您。',
|
|
157
|
+
'这边已帮您补充登记处理诉求,后续若需配合我会及时联系您。',
|
|
158
|
+
'我先整理本次处理方案和注意事项,稍后给您一份更清晰的回复口径。',
|
|
159
|
+
];
|
|
160
|
+
const CARD_CONTENT_OVERRIDES = {
|
|
161
|
+
'pending-1': {
|
|
162
|
+
messages: [
|
|
163
|
+
{ id: 'pending-1-user-1', role: 'user', text: '但是我不知道问题出在哪里' },
|
|
164
|
+
{ id: 'pending-1-user-2', role: 'user', text: '你帮我看下这个有啥问题呢' },
|
|
165
|
+
{ id: 'pending-1-agent-0', role: 'agent', text: '我先帮您看一下处罚记录和最近的申诉结果,您先别着急。' },
|
|
166
|
+
{
|
|
167
|
+
id: 'pending-1-agent-1',
|
|
168
|
+
role: 'agent',
|
|
169
|
+
text: '《抖音网络社区自律公约》:抖音app-我-右上角≡-设置-抖音规则中心,这是咱们抖音官方的一个规则呢',
|
|
170
|
+
},
|
|
171
|
+
{ id: 'pending-1-user-3', role: 'user', text: '我之前已经申诉过一次了,但是没有通过。' },
|
|
172
|
+
{ id: 'pending-1-agent-2', role: 'agent', text: '明白,我帮您再核对一下驳回原因,看看是不是材料或说明不完整。' },
|
|
173
|
+
{ id: 'pending-1-user-4', role: 'user', text: '如果需要补充什么材料你直接告诉我,我这边可以配合。' },
|
|
174
|
+
{ id: 'pending-1-agent-3', role: 'agent', text: '好的,我这边先帮您发起内部复核,稍后把需要您补充的内容整理给您。' },
|
|
175
|
+
{ id: 'pending-1-user-5', role: 'user', text: '好的明白了,谢谢,没其他问题了' },
|
|
176
|
+
],
|
|
177
|
+
status: 'generating',
|
|
178
|
+
draftText: '',
|
|
179
|
+
},
|
|
180
|
+
'managed-1': {
|
|
181
|
+
messages: [
|
|
182
|
+
{ id: 'managed-1-user-1', role: 'user', text: '你帮我看下这个有啥问题呢' },
|
|
183
|
+
{ id: 'managed-1-agent-0', role: 'agent', text: '我先帮您核对一下当前物流轨迹和异常节点,请稍等。' },
|
|
184
|
+
{
|
|
185
|
+
id: 'managed-1-agent-1',
|
|
186
|
+
role: 'agent',
|
|
187
|
+
text: '《抖音网络社区自律公约》:抖音app-我-右上角≡-设置-抖音规则中心,这是咱们抖音官方的一个规则呢',
|
|
188
|
+
},
|
|
189
|
+
{ id: 'managed-1-user-2', role: 'user', text: '我看物流一直停在转运中心,没有新的更新。' },
|
|
190
|
+
{ id: 'managed-1-agent-2', role: 'agent', text: '当前看是中转环节超时,我这边已经帮您催促承运商优先处理。' },
|
|
191
|
+
{ id: 'managed-1-user-3', role: 'user', text: '那我这边还需要额外操作什么吗?' },
|
|
192
|
+
{ id: 'managed-1-agent-3', role: 'agent', text: '暂时不需要,您保持电话畅通即可,有新进展我会第一时间同步您。' },
|
|
193
|
+
{ id: 'managed-1-user-4', role: 'user', text: '好的明白了,谢谢,没其他问题了' },
|
|
194
|
+
],
|
|
195
|
+
status: 'editable',
|
|
196
|
+
draftText: DEFAULT_CARD_DRAFT_TEXT,
|
|
197
|
+
},
|
|
198
|
+
'other-1': {
|
|
199
|
+
status: 'replied',
|
|
200
|
+
draftText: '',
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/* ── 根容器:展开 / 收起(仅头像列)── */
|
|
205
|
+
const ROOT = 'tfds-conversation-list relative flex h-full min-h-0 w-full shrink-0 overflow-hidden select-none';
|
|
206
|
+
const ROOT_EXPANDED = 'flex-col gap-4 p-4';
|
|
207
|
+
const ROOT_COLLAPSED = 'flex-col items-center gap-4 px-3 py-4';
|
|
208
|
+
|
|
209
|
+
/* ── 顶部标题栏 + 工具按钮 ── */
|
|
210
|
+
const HEADER = 'flex h-[26px] items-center justify-between gap-3';
|
|
211
|
+
const HEADER_MAIN = 'flex min-w-0 items-center gap-3';
|
|
212
|
+
const TITLE = 'm-0 truncate text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground';
|
|
213
|
+
const ACTIONS = 'flex shrink-0 items-center gap-1';
|
|
214
|
+
/* ── Tabs + 列表滚动区 ── */
|
|
215
|
+
const TABS_WRAP = 'max-w-full self-start';
|
|
216
|
+
const LIST = 'mx-[-12px] flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto px-3';
|
|
217
|
+
const CARD_LIST = 'mx-[-12px] grid min-h-0 flex-1 grid-cols-1 auto-rows-max gap-4 overflow-y-auto px-3';
|
|
218
|
+
const CARD_LIST_TWO_COLUMN = 'grid-cols-2';
|
|
219
|
+
const CARD_LIST_THREE_COLUMN = 'grid-cols-3';
|
|
220
|
+
const SECTION = 'flex flex-col gap-1';
|
|
221
|
+
const SECTION_HEADER = [
|
|
222
|
+
'flex h-5 w-full items-center justify-between gap-2 rounded-md p-0.5 text-left',
|
|
223
|
+
'text-xs leading-4 text-foreground-muted transition-colors duration-150',
|
|
224
|
+
'hover:text-foreground-secondary active:text-foreground-secondary',
|
|
225
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200',
|
|
226
|
+
].join(' ');
|
|
227
|
+
const SECTION_TITLE = 'flex min-w-0 items-center gap-1';
|
|
228
|
+
const SECTION_CARET = 'transition-transform duration-150';
|
|
229
|
+
const SECTION_CONTENT = 'flex flex-col gap-1';
|
|
230
|
+
/* ── 会话行:默认 / 选中 / 左侧色条 ── */
|
|
231
|
+
const ITEM_BASE = [
|
|
232
|
+
'group relative flex w-full items-center gap-3 overflow-hidden rounded-lg p-3 text-left',
|
|
233
|
+
'transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200',
|
|
234
|
+
].join(' ');
|
|
235
|
+
const ITEM_DEFAULT = 'bg-transparent hover:bg-fill active:bg-fill-active';
|
|
236
|
+
const ITEM_ACTIVE = 'bg-surface shadow-list';
|
|
237
|
+
const ACTIVE_RAIL = 'absolute bottom-0 left-0 top-0 w-1 bg-brand-500';
|
|
238
|
+
const ITEM_BODY = 'flex min-w-0 flex-1 flex-col gap-1';
|
|
239
|
+
const ITEM_TITLE_ROW = 'flex min-w-0 items-start justify-between gap-2';
|
|
240
|
+
const ITEM_TITLE = 'm-0 min-w-0 flex-1 truncate text-sm [font-weight:var(--font-semibold)] leading-6 text-foreground';
|
|
241
|
+
const TAGS = 'flex shrink-0 items-center gap-1';
|
|
242
|
+
const META = 'flex min-w-0 items-center gap-1 text-xs leading-4 text-foreground-muted';
|
|
243
|
+
const USER = 'block w-[50px] shrink-0 truncate';
|
|
244
|
+
const ORDER = 'min-w-0 truncate';
|
|
245
|
+
const TIME = 'ml-auto shrink-0 pl-2 text-foreground-muted';
|
|
246
|
+
|
|
247
|
+
/* ── 收起态:仅头像 + 展开入口 ── */
|
|
248
|
+
const AVATAR_ONLY_HEADER = 'flex h-8 w-full items-center justify-center';
|
|
249
|
+
const AVATAR_ONLY_LIST = 'flex min-h-0 flex-1 flex-col self-stretch gap-1 overflow-y-auto';
|
|
250
|
+
const AVATAR_ONLY_ITEM = [
|
|
251
|
+
'relative inline-flex h-[68px] w-full items-center rounded-xl px-4',
|
|
252
|
+
'transition-colors duration-150 hover:bg-fill active:bg-fill-active',
|
|
253
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200',
|
|
254
|
+
].join(' ');
|
|
255
|
+
const AVATAR_ONLY_ITEM_INACTIVE = 'justify-center py-[18px]';
|
|
256
|
+
const AVATAR_ONLY_ITEM_ACTIVE = 'justify-center overflow-hidden bg-surface py-3 shadow-list hover:bg-surface';
|
|
257
|
+
const AVATAR_ONLY_ACTIVE_RAIL = 'absolute bottom-0 left-0 top-0 w-1 bg-data-0';
|
|
258
|
+
/* ── 右侧拖拽分隔条 ── */
|
|
259
|
+
const RESIZE_HANDLE_CLASS = 'absolute right-0 top-0 bottom-0 z-20 w-3 cursor-col-resize touch-none select-none focus-visible:outline-none';
|
|
260
|
+
const RESIZE_HANDLE_BAR_CLASS = 'absolute right-0 top-0 bottom-0 w-[2px] bg-transparent transition-colors duration-150';
|
|
261
|
+
|
|
262
|
+
/* ── 卡片列表变体 ── */
|
|
263
|
+
const CARD_ITEM = [
|
|
264
|
+
'group flex flex-col rounded-xl bg-surface shadow-list transition-shadow duration-150',
|
|
265
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200',
|
|
266
|
+
].join(' ');
|
|
267
|
+
const CARD_ITEM_INTERACTIVE = 'cursor-pointer';
|
|
268
|
+
const CARD_ITEM_ACTIVE_SHADOW = 'var(--shadow-list), inset 0 0 0 1.5px var(--color-brand-500)';
|
|
269
|
+
const CARD_HEADER = 'flex items-start gap-3 px-4 pt-3 pb-2';
|
|
270
|
+
const CARD_HEADER_MAIN = 'flex min-w-0 flex-1 flex-wrap items-center gap-1';
|
|
271
|
+
const CARD_TITLE = 'min-w-0 truncate text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground';
|
|
272
|
+
const CARD_TAGS = 'flex min-w-0 flex-wrap items-center gap-1';
|
|
273
|
+
const CARD_MESSAGE_REGION = 'flex flex-col overflow-y-auto px-4 pb-2';
|
|
274
|
+
const CARD_MESSAGE_REGION_REPLIED = 'h-[196px] gap-3';
|
|
275
|
+
const CARD_MESSAGE_REGION_GENERATING = 'h-[196px] gap-3';
|
|
276
|
+
const CARD_MESSAGE_REGION_EDITABLE = 'h-[155px] gap-3';
|
|
277
|
+
const CARD_MESSAGE_ROW_BASE = 'flex w-full';
|
|
278
|
+
const CARD_MESSAGE_ROW_USER = 'justify-start';
|
|
279
|
+
const CARD_MESSAGE_ROW_AGENT = 'justify-end';
|
|
280
|
+
const CARD_MESSAGE_BUBBLE_BASE = 'max-w-[calc(100%-24px)] rounded-lg px-2 py-1.5 text-xs leading-4 text-foreground';
|
|
281
|
+
const CARD_MESSAGE_BUBBLE_USER = 'rounded-tl-none bg-fill';
|
|
282
|
+
const CARD_MESSAGE_BUBBLE_AGENT = 'rounded-tr-none bg-[image:var(--gradient-ai-fill-1)]';
|
|
283
|
+
const CARD_DIVIDER = 'h-px w-full bg-border-default';
|
|
284
|
+
const CARD_FOOTER_BASE = 'shrink-0';
|
|
285
|
+
const CARD_FOOTER_REPLIED = 'px-4 py-4 text-xs leading-4 text-[#22272759]';
|
|
286
|
+
const CARD_FOOTER_GENERATING = 'flex items-center gap-2 px-4 py-4 text-xs leading-4 text-[#22272759]';
|
|
287
|
+
const CARD_FOOTER_EDITABLE = 'flex flex-col gap-3 px-3 py-3';
|
|
288
|
+
const CARD_FOOTER_TEXT = 'w-full text-xs leading-4 text-foreground';
|
|
289
|
+
const CARD_FOOTER_BUTTON_ROW = 'flex w-full justify-end';
|
|
290
|
+
const CARD_STATUS_SPINNER_WRAP = 'relative inline-flex size-3 shrink-0 items-center justify-center';
|
|
291
|
+
const CARD_STATUS_SPINNER_TRACK = 'absolute inset-0 rounded-full border border-border-line-light';
|
|
292
|
+
const CARD_STATUS_SPINNER_ARC = 'absolute inset-0 rounded-full animate-spin [animation-duration:1.1s] [animation-timing-function:linear] [animation-iteration-count:infinite]';
|
|
293
|
+
|
|
294
|
+
function getSectionItemCount(section) {
|
|
295
|
+
return Array.isArray(section?.items) ? section.items.length : 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function getTabItemCount(sections, tabId) {
|
|
299
|
+
if (!Array.isArray(sections) || !tabId || tabId === 'all') return null;
|
|
300
|
+
const matchedSection = sections.find((section) => section.id === tabId);
|
|
301
|
+
return matchedSection ? getSectionItemCount(matchedSection) : 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function renderTabLabel(tab, count) {
|
|
305
|
+
if (count === undefined || count === null) return tab.label;
|
|
306
|
+
return `${tab.label} ${count}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function ConversationTag({ tag }) {
|
|
310
|
+
return (
|
|
311
|
+
<Tag
|
|
312
|
+
variant={tag.variant || 'grey'}
|
|
313
|
+
size="m"
|
|
314
|
+
radius="md"
|
|
315
|
+
fontWeight="regular"
|
|
316
|
+
showIcon={Boolean(tag.iconName)}
|
|
317
|
+
iconName={tag.iconName}
|
|
318
|
+
className="shrink-0"
|
|
319
|
+
>
|
|
320
|
+
{tag.label}
|
|
321
|
+
</Tag>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function ConversationItem({ item, active, onClick }) {
|
|
326
|
+
const content = (
|
|
327
|
+
<>
|
|
328
|
+
{active ? <span className={ACTIVE_RAIL} aria-hidden="true" /> : null}
|
|
329
|
+
<Avatar shape="round" size="s" type="image" src={item.avatarSrc} alt={item.userName} />
|
|
330
|
+
<div className={ITEM_BODY}>
|
|
331
|
+
<div className={ITEM_TITLE_ROW}>
|
|
332
|
+
<h3 className={ITEM_TITLE}>{item.title}</h3>
|
|
333
|
+
<div className={TAGS}>
|
|
334
|
+
{(item.tags || []).map((tag, index) => (
|
|
335
|
+
<ConversationTag key={`${item.id}-${tag.label}-${index}`} tag={tag} />
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div className={META}>
|
|
341
|
+
<Tooltip content={item.userName}>
|
|
342
|
+
<span className={USER}>{item.userName}</span>
|
|
343
|
+
</Tooltip>
|
|
344
|
+
<Icon name="file-05-stroked" size={12} aria-hidden="true" />
|
|
345
|
+
<span className={ORDER}>{item.orderId}</span>
|
|
346
|
+
<span className={TIME}>{item.time}</span>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</>
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const className = [ITEM_BASE, active ? ITEM_ACTIVE : ITEM_DEFAULT].join(' ');
|
|
353
|
+
|
|
354
|
+
if (onClick) {
|
|
355
|
+
return (
|
|
356
|
+
<button type="button" className={className} onClick={() => onClick(item)} data-tfds-component="ConversationList.Item">
|
|
357
|
+
{content}
|
|
358
|
+
</button>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return <article className={className} data-tfds-component="ConversationList.Item">{content}</article>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function ConversationAvatarItem({ item, active, onClick }) {
|
|
366
|
+
return (
|
|
367
|
+
<button
|
|
368
|
+
type="button"
|
|
369
|
+
className={[
|
|
370
|
+
AVATAR_ONLY_ITEM,
|
|
371
|
+
active ? AVATAR_ONLY_ITEM_ACTIVE : AVATAR_ONLY_ITEM_INACTIVE,
|
|
372
|
+
].join(' ')}
|
|
373
|
+
onClick={() => onClick(item)}
|
|
374
|
+
aria-label={item.title}
|
|
375
|
+
title={item.title}
|
|
376
|
+
data-tfds-component="ConversationList.AvatarItem"
|
|
377
|
+
>
|
|
378
|
+
{active ? <span className={AVATAR_ONLY_ACTIVE_RAIL} aria-hidden="true" /> : null}
|
|
379
|
+
<Avatar shape="round" size="s" type="image" src={item.avatarSrc} alt={item.userName} />
|
|
380
|
+
</button>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function ConversationCardTag({ tag }) {
|
|
385
|
+
return <ConversationTag tag={tag} />;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isPrimaryStatusTag(tag) {
|
|
389
|
+
return ['red', 'green', 'orange', 'blue'].includes(tag?.variant);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function sortCardTags(tags) {
|
|
393
|
+
if (!Array.isArray(tags) || tags.length <= 1) return Array.isArray(tags) ? tags : [];
|
|
394
|
+
|
|
395
|
+
const statusTags = [];
|
|
396
|
+
const secondaryTags = [];
|
|
397
|
+
|
|
398
|
+
tags.forEach((tag) => {
|
|
399
|
+
if (isPrimaryStatusTag(tag)) {
|
|
400
|
+
statusTags.push(tag);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
secondaryTags.push(tag);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return [...statusTags, ...secondaryTags];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function ConversationCardMessage({ message }) {
|
|
410
|
+
const isAgent = message.role === 'agent';
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<div className={[CARD_MESSAGE_ROW_BASE, isAgent ? CARD_MESSAGE_ROW_AGENT : CARD_MESSAGE_ROW_USER].join(' ')}>
|
|
414
|
+
<div className={[
|
|
415
|
+
CARD_MESSAGE_BUBBLE_BASE,
|
|
416
|
+
isAgent ? CARD_MESSAGE_BUBBLE_AGENT : CARD_MESSAGE_BUBBLE_USER,
|
|
417
|
+
].join(' ')}>
|
|
418
|
+
{message.text}
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function ConversationProcessingSpinner() {
|
|
425
|
+
return (
|
|
426
|
+
<span className={CARD_STATUS_SPINNER_WRAP} aria-hidden="true">
|
|
427
|
+
<span className={CARD_STATUS_SPINNER_TRACK} />
|
|
428
|
+
<span
|
|
429
|
+
className={CARD_STATUS_SPINNER_ARC}
|
|
430
|
+
style={{
|
|
431
|
+
background:
|
|
432
|
+
'conic-gradient(from 200deg, transparent 0deg 180deg, var(--color-brand-500) 180deg, var(--color-cyan-300) 232deg, var(--color-blue-400) 286deg, var(--color-violet-300) 334deg, var(--color-purple-300) 360deg)',
|
|
433
|
+
WebkitMask:
|
|
434
|
+
'radial-gradient(farthest-side, transparent calc(100% - 2px), var(--color-black) calc(100% - 2px))',
|
|
435
|
+
mask: 'radial-gradient(farthest-side, transparent calc(100% - 2px), var(--color-black) calc(100% - 2px))',
|
|
436
|
+
}}
|
|
437
|
+
/>
|
|
438
|
+
</span>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildSentCardMessage(cardId, draftText, sequence) {
|
|
443
|
+
return {
|
|
444
|
+
id: `${cardId}-agent-sent-${sequence}`,
|
|
445
|
+
role: 'agent',
|
|
446
|
+
text: draftText,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function buildNextCardAfterSend(card, draftText, sequence) {
|
|
451
|
+
return {
|
|
452
|
+
...card,
|
|
453
|
+
messages: [...(card.messages || []), buildSentCardMessage(card.id, draftText, sequence)],
|
|
454
|
+
draftText: '',
|
|
455
|
+
status: 'replied',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function applyCardItemOverride(item, override) {
|
|
460
|
+
if (!override) return item;
|
|
461
|
+
return {
|
|
462
|
+
...item,
|
|
463
|
+
...override,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function ConversationCardFooter({ card, onSend }) {
|
|
468
|
+
const draftText = typeof card.draftText === 'string' ? card.draftText.trim() : '';
|
|
469
|
+
const isRepliedState = card.status === 'replied';
|
|
470
|
+
const showEditableState = card.status === 'editable' && draftText.length > 0;
|
|
471
|
+
|
|
472
|
+
if (isRepliedState) {
|
|
473
|
+
return (
|
|
474
|
+
<div className={[CARD_FOOTER_BASE, CARD_FOOTER_REPLIED].join(' ')}>
|
|
475
|
+
已回复用户
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!showEditableState) {
|
|
481
|
+
return (
|
|
482
|
+
<div className={[CARD_FOOTER_BASE, CARD_FOOTER_GENERATING].join(' ')}>
|
|
483
|
+
<ConversationProcessingSpinner />
|
|
484
|
+
<span>回复内容生成中</span>
|
|
485
|
+
</div>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<>
|
|
491
|
+
<div className={CARD_DIVIDER} aria-hidden="true" />
|
|
492
|
+
<div className={[CARD_FOOTER_BASE, CARD_FOOTER_EDITABLE].join(' ')}>
|
|
493
|
+
<p className={CARD_FOOTER_TEXT}>{draftText}</p>
|
|
494
|
+
<div className={CARD_FOOTER_BUTTON_ROW}>
|
|
495
|
+
<Button
|
|
496
|
+
type="button"
|
|
497
|
+
variant="primary"
|
|
498
|
+
size="sm"
|
|
499
|
+
icon={<Icon name="send-01-stroked" size={16} aria-hidden="true" />}
|
|
500
|
+
iconOnly
|
|
501
|
+
className="!h-6 !w-6 !rounded-md !p-1"
|
|
502
|
+
onKeyDown={(event) => {
|
|
503
|
+
event.stopPropagation();
|
|
504
|
+
}}
|
|
505
|
+
onClick={(event) => {
|
|
506
|
+
event.stopPropagation();
|
|
507
|
+
if (onSend) onSend(card);
|
|
508
|
+
}}
|
|
509
|
+
aria-label="发送回复"
|
|
510
|
+
/>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
</>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function buildFallbackCardMessages(item) {
|
|
518
|
+
const statusSummary = (item.tags || []).map((tag) => tag.label).join(' / ');
|
|
519
|
+
|
|
520
|
+
return [
|
|
521
|
+
{ id: `${item.id}-user-1`, role: 'user', text: `我想继续确认「${item.title}」的处理进展。` },
|
|
522
|
+
{
|
|
523
|
+
id: `${item.id}-agent-1`,
|
|
524
|
+
role: 'agent',
|
|
525
|
+
text: statusSummary
|
|
526
|
+
? `当前记录状态为 ${statusSummary},我来继续帮您跟进处理。`
|
|
527
|
+
: '我来继续帮您跟进这个会话的处理进展。',
|
|
528
|
+
},
|
|
529
|
+
{ id: `${item.id}-user-2`, role: 'user', text: `对应单号是 ${item.orderId},麻烦再帮我确认一下。` },
|
|
530
|
+
{ id: `${item.id}-agent-2`, role: 'agent', text: '好的,我先同步核对系统记录,并帮您整理当前可执行的处理方案。' },
|
|
531
|
+
{ id: `${item.id}-user-3`, role: 'user', text: '我比较担心一直没有结果,想知道大概多久能处理完。' },
|
|
532
|
+
{ id: `${item.id}-agent-3`, role: 'agent', text: '理解您的着急,我会继续盯进度,有更新会第一时间回复您。' },
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function getDraftTextSeed(item) {
|
|
537
|
+
const source = String(item?.id || item?.title || item?.orderId || '');
|
|
538
|
+
let hash = 0;
|
|
539
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
540
|
+
hash = (hash * 31 + source.charCodeAt(index)) % 2147483647;
|
|
541
|
+
}
|
|
542
|
+
return hash;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function buildFallbackDraftText(item) {
|
|
546
|
+
const seed = getDraftTextSeed(item);
|
|
547
|
+
return DEFAULT_CARD_DRAFT_TEXTS[seed % DEFAULT_CARD_DRAFT_TEXTS.length] || DEFAULT_CARD_DRAFT_TEXT;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function normalizeCardContent(item) {
|
|
551
|
+
const override = CARD_CONTENT_OVERRIDES[item.id] || {};
|
|
552
|
+
const messages = Array.isArray(item.messages) && item.messages.length > 0
|
|
553
|
+
? item.messages
|
|
554
|
+
: (override.messages || buildFallbackCardMessages(item));
|
|
555
|
+
const draftText = typeof item.draftText === 'string'
|
|
556
|
+
? item.draftText
|
|
557
|
+
: (
|
|
558
|
+
typeof override.draftText === 'string'
|
|
559
|
+
? override.draftText
|
|
560
|
+
: ((item.status || override.status) === 'editable' ? buildFallbackDraftText(item) : '')
|
|
561
|
+
);
|
|
562
|
+
const requestedStatus = item.status || override.status || 'replied';
|
|
563
|
+
const status = requestedStatus === 'editable'
|
|
564
|
+
? (draftText.trim() ? 'editable' : 'generating')
|
|
565
|
+
: (requestedStatus === 'replied' ? 'replied' : 'generating');
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
...item,
|
|
569
|
+
messages,
|
|
570
|
+
draftText,
|
|
571
|
+
status,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function ConversationCardItem({ item, active, onClick, onSend }) {
|
|
576
|
+
const card = normalizeCardContent(item);
|
|
577
|
+
const messageRegionRef = useRef(null);
|
|
578
|
+
const interactive = Boolean(onClick);
|
|
579
|
+
const messageRegionStateClass = card.status === 'editable'
|
|
580
|
+
? CARD_MESSAGE_REGION_EDITABLE
|
|
581
|
+
: (card.status === 'replied' ? CARD_MESSAGE_REGION_REPLIED : CARD_MESSAGE_REGION_GENERATING);
|
|
582
|
+
const clickProps = interactive ? {
|
|
583
|
+
role: 'button',
|
|
584
|
+
tabIndex: 0,
|
|
585
|
+
onClick: () => onClick(item),
|
|
586
|
+
onKeyDown: (event) => {
|
|
587
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
588
|
+
event.preventDefault();
|
|
589
|
+
onClick(item);
|
|
590
|
+
},
|
|
591
|
+
} : {};
|
|
592
|
+
|
|
593
|
+
useEffect(() => {
|
|
594
|
+
const node = messageRegionRef.current;
|
|
595
|
+
if (!node) return;
|
|
596
|
+
node.scrollTop = node.scrollHeight;
|
|
597
|
+
}, [active, card.id, card.status, card.messages.length]);
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
<article
|
|
601
|
+
className={[CARD_ITEM, interactive ? CARD_ITEM_INTERACTIVE : ''].filter(Boolean).join(' ')}
|
|
602
|
+
style={active ? { boxShadow: CARD_ITEM_ACTIVE_SHADOW } : undefined}
|
|
603
|
+
aria-label={card.title}
|
|
604
|
+
data-tfds-component="ConversationList.CardItem"
|
|
605
|
+
aria-pressed={interactive ? active : undefined}
|
|
606
|
+
data-active={active ? 'true' : 'false'}
|
|
607
|
+
{...clickProps}
|
|
608
|
+
>
|
|
609
|
+
<div className={CARD_HEADER}>
|
|
610
|
+
<div className={CARD_HEADER_MAIN}>
|
|
611
|
+
<h3 className={CARD_TITLE}>{card.title}</h3>
|
|
612
|
+
<div className={CARD_TAGS}>
|
|
613
|
+
{sortCardTags(card.tags).map((tag, index) => (
|
|
614
|
+
<ConversationCardTag key={`${card.id}-${tag.label}-${index}`} tag={tag} />
|
|
615
|
+
))}
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
<Avatar shape="round" size="s" type="image" src={card.avatarSrc} alt={card.title} />
|
|
619
|
+
</div>
|
|
620
|
+
<div ref={messageRegionRef} className={[CARD_MESSAGE_REGION, messageRegionStateClass].join(' ')}>
|
|
621
|
+
{(card.messages || []).map((message) => (
|
|
622
|
+
<ConversationCardMessage key={message.id} message={message} />
|
|
623
|
+
))}
|
|
624
|
+
</div>
|
|
625
|
+
<ConversationCardFooter card={card} onSend={onSend} />
|
|
626
|
+
</article>
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function normalizeSections(sections) {
|
|
631
|
+
if (!Array.isArray(sections) || sections.length === 0) return DEFAULT_SECTIONS;
|
|
632
|
+
return sections;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function filterSectionsByTab(sections, activeTab) {
|
|
636
|
+
if (!activeTab || activeTab === 'all') return sections;
|
|
637
|
+
const filteredSections = sections.filter((section) => section.id === activeTab);
|
|
638
|
+
return filteredSections.length > 0 ? filteredSections : sections;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function getFirstItemId(sections) {
|
|
642
|
+
if (!Array.isArray(sections)) return null;
|
|
643
|
+
|
|
644
|
+
for (const section of sections) {
|
|
645
|
+
const firstItemId = section?.items?.[0]?.id;
|
|
646
|
+
if (firstItemId) return firstItemId;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function hasItem(sections, itemId) {
|
|
653
|
+
if (!itemId || !Array.isArray(sections)) return false;
|
|
654
|
+
return sections.some((section) => (section.items || []).some((item) => item.id === itemId));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function getSectionIds(sections) {
|
|
658
|
+
if (!Array.isArray(sections)) return [];
|
|
659
|
+
return sections.map((section) => section.id).filter(Boolean);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function resolveStatusTag(activeTab, sectionId) {
|
|
663
|
+
if (activeTab && activeTab !== 'all') {
|
|
664
|
+
return STATUS_TAG_BY_TAB[activeTab] || null;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return STATUS_TAG_BY_TAB[sectionId] || null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function normalizeItemTags(tags, statusTag) {
|
|
671
|
+
const resolvedTags = Array.isArray(tags) ? tags : [];
|
|
672
|
+
|
|
673
|
+
if (!statusTag) return resolvedTags;
|
|
674
|
+
if (resolvedTags.length === 0) return [statusTag];
|
|
675
|
+
|
|
676
|
+
return resolvedTags.map((tag, index) => (
|
|
677
|
+
index === resolvedTags.length - 1
|
|
678
|
+
? { ...tag, label: statusTag.label, variant: statusTag.variant }
|
|
679
|
+
: tag
|
|
680
|
+
));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function normalizeSectionsByActiveTab(sections, activeTab) {
|
|
684
|
+
if (!Array.isArray(sections)) return [];
|
|
685
|
+
|
|
686
|
+
return sections.map((section) => ({
|
|
687
|
+
...section,
|
|
688
|
+
items: (section.items || []).map((item) => ({
|
|
689
|
+
...item,
|
|
690
|
+
tags: normalizeItemTags(item.tags, resolveStatusTag(activeTab, section.id)),
|
|
691
|
+
})),
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* ConversationList — 会话 / 工单侧栏列表
|
|
697
|
+
* @prop {'default'|'card'} [variant] — 列表形态:默认分组列表 / 卡片列表
|
|
698
|
+
* @prop {'default'|'card'} [defaultVariant='default'] — 非受控初始列表形态
|
|
699
|
+
* @prop {function|null} [onVariantChange=null] — 布局切换 (nextVariant) => void
|
|
700
|
+
* @prop {string} [title='会话列表'] — 顶部标题
|
|
701
|
+
* @prop {Array<{id:string,label:string,count?:number}>} [tabs] — 分段筛选项;空数组时用内置示例
|
|
702
|
+
* @prop {string} [activeTab] — 受控当前筛选 tab id;不传则非受控
|
|
703
|
+
* @prop {string} [defaultActiveTab='all'] — 非受控初始筛选
|
|
704
|
+
* @prop {function|null} [onTabChange=null] — 筛选切换 (tabId, tab) => void
|
|
705
|
+
* @prop {Array<{id:string,title:string,count?:number,items:Array}>} [sections] — 分组数据;variant=card 时仍复用同一份会话数据,只是每个 item 额外读取 messages / draftText / status 等卡片字段
|
|
706
|
+
* @prop {string} [activeItemId] — 受控当前高亮项 id;不传则非受控
|
|
707
|
+
* @prop {string|null} [defaultActiveItemId=null] — 非受控初始高亮项 id;未传时默认选中当前可见第一项
|
|
708
|
+
* @prop {function|null} [onItemClick=null] — 行点击 (item) => void
|
|
709
|
+
* @prop {function|null} [onSend=null] — 卡片可发送态点击发送 ({ item, draftText, nextItem, sectionId }) => void
|
|
710
|
+
* @prop {boolean} [showActions=true] — 是否显示搜索 / 筛选 / 文件按钮
|
|
711
|
+
* @prop {boolean} [showLayoutToggle=true] — 是否显示右上角布局切换按钮
|
|
712
|
+
* @prop {boolean} [resizable=true] — 是否启用组件自身右侧拖拽;嵌入已有可拖拽框架时应关闭
|
|
713
|
+
* @prop {boolean} [collapsible=true] — 是否启用默认列表的纯头像收起态
|
|
714
|
+
* @prop {boolean} [autoCollapseOnNarrow=true] — 是否根据外层实际宽度自动切换纯头像态
|
|
715
|
+
* @prop {function|null} [onLayoutWidthRequest=null] — 嵌入外层框架时请求外层同步调整内容宽度 (contentWidth, meta) => void
|
|
716
|
+
* @prop {string} [className=''] — 根节点附加类名
|
|
717
|
+
* @prop {object} [style] — 根节点内联样式(与默认 width 合并)
|
|
718
|
+
*/
|
|
719
|
+
function ConversationListDefaultVariant({
|
|
720
|
+
variant,
|
|
721
|
+
defaultVariant = 'default',
|
|
722
|
+
onVariantChange = null,
|
|
723
|
+
title = '会话列表',
|
|
724
|
+
tabs = DEFAULT_TABS,
|
|
725
|
+
activeTab,
|
|
726
|
+
defaultActiveTab = 'all',
|
|
727
|
+
onTabChange = null,
|
|
728
|
+
sections,
|
|
729
|
+
activeItemId,
|
|
730
|
+
defaultActiveItemId = null,
|
|
731
|
+
onItemClick = null,
|
|
732
|
+
onSend = null,
|
|
733
|
+
showActions = true,
|
|
734
|
+
showLayoutToggle = true,
|
|
735
|
+
resizable = true,
|
|
736
|
+
collapsible = true,
|
|
737
|
+
autoCollapseOnNarrow = true,
|
|
738
|
+
onLayoutWidthRequest = null,
|
|
739
|
+
className = '',
|
|
740
|
+
style,
|
|
741
|
+
}) {
|
|
742
|
+
const rootRef = useRef(null);
|
|
743
|
+
const dragStateRef = useRef(null);
|
|
744
|
+
const sendSequenceRef = useRef(0);
|
|
745
|
+
const [innerVariant, setInnerVariant] = useState(() => variant ?? defaultVariant);
|
|
746
|
+
const [innerActiveTab, setInnerActiveTab] = useState(defaultActiveTab);
|
|
747
|
+
const [cardItemOverrides, setCardItemOverrides] = useState({});
|
|
748
|
+
const resolvedVariant = innerVariant ?? 'default';
|
|
749
|
+
const isCardVariant = resolvedVariant === 'card';
|
|
750
|
+
const resolvedActiveTab = activeTab ?? innerActiveTab;
|
|
751
|
+
const resolvedTabs = Array.isArray(tabs) && tabs.length > 0 ? tabs : DEFAULT_TABS;
|
|
752
|
+
const resolvedActiveTabIndex = Math.max(0, resolvedTabs.findIndex((tab) => tab.id === resolvedActiveTab));
|
|
753
|
+
const resolvedSections = useMemo(() => normalizeSections(sections), [sections]);
|
|
754
|
+
const resolvedTabCounts = useMemo(
|
|
755
|
+
() => Object.fromEntries(
|
|
756
|
+
resolvedTabs.map((tab) => [tab.id, getTabItemCount(resolvedSections, tab.id)])
|
|
757
|
+
),
|
|
758
|
+
[resolvedSections, resolvedTabs],
|
|
759
|
+
);
|
|
760
|
+
const filteredSections = useMemo(
|
|
761
|
+
() => filterSectionsByTab(resolvedSections, resolvedActiveTab),
|
|
762
|
+
[resolvedSections, resolvedActiveTab],
|
|
763
|
+
);
|
|
764
|
+
const visibleSections = useMemo(
|
|
765
|
+
() => normalizeSectionsByActiveTab(filteredSections, resolvedActiveTab),
|
|
766
|
+
[filteredSections, resolvedActiveTab],
|
|
767
|
+
);
|
|
768
|
+
const fallbackActiveItemId = useMemo(
|
|
769
|
+
() => getFirstItemId(visibleSections) ?? getFirstItemId(resolvedSections),
|
|
770
|
+
[visibleSections, resolvedSections],
|
|
771
|
+
);
|
|
772
|
+
const [innerActiveItemId, setInnerActiveItemId] = useState(() => defaultActiveItemId || null);
|
|
773
|
+
const [expandedSectionIds, setExpandedSectionIds] = useState(() => getSectionIds(normalizeSections(sections)));
|
|
774
|
+
const [resizedWidth, setResizedWidth] = useState(null);
|
|
775
|
+
const [lastExpandedWidth, setLastExpandedWidth] = useState(DEFAULT_WIDTH);
|
|
776
|
+
const [availableWidth, setAvailableWidth] = useState(DEFAULT_WIDTH);
|
|
777
|
+
const [isAvatarOnly, setIsAvatarOnly] = useState(false);
|
|
778
|
+
const [isResizeActive, setIsResizeActive] = useState(false);
|
|
779
|
+
const resolvedActiveItemId = activeItemId ?? innerActiveItemId ?? fallbackActiveItemId;
|
|
780
|
+
const avatarOnlyItems = useMemo(
|
|
781
|
+
() => visibleSections.flatMap((section) => section.items || []),
|
|
782
|
+
[visibleSections],
|
|
783
|
+
);
|
|
784
|
+
const currentListWidth = resizedWidth ?? availableWidth ?? lastExpandedWidth ?? DEFAULT_WIDTH;
|
|
785
|
+
const isCardTwoColumn = isCardVariant && currentListWidth > CARD_TWO_COLUMN_THRESHOLD;
|
|
786
|
+
const isCardThreeColumn = isCardVariant && currentListWidth > CARD_THREE_COLUMN_THRESHOLD;
|
|
787
|
+
|
|
788
|
+
const handleTabClick = (tab) => {
|
|
789
|
+
if (activeTab === undefined) setInnerActiveTab(tab.id);
|
|
790
|
+
if (onTabChange) onTabChange(tab.id, tab);
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const handleVariantToggle = useCallback(() => {
|
|
794
|
+
const nextVariant = resolvedVariant === 'card' ? 'default' : 'card';
|
|
795
|
+
setInnerVariant(nextVariant);
|
|
796
|
+
if (!resizable && onLayoutWidthRequest) {
|
|
797
|
+
const requestedWidth = nextVariant === 'card'
|
|
798
|
+
? Math.max(availableWidth, CARD_MIN_WIDTH)
|
|
799
|
+
: Math.max(availableWidth, AVATAR_ONLY_THRESHOLD);
|
|
800
|
+
onLayoutWidthRequest(requestedWidth, { reason: 'variant-change', variant: nextVariant });
|
|
801
|
+
}
|
|
802
|
+
if (onVariantChange) onVariantChange(nextVariant);
|
|
803
|
+
}, [availableWidth, onLayoutWidthRequest, onVariantChange, resizable, resolvedVariant]);
|
|
804
|
+
|
|
805
|
+
const updateResizeState = useCallback((nextRawWidth, nextMaxWidth = null) => {
|
|
806
|
+
const maxWidth = nextMaxWidth ?? rootRef.current?.parentElement?.getBoundingClientRect?.().width ?? DEFAULT_WIDTH;
|
|
807
|
+
const minWidth = isCardVariant ? CARD_MIN_WIDTH : AVATAR_ONLY_WIDTH;
|
|
808
|
+
const clampedWidth = Math.max(minWidth, Math.min(nextRawWidth, maxWidth));
|
|
809
|
+
|
|
810
|
+
if (isCardVariant) {
|
|
811
|
+
setIsAvatarOnly(false);
|
|
812
|
+
setResizedWidth(clampedWidth);
|
|
813
|
+
setLastExpandedWidth(clampedWidth);
|
|
814
|
+
return {
|
|
815
|
+
clampedWidth,
|
|
816
|
+
nextAvatarOnly: false,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const nextAvatarOnly = clampedWidth < AVATAR_ONLY_THRESHOLD;
|
|
821
|
+
|
|
822
|
+
setIsAvatarOnly(nextAvatarOnly);
|
|
823
|
+
if (nextAvatarOnly) {
|
|
824
|
+
setResizedWidth(AVATAR_ONLY_WIDTH);
|
|
825
|
+
} else {
|
|
826
|
+
setResizedWidth(clampedWidth);
|
|
827
|
+
setLastExpandedWidth(clampedWidth);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
clampedWidth,
|
|
832
|
+
nextAvatarOnly,
|
|
833
|
+
};
|
|
834
|
+
}, [isCardVariant]);
|
|
835
|
+
|
|
836
|
+
const handleResizePointerDown = useCallback((event) => {
|
|
837
|
+
if (!resizable) return;
|
|
838
|
+
event.preventDefault();
|
|
839
|
+
setIsResizeActive(true);
|
|
840
|
+
|
|
841
|
+
const rootWidth = rootRef.current?.getBoundingClientRect?.().width ?? lastExpandedWidth;
|
|
842
|
+
const parentWidth = rootRef.current?.parentElement?.getBoundingClientRect?.().width ?? DEFAULT_WIDTH;
|
|
843
|
+
|
|
844
|
+
dragStateRef.current = {
|
|
845
|
+
startX: event.clientX,
|
|
846
|
+
startWidth: rootWidth,
|
|
847
|
+
maxWidth: parentWidth,
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const previousUserSelect = document.body.style.userSelect;
|
|
851
|
+
document.body.style.userSelect = 'none';
|
|
852
|
+
|
|
853
|
+
const handlePointerMove = (moveEvent) => {
|
|
854
|
+
if (!dragStateRef.current) return;
|
|
855
|
+
const deltaX = moveEvent.clientX - dragStateRef.current.startX;
|
|
856
|
+
const nextRawWidth = dragStateRef.current.startWidth + deltaX;
|
|
857
|
+
updateResizeState(nextRawWidth, dragStateRef.current.maxWidth);
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const stopDragging = (upEvent) => {
|
|
861
|
+
const finalRawWidth = dragStateRef.current
|
|
862
|
+
? dragStateRef.current.startWidth + (upEvent.clientX - dragStateRef.current.startX)
|
|
863
|
+
: rootWidth;
|
|
864
|
+
|
|
865
|
+
updateResizeState(finalRawWidth, dragStateRef.current?.maxWidth ?? parentWidth);
|
|
866
|
+
dragStateRef.current = null;
|
|
867
|
+
setIsResizeActive(false);
|
|
868
|
+
document.body.style.userSelect = previousUserSelect;
|
|
869
|
+
window.removeEventListener('pointermove', handlePointerMove);
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
window.addEventListener('pointermove', handlePointerMove);
|
|
873
|
+
window.addEventListener('pointerup', stopDragging, { once: true });
|
|
874
|
+
}, [lastExpandedWidth, resizable, updateResizeState]);
|
|
875
|
+
|
|
876
|
+
const handleResizeKeyDown = useCallback((event) => {
|
|
877
|
+
if (!resizable) return;
|
|
878
|
+
const currentWidth = isAvatarOnly
|
|
879
|
+
? AVATAR_ONLY_WIDTH
|
|
880
|
+
: (resizedWidth ?? rootRef.current?.getBoundingClientRect?.().width ?? lastExpandedWidth);
|
|
881
|
+
|
|
882
|
+
let nextWidth = currentWidth;
|
|
883
|
+
if (event.key === 'ArrowLeft') nextWidth -= RESIZE_STEP;
|
|
884
|
+
if (event.key === 'ArrowRight') nextWidth += RESIZE_STEP;
|
|
885
|
+
if (event.key === 'Home') nextWidth = AVATAR_ONLY_WIDTH;
|
|
886
|
+
if (event.key === 'End') {
|
|
887
|
+
nextWidth = Math.max(lastExpandedWidth, AVATAR_ONLY_THRESHOLD);
|
|
888
|
+
}
|
|
889
|
+
if (nextWidth === currentWidth) return;
|
|
890
|
+
|
|
891
|
+
event.preventDefault();
|
|
892
|
+
updateResizeState(nextWidth);
|
|
893
|
+
}, [isAvatarOnly, lastExpandedWidth, resizable, resizedWidth, updateResizeState]);
|
|
894
|
+
|
|
895
|
+
const handleExpandFromAvatarOnly = useCallback(() => {
|
|
896
|
+
if (!resizable && onLayoutWidthRequest) {
|
|
897
|
+
const requestedWidth = Math.max(lastExpandedWidth, DEFAULT_WIDTH, AVATAR_ONLY_THRESHOLD);
|
|
898
|
+
setIsAvatarOnly(false);
|
|
899
|
+
setResizedWidth(null);
|
|
900
|
+
onLayoutWidthRequest(requestedWidth, { reason: 'expand-from-avatar', variant: resolvedVariant });
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
updateResizeState(Math.max(lastExpandedWidth, AVATAR_ONLY_THRESHOLD));
|
|
904
|
+
}, [lastExpandedWidth, onLayoutWidthRequest, resizable, resolvedVariant, updateResizeState]);
|
|
905
|
+
|
|
906
|
+
const handleCollapseToAvatarOnly = useCallback(() => {
|
|
907
|
+
if (!resizable && onLayoutWidthRequest) {
|
|
908
|
+
setIsAvatarOnly(true);
|
|
909
|
+
setResizedWidth(AVATAR_ONLY_WIDTH);
|
|
910
|
+
onLayoutWidthRequest(AVATAR_ONLY_WIDTH, { reason: 'collapse-to-avatar', variant: resolvedVariant });
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
updateResizeState(AVATAR_ONLY_WIDTH);
|
|
914
|
+
}, [onLayoutWidthRequest, resizable, resolvedVariant, updateResizeState]);
|
|
915
|
+
|
|
916
|
+
const handleSectionToggle = (sectionId) => {
|
|
917
|
+
setExpandedSectionIds((current) => (
|
|
918
|
+
current.includes(sectionId)
|
|
919
|
+
? current.filter((id) => id !== sectionId)
|
|
920
|
+
: [...current, sectionId]
|
|
921
|
+
));
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
useEffect(() => {
|
|
925
|
+
setInnerVariant(variant ?? defaultVariant);
|
|
926
|
+
}, [defaultVariant, variant]);
|
|
927
|
+
|
|
928
|
+
useEffect(() => {
|
|
929
|
+
setCardItemOverrides({});
|
|
930
|
+
}, [sections]);
|
|
931
|
+
|
|
932
|
+
useEffect(() => {
|
|
933
|
+
const rootNode = rootRef.current;
|
|
934
|
+
if (!rootNode) return undefined;
|
|
935
|
+
const observedNode = resizable ? rootNode : (rootNode.parentElement ?? rootNode);
|
|
936
|
+
|
|
937
|
+
function syncAvatarOnlyByActualWidth() {
|
|
938
|
+
const availableWidth = observedNode.getBoundingClientRect().width;
|
|
939
|
+
if (!availableWidth) return;
|
|
940
|
+
setAvailableWidth(availableWidth);
|
|
941
|
+
|
|
942
|
+
if (isCardVariant) {
|
|
943
|
+
setIsAvatarOnly(false);
|
|
944
|
+
setLastExpandedWidth(availableWidth);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (!autoCollapseOnNarrow || !collapsible) return;
|
|
949
|
+
const nextAvatarOnly = availableWidth < AVATAR_ONLY_THRESHOLD;
|
|
950
|
+
setIsAvatarOnly(nextAvatarOnly);
|
|
951
|
+
if (!nextAvatarOnly) {
|
|
952
|
+
setLastExpandedWidth((current) => Math.max(current, availableWidth));
|
|
953
|
+
} else if (!resizable && onLayoutWidthRequest) {
|
|
954
|
+
onLayoutWidthRequest(AVATAR_ONLY_WIDTH, { reason: 'auto-collapse-on-narrow', variant: resolvedVariant });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
syncAvatarOnlyByActualWidth();
|
|
959
|
+
const observer = typeof ResizeObserver !== 'undefined'
|
|
960
|
+
? new ResizeObserver(syncAvatarOnlyByActualWidth)
|
|
961
|
+
: null;
|
|
962
|
+
observer?.observe(observedNode);
|
|
963
|
+
window.addEventListener('resize', syncAvatarOnlyByActualWidth);
|
|
964
|
+
|
|
965
|
+
return () => {
|
|
966
|
+
observer?.disconnect();
|
|
967
|
+
window.removeEventListener('resize', syncAvatarOnlyByActualWidth);
|
|
968
|
+
};
|
|
969
|
+
}, [autoCollapseOnNarrow, collapsible, isCardVariant, onLayoutWidthRequest, resizable, resolvedVariant]);
|
|
970
|
+
|
|
971
|
+
useEffect(() => {
|
|
972
|
+
const nextSectionIds = getSectionIds(resolvedSections);
|
|
973
|
+
setExpandedSectionIds((current) => {
|
|
974
|
+
const keptIds = current.filter((id) => nextSectionIds.includes(id));
|
|
975
|
+
const missingIds = nextSectionIds.filter((id) => !keptIds.includes(id));
|
|
976
|
+
return [...keptIds, ...missingIds];
|
|
977
|
+
});
|
|
978
|
+
}, [resolvedSections]);
|
|
979
|
+
|
|
980
|
+
useEffect(() => {
|
|
981
|
+
if (activeItemId !== undefined) return;
|
|
982
|
+
if (!hasItem(resolvedSections, innerActiveItemId)) {
|
|
983
|
+
setInnerActiveItemId(defaultActiveItemId || fallbackActiveItemId || null);
|
|
984
|
+
}
|
|
985
|
+
}, [activeItemId, defaultActiveItemId, fallbackActiveItemId, innerActiveItemId, resolvedSections]);
|
|
986
|
+
|
|
987
|
+
useEffect(() => {
|
|
988
|
+
if (activeItemId !== undefined) return;
|
|
989
|
+
if (!hasItem(visibleSections, innerActiveItemId)) {
|
|
990
|
+
setInnerActiveItemId(getFirstItemId(visibleSections) ?? fallbackActiveItemId ?? null);
|
|
991
|
+
}
|
|
992
|
+
}, [activeItemId, fallbackActiveItemId, innerActiveItemId, visibleSections]);
|
|
993
|
+
|
|
994
|
+
const handleItemClick = (item) => {
|
|
995
|
+
if (activeItemId === undefined) setInnerActiveItemId(item.id);
|
|
996
|
+
if (onItemClick) onItemClick(item);
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const handleCardSend = useCallback((card, sectionId) => {
|
|
1000
|
+
const draftText = typeof card?.draftText === 'string' ? card.draftText.trim() : '';
|
|
1001
|
+
if (!draftText) return;
|
|
1002
|
+
|
|
1003
|
+
sendSequenceRef.current += 1;
|
|
1004
|
+
const nextItem = buildNextCardAfterSend(card, draftText, sendSequenceRef.current);
|
|
1005
|
+
|
|
1006
|
+
setCardItemOverrides((current) => ({
|
|
1007
|
+
...current,
|
|
1008
|
+
[card.id]: {
|
|
1009
|
+
messages: nextItem.messages,
|
|
1010
|
+
draftText: nextItem.draftText,
|
|
1011
|
+
status: nextItem.status,
|
|
1012
|
+
},
|
|
1013
|
+
}));
|
|
1014
|
+
|
|
1015
|
+
if (activeItemId === undefined) setInnerActiveItemId(card.id);
|
|
1016
|
+
if (onSend) {
|
|
1017
|
+
onSend({
|
|
1018
|
+
item: card,
|
|
1019
|
+
draftText,
|
|
1020
|
+
nextItem,
|
|
1021
|
+
sectionId,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}, [activeItemId, onSend]);
|
|
1025
|
+
|
|
1026
|
+
const expandedWidth = resizedWidth ?? lastExpandedWidth ?? DEFAULT_WIDTH;
|
|
1027
|
+
const resolvedRootWidth = !isCardVariant && isAvatarOnly
|
|
1028
|
+
? `${AVATAR_ONLY_WIDTH}px`
|
|
1029
|
+
: (style?.width ?? `${expandedWidth}px`);
|
|
1030
|
+
const rootStyle = {
|
|
1031
|
+
...style,
|
|
1032
|
+
width: resolvedRootWidth,
|
|
1033
|
+
maxWidth: style?.maxWidth ?? '100%',
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
return (
|
|
1037
|
+
<section
|
|
1038
|
+
ref={rootRef}
|
|
1039
|
+
className={[
|
|
1040
|
+
ROOT,
|
|
1041
|
+
!isCardVariant && isAvatarOnly ? ROOT_COLLAPSED : ROOT_EXPANDED,
|
|
1042
|
+
className,
|
|
1043
|
+
].filter(Boolean).join(' ')}
|
|
1044
|
+
style={rootStyle}
|
|
1045
|
+
aria-label={title}
|
|
1046
|
+
data-tfds-component="ConversationList"
|
|
1047
|
+
data-tfds-variant={resolvedVariant}
|
|
1048
|
+
>
|
|
1049
|
+
{!isCardVariant && isAvatarOnly ? (
|
|
1050
|
+
<>
|
|
1051
|
+
<div className={AVATAR_ONLY_HEADER}>
|
|
1052
|
+
<Tooltip content="展开会话列表">
|
|
1053
|
+
<Button
|
|
1054
|
+
type="button"
|
|
1055
|
+
variant="ghost-black"
|
|
1056
|
+
size="sm"
|
|
1057
|
+
icon={<Icon name="layout-right-stroked" size={16} />}
|
|
1058
|
+
iconOnly
|
|
1059
|
+
onClick={handleExpandFromAvatarOnly}
|
|
1060
|
+
aria-label="展开会话列表"
|
|
1061
|
+
/>
|
|
1062
|
+
</Tooltip>
|
|
1063
|
+
</div>
|
|
1064
|
+
<div className={AVATAR_ONLY_LIST}>
|
|
1065
|
+
{avatarOnlyItems.map((item) => (
|
|
1066
|
+
<ConversationAvatarItem
|
|
1067
|
+
key={item.id}
|
|
1068
|
+
item={item}
|
|
1069
|
+
active={item.id === resolvedActiveItemId}
|
|
1070
|
+
onClick={handleItemClick}
|
|
1071
|
+
/>
|
|
1072
|
+
))}
|
|
1073
|
+
</div>
|
|
1074
|
+
</>
|
|
1075
|
+
) : (
|
|
1076
|
+
<>
|
|
1077
|
+
<header className={HEADER}>
|
|
1078
|
+
<div className={HEADER_MAIN}>
|
|
1079
|
+
{!isCardVariant && collapsible ? (
|
|
1080
|
+
<Tooltip content="收起会话列表">
|
|
1081
|
+
<Button
|
|
1082
|
+
type="button"
|
|
1083
|
+
variant="ghost-black"
|
|
1084
|
+
size="sm"
|
|
1085
|
+
icon={<Icon name="layout-right-stroked" size={16} />}
|
|
1086
|
+
iconOnly
|
|
1087
|
+
onClick={handleCollapseToAvatarOnly}
|
|
1088
|
+
aria-label="收起会话列表"
|
|
1089
|
+
/>
|
|
1090
|
+
</Tooltip>
|
|
1091
|
+
) : null}
|
|
1092
|
+
<h2 className={TITLE}>{title}</h2>
|
|
1093
|
+
</div>
|
|
1094
|
+
{showActions ? (
|
|
1095
|
+
<div className={ACTIONS}>
|
|
1096
|
+
{showLayoutToggle ? (
|
|
1097
|
+
<Tooltip content={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}>
|
|
1098
|
+
<Button
|
|
1099
|
+
type="button"
|
|
1100
|
+
variant="ghost-black"
|
|
1101
|
+
size="sm"
|
|
1102
|
+
icon={<Icon name="switch-horizontal-01-stroked" size={16} />}
|
|
1103
|
+
iconOnly
|
|
1104
|
+
onClick={handleVariantToggle}
|
|
1105
|
+
aria-label={isCardVariant ? '切换为默认列表' : '切换为卡片列表'}
|
|
1106
|
+
/>
|
|
1107
|
+
</Tooltip>
|
|
1108
|
+
) : null}
|
|
1109
|
+
<Tooltip content="搜索会话">
|
|
1110
|
+
<Button
|
|
1111
|
+
type="button"
|
|
1112
|
+
variant="ghost-black"
|
|
1113
|
+
size="sm"
|
|
1114
|
+
icon={<Icon name="search-lg-stroked" size={16} />}
|
|
1115
|
+
iconOnly
|
|
1116
|
+
aria-label="搜索会话"
|
|
1117
|
+
/>
|
|
1118
|
+
</Tooltip>
|
|
1119
|
+
<Tooltip content="筛选会话">
|
|
1120
|
+
<Button
|
|
1121
|
+
type="button"
|
|
1122
|
+
variant="ghost-black"
|
|
1123
|
+
size="sm"
|
|
1124
|
+
icon={<Icon name="filter-funnel-01-stroked" size={16} />}
|
|
1125
|
+
iconOnly
|
|
1126
|
+
aria-label="筛选会话"
|
|
1127
|
+
/>
|
|
1128
|
+
</Tooltip>
|
|
1129
|
+
<Tooltip content="查看工单文件">
|
|
1130
|
+
<Button
|
|
1131
|
+
type="button"
|
|
1132
|
+
variant="ghost-black"
|
|
1133
|
+
size="sm"
|
|
1134
|
+
icon={<Icon name="file-05-stroked" size={16} />}
|
|
1135
|
+
iconOnly
|
|
1136
|
+
aria-label="查看工单文件"
|
|
1137
|
+
/>
|
|
1138
|
+
</Tooltip>
|
|
1139
|
+
</div>
|
|
1140
|
+
) : null}
|
|
1141
|
+
</header>
|
|
1142
|
+
|
|
1143
|
+
<div className={TABS_WRAP}>
|
|
1144
|
+
<Tabs
|
|
1145
|
+
variant="segment"
|
|
1146
|
+
size="sm"
|
|
1147
|
+
items={resolvedTabs.map((tab) => ({ label: renderTabLabel(tab, resolvedTabCounts[tab.id]) }))}
|
|
1148
|
+
activeIndex={resolvedActiveTabIndex}
|
|
1149
|
+
onChange={(index) => {
|
|
1150
|
+
const nextTab = resolvedTabs[index];
|
|
1151
|
+
if (nextTab) handleTabClick(nextTab);
|
|
1152
|
+
}}
|
|
1153
|
+
className="max-w-full flex-wrap"
|
|
1154
|
+
aria-label="会话状态筛选"
|
|
1155
|
+
/>
|
|
1156
|
+
</div>
|
|
1157
|
+
|
|
1158
|
+
<div className={isCardVariant ? [
|
|
1159
|
+
CARD_LIST,
|
|
1160
|
+
isCardThreeColumn ? CARD_LIST_THREE_COLUMN : '',
|
|
1161
|
+
!isCardThreeColumn && isCardTwoColumn ? CARD_LIST_TWO_COLUMN : '',
|
|
1162
|
+
].filter(Boolean).join(' ') : LIST}>
|
|
1163
|
+
{isCardVariant ? (
|
|
1164
|
+
visibleSections.flatMap((section) => (
|
|
1165
|
+
(section.items || []).map((item) => {
|
|
1166
|
+
const resolvedItem = applyCardItemOverride(item, cardItemOverrides[item.id]);
|
|
1167
|
+
return (
|
|
1168
|
+
<ConversationCardItem
|
|
1169
|
+
key={item.id}
|
|
1170
|
+
item={resolvedItem}
|
|
1171
|
+
active={item.id === resolvedActiveItemId}
|
|
1172
|
+
onClick={handleItemClick}
|
|
1173
|
+
onSend={(card) => handleCardSend(card, section.id)}
|
|
1174
|
+
/>
|
|
1175
|
+
);
|
|
1176
|
+
})
|
|
1177
|
+
))
|
|
1178
|
+
) : (
|
|
1179
|
+
visibleSections.map((section) => {
|
|
1180
|
+
const expanded = expandedSectionIds.includes(section.id);
|
|
1181
|
+
const sectionContentId = `conversation-list-section-${section.id}`;
|
|
1182
|
+
const sectionItemCount = getSectionItemCount(section);
|
|
1183
|
+
|
|
1184
|
+
return (
|
|
1185
|
+
<section key={section.id} className={SECTION} aria-label={`${section.title}${sectionItemCount}`}>
|
|
1186
|
+
<button
|
|
1187
|
+
type="button"
|
|
1188
|
+
className={SECTION_HEADER}
|
|
1189
|
+
aria-expanded={expanded}
|
|
1190
|
+
aria-controls={sectionContentId}
|
|
1191
|
+
onClick={() => handleSectionToggle(section.id)}
|
|
1192
|
+
data-tfds-component="ConversationList.Section"
|
|
1193
|
+
>
|
|
1194
|
+
<span className={SECTION_TITLE}>
|
|
1195
|
+
<span className="truncate">{section.title}</span>
|
|
1196
|
+
<span>{sectionItemCount}</span>
|
|
1197
|
+
</span>
|
|
1198
|
+
<Icon
|
|
1199
|
+
name="chevron-up-stroked"
|
|
1200
|
+
size={16}
|
|
1201
|
+
aria-hidden="true"
|
|
1202
|
+
className={[SECTION_CARET, expanded ? '' : 'rotate-180'].filter(Boolean).join(' ')}
|
|
1203
|
+
/>
|
|
1204
|
+
</button>
|
|
1205
|
+
{expanded ? (
|
|
1206
|
+
<div id={sectionContentId} className={SECTION_CONTENT}>
|
|
1207
|
+
{(section.items || []).map((item) => (
|
|
1208
|
+
<ConversationItem
|
|
1209
|
+
key={item.id}
|
|
1210
|
+
item={item}
|
|
1211
|
+
active={item.id === resolvedActiveItemId}
|
|
1212
|
+
onClick={handleItemClick}
|
|
1213
|
+
/>
|
|
1214
|
+
))}
|
|
1215
|
+
</div>
|
|
1216
|
+
) : null}
|
|
1217
|
+
</section>
|
|
1218
|
+
);
|
|
1219
|
+
})
|
|
1220
|
+
)}
|
|
1221
|
+
</div>
|
|
1222
|
+
</>
|
|
1223
|
+
)}
|
|
1224
|
+
|
|
1225
|
+
{resizable && (isCardVariant || !isAvatarOnly) ? (
|
|
1226
|
+
<div
|
|
1227
|
+
className={RESIZE_HANDLE_CLASS}
|
|
1228
|
+
role="separator"
|
|
1229
|
+
aria-label="调整会话列表宽度"
|
|
1230
|
+
aria-orientation="vertical"
|
|
1231
|
+
aria-valuemin={isCardVariant ? CARD_MIN_WIDTH : AVATAR_ONLY_WIDTH}
|
|
1232
|
+
aria-valuenow={Math.round(isCardVariant ? currentListWidth : (isAvatarOnly ? AVATAR_ONLY_WIDTH : (resizedWidth ?? rootRef.current?.getBoundingClientRect?.().width ?? lastExpandedWidth)))}
|
|
1233
|
+
tabIndex={0}
|
|
1234
|
+
onPointerDown={handleResizePointerDown}
|
|
1235
|
+
onKeyDown={handleResizeKeyDown}
|
|
1236
|
+
onPointerEnter={() => setIsResizeActive(true)}
|
|
1237
|
+
onPointerLeave={() => {
|
|
1238
|
+
if (!dragStateRef.current) setIsResizeActive(false);
|
|
1239
|
+
}}
|
|
1240
|
+
onFocus={() => setIsResizeActive(true)}
|
|
1241
|
+
onBlur={() => {
|
|
1242
|
+
if (!dragStateRef.current) setIsResizeActive(false);
|
|
1243
|
+
}}
|
|
1244
|
+
data-tfds-component="ConversationList.ResizeHandle"
|
|
1245
|
+
>
|
|
1246
|
+
<span className={[RESIZE_HANDLE_BAR_CLASS, isResizeActive ? 'bg-brand-500' : 'bg-transparent'].join(' ')} />
|
|
1247
|
+
</div>
|
|
1248
|
+
) : null}
|
|
1249
|
+
</section>
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export default function ConversationList({
|
|
1254
|
+
variant = 'default',
|
|
1255
|
+
...props
|
|
1256
|
+
}) {
|
|
1257
|
+
return <ConversationListDefaultVariant variant={variant} {...props} />;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
export {
|
|
1261
|
+
DEFAULT_SECTIONS as CONVERSATION_LIST_SAMPLE_SECTIONS,
|
|
1262
|
+
DEFAULT_TABS as CONVERSATION_LIST_SAMPLE_TABS,
|
|
1263
|
+
buildFallbackDraftText as getConversationListEditableDraftText,
|
|
1264
|
+
};
|