@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.
- package/CHANGELOG.md +8 -0
- package/package.json +4 -4
- package/src/components/biz-comp/FieldCheckerListMsg.tsx +12 -2
- package/src/components/biz-comp/FieldValueChecker.tsx +27 -1
- package/src/components/biz-comp/SubformFieldsValueChecker.tsx +22 -1
- package/src/components/biz-comp/chat-client.tsx +4 -2
- package/src/components/biz-comp/multi-modal-input/index.tsx +31 -34
- package/src/components/biz-comp/multi-modal-input/prompt-input.tsx +1 -4
- package/src/components/biz-comp/opening-lines.tsx +1 -1
- package/src/components/biz-comp/preview-message-wrapper.tsx +8 -1
- package/src/components/biz-comp/preview-message.tsx +10 -1
- package/src/components/bs-ui/attachments-previewer.tsx +124 -32
- package/src/components/bs-ui/bot-avatar-name.tsx +1 -1
- package/src/components/bs-ui/card.tsx +1 -1
- package/src/components/bs-ui/fields-previewer.tsx +5 -0
- package/src/components/bs-ui/form-info-editor.tsx +2 -3
- package/src/components/bs-ui/line-checker.tsx +6 -1
- package/src/components/bs-ui/previewer-header.tsx +32 -5
- package/src/components/bs-ui/tab-radio-group.tsx +12 -3
- package/src/components/bs-ui/tooltip.tsx +37 -2
- package/src/components/ui/tooltip.tsx +37 -18
- package/src/plugins/form-builder-plugin/index.ts +3 -0
- package/src/plugins/form-filling-plugin/components/FormFillingOpeningLines.tsx +1 -1
- package/src/plugins/form-filling-plugin/components/mode-select.tsx +10 -2
- package/src/plugins/form-filling-plugin/components/msg-part.tsx +26 -1
- package/src/plugins/form-filling-plugin/const.ts +1 -1
- package/src/plugins/form-filling-plugin/index.ts +9 -0
- package/src/plugins/report-query-plugin/index.ts +2 -2
- package/src/stories/AttachmentsPreviewer.stories.tsx +118 -0
- package/src/style.css +20 -0
- package/dist/chat-sdk.js +0 -58809
- package/dist/chat-sdk.js.map +0 -1
- package/dist/chat-sdk.umd.cjs +0 -663
- package/dist/chat-sdk.umd.cjs.map +0 -1
- package/dist/index.css +0 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baishuyun/chat-sdk",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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/
|
|
55
|
-
"@baishuyun/
|
|
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
|
-
({
|
|
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
|
-
({
|
|
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 =
|
|
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 '
|
|
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 {
|
|
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
|
-
|
|
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
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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>;
|
|
@@ -53,13 +53,20 @@ export const PreviewMessageWrapper = ({
|
|
|
53
53
|
pointerEventsCls
|
|
54
54
|
)}
|
|
55
55
|
>
|
|
56
|
-
<IconBtn
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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 */}
|
|
@@ -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:
|
|
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
|
-
'
|
|
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
|
/>
|