@baishuyun/chat-sdk 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +4 -4
  3. package/src/components/biz-comp/FieldCheckerListMsg.tsx +12 -2
  4. package/src/components/biz-comp/FieldValueChecker.tsx +27 -1
  5. package/src/components/biz-comp/SubformFieldsValueChecker.tsx +22 -1
  6. package/src/components/biz-comp/chat-client.tsx +4 -2
  7. package/src/components/biz-comp/multi-modal-input/index.tsx +31 -34
  8. package/src/components/biz-comp/multi-modal-input/prompt-input.tsx +1 -4
  9. package/src/components/biz-comp/opening-lines.tsx +1 -1
  10. package/src/components/biz-comp/preview-message-wrapper.tsx +8 -1
  11. package/src/components/biz-comp/preview-message.tsx +10 -1
  12. package/src/components/bs-ui/attachments-previewer.tsx +124 -32
  13. package/src/components/bs-ui/bot-avatar-name.tsx +1 -1
  14. package/src/components/bs-ui/card.tsx +1 -1
  15. package/src/components/bs-ui/fields-previewer.tsx +5 -0
  16. package/src/components/bs-ui/form-info-editor.tsx +2 -3
  17. package/src/components/bs-ui/line-checker.tsx +6 -1
  18. package/src/components/bs-ui/previewer-header.tsx +32 -5
  19. package/src/components/bs-ui/tab-radio-group.tsx +12 -3
  20. package/src/components/bs-ui/tooltip.tsx +37 -2
  21. package/src/components/ui/tooltip.tsx +37 -18
  22. package/src/plugins/form-builder-plugin/index.ts +3 -0
  23. package/src/plugins/form-filling-plugin/components/FormFillingOpeningLines.tsx +1 -1
  24. package/src/plugins/form-filling-plugin/components/mode-select.tsx +10 -2
  25. package/src/plugins/form-filling-plugin/components/msg-part.tsx +26 -1
  26. package/src/plugins/form-filling-plugin/const.ts +1 -1
  27. package/src/plugins/form-filling-plugin/index.ts +9 -0
  28. package/src/plugins/report-query-plugin/index.ts +2 -2
  29. package/src/stories/AttachmentsPreviewer.stories.tsx +118 -0
  30. package/src/style.css +20 -0
  31. package/dist/chat-sdk.js +0 -58809
  32. package/dist/chat-sdk.js.map +0 -1
  33. package/dist/chat-sdk.umd.cjs +0 -663
  34. package/dist/chat-sdk.umd.cjs.map +0 -1
  35. package/dist/index.css +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @baishuyun/chat-sdk
2
2
 
3
+ ## 0.0.12
4
+
5
+ ### Patch Changes
6
+
7
+ - udpate style
8
+ - Updated dependencies
9
+ - @baishuyun/agents@0.0.12
10
+
3
11
  ## 0.0.11
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/chat-sdk",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "",
5
5
  "main": "src/index.jsx",
6
6
  "module": "dist/chat-sdk.js",
@@ -35,7 +35,7 @@
35
35
  "tw-animate-css": "^1.4.0",
36
36
  "use-stick-to-bottom": "^1.1.1",
37
37
  "zustand": "^5.0.8",
38
- "@baishuyun/agents": "0.0.11"
38
+ "@baishuyun/agents": "0.0.12"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@storybook/react-vite": "^10.1.11",
@@ -51,8 +51,8 @@
51
51
  "tailwindcss": "^4.1.17",
52
52
  "vite": "^5.1.4",
53
53
  "vite-plugin-dts": "^4.5.4",
54
- "@baishuyun/typescript-config": "0.0.11",
55
- "@baishuyun/types": "1.0.11"
54
+ "@baishuyun/types": "1.0.12",
55
+ "@baishuyun/typescript-config": "0.0.12"
56
56
  },
