@baishuyun/ai-design 2.0.35 → 2.0.37
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/CHANGELOG.md +17 -0
- package/package.json +4 -4
- package/src/components/attachment-part.tsx +123 -6
- package/src/components/attachments-previewer.tsx +6 -58
- package/src/components/bs-icons.tsx +0 -2
- package/src/components/collapsible-txt-msg.tsx +14 -2
- package/src/components/font-icon.tsx +3 -1
- package/src/components/ui/drawer.tsx +4 -1
- package/src/components/ui/popover.tsx +8 -1
- package/src/components/user-txt-msg-bubble.tsx +29 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @baishuyun/ai-design
|
|
2
2
|
|
|
3
|
+
## 2.0.37
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 移动端优化
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @baishuyun/ai-feat-utils@2.0.33
|
|
10
|
+
|
|
11
|
+
## 2.0.36
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 消息过长折叠显示,附件图标完善
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @baishuyun/ai-feat-utils@2.0.32
|
|
18
|
+
- @baishuyun/types@3.0.30
|
|
19
|
+
|
|
3
20
|
## 2.0.35
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baishuyun/ai-design",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.37",
|
|
4
4
|
"description": "AI 功能 UI 组件库 - 所有 AI 功能的 UI 表现唯一真相来源",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"tw-animate-css": "^1.4.0",
|
|
61
61
|
"vaul": "^1.1.2",
|
|
62
62
|
"zod": "^4.1.13",
|
|
63
|
-
"@baishuyun/ai-feat-utils": "2.0.
|
|
64
|
-
"@baishuyun/types": "3.0.
|
|
63
|
+
"@baishuyun/ai-feat-utils": "2.0.33",
|
|
64
|
+
"@baishuyun/types": "3.0.30"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"react": "^19.0.0",
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
"typescript": "^5.5.0",
|
|
84
84
|
"vite": "^5.1.4",
|
|
85
85
|
"vite-plugin-dts": "^4.5.4",
|
|
86
|
+
"@baishuyun/types": "3.0.30",
|
|
86
87
|
"@baishuyun/tailwind-config": "3.0.9",
|
|
87
|
-
"@baishuyun/types": "3.0.29",
|
|
88
88
|
"@baishuyun/typescript-config": "2.0.9"
|
|
89
89
|
},
|
|
90
90
|
"keywords": [],
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ComponentType } from 'react';
|
|
2
|
+
import { getIconUnicode } from '@baishuyun/ai-feat-utils';
|
|
2
3
|
import { cn } from '../lib/utils';
|
|
4
|
+
import { FontIcon } from './font-icon';
|
|
3
5
|
import { ImgMsgPart } from './img-part';
|
|
4
6
|
import { XlsxIcon, CsvFileIcon, TxtFileIcon, UnknownFileIcon } from './bs-icons';
|
|
5
7
|
|
|
@@ -22,12 +24,45 @@ export interface AttachmentPartProps {
|
|
|
22
24
|
onClick?: (part: FilePart) => void;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
type FileIconComponent = ComponentType
|
|
27
|
+
type FileIconComponent = ComponentType<{ color?: string }>;
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
/**
|
|
30
|
+
* 字体图标方案:文件类型图标使用 baishu 主字体的 icon-bsy-* 字形
|
|
31
|
+
* (码位见 ai-feat-utils/iconfont.ts)。注意宿主的 .icon-bsy-form 类自带
|
|
32
|
+
* color 规则(PC 为 #0265ff),继承色会被覆盖,因此颜色必须通过
|
|
33
|
+
* FontIcon 的 color prop 以内联样式落到 <i> 上
|
|
34
|
+
*/
|
|
35
|
+
const createFontFileIcon = (iconName: string): FileIconComponent => {
|
|
36
|
+
const FontFileIcon = ({ color }: { color?: string }) => (
|
|
37
|
+
<FontIcon
|
|
38
|
+
fontCls="icon-bsy-form"
|
|
39
|
+
code={getIconUnicode(iconName)}
|
|
40
|
+
color={color}
|
|
41
|
+
style={{ fontSize: 16, lineHeight: 1 }}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
FontFileIcon.displayName = `FontFileIcon(${iconName})`;
|
|
45
|
+
return FontFileIcon;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const DocFileIcon = createFontFileIcon('bsy-doc');
|
|
49
|
+
const DocxFileIcon = createFontFileIcon('bsy-docx');
|
|
50
|
+
const PptFileIcon = createFontFileIcon('bsy-ppt');
|
|
51
|
+
const PptxFileIcon = createFontFileIcon('bsy-pptx');
|
|
52
|
+
const PdfFileIcon = createFontFileIcon('bsy-pdf');
|
|
53
|
+
const ZipFileIcon = createFontFileIcon('bsy-zip');
|
|
54
|
+
const RarFileIcon = createFontFileIcon('bsy-rar');
|
|
55
|
+
const Mp4FileIcon = createFontFileIcon('bsy-mp4');
|
|
56
|
+
const WebmFileIcon = createFontFileIcon('bsy-webm');
|
|
57
|
+
const MovFileIcon = createFontFileIcon('bsy-mov');
|
|
58
|
+
const AudioFileIcon = createFontFileIcon('bsy-ogg');
|
|
59
|
+
|
|
60
|
+
export interface FileTypeConfig {
|
|
28
61
|
icon: FileIconComponent;
|
|
29
62
|
/** Icon original size for scaling */
|
|
30
63
|
iconSize: number;
|
|
64
|
+
/** Icon color (对齐 baishu BsyFileUpload 的 icon-bsy-* 配色),仅对字体图标生效 */
|
|
65
|
+
color?: string;
|
|
31
66
|
}
|
|
32
67
|
|
|
33
68
|
/** Map media types to icon configurations */
|
|
@@ -45,6 +80,44 @@ const FILE_TYPE_MAP: Record<string, FileTypeConfig> = {
|
|
|
45
80
|
// Text files
|
|
46
81
|
'text/plain': { icon: TxtFileIcon, iconSize: 16 },
|
|
47
82
|
'application/octet-stream': { icon: TxtFileIcon, iconSize: 16 }, // treat as text for ts/js files
|
|
83
|
+
|
|
84
|
+
// Word files
|
|
85
|
+
'application/msword': { icon: DocFileIcon, iconSize: 16, color: '#096dd9' },
|
|
86
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': {
|
|
87
|
+
icon: DocxFileIcon,
|
|
88
|
+
iconSize: 16,
|
|
89
|
+
color: '#096dd9',
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// PowerPoint files
|
|
93
|
+
'application/vnd.ms-powerpoint': { icon: PptFileIcon, iconSize: 16, color: '#d46b08' },
|
|
94
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': {
|
|
95
|
+
icon: PptxFileIcon,
|
|
96
|
+
iconSize: 16,
|
|
97
|
+
color: '#08979c',
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// PDF files
|
|
101
|
+
'application/pdf': { icon: PdfFileIcon, iconSize: 16, color: '#d4380d' },
|
|
102
|
+
|
|
103
|
+
// Archive files
|
|
104
|
+
'application/zip': { icon: ZipFileIcon, iconSize: 16, color: '#d46b08' },
|
|
105
|
+
'application/x-zip-compressed': { icon: ZipFileIcon, iconSize: 16, color: '#d46b08' },
|
|
106
|
+
'application/x-rar-compressed': { icon: RarFileIcon, iconSize: 16, color: '#389e0d' },
|
|
107
|
+
'application/vnd.rar': { icon: RarFileIcon, iconSize: 16, color: '#389e0d' },
|
|
108
|
+
'application/x-7z-compressed': { icon: ZipFileIcon, iconSize: 16, color: '#d46b08' },
|
|
109
|
+
'application/gzip': { icon: ZipFileIcon, iconSize: 16, color: '#d46b08' },
|
|
110
|
+
|
|
111
|
+
// Video files
|
|
112
|
+
'video/mp4': { icon: Mp4FileIcon, iconSize: 16, color: '#1d39c4' },
|
|
113
|
+
'video/webm': { icon: WebmFileIcon, iconSize: 16, color: '#d46b08' },
|
|
114
|
+
'video/quicktime': { icon: MovFileIcon, iconSize: 16, color: '#1d39c4' }, // mov
|
|
115
|
+
|
|
116
|
+
// Audio files
|
|
117
|
+
'audio/ogg': { icon: AudioFileIcon, iconSize: 16, color: '#d4380d' },
|
|
118
|
+
'audio/mpeg': { icon: AudioFileIcon, iconSize: 16, color: '#d4380d' }, // mp3
|
|
119
|
+
'audio/wav': { icon: AudioFileIcon, iconSize: 16, color: '#d4380d' },
|
|
120
|
+
'audio/x-wav': { icon: AudioFileIcon, iconSize: 16, color: '#d4380d' },
|
|
48
121
|
};
|
|
49
122
|
|
|
50
123
|
const DEFAULT_FILE_CONFIG: FileTypeConfig = {
|
|
@@ -53,17 +126,25 @@ const DEFAULT_FILE_CONFIG: FileTypeConfig = {
|
|
|
53
126
|
};
|
|
54
127
|
|
|
55
128
|
/** Check if media type is an image */
|
|
56
|
-
const isImageType = (mediaType?: string): boolean => {
|
|
129
|
+
export const isImageType = (mediaType?: string): boolean => {
|
|
57
130
|
return Boolean(mediaType?.startsWith('image/'));
|
|
58
131
|
};
|
|
59
132
|
|
|
60
133
|
/** Get file config based on media type and file extension */
|
|
61
|
-
const getFileConfig = (mediaType?: string, fileName?: string): FileTypeConfig => {
|
|
134
|
+
export const getFileConfig = (mediaType?: string, fileName?: string): FileTypeConfig => {
|
|
62
135
|
// Check media type first
|
|
63
136
|
if (mediaType && FILE_TYPE_MAP[mediaType]) {
|
|
64
137
|
return FILE_TYPE_MAP[mediaType];
|
|
65
138
|
}
|
|
66
139
|
|
|
140
|
+
// Prefix match for video/audio (covers mkv/avi/flac/m4a etc.)
|
|
141
|
+
if (mediaType?.startsWith('video/')) {
|
|
142
|
+
return FILE_TYPE_MAP['video/mp4'];
|
|
143
|
+
}
|
|
144
|
+
if (mediaType?.startsWith('audio/')) {
|
|
145
|
+
return FILE_TYPE_MAP['audio/ogg'];
|
|
146
|
+
}
|
|
147
|
+
|
|
67
148
|
// Fallback to file extension
|
|
68
149
|
if (fileName) {
|
|
69
150
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
@@ -81,6 +162,38 @@ const getFileConfig = (mediaType?: string, fileName?: string): FileTypeConfig =>
|
|
|
81
162
|
case 'json':
|
|
82
163
|
case 'md':
|
|
83
164
|
return FILE_TYPE_MAP['text/plain'];
|
|
165
|
+
case 'doc':
|
|
166
|
+
return FILE_TYPE_MAP['application/msword'];
|
|
167
|
+
case 'docx':
|
|
168
|
+
return FILE_TYPE_MAP['application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
|
169
|
+
case 'ppt':
|
|
170
|
+
return FILE_TYPE_MAP['application/vnd.ms-powerpoint'];
|
|
171
|
+
case 'pptx':
|
|
172
|
+
return FILE_TYPE_MAP['application/vnd.openxmlformats-officedocument.presentationml.presentation'];
|
|
173
|
+
case 'pdf':
|
|
174
|
+
return FILE_TYPE_MAP['application/pdf'];
|
|
175
|
+
case 'zip':
|
|
176
|
+
case '7z':
|
|
177
|
+
case 'gz':
|
|
178
|
+
case 'tar':
|
|
179
|
+
return FILE_TYPE_MAP['application/zip'];
|
|
180
|
+
case 'rar':
|
|
181
|
+
return FILE_TYPE_MAP['application/x-rar-compressed'];
|
|
182
|
+
case 'mp4':
|
|
183
|
+
case 'mkv':
|
|
184
|
+
case 'avi':
|
|
185
|
+
return FILE_TYPE_MAP['video/mp4'];
|
|
186
|
+
case 'webm':
|
|
187
|
+
return FILE_TYPE_MAP['video/webm'];
|
|
188
|
+
case 'mov':
|
|
189
|
+
return FILE_TYPE_MAP['video/quicktime'];
|
|
190
|
+
case 'ogg':
|
|
191
|
+
case 'mp3':
|
|
192
|
+
case 'wav':
|
|
193
|
+
case 'flac':
|
|
194
|
+
case 'aac':
|
|
195
|
+
case 'm4a':
|
|
196
|
+
return FILE_TYPE_MAP['audio/ogg'];
|
|
84
197
|
}
|
|
85
198
|
}
|
|
86
199
|
|
|
@@ -100,12 +213,14 @@ const FileMsgPart = ({
|
|
|
100
213
|
fileName,
|
|
101
214
|
icon: Icon,
|
|
102
215
|
iconSize,
|
|
216
|
+
color,
|
|
103
217
|
className,
|
|
104
218
|
size = 102,
|
|
105
219
|
}: {
|
|
106
220
|
fileName?: string;
|
|
107
221
|
icon: FileIconComponent;
|
|
108
222
|
iconSize: number;
|
|
223
|
+
color?: string;
|
|
109
224
|
className?: string;
|
|
110
225
|
size?: number;
|
|
111
226
|
}) => {
|
|
@@ -128,6 +243,7 @@ const FileMsgPart = ({
|
|
|
128
243
|
style={{
|
|
129
244
|
width: scaledIconSize,
|
|
130
245
|
height: scaledIconSize,
|
|
246
|
+
color,
|
|
131
247
|
}}
|
|
132
248
|
>
|
|
133
249
|
<div
|
|
@@ -136,7 +252,7 @@ const FileMsgPart = ({
|
|
|
136
252
|
transformOrigin: 'center',
|
|
137
253
|
}}
|
|
138
254
|
>
|
|
139
|
-
<Icon />
|
|
255
|
+
<Icon color={color} />
|
|
140
256
|
</div>
|
|
141
257
|
</div>
|
|
142
258
|
{hasFileName && (
|
|
@@ -162,7 +278,7 @@ export const AttachmentPart = ({ part, className, size = 102, iconOnly, onClick
|
|
|
162
278
|
}
|
|
163
279
|
|
|
164
280
|
// Render file icon for other types
|
|
165
|
-
const { icon, iconSize } = getFileConfig(mediaType, name);
|
|
281
|
+
const { icon, iconSize, color } = getFileConfig(mediaType, name);
|
|
166
282
|
const displayName = iconOnly ? undefined : getDisplayFileName(name);
|
|
167
283
|
|
|
168
284
|
return (
|
|
@@ -171,6 +287,7 @@ export const AttachmentPart = ({ part, className, size = 102, iconOnly, onClick
|
|
|
171
287
|
fileName={displayName}
|
|
172
288
|
icon={icon}
|
|
173
289
|
iconSize={iconSize}
|
|
290
|
+
color={color}
|
|
174
291
|
className={className}
|
|
175
292
|
size={size}
|
|
176
293
|
/>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cn } from '../lib/utils';
|
|
2
2
|
import { transCls } from '../const/ui';
|
|
3
|
-
import { useRef, useState, useEffect, useCallback
|
|
4
|
-
import { RemoveCircleIcon
|
|
3
|
+
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { RemoveCircleIcon } from './bs-icons';
|
|
5
5
|
import { LoaderIcon } from './ui/icons';
|
|
6
|
+
import { getFileConfig, isImageType } from './attachment-part';
|
|
6
7
|
|
|
7
8
|
export interface AttachmentItem {
|
|
8
9
|
id: string;
|
|
@@ -22,59 +23,6 @@ export interface AttachmentsPreviewerProps {
|
|
|
22
23
|
onImageRemove?: (attachment: AttachmentItem, index: number) => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
type FileIconComponent = ComponentType;
|
|
26
|
-
|
|
27
|
-
interface FileTypeConfig {
|
|
28
|
-
icon: FileIconComponent;
|
|
29
|
-
/** Icon original size for scaling */
|
|
30
|
-
iconSize: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const FILE_TYPE_MAP: Record<string, FileTypeConfig> = {
|
|
34
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
|
|
35
|
-
icon: XlsxIcon,
|
|
36
|
-
iconSize: 52,
|
|
37
|
-
},
|
|
38
|
-
'application/vnd.ms-excel': { icon: XlsxIcon, iconSize: 52 },
|
|
39
|
-
'text/csv': { icon: CsvFileIcon, iconSize: 16 },
|
|
40
|
-
'text/plain': { icon: TxtFileIcon, iconSize: 16 },
|
|
41
|
-
'application/octet-stream': { icon: TxtFileIcon, iconSize: 16 },
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const DEFAULT_FILE_CONFIG: FileTypeConfig = {
|
|
45
|
-
icon: UnknownFileIcon,
|
|
46
|
-
iconSize: 16,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const isImageType = (mediaType?: string): boolean => Boolean(mediaType?.startsWith('image/'));
|
|
50
|
-
|
|
51
|
-
const getFileConfig = (mediaType?: string, fileName?: string): FileTypeConfig => {
|
|
52
|
-
if (mediaType && FILE_TYPE_MAP[mediaType]) {
|
|
53
|
-
return FILE_TYPE_MAP[mediaType];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (fileName) {
|
|
57
|
-
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
58
|
-
switch (ext) {
|
|
59
|
-
case 'xlsx':
|
|
60
|
-
case 'xls':
|
|
61
|
-
return FILE_TYPE_MAP['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
|
|
62
|
-
case 'csv':
|
|
63
|
-
return FILE_TYPE_MAP['text/csv'];
|
|
64
|
-
case 'txt':
|
|
65
|
-
case 'ts':
|
|
66
|
-
case 'tsx':
|
|
67
|
-
case 'js':
|
|
68
|
-
case 'jsx':
|
|
69
|
-
case 'json':
|
|
70
|
-
case 'md':
|
|
71
|
-
return FILE_TYPE_MAP['text/plain'];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return DEFAULT_FILE_CONFIG;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
26
|
const ChevronRightIcon = () => (
|
|
79
27
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
80
28
|
<path
|
|
@@ -231,7 +179,7 @@ export const AttachmentsPreviewer = ({
|
|
|
231
179
|
>
|
|
232
180
|
{attachments.map((attachment, index) => {
|
|
233
181
|
const isImage = isImageType(attachment.contentType);
|
|
234
|
-
const { icon: Icon, iconSize } = getFileConfig(attachment.contentType, attachment.alt);
|
|
182
|
+
const { icon: Icon, iconSize, color } = getFileConfig(attachment.contentType, attachment.alt);
|
|
235
183
|
const scaledIconSize = (52 / 102) * imageSize;
|
|
236
184
|
const iconScale = scaledIconSize / iconSize;
|
|
237
185
|
|
|
@@ -255,7 +203,7 @@ export const AttachmentsPreviewer = ({
|
|
|
255
203
|
<div className="absolute inset-0 flex h-full w-full items-center justify-center rounded-lg bg-[#EFF0F6]">
|
|
256
204
|
<div
|
|
257
205
|
className="shrink-0 flex items-center justify-center"
|
|
258
|
-
style={{ width: scaledIconSize, height: scaledIconSize }}
|
|
206
|
+
style={{ width: scaledIconSize, height: scaledIconSize, color }}
|
|
259
207
|
>
|
|
260
208
|
<div
|
|
261
209
|
style={{
|
|
@@ -263,7 +211,7 @@ export const AttachmentsPreviewer = ({
|
|
|
263
211
|
transformOrigin: 'center',
|
|
264
212
|
}}
|
|
265
213
|
>
|
|
266
|
-
<Icon />
|
|
214
|
+
<Icon color={color} />
|
|
267
215
|
</div>
|
|
268
216
|
</div>
|
|
269
217
|
</div>
|
|
@@ -2,6 +2,7 @@ import { ReactNode, useState } from 'react';
|
|
|
2
2
|
import { cn } from '../lib/utils';
|
|
3
3
|
import { transCls } from '../const/ui';
|
|
4
4
|
import { AnalysisIcon, ChevronIcon } from './bs-icons';
|
|
5
|
+
import classNames from 'classnames';
|
|
5
6
|
|
|
6
7
|
export interface CollapsibleTxtMsgProps {
|
|
7
8
|
title: string;
|
|
@@ -10,11 +11,14 @@ export interface CollapsibleTxtMsgProps {
|
|
|
10
11
|
defaultOpen?: boolean;
|
|
11
12
|
children?: ReactNode;
|
|
12
13
|
|
|
14
|
+
ghost?: boolean;
|
|
15
|
+
|
|
13
16
|
disableExpand?: boolean;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export const CollapsibleTxtMsg = ({
|
|
17
20
|
title,
|
|
21
|
+
ghost,
|
|
18
22
|
content,
|
|
19
23
|
icon,
|
|
20
24
|
disableExpand,
|
|
@@ -23,8 +27,16 @@ export const CollapsibleTxtMsg = ({
|
|
|
23
27
|
}: CollapsibleTxtMsgProps) => {
|
|
24
28
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
25
29
|
|
|
30
|
+
const cls = classNames({
|
|
31
|
+
'bg-[#fafafa] border border-[#e0e0e0] border-solid p-2.5': !ghost
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const isGhostOpen = isOpen && ghost;
|
|
35
|
+
|
|
36
|
+
const t = isGhostOpen ? '' : title;
|
|
37
|
+
|
|
26
38
|
return (
|
|
27
|
-
<div className="
|
|
39
|
+
<div className={classNames("flex flex-col gap-2.5 rounded-lg w-full", cls)}>
|
|
28
40
|
<div
|
|
29
41
|
className={cn('flex items-center justify-between w-full', {
|
|
30
42
|
'cursor-pointer': !disableExpand,
|
|
@@ -37,7 +49,7 @@ export const CollapsibleTxtMsg = ({
|
|
|
37
49
|
>
|
|
38
50
|
<div className="flex gap-2.5 items-center">
|
|
39
51
|
<div className="w-4 h-4 shrink-0 flex items-center">{icon || <AnalysisIcon />}</div>
|
|
40
|
-
<p className="text-[#666] text-[14px] leading-normal">{
|
|
52
|
+
<p className="text-[#666] text-[14px] leading-normal">{t}</p>
|
|
41
53
|
</div>
|
|
42
54
|
<button
|
|
43
55
|
onClick={() => {
|
|
@@ -20,12 +20,14 @@ export const FontIcon = ({
|
|
|
20
20
|
code,
|
|
21
21
|
name,
|
|
22
22
|
className,
|
|
23
|
+
fontCls= 'sdk-icon',
|
|
23
24
|
style,
|
|
24
25
|
color,
|
|
25
26
|
}: {
|
|
26
27
|
code?: string;
|
|
27
28
|
name?: number;
|
|
28
29
|
className?: string;
|
|
30
|
+
fontCls?: string;
|
|
29
31
|
style?: React.CSSProperties;
|
|
30
32
|
color?: string;
|
|
31
33
|
}) => {
|
|
@@ -36,7 +38,7 @@ export const FontIcon = ({
|
|
|
36
38
|
if (code) {
|
|
37
39
|
return (
|
|
38
40
|
<i
|
|
39
|
-
className={
|
|
41
|
+
className={`${fontCls} ${className || ''}`}
|
|
40
42
|
style={{ ...style, color }}
|
|
41
43
|
dangerouslySetInnerHTML={{ __html: code }}
|
|
42
44
|
/>
|
|
@@ -58,9 +58,12 @@ function DrawerContent({
|
|
|
58
58
|
className,
|
|
59
59
|
children,
|
|
60
60
|
container: containerProp,
|
|
61
|
+
hideOverlay = false,
|
|
61
62
|
...props
|
|
62
63
|
}: React.ComponentProps<typeof DrawerPrimitive.Content> & {
|
|
63
64
|
container?: HTMLElement | null;
|
|
65
|
+
/** 全屏等场景下头部需保持可交互时,可隐藏遮罩 */
|
|
66
|
+
hideOverlay?: boolean;
|
|
64
67
|
}) {
|
|
65
68
|
const [autoContainer, setAutoContainer] = React.useState<HTMLElement | null>(null);
|
|
66
69
|
|
|
@@ -102,7 +105,7 @@ function DrawerContent({
|
|
|
102
105
|
<span ref={setDetectorRef} style={{ display: 'none' }} aria-hidden />
|
|
103
106
|
)}
|
|
104
107
|
<DrawerPortal container={effectiveContainer ?? undefined}>
|
|
105
|
-
<DrawerOverlay />
|
|
108
|
+
{!hideOverlay && <DrawerOverlay />}
|
|
106
109
|
<DrawerPrimitive.Content
|
|
107
110
|
data-slot="drawer-content"
|
|
108
111
|
className={cn(
|
|
@@ -20,7 +20,7 @@ const PopoverContent = React.forwardRef<
|
|
|
20
20
|
PopoverContentProps
|
|
21
21
|
>(
|
|
22
22
|
(
|
|
23
|
-
{ className, align = 'start', sideOffset = 4, portalContainer, style, onWheel, ...props },
|
|
23
|
+
{ className, align = 'start', sideOffset = 4, portalContainer, style, onWheel, onOpenAutoFocus, ...props },
|
|
24
24
|
ref
|
|
25
25
|
) => {
|
|
26
26
|
const handleWheel = portalContainer
|
|
@@ -45,6 +45,12 @@ const PopoverContent = React.forwardRef<
|
|
|
45
45
|
}
|
|
46
46
|
: onWheel;
|
|
47
47
|
|
|
48
|
+
// 默认阻止 Radix 打开浮层时自动聚焦内部第一个可聚焦元素(通常是搜索框),
|
|
49
|
+
// 移动端会因此唤起软键盘,不是期望的交互;调用方仍可显式传 onOpenAutoFocus 覆盖
|
|
50
|
+
const handleOpenAutoFocus = onOpenAutoFocus ?? ((event: Event) => {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
});
|
|
53
|
+
|
|
48
54
|
return (
|
|
49
55
|
<PopoverPrimitive.Portal container={portalContainer ?? undefined}>
|
|
50
56
|
<PopoverPrimitive.Content
|
|
@@ -52,6 +58,7 @@ const PopoverContent = React.forwardRef<
|
|
|
52
58
|
align={align}
|
|
53
59
|
sideOffset={sideOffset}
|
|
54
60
|
onWheel={handleWheel}
|
|
61
|
+
onOpenAutoFocus={handleOpenAutoFocus}
|
|
55
62
|
className={cn(
|
|
56
63
|
'z-50 w-60 rounded-[8px] bg-white p-1 text-[#121111] shadow-[0px_6px_16px_rgba(0,0,0,0.06)] outline-none',
|
|
57
64
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
+
import { TextIcon } from "./bs-icons";
|
|
2
|
+
import { CollapsibleTxtMsg } from "./collapsible-txt-msg";
|
|
3
|
+
|
|
1
4
|
export interface UserTextMsgBubbleProps {
|
|
2
5
|
text?: string;
|
|
3
6
|
children?: string;
|
|
4
7
|
}
|
|
5
8
|
|
|
6
9
|
export const UserTextMsgBubble = ({ text, children }: UserTextMsgBubbleProps) => {
|
|
10
|
+
const content = (text || children) as string;
|
|
11
|
+
|
|
12
|
+
if (typeof content !== 'string') {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className="px-2.5 py-[7px] text-[#0265FF] text-[14px] w-fit"
|
|
16
|
+
style={{
|
|
17
|
+
borderRadius: `10px 0 10px 10px`,
|
|
18
|
+
wordWrap: 'break-word',
|
|
19
|
+
background: `linear-gradient(180deg, #DBE9FF 0%, #E9FFFD 100%)`,
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
{content}
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isTooLong = content.length > 200;
|
|
28
|
+
const summary = `${content?.slice(0, 10)}...`;
|
|
7
29
|
return (
|
|
8
30
|
<div
|
|
9
31
|
className="px-2.5 py-[7px] text-[#0265FF] text-[14px] w-fit"
|
|
@@ -13,7 +35,13 @@ export const UserTextMsgBubble = ({ text, children }: UserTextMsgBubbleProps) =>
|
|
|
13
35
|
background: `linear-gradient(180deg, #DBE9FF 0%, #E9FFFD 100%)`,
|
|
14
36
|
}}
|
|
15
37
|
>
|
|
16
|
-
{
|
|
38
|
+
{isTooLong ? (
|
|
39
|
+
<CollapsibleTxtMsg
|
|
40
|
+
icon={<TextIcon />}
|
|
41
|
+
ghost
|
|
42
|
+
title={summary}
|
|
43
|
+
>{content}</CollapsibleTxtMsg>
|
|
44
|
+
) : content}
|
|
17
45
|
</div>
|
|
18
46
|
);
|
|
19
47
|
};
|