@fe-free/ai 4.0.0
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 +13 -0
- package/package.json +27 -0
- package/src/index.ts +3 -0
- package/src/sender/actions.tsx +74 -0
- package/src/sender/files.tsx +202 -0
- package/src/sender/helper.tsx +0 -0
- package/src/sender/index.tsx +135 -0
- package/src/sender/record.tsx +32 -0
- package/src/sender/sender.stories.tsx +65 -0
- package/src/sender/style.scss +75 -0
- package/src/sender/types.ts +35 -0
- package/src/svgs/files.svg +1 -0
- package/src/svgs/send.svg +1 -0
- package/src/tip.tsx +5 -0
- package/types/svg.d.ts +5 -0
package/CHANGELOG.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fe-free/ai",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"author": "",
|
|
7
|
+
"license": "ISC",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public",
|
|
10
|
+
"registry": "https://registry.npmjs.org/"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"ahooks": "^3.7.8",
|
|
14
|
+
"classnames": "^2.5.1",
|
|
15
|
+
"@fe-free/core": "4.0.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"antd": "^5.27.1",
|
|
19
|
+
"dayjs": "~1.11.10",
|
|
20
|
+
"react": "^19.2.0",
|
|
21
|
+
"@fe-free/icons": "4.0.0",
|
|
22
|
+
"@fe-free/tool": "4.0.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Icons from '@fe-free/icons';
|
|
2
|
+
import { Button, Divider } from 'antd';
|
|
3
|
+
import type { RefObject } from 'react';
|
|
4
|
+
import SendIcon from '../svgs/send.svg?react';
|
|
5
|
+
import { FileAction } from './files';
|
|
6
|
+
import { RecordAction } from './record';
|
|
7
|
+
import './style.scss';
|
|
8
|
+
import type { SenderProps } from './types';
|
|
9
|
+
|
|
10
|
+
function Actions(
|
|
11
|
+
props: SenderProps & {
|
|
12
|
+
refUpload: RefObject<HTMLDivElement>;
|
|
13
|
+
isUploading: boolean;
|
|
14
|
+
fileUrls: string[];
|
|
15
|
+
setFileUrls: (fileUrls: string[]) => void;
|
|
16
|
+
},
|
|
17
|
+
) {
|
|
18
|
+
const {
|
|
19
|
+
loading,
|
|
20
|
+
onSubmit,
|
|
21
|
+
value,
|
|
22
|
+
refUpload,
|
|
23
|
+
isUploading,
|
|
24
|
+
fileUrls,
|
|
25
|
+
setFileUrls,
|
|
26
|
+
allowUpload,
|
|
27
|
+
allowSpeech,
|
|
28
|
+
} = props;
|
|
29
|
+
|
|
30
|
+
const isLoading = loading || isUploading;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex items-center gap-2">
|
|
34
|
+
<div className="flex flex-1 gap-1">
|
|
35
|
+
{allowUpload && (
|
|
36
|
+
<FileAction
|
|
37
|
+
{...props}
|
|
38
|
+
refUpload={refUpload}
|
|
39
|
+
fileUrls={fileUrls}
|
|
40
|
+
setFileUrls={setFileUrls}
|
|
41
|
+
/>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
<Divider type="vertical" />
|
|
45
|
+
<div className="flex items-center gap-2">
|
|
46
|
+
{allowSpeech && <RecordAction {...props} />}
|
|
47
|
+
<Button
|
|
48
|
+
type="primary"
|
|
49
|
+
shape="circle"
|
|
50
|
+
icon={<Icons component={SendIcon} className="!text-lg" />}
|
|
51
|
+
loading={isLoading}
|
|
52
|
+
// disabled={loading}
|
|
53
|
+
onClick={() => {
|
|
54
|
+
if (isLoading) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const newValue = {
|
|
59
|
+
...value,
|
|
60
|
+
text: value?.text?.trim(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 有内容才提交
|
|
64
|
+
if (newValue.text || (newValue.files && newValue.files.length > 0)) {
|
|
65
|
+
onSubmit?.(newValue);
|
|
66
|
+
}
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { Actions };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { FileCard } from '@fe-free/core';
|
|
2
|
+
import Icons, { CloseOutlined, LinkOutlined, PlusOutlined } from '@fe-free/icons';
|
|
3
|
+
import type { UploadFile } from 'antd';
|
|
4
|
+
import { App, Button, Dropdown, Input, Modal, Upload } from 'antd';
|
|
5
|
+
import type { RefObject } from 'react';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import FilesIcon from '../svgs/files.svg?react';
|
|
8
|
+
import type { SenderProps } from './types';
|
|
9
|
+
|
|
10
|
+
function FileAction(
|
|
11
|
+
props: SenderProps & {
|
|
12
|
+
refUpload: RefObject<HTMLDivElement>;
|
|
13
|
+
fileUrls: string[];
|
|
14
|
+
setFileUrls: (fileUrls: string[]) => void;
|
|
15
|
+
},
|
|
16
|
+
) {
|
|
17
|
+
const { value, refUpload, fileUrls, setFileUrls, allowUpload } = props;
|
|
18
|
+
const { filesMaxCount } = allowUpload || {};
|
|
19
|
+
|
|
20
|
+
const { message } = App.useApp();
|
|
21
|
+
|
|
22
|
+
const [url, setUrl] = useState<string>('');
|
|
23
|
+
const [open, setOpen] = useState<boolean>(false);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<Dropdown
|
|
28
|
+
trigger={['click']}
|
|
29
|
+
placement="topLeft"
|
|
30
|
+
menu={{
|
|
31
|
+
items: [
|
|
32
|
+
{
|
|
33
|
+
key: 'add-file',
|
|
34
|
+
label: '添加图片或文件',
|
|
35
|
+
icon: <Icons component={FilesIcon} />,
|
|
36
|
+
onClick: () => {
|
|
37
|
+
refUpload.current?.click();
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'add-file-url',
|
|
42
|
+
label: '添加文件URL',
|
|
43
|
+
icon: <Icons component={LinkOutlined} />,
|
|
44
|
+
onClick: () => {
|
|
45
|
+
setUrl('');
|
|
46
|
+
|
|
47
|
+
setOpen(true);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<Button shape="circle" icon={<Icons component={PlusOutlined} className="!text-lg" />} />
|
|
54
|
+
</Dropdown>
|
|
55
|
+
{open && (
|
|
56
|
+
<Modal
|
|
57
|
+
title="添加文件URL"
|
|
58
|
+
open
|
|
59
|
+
onCancel={() => setOpen(false)}
|
|
60
|
+
onOk={() => {
|
|
61
|
+
if (filesMaxCount && value?.files && value.files.length >= filesMaxCount) {
|
|
62
|
+
message.warning(`超过最大上传数量${filesMaxCount}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (url.trim()) {
|
|
67
|
+
setFileUrls([...fileUrls, url]);
|
|
68
|
+
}
|
|
69
|
+
setOpen(false);
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<Input.TextArea
|
|
73
|
+
placeholder="请输入文件URL"
|
|
74
|
+
value={url}
|
|
75
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
76
|
+
/>
|
|
77
|
+
</Modal>
|
|
78
|
+
)}
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function FileUpload(
|
|
84
|
+
props: SenderProps & {
|
|
85
|
+
refUpload: RefObject<HTMLDivElement>;
|
|
86
|
+
fileList: UploadFile[];
|
|
87
|
+
setFileList: (fileList: UploadFile[]) => void;
|
|
88
|
+
uploadMaxCount?: number;
|
|
89
|
+
},
|
|
90
|
+
) {
|
|
91
|
+
const { allowUpload, refUpload, fileList, setFileList, uploadMaxCount } = props;
|
|
92
|
+
const { uploadAction, filesMaxCount } = allowUpload || {};
|
|
93
|
+
|
|
94
|
+
const { message } = App.useApp();
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Upload.Dragger
|
|
98
|
+
action={uploadAction}
|
|
99
|
+
fileList={fileList}
|
|
100
|
+
multiple
|
|
101
|
+
pastable
|
|
102
|
+
maxCount={uploadMaxCount ? uploadMaxCount + 1 : undefined}
|
|
103
|
+
onChange={(info) => {
|
|
104
|
+
if (uploadMaxCount && info.fileList.length > uploadMaxCount) {
|
|
105
|
+
message.warning(`超过最大上传数量${filesMaxCount}`);
|
|
106
|
+
|
|
107
|
+
setFileList(info.fileList.slice(-uploadMaxCount));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setFileList(info.fileList);
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<div ref={refUpload}>在此处拖放文件</div>
|
|
115
|
+
</Upload.Dragger>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function UploadFileItem({ file, onDelete }: { file: UploadFile; onDelete: () => void }) {
|
|
120
|
+
const isImage = FileCard.isImage(file.name);
|
|
121
|
+
|
|
122
|
+
// 先写死这样
|
|
123
|
+
const isDone = file.response?.data?.url;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="group relative">
|
|
127
|
+
{isImage ? (
|
|
128
|
+
<img
|
|
129
|
+
src={file.originFileObj && URL.createObjectURL(file.originFileObj)}
|
|
130
|
+
className="h-[53px] w-[53px] rounded-lg border border-01 bg-01 object-cover"
|
|
131
|
+
/>
|
|
132
|
+
) : (
|
|
133
|
+
<div className="flex h-[53px] w-[200px] items-center rounded bg-01 px-1">
|
|
134
|
+
<FileCard name={file.name} size={file.size} />
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
{!isDone && (
|
|
138
|
+
<div className="absolute inset-0 flex items-center justify-center bg-01/80">
|
|
139
|
+
{(file.percent ?? 0).toFixed(0)}%
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
<CloseOutlined
|
|
143
|
+
className="absolute right-1 top-1 hidden cursor-pointer rounded-full bg-04 text-white group-hover:block"
|
|
144
|
+
onClick={onDelete}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function UrlFileItem({ url, onDelete }: { url: string; onDelete: () => void }) {
|
|
151
|
+
return (
|
|
152
|
+
<div className="group relative">
|
|
153
|
+
<div className="flex h-[53px] w-[200px] items-center rounded bg-01 px-2">
|
|
154
|
+
<div className="line-clamp-2">{url}</div>
|
|
155
|
+
</div>
|
|
156
|
+
<CloseOutlined
|
|
157
|
+
className="absolute right-1 top-1 hidden cursor-pointer rounded-full bg-04 text-white group-hover:block"
|
|
158
|
+
onClick={onDelete}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Files(
|
|
165
|
+
props: SenderProps & {
|
|
166
|
+
fileList: UploadFile[];
|
|
167
|
+
setFileList: (fileList: UploadFile[]) => void;
|
|
168
|
+
fileUrls: string[];
|
|
169
|
+
setFileUrls: (fileUrls: string[]) => void;
|
|
170
|
+
},
|
|
171
|
+
) {
|
|
172
|
+
const { fileList, setFileList, fileUrls, setFileUrls } = props;
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
{fileList && fileList.length > 0 && (
|
|
177
|
+
<div className="scrollbar-hide mb-2 flex gap-2 overflow-x-auto">
|
|
178
|
+
{fileList.map((file) => (
|
|
179
|
+
<UploadFileItem
|
|
180
|
+
key={file.uid}
|
|
181
|
+
file={file}
|
|
182
|
+
onDelete={() => {
|
|
183
|
+
setFileList(fileList.filter((f) => f.uid !== file.uid));
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
))}
|
|
187
|
+
{fileUrls.map((url) => (
|
|
188
|
+
<UrlFileItem
|
|
189
|
+
key={url}
|
|
190
|
+
url={url}
|
|
191
|
+
onDelete={() => {
|
|
192
|
+
setFileUrls(fileUrls.filter((u) => u !== url));
|
|
193
|
+
}}
|
|
194
|
+
/>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { FileAction, Files, FileUpload };
|
|
File without changes
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useDrop } from 'ahooks';
|
|
2
|
+
import { Input } from 'antd';
|
|
3
|
+
import type { UploadFile } from 'antd/lib';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { Actions } from './actions';
|
|
7
|
+
import { FileUpload, Files } from './files';
|
|
8
|
+
import './style.scss';
|
|
9
|
+
import type { SenderProps, SenderRef } from './types';
|
|
10
|
+
|
|
11
|
+
function Text(props: SenderProps) {
|
|
12
|
+
const { value, onChange, placeholder } = props;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Input.TextArea
|
|
16
|
+
value={value?.text}
|
|
17
|
+
onChange={(e) => {
|
|
18
|
+
onChange?.({ ...value, text: e.target.value });
|
|
19
|
+
}}
|
|
20
|
+
placeholder={placeholder}
|
|
21
|
+
autoSize={{ minRows: 2, maxRows: 8 }}
|
|
22
|
+
className="mb-1 px-1 py-0"
|
|
23
|
+
variant="borderless"
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaultProps = {
|
|
29
|
+
placeholder: '描述你的问题',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function Sender(originProps: SenderProps) {
|
|
33
|
+
const props = useMemo(() => {
|
|
34
|
+
return {
|
|
35
|
+
...defaultProps,
|
|
36
|
+
...originProps,
|
|
37
|
+
};
|
|
38
|
+
}, [originProps]);
|
|
39
|
+
|
|
40
|
+
const { value, onChange, filesMaxCount } = props;
|
|
41
|
+
|
|
42
|
+
const refContainer = useRef<HTMLDivElement>(null);
|
|
43
|
+
const refUpload = useRef<HTMLDivElement>(null);
|
|
44
|
+
const [dragHover, setDragHover] = useState(false);
|
|
45
|
+
|
|
46
|
+
useDrop(refContainer, {
|
|
47
|
+
onDragEnter: () => {
|
|
48
|
+
setDragHover(true);
|
|
49
|
+
},
|
|
50
|
+
onDragLeave: () => {
|
|
51
|
+
setDragHover(false);
|
|
52
|
+
},
|
|
53
|
+
onDrop: () => {
|
|
54
|
+
setDragHover(false);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 手动输入的 url file
|
|
59
|
+
const [fileUrls, originSetFileUrls] = useState<string[]>([]);
|
|
60
|
+
// 上传的 upload file
|
|
61
|
+
const [fileList, originSetFileList] = useState<UploadFile[]>([]);
|
|
62
|
+
|
|
63
|
+
const handleFilesChange = useCallback(
|
|
64
|
+
({ fileUrls, fileList }) => {
|
|
65
|
+
onChange?.({
|
|
66
|
+
...value,
|
|
67
|
+
files: [...(fileList.map((file) => file.response?.data?.url) || []), ...fileUrls],
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
[value, onChange],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const setFileUrls = useCallback(
|
|
74
|
+
(fileUrls: string[]) => {
|
|
75
|
+
originSetFileUrls(fileUrls);
|
|
76
|
+
handleFilesChange({ fileUrls, fileList });
|
|
77
|
+
},
|
|
78
|
+
[fileList],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const setFileList = useCallback(
|
|
82
|
+
(fileList: UploadFile[]) => {
|
|
83
|
+
originSetFileList(fileList);
|
|
84
|
+
handleFilesChange({ fileUrls, fileList });
|
|
85
|
+
},
|
|
86
|
+
[fileUrls],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const isUploading = useMemo(() => {
|
|
90
|
+
// 存在没有 url 的
|
|
91
|
+
return fileList.some((file) => !file.response?.data?.url);
|
|
92
|
+
}, [fileList]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="fea-sender-wrap">
|
|
96
|
+
<div
|
|
97
|
+
ref={refContainer}
|
|
98
|
+
className={classNames('fea-sender relative flex flex-col rounded-lg border border-01 p-2', {
|
|
99
|
+
'fea-sender-drag-hover': dragHover,
|
|
100
|
+
})}
|
|
101
|
+
>
|
|
102
|
+
<Files
|
|
103
|
+
{...props}
|
|
104
|
+
fileList={fileList}
|
|
105
|
+
setFileList={setFileList}
|
|
106
|
+
fileUrls={fileUrls}
|
|
107
|
+
setFileUrls={setFileUrls}
|
|
108
|
+
/>
|
|
109
|
+
<div className="flex">
|
|
110
|
+
<Text {...props} />
|
|
111
|
+
</div>
|
|
112
|
+
<Actions
|
|
113
|
+
{...props}
|
|
114
|
+
refUpload={refUpload}
|
|
115
|
+
isUploading={isUploading}
|
|
116
|
+
fileUrls={fileUrls}
|
|
117
|
+
setFileUrls={setFileUrls}
|
|
118
|
+
/>
|
|
119
|
+
<FileUpload
|
|
120
|
+
{...props}
|
|
121
|
+
refUpload={refUpload}
|
|
122
|
+
fileList={fileList}
|
|
123
|
+
setFileList={setFileList}
|
|
124
|
+
uploadMaxCount={filesMaxCount ? filesMaxCount - fileUrls.length : undefined}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="mt-1 text-center text-xs text-03">
|
|
128
|
+
内容由 AI 生成,无法确保信息的真实准确,仅供参考
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { Sender };
|
|
135
|
+
export type { SenderProps, SenderRef };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AudioOutlined } from '@fe-free/icons';
|
|
2
|
+
import { Button } from 'antd';
|
|
3
|
+
import type { SenderProps } from './types';
|
|
4
|
+
|
|
5
|
+
function RecordAction(props: SenderProps) {
|
|
6
|
+
const { allowSpeech } = props;
|
|
7
|
+
const { recording, onRecordingChange } = allowSpeech || {};
|
|
8
|
+
|
|
9
|
+
if (recording) {
|
|
10
|
+
return (
|
|
11
|
+
<Button type="text" shape="circle" onClick={() => onRecordingChange?.(false)}>
|
|
12
|
+
<div className="fea-sender-spinner">
|
|
13
|
+
<div className="fea-sender-spinner-line fea-sender-spinner-line1" />
|
|
14
|
+
<div className="fea-sender-spinner-line fea-sender-spinner-line2" />
|
|
15
|
+
<div className="fea-sender-spinner-line fea-sender-spinner-line3" />
|
|
16
|
+
<div className="fea-sender-spinner-line fea-sender-spinner-line4" />
|
|
17
|
+
</div>
|
|
18
|
+
</Button>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Button
|
|
24
|
+
type="text"
|
|
25
|
+
shape="circle"
|
|
26
|
+
icon={<AudioOutlined className="!text-lg" />}
|
|
27
|
+
onClick={() => onRecordingChange?.(true)}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { RecordAction };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Sender } from '@fe-free/ai';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { SenderProps, SenderValue } from './types';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Sender> = {
|
|
7
|
+
title: '@fe-free/ai/Sender',
|
|
8
|
+
component: Sender,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof Sender>;
|
|
13
|
+
|
|
14
|
+
function Component(props: Omit<SenderProps, 'value' | 'onChange'>) {
|
|
15
|
+
const [v, setV] = useState<SenderValue | undefined>(undefined);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Sender
|
|
19
|
+
value={v}
|
|
20
|
+
onChange={(v) => {
|
|
21
|
+
console.log('newValue', v);
|
|
22
|
+
setV(v);
|
|
23
|
+
}}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
render: (props) => <Component {...props} />,
|
|
31
|
+
args: {
|
|
32
|
+
onSubmit: (value) => {
|
|
33
|
+
console.log(value);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Loading: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
loading: true,
|
|
41
|
+
onSubmit: (value) => {
|
|
42
|
+
console.log(value);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
render: (props) => <Component {...props} />,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const AllowUpload: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
allowUpload: {
|
|
51
|
+
uploadAction: '/api/ai-service/v1/file_upload/upload',
|
|
52
|
+
filesMaxCount: 3,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
render: (props) => <Component {...props} />,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const AllowSpeech: Story = {
|
|
59
|
+
render: (props) => {
|
|
60
|
+
const [recording, setRecording] = useState(false);
|
|
61
|
+
return <Component {...props} allowSpeech={{ recording, onRecordingChange: setRecording }} />;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default meta;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.fea-sender {
|
|
2
|
+
.ant-upload-select {
|
|
3
|
+
display: none !important;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.ant-upload-list {
|
|
7
|
+
display: none !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ant-upload-wrapper {
|
|
11
|
+
display: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
&.fea-sender-drag-hover {
|
|
15
|
+
.ant-upload-wrapper {
|
|
16
|
+
display: block;
|
|
17
|
+
position: absolute;
|
|
18
|
+
inset: 0;
|
|
19
|
+
z-index: 10;
|
|
20
|
+
|
|
21
|
+
.ant-upload-drag {
|
|
22
|
+
border-color: theme('colors.primary');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.fea-sender-spinner {
|
|
28
|
+
display: block;
|
|
29
|
+
position: relative;
|
|
30
|
+
width: 2px;
|
|
31
|
+
height: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.fea-sender-spinner .fea-sender-spinner-line {
|
|
35
|
+
position: absolute;
|
|
36
|
+
width: 2px;
|
|
37
|
+
height: 4px;
|
|
38
|
+
content: '';
|
|
39
|
+
background-color: theme('colors.primary');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.fea-sender-spinner .fea-sender-spinner-line1 {
|
|
43
|
+
left: -6px;
|
|
44
|
+
animation: rectangle infinite 1s ease-in-out 0s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.fea-sender-spinner .fea-sender-spinner-line2 {
|
|
48
|
+
left: -2px;
|
|
49
|
+
animation: rectangle infinite 1s ease-in-out 0.25s;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.fea-sender-spinner .fea-sender-spinner-line3 {
|
|
53
|
+
right: -2px;
|
|
54
|
+
animation: rectangle infinite 1s ease-in-out 0.5s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.fea-sender-spinner .fea-sender-spinner-line4 {
|
|
58
|
+
right: -6px;
|
|
59
|
+
animation: rectangle infinite 1s ease-in-out 0.75s;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@keyframes rectangle {
|
|
63
|
+
0%,
|
|
64
|
+
80%,
|
|
65
|
+
100% {
|
|
66
|
+
height: 4px;
|
|
67
|
+
box-shadow: 0 0 theme('colors.primary');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
40% {
|
|
71
|
+
height: 6px;
|
|
72
|
+
box-shadow: 0 -6px theme('colors.primary');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
interface SenderRef {
|
|
2
|
+
focus: () => void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface SenderValue {
|
|
6
|
+
text?: string;
|
|
7
|
+
files?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SenderProps {
|
|
11
|
+
value?: SenderValue;
|
|
12
|
+
onChange: (value?: SenderValue) => void;
|
|
13
|
+
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
onSubmit: (value?: SenderValue) => void;
|
|
16
|
+
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
|
|
19
|
+
allowUpload?: {
|
|
20
|
+
/** 上传文件的接口地址,约定返回的 {data: {url: string}} */
|
|
21
|
+
uploadAction?: string;
|
|
22
|
+
/** files 最大数量 */
|
|
23
|
+
filesMaxCount?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** 是否允许语音输入 */
|
|
27
|
+
allowSpeech?: {
|
|
28
|
+
/** 是否正在录音 */
|
|
29
|
+
recording?: boolean;
|
|
30
|
+
/** 录音状态变化时回调 */
|
|
31
|
+
onRecordingChange?: (recording: boolean) => void;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type { SenderProps, SenderRef, SenderValue };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M17.3977 3.9588C15.8361 2.39727 13.3037 2.39727 11.7422 3.9588L5.03365 10.6673C2.60612 13.0952 2.60612 17.0314 5.03365 19.4592C7.46144 21.887 11.3983 21.8875 13.8262 19.4599L20.5348 12.7514C20.8472 12.439 21.3534 12.439 21.6658 12.7514C21.9781 13.0638 21.9782 13.5701 21.6658 13.8825L14.9573 20.591C11.9046 23.6435 6.95518 23.6429 3.90255 20.5903C0.850191 17.5377 0.850191 12.5889 3.90255 9.53624L10.6111 2.82771C12.7975 0.641334 16.3424 0.641334 18.5288 2.82771C20.7149 5.01409 20.7151 8.55906 18.5288 10.7454L11.8699 17.4042C10.5369 18.7372 8.37542 18.7365 7.04241 17.4035C5.70963 16.0705 5.7095 13.9096 7.04241 12.5767L13.7012 5.91785C14.0136 5.60547 14.5199 5.60557 14.8323 5.91785C15.1447 6.23027 15.1447 6.73652 14.8323 7.04894L8.1735 13.7078C7.46543 14.4159 7.46556 15.5642 8.1735 16.2724C8.88167 16.9806 10.03 16.9806 10.7381 16.2724L17.397 9.61358C18.9584 8.05211 18.959 5.52035 17.3977 3.9588Z" fill="currentColor"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="size-18 text-dbx-static-white"><path d="M10.6254 20.3752V6.69549L5.47304 11.8478C4.93607 12.3848 4.0647 12.3848 3.52773 11.8478C2.99076 11.3109 2.99076 10.4395 3.52773 9.90252L11.0277 2.40252L11.1322 2.30877C11.6723 1.86801 12.4695 1.89901 12.973 2.40252L20.473 9.90252L20.5668 10.007C21.0075 10.5471 20.9766 11.3443 20.473 11.8478C19.9695 12.3513 19.1723 12.3823 18.6322 11.9416L18.5277 11.8478L13.3754 6.69549V20.3752C13.3754 21.1346 12.7598 21.7502 12.0004 21.7502C11.241 21.7502 10.6254 21.1346 10.6254 20.3752Z" fill="currentColor"></path></svg>
|
package/src/tip.tsx
ADDED