57
57
  "exports": {
58
58
  ".": {
@@ -36,11 +36,21 @@ export const FieldCheckerListMsg = memo(
36
36
  );
37
37
 
38
38
  export const FieldValueCheckerListMsg = memo(
39
- ({ props, children }: { props: Array<FieldValueCheckerProps>; children?: ReactNode }) => {
39
+ ({
40
+ props,
41
+ readonly,
42
+ children,
43
+ streaming,
44
+ }: {
45
+ props: Array<FieldValueCheckerProps>;
46
+ children?: ReactNode;
47
+ readonly?: boolean;
48
+ streaming?: boolean;
49
+ }) => {
40
50
  return (
41
51
  <CollapsibleTxtMsg title="字段填写" icon={<EditIcon />} defaultOpen>
42
52
  {props.map((f) => (
43
- <FieldValueChecker {...f} />
53
+ <FieldValueChecker {...f} readonly={readonly} streaming={streaming} />
44
54
  ))}
45
55
  {children}
46
56
  </CollapsibleTxtMsg>
@@ -14,12 +14,22 @@ export interface FieldValueCheckerProps {
14
14
  field: ValueOf<Pick<ToolUIPart<FieldsTools>, 'output'>>;
15
15
  isSubField?: boolean;
16
16
  disabled?: boolean;
17
+ readonly?: boolean;
17
18
  onChange?: (checked: boolean, field: Field, parentFieldName?: string, index?: number) => void;
18
19
  onLoaded?: (field: Field) => void;
20
+ streaming?: boolean;
19
21
  }
20
22
 
21
23
  export const FieldValueChecker = memo(
22
- ({ field, disabled, isSubField, onChange, onLoaded }: FieldValueCheckerProps) => {
24
+ ({
25
+ field,
26
+ disabled,
27
+ isSubField,
28
+ readonly,
29
+ streaming,
30
+ onChange,
31
+ onLoaded,
32
+ }: FieldValueCheckerProps) => {
23
33
  const { widget, label } = field;
24
34
  if (!widget) {
25
35
  return null;
@@ -52,20 +62,24 @@ export const FieldValueChecker = memo(
52
62
  return (
53
63
  <>
54
64
  <LineChecker
65
+ streaming={streaming}
55
66
  title={label}
56
67
  disabled={disabled}
57
68
  defaultChecked={true}
58
69
  onCheckedChange={handleCheck}
59
70
  className={extraPadding}
71
+ readonly={readonly}
60
72
  shortDesc={isSubForm ? '' : (value as string)}
61
73
  // extra={typeDesc}
62
74
  />
63
75
  {isSubForm && (
64
76
  <SubformFieldsValueChecker
77
+ readonly={readonly}
65
78
  subformField={field}
66
79
  onChange={(detail) => {
67
80
  onChange?.(detail.checked, detail.field, detail.parentFieldName, detail.row);
68
81
  }}
82
+ streaming={streaming}
69
83
  />
70
84
  )}
71
85
  </>
@@ -90,6 +104,18 @@ export const FieldValueChecker = memo(
90
104
  return false;
91
105
  }
92
106
 
107
+ // 只读状态一致
108
+ const isReadonlyEqual = p1.readonly === p2.readonly;
109
+ if (!isReadonlyEqual) {
110
+ return false;
111
+ }
112
+
113
+ // 流式状态一致
114
+ const isStreamingEqual = p1.streaming === p2.streaming;
115
+ if (!isStreamingEqual) {
116
+ return false;
117
+ }
118
+
93
119
  // 进一步比较子表单下的字段
94
120
  return isSubformFieldEqual(p1?.field, p2?.field);
95
121
  }
@@ -13,6 +13,8 @@ const Checker = ({
13
13
 
14
14
  label,
15
15
  val,
16
+ streaming,
17
+ readonly,
16
18
  // desc,
17
19
  }: {
18
20
  label: string;
@@ -20,9 +22,13 @@ const Checker = ({
20
22
  onCheckedChange: (v: boolean) => void;
21
23
  disabled?: boolean;
22
24
  desc: string;
25
+ readonly?: boolean;
26
+ streaming?: boolean;
23
27
  }) => {
24
28
  return (
25
29
  <LineChecker
30
+ streaming={streaming}
31
+ readonly={readonly}
26
32
  title={label}
27
33
  disabled={disabled}
28
34
  defaultChecked={true}
@@ -37,12 +43,16 @@ const Checker = ({
37
43
  const SubFieldChecker = memo(
38
44
  ({
39
45
  field,
46
+ readonly,
40
47
  onCheckedChange,
48
+ streaming,
41
49
  }: {
42
50
  onCheckedChange: (
43
51
  checked: boolean,
44
52
  field: ValueOf<Pick<ToolUIPart<FieldsTools>, 'output'>>
45
53
  ) => void;
54
+ readonly?: boolean;
55
+ streaming?: boolean;
46
56
  field: ValueOf<Pick<ToolUIPart<FieldsTools>, 'output'>>;
47
57
  }) => {
48
58
  const { widget, label } = field;
@@ -74,6 +84,8 @@ const SubFieldChecker = memo(
74
84
 
75
85
  return (
76
86
  <Checker
87
+ streaming={streaming}
88
+ readonly={readonly}
77
89
  desc={typeDesc}
78
90
  label={label}
79
91
  val={value as string}
@@ -82,7 +94,10 @@ const SubFieldChecker = memo(
82
94
  );
83
95
  },
84
96
  (p1, p2) => {
85
- const isNameEqual = p1.field.widget?.widgetName === p2.field.widget?.widgetName;
97
+ const isNameEqual =
98
+ p1.field.widget?.widgetName === p2.field.widget?.widgetName &&
99
+ p1.readonly === p2.readonly &&
100
+ p1.streaming === p2.streaming;
86
101
  return isNameEqual;
87
102
  }
88
103
  );
@@ -110,9 +125,13 @@ type onSubformFieldChangeParam = {
110
125
  export const SubformFieldsValueChecker = ({
111
126
  subformField,
112
127
  onChange,
128
+ streaming,
129
+ readonly,
113
130
  }: {
114
131
  onChange: (p: onSubformFieldChangeParam) => void;
115
132
  subformField: ValueOf<Pick<ToolUIPart<FieldsTools>, 'output'>>;
133
+ streaming?: boolean;
134
+ readonly?: boolean;
116
135
  }) => {
117
136
  if (!subformField.widget.value || !subformField.widget.value.length) {
118
137
  return null;
@@ -135,6 +154,8 @@ export const SubformFieldsValueChecker = ({
135
154
  {rowData.map((fData: generatedField, col: number) => {
136
155
  return (
137
156
  <SubFieldChecker
157
+ readonly={readonly}
158
+ streaming={streaming}
138
159
  field={{
139
160
  label: fData.label,
140
161
  widget: {
@@ -14,7 +14,7 @@ import { useDefaultPluginCustomComponent } from '@/hooks/use-plugin-custom-compo
14
14
  import { BotAvatarAndName } from '../bs-ui/bot-avatar-name';
15
15
  import { ChatEntryButtonConfig } from '@baishuyun/types';
16
16
  import { useDraggable } from '@/hooks/use-draggable';
17
- import { TooltipProvider } from '@radix-ui/react-tooltip';
17
+ import { TooltipProvider } from '../ui/tooltip';
18
18
 
19
19
  const ChatSlot = ({ client }: { client?: ChatSDK }) => {
20
20
  const { setStatus, isVisible, isFloat, isDock } = useChatStatus();
@@ -283,7 +283,9 @@ export const ChatWidgets = ({ client }: { client: ChatSDK }) => {
283
283
  export const ChatClient = ({ store, client }: { client: ChatSDK; store: ChatStore }) => {
284
284
  return (
285
285
  <ChatStoreProvider store={store}>
286
- <TooltipProvider>
286
+ <TooltipProvider
287
+ zIndex={client?.options?.safeZIndex != null ? client.options.safeZIndex + 1 : 50}
288
+ >
287
289
  <ChatWidgets client={client} />
288
290
  </TooltipProvider>
289
291
  </ChatStoreProvider>
@@ -19,7 +19,7 @@ import {
19
19
  PromptInputToolbar,
20
20
  PromptInputTools,
21
21
  } from './prompt-input';
22
- import { PreviewAttachment } from './preview-attachment';
22
+ import { AttachmentsPreviewer } from '@/components/bs-ui/attachments-previewer';
23
23
  import { ChatMessage, Attachment } from '@baishuyun/types';
24
24
  import { Button } from '@/components/ui/button';
25
25
  import { usePluginLifeCycleChainRunner } from '@/hooks/use-plugin-life-cycle-chain-runner';
@@ -221,6 +221,9 @@ function PureMultimodalInput({
221
221
  [setAttachments, uploadFile]
222
222
  );
223
223
 
224
+ const accept = useChatPreference()?.acceptAttachmentFileType?.join(',');
225
+ const hasAcceptableFileType = !!accept;
226
+
224
227
  // Add paste event listener to textarea
225
228
  useEffect(() => {
226
229
  const textarea = textareaRef.current;
@@ -241,7 +244,7 @@ function PureMultimodalInput({
241
244
  ref={fileInputRef}
242
245
  tabIndex={-1}
243
246
  type="file"
244
- // accept=".csv,text/csv,text/plain,.txt"
247
+ accept={accept}
245
248
  />
246
249
 
247
250
  <PromptInput
@@ -257,37 +260,29 @@ function PureMultimodalInput({
257
260
  }}
258
261
  >
259
262
  {(attachments.length > 0 || uploadQueue.length > 0) && (
260
- <div
261
- className="flex flex-row items-end gap-2 overflow-x-scroll"
262
- data-testid="attachments-preview"
263
- >
264
- {attachments.map((attachment) => (
265
- <PreviewAttachment
266
- attachment={attachment}
267
- key={attachment.url}
268
- onRemove={() => {
269
- setAttachments((currentAttachments) =>
270
- currentAttachments.filter((a) => a.url !== attachment.url)
271
- );
272
- if (fileInputRef.current) {
273
- fileInputRef.current.value = '';
274
- }
275
- }}
276
- />
277
- ))}
278
-
279
- {uploadQueue.map((filename) => (
280
- <PreviewAttachment
281
- attachment={{
282
- url: '',
283
- name: filename,
284
- contentType: '',
285
- }}
286
- isUploading={true}
287
- key={filename}
288
- />
289
- ))}
290
- </div>
263
+ <AttachmentsPreviewer
264
+ attachments={[
265
+ ...attachments.map((a) => ({
266
+ id: a.url,
267
+ url: a.url,
268
+ alt: a.name,
269
+ contentType: a.contentType,
270
+ })),
271
+ ...uploadQueue.map((filename) => ({
272
+ id: `uploading-${filename}`,
273
+ url: '',
274
+ alt: filename,
275
+ isLoading: true,
276
+ })),
277
+ ]}
278
+ onImageRemove={(item) => {
279
+ if (item.id.startsWith('uploading-')) return;
280
+ setAttachments((cur) => cur.filter((a) => a.url !== item.id));
281
+ if (fileInputRef.current) {
282
+ fileInputRef.current.value = '';
283
+ }
284
+ }}
285
+ />
291
286
  )}
292
287
  <div className="flex flex-row items-start gap-1 sm:gap-2">
293
288
  <PromptInputTextarea
@@ -306,7 +301,9 @@ function PureMultimodalInput({
306
301
  <PromptInputToolbar className="border-top-0! border-t-0! p-0 shadow-none dark:border-0 dark:border-transparent!">
307
302
  <div></div>
308
303
  <PromptInputTools className="gap-2.5">
309
- <AttachmentsButton fileInputRef={fileInputRef} status={status} />
304
+ {hasAcceptableFileType ? (
305
+ <AttachmentsButton fileInputRef={fileInputRef} status={status} />
306
+ ) : null}
310
307
  <ClearBtn setMessages={setMessages} status={status} />
311
308
  <DividerIcon />
312
309
  {status === 'submitted' || status === 'streaming' ? (
@@ -104,10 +104,7 @@ export const PromptInputToolbar = ({ className, ...props }: PromptInputToolbarPr
104
104
  export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
105
105
 
106
106
  export const PromptInputTools = ({ className, ...props }: PromptInputToolsProps) => (
107
- <div
108
- className={cn('flex items-center gap-1', '[&_button:first-child]:rounded-bl-xl', className)}
109
- {...props}
110
- />
107
+ <div className={cn('flex items-center gap-1', className)} {...props} />
111
108
  );
112
109
 
113
110
  export type PromptInputButtonProps = ComponentProps<typeof Button>;
@@ -11,7 +11,7 @@ export const OpeningLines = ({ sendMessage, setMessages }: OpeningLinesProps) =>
11
11
 
12
12
  return (
13
13
  <>
14
- 请问可以帮你创建什么表单?试试下面的建议吧!
14
+ 👋 你好,你想创建什么表单呢,不知道的话,可以试试下面的建议~
15
15
  <Suggestions
16
16
  onSelect={(title) => {
17
17
  sendMessage(
@@ -53,13 +53,20 @@ export const PreviewMessageWrapper = ({
53
53
  pointerEventsCls
54
54
  )}
55
55
  >
56
- <IconBtn icon={<CopyIcon />} onClick={onCopy} />
56
+ <IconBtn
57
+ tooltip="复制"
58
+ icon={<CopyIcon />}
59
+ onClick={onCopy}
60
+ className="!hover:bg-[#eff0f6]"
61
+ />
57
62
  {onGenerate ? (
58
63
  <IconBtn
64
+ tooltip="重新生成"
59
65
  icon={<RefreshIcon />}
60
66
  onClick={() => {
61
67
  onGenerate && onGenerate();
62
68
  }}
69
+ className="!hover:bg-[#eff0f6]"
63
70
  />
64
71
  ) : null}
65
72
  </div>
@@ -21,6 +21,7 @@ import { GenerateAnimation } from '../bs-ui/generate-animation';
21
21
  import { AttachmentPartGroup } from '../bs-ui/attachment-part-group';
22
22
  import { AttachmentPart } from '../bs-ui/attachment-part';
23
23
  import { useChatStatus } from '@/hooks/use-frame-mode';
24
+ import { useEvtBus } from '@/hooks/use-evt-bus';
24
25
 
25
26
  const PurePreviewMessage = ({
26
27
  message,
@@ -69,6 +70,8 @@ const PurePreviewMessage = ({
69
70
 
70
71
  const parts = onBeforePartsRender(fileGroupedParts);
71
72
 
73
+ const evtBus = useEvtBus();
74
+
72
75
  const { exec: onBeforeSendMsg } = usePluginLifeCycleChainRunner('onBeforeMessageSend');
73
76
 
74
77
  const handleCopy = () => {
@@ -78,7 +81,13 @@ const PurePreviewMessage = ({
78
81
  .join('\n');
79
82
 
80
83
  if (textParts) {
81
- navigator.clipboard.writeText(textParts);
84
+ try {
85
+ navigator.clipboard.writeText(textParts);
86
+ evtBus.emit('chat-msg-copied', {
87
+ messageId: message.id,
88
+ content: textParts,
89
+ });
90
+ } catch (err) {}
82
91
  }
83
92
  };
84
93
 
@@ -1,12 +1,16 @@
1
1
  import { cn } from '@/lib/utils';
2
2
  import { transCls } from '@/const/ui';
3
- import { useRef, useState, useEffect, useCallback } from 'react';
4
- import { RemoveCircleIcon } from './bs-icons';
3
+ import { useRef, useState, useEffect, useCallback, ComponentType } from 'react';
4
+ import { RemoveCircleIcon, XlsxIcon, CsvFileIcon, TxtFileIcon, UnknownFileIcon } from './bs-icons';
5
+ import { LoaderIcon } from '@/components/ui/icons';
5
6
 
6
7
  export interface AttachmentItem {
7
8
  id: string;
8
9
  url: string;
9
10
  alt?: string;
11
+ isLoading?: boolean;
12
+ /** MIME type, used to decide whether to show image or icon */
13
+ contentType?: string;
10
14
  }
11
15
 
12
16
  export interface AttachmentsPreviewerProps {
@@ -18,6 +22,59 @@ export interface AttachmentsPreviewerProps {
18
22
  onImageRemove?: (attachment: AttachmentItem, index: number) => void;
19
23
  }
20
24
 
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
+
21
78
  const ChevronRightIcon = () => (
22
79
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
23
80
  <path
@@ -53,6 +110,7 @@ const NavButton = ({
53
110
  }) => {
54
111
  return (
55
112
  <button
113
+ type="button"
56
114
  onClick={onClick}
57
115
  className={cn(
58
116
  'flex items-center justify-center',
@@ -171,36 +229,70 @@ export const AttachmentsPreviewer = ({
171
229
  className="flex overflow-x-auto scrollbar-hide py-[8px]"
172
230
  style={{ gap, height: imageSize + 16 }}
173
231
  >
174
- {attachments.map((attachment, index) => (
175
- <div
176
- key={attachment.id}
177
- className={cn('relative shrink-0 cursor-pointer group', transCls)}
178
- style={{ width: imageSize, height: imageSize }}
179
- onClick={() => onImageClick?.(attachment, index)}
180
- >
181
- <img
182
- src={attachment.url}
183
- alt={attachment.alt || `Attachment ${index + 1}`}
184
- className="absolute inset-0 w-full h-full object-cover rounded-[10px]"
185
- />
186
- {onImageRemove && (
187
- <button
188
- className={cn(
189
- 'absolute -top-[6px] -right-[6px]',
190
- 'flex items-center justify-center',
191
- 'opacity-0 group-hover:opacity-100 cursor-pointer',
192
- transCls
193
- )}
194
- onClick={(e) => {
195
- e.stopPropagation();
196
- onImageRemove(attachment, index);
197
- }}
198
- >
199
- <RemoveCircleIcon />
200
- </button>
201
- )}
202
- </div>
203
- ))}
232
+ {attachments.map((attachment, index) => {
233
+ const isImage = isImageType(attachment.contentType);
234
+ const { icon: Icon, iconSize } = getFileConfig(attachment.contentType, attachment.alt);
235
+ const scaledIconSize = (52 / 102) * imageSize;
236
+ const iconScale = scaledIconSize / iconSize;
237
+
238
+ return (
239
+ <div
240
+ key={attachment.id}
241
+ className={cn('relative shrink-0 cursor-pointer group', transCls)}
242
+ style={{ width: imageSize, height: imageSize }}
243
+ onClick={() => onImageClick?.(attachment, index)}
244
+ >
245
+ {isImage && attachment.url ? (
246
+ <img
247
+ src={attachment.url}
248
+ alt={attachment.alt || `Attachment ${index + 1}`}
249
+ className="absolute inset-0 h-full w-full rounded-[10px] object-cover"
250
+ />
251
+ ) : (
252
+ <div className="absolute inset-0 flex h-full w-full items-center justify-center rounded-[10px] bg-[#EFF0F6]">
253
+ <div
254
+ className="shrink-0 flex items-center justify-center"
255
+ style={{ width: scaledIconSize, height: scaledIconSize }}
256
+ >
257
+ <div
258
+ style={{
259
+ transform: iconScale !== 1 ? `scale(${iconScale})` : undefined,
260
+ transformOrigin: 'center',
261
+ }}
262
+ >
263
+ <Icon />
264
+ </div>
265
+ </div>
266
+ </div>
267
+ )}
268
+
269
+ {attachment.isLoading && (
270
+ <div className="absolute inset-0 flex items-center justify-center rounded-[10px] bg-black/50">
271
+ <div className="inline-flex animate-spin items-center justify-center">
272
+ <LoaderIcon size={16} />
273
+ </div>
274
+ </div>
275
+ )}
276
+
277
+ {onImageRemove && !attachment.isLoading && (
278
+ <button
279
+ className={cn(
280
+ 'absolute -top-[6px] -right-[6px]',
281
+ 'flex items-center justify-center',
282
+ 'opacity-0 group-hover:opacity-100 cursor-pointer',
283
+ transCls
284
+ )}
285
+ onClick={(e) => {
286
+ e.stopPropagation();
287
+ onImageRemove(attachment, index);
288
+ }}
289
+ >
290
+ <RemoveCircleIcon />
291
+ </button>
292
+ )}
293
+ </div>
294
+ );
295
+ })}
204
296
  </div>
205
297
 
206
298
  {/* Left navigation */}
@@ -13,7 +13,7 @@ export const BotAvatarAndName = ({
13
13
 
14
14
  const variantStyles = {
15
15
  default: 'text-[#999] text-[14px] font-[400]',
16
- active: 'text-[#0265FF] text-[14px] font-[500]',
16
+ active: 'text-[#0265FF] text-[14px] font-[700]',
17
17
  };
18
18
 
19
19
  return (
@@ -27,7 +27,7 @@ export const Card = ({ title, desc, index, icon, onClick }: CardProps) => {
27
27
  {index === undefined ? null : <div>{index}</div>}
28
28
  {icon === undefined ? null : <div className="text-[#0265FF]">{icon}</div>}
29
29
  {/* make title text ellipsis */}
30
- <div className="truncate text-[#12111] group-hover:text-[#0265FF]">{title}</div>
30
+ <div className="truncate !text-[#12111] group-hover:text-[#0265FF]">{title}</div>
31
31
  </div>
32
32
  <div className="text-[#666] leading-5 text-[12px]">{desc}</div>
33
33
  </div>
@@ -73,6 +73,9 @@ export const FieldsPreviewer = forwardRef<HTMLDivElement, FieldsPreviewerProps>(
73
73
  confirmTitle,
74
74
  containerStyle,
75
75
  readonly,
76
+
77
+ closeBtnLabel,
78
+ disableOperation,
76
79
  }: FieldsPreviewerProps) => {
77
80
  const p = fullscreen ? 0 : padding;
78
81
  const pr = fullscreen ? 0 : rightSpaceWidth;
@@ -132,6 +135,8 @@ export const FieldsPreviewer = forwardRef<HTMLDivElement, FieldsPreviewerProps>(
132
135
  dialogContainer={shadowContainer || absContainer!}
133
136
  confirmTitle={confirmTitle}
134
137
  confirmContent={confirmContent}
138
+ closeBtnLabel={closeBtnLabel}
139
+ disableOperation={disableOperation}
135
140
  />
136
141
  <div className={contentCls} style={fullContentStyle}>
137
142
  {empty && isFields ? <PlaceholderDashedBox /> : children}
@@ -168,7 +168,7 @@ export const FormInfoEditor = memo(
168
168
  setExpanded(true);
169
169
  }}
170
170
  className={cn(
171
- 'w-[16px] h-[16px] flex items-center justify-center cursor-pointer hover:opacity-70',
171
+ 'w-[16px] h-[16px] flex items-center justify-center cursor-pointer hover:text-[#0265ff]',
172
172
  transCls,
173
173
  {
174
174
  'pointer-events-none opacity-0': readonly,
@@ -255,7 +255,7 @@ export const FormInfoEditor = memo(
255
255
  setIsDropdownOpen(false);
256
256
  }}
257
257
  className={cn(
258
- 'w-[40px] h-[40px] rounded-[9.5px] border flex items-center justify-center cursor-pointer',
258
+ 'aspect-square rounded-[9.5px] border flex items-center justify-center cursor-pointer overflow-hidden',
259
259
  iconOpt?.id === option.id
260
260
  ? 'border-[#0265ff] border-2'
261
261
  : 'border-[#e0e0e0] hover:bg-[#eff0f6]',
@@ -264,7 +264,6 @@ export const FormInfoEditor = memo(
264
264
  title={option.label}
265
265
  >
266
266
  <FormIcon
267
- size={40}
268
267
  code={option.code}
269
268
  style={{ background: 'transparent', color: '#0265ff' }}
270
269
  />