@agentscope-ai/chat 1.1.53 → 1.1.54-beta.1773466321797

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.
@@ -2,16 +2,14 @@ import classnames from 'classnames';
2
2
  import React from 'react';
3
3
  import type { Attachment } from '..';
4
4
  import { AttachmentContext } from '../context';
5
- import { previewImage } from '../util';
6
- import AudioIcon from './AudioIcon';
7
- import Progress from './Progress';
8
- import VideoIcon from './VideoIcon';
9
5
  import Style from '../style/fileCard';
10
6
  import { useProviderContext } from '@agentscope-ai/chat';
11
7
  import { SparkFalseLine } from '@agentscope-ai/icons';
8
+ import ImageCard from './ImageCard';
9
+ import type { ImageCardProps } from './ImageCard';
12
10
 
13
11
 
14
- export interface FileListCardProps {
12
+ export interface FileListCardProps extends Pick<ImageCardProps, 'onReplace'> {
15
13
  /**
16
14
  * @description 自定义CSS类名前缀,用于样式隔离和主题定制
17
15
  * @descriptionEn Custom CSS class name prefix for style isolation and theme customization
@@ -42,7 +40,6 @@ export interface FileListCardProps {
42
40
  * @descriptionEn Render type, currently only supports default render mode
43
41
  */
44
42
  renderType?: 'default',
45
-
46
43
  }
47
44
 
48
45
  const EMPTY = '\u00A0';
@@ -126,7 +123,7 @@ function getSize(size: number) {
126
123
 
127
124
  function FileListCard(props: FileListCardProps, ref: React.Ref<HTMLDivElement>) {
128
125
  const { getPrefixCls } = useProviderContext();
129
- const { item, onRemove, className, style } = props;
126
+ const { item, onRemove, onReplace, className, style } = props;
130
127
  const context = React.useContext(AttachmentContext);
131
128
  const { disabled } = context || {};
132
129
  const { name, size, percent, status = 'done', description } = item;
@@ -141,7 +138,23 @@ function FileListCard(props: FileListCardProps, ref: React.Ref<HTMLDivElement>)
141
138
 
142
139
  const isImg = React.useMemo(() => matchExt(nameSuffix, IMG_EXTS), [nameSuffix]);
143
140
 
144
- const desc = React.useMemo(() => {
141
+ const renderType = props.renderType || 'default';
142
+ const isImgPreview = isImg && (item.originFileObj || item.thumbUrl || item.url) && renderType === 'default';
143
+
144
+ if (isImgPreview) {
145
+ return (
146
+ <ImageCard
147
+ ref={ref}
148
+ item={item}
149
+ onRemove={onRemove}
150
+ onReplace={onReplace}
151
+ className={className}
152
+ style={style}
153
+ />
154
+ );
155
+ }
156
+
157
+ const desc = (() => {
145
158
  if (description) {
146
159
  return description;
147
160
  }
@@ -155,9 +168,9 @@ function FileListCard(props: FileListCardProps, ref: React.Ref<HTMLDivElement>)
155
168
  }
156
169
 
157
170
  return size ? getSize(size) : EMPTY;
158
- }, [status, percent]);
171
+ })();
159
172
 
160
- const [icon, iconColor] = React.useMemo(() => {
173
+ const [icon, iconColor] = (() => {
161
174
  for (const { ext, icon, color } of PRESET_FILE_ICONS) {
162
175
  if (matchExt(nameSuffix, ext)) {
163
176
  return [icon, color];
@@ -165,57 +178,25 @@ function FileListCard(props: FileListCardProps, ref: React.Ref<HTMLDivElement>)
165
178
  }
166
179
 
167
180
  return [<IconImage url="https://gw.alicdn.com/imgextra/i1/O1CN01K7jgEj1sywWTkPSGY_!!6000000005836-55-tps-40-40.svg" key="defaultIcon" />, DEFAULT_ICON_COLOR];
168
- }, [nameSuffix]);
169
-
170
- const [previewImg, setPreviewImg] = React.useState<string>();
171
-
172
- React.useEffect(() => {
173
- if (item.originFileObj) {
174
- let synced = true;
175
- previewImage(item.originFileObj).then((url) => {
176
- if (synced) {
177
- setPreviewImg(url);
178
- }
179
- });
180
-
181
- return () => {
182
- synced = false;
183
- };
184
- }
185
- setPreviewImg(undefined);
186
- }, [item.originFileObj]);
187
-
188
- let content: React.ReactNode = null;
189
- const previewUrl = item.thumbUrl || item.url || previewImg;
190
- const renderType = props.renderType || 'default';
191
- const isImgPreview = isImg && (item.originFileObj || previewUrl) && renderType === 'default';
192
-
193
- if (isImgPreview) {
194
- content = (
195
- <>
196
- {
197
- previewUrl && (
198
- <img alt="preview" src={previewUrl} />
199
- )
200
- }
201
-
202
- {status !== 'done' && (
203
- <div className={`${cardCls}-img-mask`}>
204
- {status === 'uploading' && percent !== undefined && (
205
- <Progress percent={percent} prefixCls={cardCls} />
206
- )}
207
- {status === 'error' && (
208
- <div className={`${cardCls}-desc`}>
209
- <div className={`${cardCls}-ellipsis-prefix`}>{desc}</div>
210
- </div>
211
- )}
212
- </div>
181
+ })();
182
+
183
+ return (
184
+ <>
185
+ <Style />
186
+ <div
187
+ className={classnames(
188
+ cardCls,
189
+ {
190
+ [`${cardCls}-status-${status}`]: status,
191
+ [`${cardCls}-type-overview`]: true,
192
+ [`${cardCls}-type-${renderType}`]: true,
193
+ [`${cardCls}-hoverable`]: !disabled && onRemove,
194
+ },
195
+ className,
213
196
  )}
214
- </>
215
- );
216
- } else {
217
- content = (
218
- <>
197
+ style={style}
198
+ ref={ref}
199
+ >
219
200
  <div className={`${cardCls}-icon`} style={{ color: iconColor }}>
220
201
  {icon}
221
202
  </div>
@@ -227,44 +208,23 @@ function FileListCard(props: FileListCardProps, ref: React.Ref<HTMLDivElement>)
227
208
  <div className={`${cardCls}-ellipsis-prefix`}>{desc}</div>
228
209
  </div>
229
210
  </div>
230
- </>
231
- );
232
- }
233
-
234
- return <>
235
- <Style />
236
- <div
237
- className={classnames(
238
- cardCls,
239
- {
240
- [`${cardCls}-status-${status}`]: status,
241
- [`${cardCls}-type-preview`]: isImgPreview,
242
- [`${cardCls}-type-overview`]: !isImgPreview,
243
- [`${cardCls}-type-${renderType}`]: true,
244
- [`${cardCls}-hoverable`]: !disabled && onRemove,
245
- },
246
- className,
247
- )}
248
- style={style}
249
- ref={ref}
250
- >
251
- {content}
252
-
253
- <button
254
- style={{
255
- opacity: !disabled && onRemove ? 1 : 0,
256
- }}
257
- className={`${cardCls}-remove`}
258
- onClick={() => {
259
- if (!disabled && onRemove) {
260
- onRemove(item);
261
- }
262
- }}
263
- >
264
- <SparkFalseLine />
265
211
 
266
- </button>
267
- </div></>
212
+ <button
213
+ style={{
214
+ opacity: !disabled && onRemove ? 1 : 0,
215
+ }}
216
+ className={`${cardCls}-remove`}
217
+ onClick={() => {
218
+ if (!disabled && onRemove) {
219
+ onRemove(item);
220
+ }
221
+ }}
222
+ >
223
+ <SparkFalseLine />
224
+ </button>
225
+ </div>
226
+ </>
227
+ );
268
228
  }
269
229
 
270
230
  export default React.forwardRef(FileListCard);
@@ -0,0 +1,197 @@
1
+ import classnames from 'classnames';
2
+ import React from 'react';
3
+ import { Image } from 'antd';
4
+ import type { Attachment } from '..';
5
+ import { AttachmentContext } from '../context';
6
+ import { previewImage } from '../util';
7
+ import Progress from './Progress';
8
+ import Style from '../style/fileCard';
9
+ import { useProviderContext } from '@agentscope-ai/chat';
10
+ import { SparkFalseLine, SparkVisibleLine, SparkRefreshLine, SparkReplaceLine } from '@agentscope-ai/icons';
11
+
12
+ export interface ImageCardProps {
13
+ /**
14
+ * @description 文件附件数据对象,包含文件的基本信息
15
+ * @descriptionEn File attachment data object containing basic file information
16
+ */
17
+ item: Attachment;
18
+ /**
19
+ * @description 文件移除时的回调函数,用于处理文件删除操作
20
+ * @descriptionEn Callback function when file is removed for handling file deletion operations
21
+ */
22
+ onRemove?: (item: Attachment) => void;
23
+ /**
24
+ * @description 替换当前图片的回调,传入原始附件和新选择的文件
25
+ * @descriptionEn Callback to replace current image, receives the original attachment and newly selected file
26
+ */
27
+ onReplace?: (oldItem: Attachment, file: File) => void;
28
+ /**
29
+ * @description 组件的CSS类名
30
+ * @descriptionEn CSS class name for the component
31
+ */
32
+ className?: string;
33
+ /**
34
+ * @description 组件的内联样式对象
35
+ * @descriptionEn Inline style object for the component
36
+ */
37
+ style?: React.CSSProperties;
38
+ }
39
+
40
+ const EMPTY = '\u00A0';
41
+
42
+ const IMG_ACCEPT = 'image/png,image/jpeg,image/jpg,image/gif,image/bmp,image/webp,image/svg+xml';
43
+
44
+ function ImageCard(props: ImageCardProps, ref: React.Ref<HTMLDivElement>) {
45
+ const { getPrefixCls } = useProviderContext();
46
+ const { item, onRemove, onReplace, className, style } = props;
47
+ const context = React.useContext(AttachmentContext);
48
+ const { disabled } = context || {};
49
+ const { percent, status = 'done', description } = item;
50
+ const prefixCls = getPrefixCls('attachment');
51
+ const cardCls = `${prefixCls}-list-card`;
52
+
53
+ const [previewVisible, setPreviewVisible] = React.useState(false);
54
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
55
+
56
+ const previewConfig = React.useMemo(() => ({
57
+ visible: previewVisible,
58
+ onVisibleChange: setPreviewVisible,
59
+ }), [previewVisible]);
60
+
61
+ const desc = React.useMemo(() => {
62
+ if (description) {
63
+ return description;
64
+ }
65
+
66
+ if (status === 'uploading') {
67
+ return `${percent || 0}%`;
68
+ }
69
+
70
+ if (status === 'error') {
71
+ return item.response || EMPTY;
72
+ }
73
+
74
+ return EMPTY;
75
+ }, [description, status, percent, item.response]);
76
+
77
+ const [previewImg, setPreviewImg] = React.useState<string>();
78
+
79
+ React.useEffect(() => {
80
+ if (item.originFileObj) {
81
+ let synced = true;
82
+ previewImage(item.originFileObj).then((url) => {
83
+ if (synced) {
84
+ setPreviewImg(url);
85
+ }
86
+ });
87
+
88
+ return () => {
89
+ synced = false;
90
+ };
91
+ }
92
+ setPreviewImg(undefined);
93
+ }, [item.originFileObj]);
94
+
95
+ const previewUrl = item.thumbUrl || item.url || previewImg;
96
+
97
+ const handleRefreshClick = (e: React.MouseEvent) => {
98
+ e.stopPropagation();
99
+ fileInputRef.current?.click();
100
+ };
101
+
102
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
103
+ const file = e.target.files?.[0];
104
+ if (file && onReplace) {
105
+ onReplace(item, file);
106
+ }
107
+ if (fileInputRef.current) {
108
+ fileInputRef.current.value = '';
109
+ }
110
+ };
111
+
112
+ return (
113
+ <>
114
+ <Style />
115
+ <div
116
+ className={classnames(
117
+ cardCls,
118
+ {
119
+ [`${cardCls}-status-${status}`]: status,
120
+ [`${cardCls}-type-preview`]: true,
121
+ [`${cardCls}-hoverable`]: !disabled && onRemove,
122
+ },
123
+ className,
124
+ )}
125
+ style={style}
126
+ ref={ref}
127
+ >
128
+ {previewUrl && <img alt="preview" src={previewUrl} />}
129
+
130
+ <Image
131
+ src={previewUrl}
132
+ style={{ display: 'none' }}
133
+ preview={previewConfig}
134
+ />
135
+
136
+ {status !== 'done' && (
137
+ <div className={`${cardCls}-img-mask`}>
138
+ {status === 'uploading' && percent !== undefined && (
139
+ <Progress percent={percent} prefixCls={cardCls} />
140
+ )}
141
+ {status === 'error' && (
142
+ <div className={`${cardCls}-desc`}>
143
+ <div className={`${cardCls}-ellipsis-prefix`}>{desc}</div>
144
+ </div>
145
+ )}
146
+ </div>
147
+ )}
148
+
149
+ {status === 'done' && (
150
+ <div className={`${cardCls}-img-hover-mask`}>
151
+ <button
152
+ className={`${cardCls}-img-action`}
153
+ onClick={(e) => {
154
+ e.stopPropagation();
155
+ setPreviewVisible(true);
156
+ }}
157
+ >
158
+ <SparkVisibleLine />
159
+ </button>
160
+ {onReplace && (
161
+ <button
162
+ className={`${cardCls}-img-action`}
163
+ onClick={handleRefreshClick}
164
+ >
165
+ <SparkReplaceLine />
166
+ </button>
167
+ )}
168
+ </div>
169
+ )}
170
+
171
+ <input
172
+ ref={fileInputRef}
173
+ type="file"
174
+ accept={IMG_ACCEPT}
175
+ style={{ display: 'none' }}
176
+ onChange={handleFileChange}
177
+ />
178
+
179
+ <button
180
+ style={{
181
+ opacity: !disabled && onRemove ? 1 : 0,
182
+ }}
183
+ className={`${cardCls}-remove`}
184
+ onClick={() => {
185
+ if (!disabled && onRemove) {
186
+ onRemove(item);
187
+ }
188
+ }}
189
+ >
190
+ <SparkFalseLine />
191
+ </button>
192
+ </div>
193
+ </>
194
+ );
195
+ }
196
+
197
+ export default React.forwardRef(ImageCard);
@@ -22,6 +22,11 @@ export interface FileListProps {
22
22
  * @descriptionEn Callback function when file is removed for handling file deletion operations
23
23
  */
24
24
  onRemove: (item: Attachment) => void;
25
+ /**
26
+ * @description 替换图片文件的回调,传入原始附件和新选择的文件
27
+ * @descriptionEn Callback to replace an image file, receives the original attachment and newly selected file
28
+ */
29
+ onReplace?: (oldItem: Attachment, file: File) => void;
25
30
  /**
26
31
  * @description 文件列表的溢出处理方式,影响滚动和布局行为
27
32
  * @descriptionEn Overflow handling method for file list, affects scrolling and layout behavior
@@ -68,6 +73,7 @@ export default function FileList(props: FileListProps) {
68
73
  prefixCls,
69
74
  items,
70
75
  onRemove,
76
+ onReplace,
71
77
  overflow,
72
78
  listClassName,
73
79
  listStyle,
@@ -167,6 +173,7 @@ export default function FileList(props: FileListProps) {
167
173
  prefixCls={prefixCls}
168
174
  item={item}
169
175
  onRemove={onRemove}
176
+ onReplace={onReplace}
170
177
  className={classnames(motionCls, itemClassName)}
171
178
  style={{
172
179
  ...motionStyle,
@@ -7,6 +7,7 @@ import { useEvent, useMergedState } from 'rc-util';
7
7
  import DropArea from './DropArea';
8
8
  import FileList, { type FileListProps } from './FileList';
9
9
  import FileListCard from './FileList/FileListCard';
10
+ import ImageCard from './FileList/ImageCard';
10
11
  import PlaceholderUploader, {
11
12
  type PlaceholderProps,
12
13
  type PlaceholderType,
@@ -92,6 +93,11 @@ export interface AttachmentsProps extends Omit<UploadProps, 'fileList'> {
92
93
  * @descriptionEn Render type, currently only supports default render mode
93
94
  */
94
95
  renderType?: 'default',
96
+ /**
97
+ * @description 图片类型文件是否支持点击刷新按钮直接替换上传
98
+ * @descriptionEn Whether image files support direct replacement upload via the refresh button
99
+ */
100
+ replaceable?: boolean;
95
101
  }
96
102
 
97
103
  export interface AttachmentsRef {
@@ -121,6 +127,7 @@ function Attachments(props: AttachmentsProps, ref: React.Ref<AttachmentsRef>) {
121
127
  onChange,
122
128
  overflow,
123
129
  disabled,
130
+ replaceable,
124
131
  classNames = {},
125
132
  styles = {},
126
133
  ...uploadProps
@@ -170,6 +177,25 @@ function Attachments(props: AttachmentsProps, ref: React.Ref<AttachmentsRef>) {
170
177
  });
171
178
  };
172
179
 
180
+ const onItemReplace = useEvent((oldItem: Attachment, file: File) => {
181
+ const newAttachment: Attachment = {
182
+ uid: oldItem.uid,
183
+ name: file.name,
184
+ size: file.size,
185
+ type: file.type,
186
+ originFileObj: file as any,
187
+ status: 'done',
188
+ percent: 100,
189
+ };
190
+ const newFileList = fileList.map((fileItem) =>
191
+ fileItem.uid === oldItem.uid ? newAttachment : fileItem,
192
+ );
193
+ triggerChange({
194
+ file: newAttachment,
195
+ fileList: newFileList,
196
+ });
197
+ });
198
+
173
199
  let renderChildren: React.ReactElement;
174
200
 
175
201
  const getPlaceholderNode = (
@@ -233,6 +259,7 @@ function Attachments(props: AttachmentsProps, ref: React.Ref<AttachmentsRef>) {
233
259
  prefixCls={prefixCls}
234
260
  items={fileList}
235
261
  onRemove={onItemRemove}
262
+ onReplace={replaceable ? onItemReplace : undefined}
236
263
  overflow={overflow}
237
264
  upload={mergedUploadProps}
238
265
  listClassName={classnames(classNames.list)}
@@ -267,8 +294,10 @@ const ForwardAttachments = React.forwardRef(Attachments) as React.ForwardRefExot
267
294
  AttachmentsProps & React.RefAttributes<AttachmentsRef>
268
295
  > & {
269
296
  FileCard: typeof FileListCard;
297
+ ImageCard: typeof ImageCard;
270
298
  };
271
299
 
272
300
  ForwardAttachments.FileCard = FileListCard;
301
+ ForwardAttachments.ImageCard = ImageCard;
273
302
 
274
303
  export default ForwardAttachments;
@@ -61,7 +61,7 @@ export default createGlobalStyle`
61
61
  }
62
62
 
63
63
  &-type-preview {
64
- width: 56px;
64
+ width: 100px;
65
65
  height: 56px;
66
66
  line-height: 1;
67
67
 
@@ -70,7 +70,7 @@ export default createGlobalStyle`
70
70
  height: 100%;
71
71
  vertical-align: top;
72
72
  object-fit: cover;
73
- border-radius: 5px;
73
+ border-radius: 6px;
74
74
  }
75
75
 
76
76
  .${(p) => p.theme.prefixCls}-attachment-list-card-img-mask {
@@ -83,6 +83,44 @@ export default createGlobalStyle`
83
83
  border-radius: inherit;
84
84
  }
85
85
 
86
+ .${(p) => p.theme.prefixCls}-attachment-list-card-img-hover-mask {
87
+ position: absolute;
88
+ inset: 0;
89
+ display: flex;
90
+ flex-direction: row;
91
+ justify-content: center;
92
+ align-items: center;
93
+ gap: 16px;
94
+ background: rgba(20, 19, 39, 0.45);
95
+ border-radius: 6px;
96
+ opacity: 0;
97
+ transition: opacity 0.2s;
98
+ }
99
+
100
+ &:hover .${(p) => p.theme.prefixCls}-attachment-list-card-img-hover-mask {
101
+ opacity: 1;
102
+ }
103
+
104
+ .${(p) => p.theme.prefixCls}-attachment-list-card-img-action {
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ width: 20px;
109
+ height: 20px;
110
+ padding: 0;
111
+ border: none;
112
+ background: transparent;
113
+ color: ${(p) => p.theme.colorWhite};
114
+ font-size: 20px;
115
+ cursor: pointer;
116
+ line-height: 1;
117
+ transition: opacity 0.2s;
118
+
119
+ &:hover {
120
+ opacity: 0.8;
121
+ }
122
+ }
123
+
86
124
  &.${(p) => p.theme.prefixCls}-attachment-list-card-status-error {
87
125
 
88
126
  img,
@@ -161,7 +199,9 @@ export default createGlobalStyle`
161
199
 
162
200
  &:hover {
163
201
  border-color: ${(p) => p.theme.colorPrimary};
202
+ }
164
203
 
204
+ &.${(p) => p.theme.prefixCls}-attachment-list-card-type-overview:hover {
165
205
  &::after {
166
206
  content: '';
167
207
  position: absolute;
@@ -173,6 +213,5 @@ export default createGlobalStyle`
173
213
  background-color: rgba(0, 0, 0, 0.45);
174
214
  }
175
215
  }
176
-
177
216
  }
178
217
  `;
@@ -159,6 +159,7 @@ export default forwardRef(function (_, ref) {
159
159
  return <Attachments
160
160
  key={index}
161
161
  items={files}
162
+ replaceable={true}
162
163
  onChange={(info) => handleFileChange(index, info.fileList)}
163
164
  />
164
165
  })