@fe-free/ai 4.1.4 → 4.1.6
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 +18 -0
- package/package.json +5 -4
- package/src/files/files.stories.tsx +22 -0
- package/src/files/index.tsx +41 -0
- package/src/files/style.scss +7 -0
- package/src/helper.tsx +31 -0
- package/src/index.ts +4 -0
- package/src/m_sender/actions.tsx +53 -0
- package/src/m_sender/helper.tsx +0 -0
- package/src/m_sender/index.tsx +79 -0
- package/src/m_sender/m_sender.stories.tsx +114 -0
- package/src/m_sender/record.tsx +196 -0
- package/src/m_sender/types.ts +31 -0
- package/src/sender/actions.tsx +0 -1
- package/src/sender/files.tsx +5 -11
- package/src/sender/record.tsx +2 -6
- package/src/sender/style.scss +0 -49
- package/src/style.scss +27 -0
- package/src/svgs/keyboard.svg +1 -0
- package/src/svgs/record.svg +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @fe-free/ai
|
|
2
2
|
|
|
3
|
+
## 4.1.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat: ai
|
|
8
|
+
- @fe-free/core@4.1.6
|
|
9
|
+
- @fe-free/icons@4.1.6
|
|
10
|
+
- @fe-free/tool@4.1.6
|
|
11
|
+
|
|
12
|
+
## 4.1.5
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- feat: ai
|
|
17
|
+
- @fe-free/core@4.1.5
|
|
18
|
+
- @fe-free/icons@4.1.5
|
|
19
|
+
- @fe-free/tool@4.1.5
|
|
20
|
+
|
|
3
21
|
## 4.1.4
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fe-free/ai",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"author": "",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"ahooks": "^3.7.8",
|
|
14
14
|
"classnames": "^2.5.1",
|
|
15
|
-
"
|
|
15
|
+
"lodash-es": "^4.17.21",
|
|
16
|
+
"@fe-free/core": "4.1.6"
|
|
16
17
|
},
|
|
17
18
|
"peerDependencies": {
|
|
18
19
|
"antd": "^5.27.1",
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
"i18next-icu": "^2.4.1",
|
|
23
24
|
"react": "^19.2.0",
|
|
24
25
|
"react-i18next": "^16.4.0",
|
|
25
|
-
"@fe-free/icons": "4.1.
|
|
26
|
-
"@fe-free/tool": "4.1.
|
|
26
|
+
"@fe-free/icons": "4.1.6",
|
|
27
|
+
"@fe-free/tool": "4.1.6"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
30
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FileViewList } from '@fe-free/ai';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof FileViewList> = {
|
|
5
|
+
title: '@fe-free/ai/FileViewList',
|
|
6
|
+
component: FileViewList,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type Story = StoryObj<typeof FileViewList>;
|
|
11
|
+
|
|
12
|
+
export const Default: Story = {
|
|
13
|
+
args: {
|
|
14
|
+
urls: [
|
|
15
|
+
'https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png',
|
|
16
|
+
'https://minio-api-dev.pre-ai.pivotecho.cn/ai-agent/YWIW_%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20260108121456_230_1.png',
|
|
17
|
+
'https://minio-api-dev.pre-ai.pivotecho.cn/ai-agent/3Uck_%E5%8D%8E%E4%BD%8FAI%E9%A2%84%E8%AE%A2%E7%AC%AC%E4%BA%8C%E8%BD%AEPOC%E8%A6%81%E6%B1%82%282%29.pdf',
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FileCard } from '@fe-free/core';
|
|
2
|
+
import { Image } from 'antd';
|
|
3
|
+
import './style.scss';
|
|
4
|
+
|
|
5
|
+
function isUrl(url: string) {
|
|
6
|
+
return url.startsWith('http') || url.startsWith('https');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function FileView({ url, isImage: propsIsImage }: { url: string; isImage?: boolean }) {
|
|
10
|
+
const isImage = propsIsImage ?? FileCard.isImage(url);
|
|
11
|
+
|
|
12
|
+
// 判断是 url 才 decodeURIComponent
|
|
13
|
+
const decodedUrl = isUrl(url) ? decodeURIComponent(url) : url;
|
|
14
|
+
const name = decodedUrl.split('/').pop() || decodedUrl;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="fea-file-view">
|
|
18
|
+
{isImage ? (
|
|
19
|
+
<Image width={60} height={60} src={url} />
|
|
20
|
+
) : (
|
|
21
|
+
<div className="flex h-[60px] w-[250px] items-center rounded bg-01 px-1">
|
|
22
|
+
<div className="min-w-0">
|
|
23
|
+
<FileCard name={name} />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function FileViewList({ urls }: { urls: string[] }) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-wrap gap-2">
|
|
34
|
+
{urls.map((url) => (
|
|
35
|
+
<FileView key={url} url={url} />
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { FileView, FileViewList };
|
package/src/helper.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { range } from 'lodash-es';
|
|
3
|
+
|
|
4
|
+
function RecordLoading({
|
|
5
|
+
count = 4,
|
|
6
|
+
gap = 2,
|
|
7
|
+
color = 'white',
|
|
8
|
+
}: {
|
|
9
|
+
count?: number;
|
|
10
|
+
gap?: number;
|
|
11
|
+
color?: 'white' | 'primary';
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex h-0 gap-[2px]" style={{ gap: `${gap}px` }}>
|
|
15
|
+
{range(count).map((index) => (
|
|
16
|
+
<div
|
|
17
|
+
key={index}
|
|
18
|
+
className={classNames('h-[4px] w-[2px]', {
|
|
19
|
+
'bg-white': color === 'white',
|
|
20
|
+
'bg-primary': color === 'primary',
|
|
21
|
+
})}
|
|
22
|
+
style={{
|
|
23
|
+
animation: `sender-rectangle-${color} infinite 1s ease-in-out ${((index * 1.5) / count) % 1}s`,
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { RecordLoading };
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export { FileView, FileViewList } from './files';
|
|
2
|
+
export { MSender } from './m_sender';
|
|
3
|
+
export type { MSenderProps, MSenderRef } from './m_sender';
|
|
1
4
|
export { Sender } from './sender';
|
|
2
5
|
export type { SenderProps, SenderRef } from './sender';
|
|
3
6
|
export { Tip } from './tip';
|
|
7
|
+
import './style.scss';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Icons from '@fe-free/icons';
|
|
2
|
+
import { Button } from 'antd';
|
|
3
|
+
import { useCallback, type RefObject } from 'react';
|
|
4
|
+
import SendIcon from '../svgs/send.svg?react';
|
|
5
|
+
import type { MSenderProps } from './types';
|
|
6
|
+
|
|
7
|
+
function Actions(
|
|
8
|
+
props: MSenderProps & {
|
|
9
|
+
refText: RefObject<HTMLTextAreaElement>;
|
|
10
|
+
},
|
|
11
|
+
) {
|
|
12
|
+
const { refText, loading, onSubmit, value, onChange } = props;
|
|
13
|
+
|
|
14
|
+
const isLoading = loading;
|
|
15
|
+
|
|
16
|
+
const handleSubmit = useCallback(async () => {
|
|
17
|
+
if (isLoading) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const newValue = {
|
|
22
|
+
...value,
|
|
23
|
+
text: value?.text?.trim(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// 有内容才提交
|
|
27
|
+
if (newValue.text || (newValue.files && newValue.files.length > 0)) {
|
|
28
|
+
await Promise.resolve(onSubmit?.(newValue));
|
|
29
|
+
|
|
30
|
+
// reset
|
|
31
|
+
onChange?.({});
|
|
32
|
+
|
|
33
|
+
// focus
|
|
34
|
+
refText.current?.focus();
|
|
35
|
+
}
|
|
36
|
+
}, [isLoading, value, onSubmit, onChange, refText]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
<Button
|
|
41
|
+
shape="circle"
|
|
42
|
+
size="small"
|
|
43
|
+
type="primary"
|
|
44
|
+
icon={<Icons component={SendIcon} />}
|
|
45
|
+
loading={isLoading}
|
|
46
|
+
disabled={!value?.text}
|
|
47
|
+
onClick={handleSubmit}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { Actions };
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Input } from 'antd';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useMemo, useRef, useState, type RefObject } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Actions } from './actions';
|
|
6
|
+
import { InputRecordSwitch, RecordAction } from './record';
|
|
7
|
+
import type { MSenderProps, MSenderRef } from './types';
|
|
8
|
+
|
|
9
|
+
function Text(props: MSenderProps & { refText: RefObject<HTMLTextAreaElement> }) {
|
|
10
|
+
const { value, onChange, placeholder, refText } = props;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Input.TextArea
|
|
14
|
+
ref={refText}
|
|
15
|
+
value={value?.text}
|
|
16
|
+
onChange={(e) => {
|
|
17
|
+
onChange?.({ ...value, text: e.target.value });
|
|
18
|
+
}}
|
|
19
|
+
placeholder={placeholder}
|
|
20
|
+
autoSize={{ minRows: 1, maxRows: 3 }}
|
|
21
|
+
className="mb-[1px] px-2 py-0"
|
|
22
|
+
variant="borderless"
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function useProps(originProps: MSenderProps) {
|
|
28
|
+
const { t } = useTranslation();
|
|
29
|
+
|
|
30
|
+
return useMemo(() => {
|
|
31
|
+
return {
|
|
32
|
+
...originProps,
|
|
33
|
+
placeholder:
|
|
34
|
+
originProps.placeholder ?? t('@fe-free/ai.sender.describeYourQuestion', '描述你的问题'),
|
|
35
|
+
statement:
|
|
36
|
+
originProps.statement ??
|
|
37
|
+
t(
|
|
38
|
+
'@fe-free/ai.sender.aiGeneratedDisclaimer',
|
|
39
|
+
'内容由 AI 生成,无法确保信息的真实准确,仅供参考',
|
|
40
|
+
),
|
|
41
|
+
defaultType: originProps.defaultType ?? 'input',
|
|
42
|
+
};
|
|
43
|
+
}, [originProps, t]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function MSender(originProps: MSenderProps) {
|
|
47
|
+
const refText = useRef<HTMLTextAreaElement>(null);
|
|
48
|
+
|
|
49
|
+
const props = useProps(originProps);
|
|
50
|
+
const { value, statement, allowSpeech, defaultType } = props;
|
|
51
|
+
|
|
52
|
+
const [type, setType] = useState<'input' | 'record'>(defaultType);
|
|
53
|
+
|
|
54
|
+
const refContainer = useRef<HTMLDivElement>(null);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="fea-m-sender-wrap">
|
|
58
|
+
<div
|
|
59
|
+
ref={refContainer}
|
|
60
|
+
className={classNames(
|
|
61
|
+
'fea-m-sender relative flex items-end rounded-full border border-01 bg-white p-2',
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{allowSpeech && !value?.text && (
|
|
65
|
+
<InputRecordSwitch {...props} type={type} setType={setType} />
|
|
66
|
+
)}
|
|
67
|
+
<div className="flex flex-1">
|
|
68
|
+
<Text {...props} refText={refText} />
|
|
69
|
+
</div>
|
|
70
|
+
<Actions {...props} refText={refText} />
|
|
71
|
+
{type === 'record' && <RecordAction {...props} setType={setType} />}
|
|
72
|
+
</div>
|
|
73
|
+
{statement && <div className="mt-1 text-center text-xs text-04">*{statement}</div>}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { MSender };
|
|
79
|
+
export type { MSenderProps, MSenderRef };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { MSender } from '@fe-free/ai';
|
|
2
|
+
import { sleep } from '@fe-free/tool';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
import { useCallback, useState } from 'react';
|
|
5
|
+
import type { MSenderProps, MSenderValue } from './types';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof MSender> = {
|
|
8
|
+
title: '@fe-free/ai/MSender',
|
|
9
|
+
component: MSender,
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof MSender>;
|
|
14
|
+
|
|
15
|
+
function Component(props: Omit<MSenderProps, 'value' | 'onChange' | 'onSubmit'>) {
|
|
16
|
+
const [v, setV] = useState<MSenderValue | undefined>(undefined);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<MSender
|
|
20
|
+
value={v}
|
|
21
|
+
onChange={(v) => {
|
|
22
|
+
console.log('newValue', v);
|
|
23
|
+
setV(v);
|
|
24
|
+
}}
|
|
25
|
+
onSubmit={(value) => {
|
|
26
|
+
console.log('onSubmit', value);
|
|
27
|
+
}}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
render: (props) => <Component {...props} />,
|
|
35
|
+
args: {
|
|
36
|
+
onSubmit: (value) => {
|
|
37
|
+
console.log(value);
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Loading: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
loading: true,
|
|
45
|
+
onSubmit: (value) => {
|
|
46
|
+
console.log(value);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
render: (props) => <Component {...props} />,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const AllowSpeech: Story = {
|
|
53
|
+
render: (props) => {
|
|
54
|
+
// 假设是字符串,实则是 buffer
|
|
55
|
+
const [recordVoice, setRecordVoice] = useState<string | undefined>(undefined);
|
|
56
|
+
|
|
57
|
+
const handleSubmit = (value: MSenderValue) => {
|
|
58
|
+
console.log('handleSubmit', value);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleRecordStart = useCallback(async () => {
|
|
62
|
+
// 假设这是录音的文本
|
|
63
|
+
setRecordVoice('这是录音的文本');
|
|
64
|
+
|
|
65
|
+
return;
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const handleRecordEnd = useCallback(
|
|
69
|
+
async (isSend: boolean) => {
|
|
70
|
+
console.log('handleRecordEnd isSend', isSend);
|
|
71
|
+
if (isSend) {
|
|
72
|
+
await sleep(1000);
|
|
73
|
+
const recordResult = recordVoice;
|
|
74
|
+
|
|
75
|
+
handleSubmit({ ...(props.value || {}), text: recordResult });
|
|
76
|
+
alert('submit');
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
[props.value, recordVoice],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div>
|
|
84
|
+
<Component
|
|
85
|
+
{...props}
|
|
86
|
+
allowSpeech={{
|
|
87
|
+
onRecordStart: handleRecordStart,
|
|
88
|
+
onRecordEnd: handleRecordEnd,
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<Component
|
|
93
|
+
{...props}
|
|
94
|
+
defaultType="record"
|
|
95
|
+
allowSpeech={{
|
|
96
|
+
onRecordStart: handleRecordStart,
|
|
97
|
+
onRecordEnd: handleRecordEnd,
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<Component
|
|
102
|
+
{...props}
|
|
103
|
+
defaultType="record"
|
|
104
|
+
allowSpeech={{
|
|
105
|
+
onRecordStart: () => Promise.reject(new Error('no permission')),
|
|
106
|
+
onRecordEnd: handleRecordEnd,
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default meta;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import Icons from '@fe-free/icons';
|
|
2
|
+
import { Button } from 'antd';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { RecordLoading } from '../helper';
|
|
6
|
+
import IconKeyboard from '../svgs/keyboard.svg?react';
|
|
7
|
+
import IconRecord from '../svgs/record.svg?react';
|
|
8
|
+
import type { MSenderProps } from './types';
|
|
9
|
+
|
|
10
|
+
function InputRecordSwitch(
|
|
11
|
+
props: MSenderProps & { type: 'input' | 'record'; setType: (type: 'input' | 'record') => void },
|
|
12
|
+
) {
|
|
13
|
+
const { allowSpeech, type, setType } = props;
|
|
14
|
+
|
|
15
|
+
if (!allowSpeech) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (type === 'record') {
|
|
20
|
+
return (
|
|
21
|
+
<Button
|
|
22
|
+
type="text"
|
|
23
|
+
shape="circle"
|
|
24
|
+
size="small"
|
|
25
|
+
icon={<Icons component={IconKeyboard} className="!text-[24px]" />}
|
|
26
|
+
onClick={() => setType('input')}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Button
|
|
33
|
+
type="text"
|
|
34
|
+
shape="circle"
|
|
35
|
+
size="small"
|
|
36
|
+
icon={<Icons component={IconRecord} className="!text-[24px]" />}
|
|
37
|
+
onClick={() => setType('record')}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function RecordAction(props: MSenderProps & { setType }) {
|
|
43
|
+
const { allowSpeech, setType } = props;
|
|
44
|
+
|
|
45
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const touchStartYRef = useRef<number>(0);
|
|
47
|
+
|
|
48
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
49
|
+
|
|
50
|
+
const isCancelledRef = useRef(false);
|
|
51
|
+
const [isCancel, setIsCancel] = useState(false);
|
|
52
|
+
|
|
53
|
+
const handleTouchStart = useCallback(
|
|
54
|
+
async (e: TouchEvent) => {
|
|
55
|
+
// 阻止默认行为,避免触发文本选择、上下文菜单等
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
|
|
58
|
+
await allowSpeech?.onRecordStart?.();
|
|
59
|
+
|
|
60
|
+
const touch = e.touches[0];
|
|
61
|
+
touchStartYRef.current = touch.clientY;
|
|
62
|
+
|
|
63
|
+
isCancelledRef.current = false;
|
|
64
|
+
setIsCancel(false);
|
|
65
|
+
|
|
66
|
+
setIsRecording(true);
|
|
67
|
+
},
|
|
68
|
+
[allowSpeech],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleTouchMove = useCallback(
|
|
72
|
+
(e: TouchEvent) => {
|
|
73
|
+
// 阻止默认行为,避免页面滚动
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
|
|
76
|
+
// 没有录音,不继续
|
|
77
|
+
if (!isRecording) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const touch = e.touches[0];
|
|
82
|
+
const deltaY = touchStartYRef.current - touch.clientY; // 向上移动为正
|
|
83
|
+
|
|
84
|
+
// 如果上移超过 50px,则判定为取消
|
|
85
|
+
if (deltaY > 50) {
|
|
86
|
+
if (!isCancelledRef.current) {
|
|
87
|
+
isCancelledRef.current = true;
|
|
88
|
+
setIsCancel(true);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
if (isCancelledRef.current) {
|
|
92
|
+
isCancelledRef.current = false;
|
|
93
|
+
setIsCancel(false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[isRecording],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const handleTouchEnd = useCallback(
|
|
101
|
+
async (e: TouchEvent) => {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
|
|
104
|
+
// 没有考试,不继续
|
|
105
|
+
if (!isRecording) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await allowSpeech?.onRecordEnd?.(!isCancelledRef.current);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// nothing
|
|
113
|
+
console.error(err);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 重置状态
|
|
117
|
+
setIsRecording(false);
|
|
118
|
+
setIsCancel(false);
|
|
119
|
+
isCancelledRef.current = false;
|
|
120
|
+
touchStartYRef.current = 0;
|
|
121
|
+
},
|
|
122
|
+
[allowSpeech, isRecording],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const handleTouchCancel = useCallback(
|
|
126
|
+
async (e: TouchEvent) => {
|
|
127
|
+
// touchcancel 在触摸被中断时触发(如系统手势、来电等)
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
|
|
130
|
+
await allowSpeech?.onRecordEnd?.(false);
|
|
131
|
+
|
|
132
|
+
// 重置状态
|
|
133
|
+
setIsRecording(false);
|
|
134
|
+
setIsCancel(false);
|
|
135
|
+
isCancelledRef.current = false;
|
|
136
|
+
touchStartYRef.current = 0;
|
|
137
|
+
},
|
|
138
|
+
[allowSpeech],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// 使用原生事件监听器,设置 passive: false 以支持 preventDefault
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const container = containerRef.current;
|
|
144
|
+
if (!container) return;
|
|
145
|
+
|
|
146
|
+
// 使用 { passive: false } 选项,允许 preventDefault
|
|
147
|
+
container.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
148
|
+
container.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
149
|
+
container.addEventListener('touchend', handleTouchEnd, { passive: false });
|
|
150
|
+
container.addEventListener('touchcancel', handleTouchCancel, { passive: false });
|
|
151
|
+
|
|
152
|
+
return () => {
|
|
153
|
+
container.removeEventListener('touchstart', handleTouchStart);
|
|
154
|
+
container.removeEventListener('touchmove', handleTouchMove);
|
|
155
|
+
container.removeEventListener('touchend', handleTouchEnd);
|
|
156
|
+
container.removeEventListener('touchcancel', handleTouchCancel);
|
|
157
|
+
};
|
|
158
|
+
}, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel]);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
className={classNames('absolute inset-0 flex items-center justify-center rounded-full', {
|
|
163
|
+
'bg-white': !isRecording,
|
|
164
|
+
'bg-red-500': isRecording && isCancel,
|
|
165
|
+
'bg-primary': isRecording && !isCancel,
|
|
166
|
+
})}
|
|
167
|
+
>
|
|
168
|
+
{isRecording ? (
|
|
169
|
+
<>
|
|
170
|
+
<RecordLoading count={30} gap={4} />
|
|
171
|
+
{isCancel && <div className="absolute top-0 -mt-[2em] text-red08">松手取消</div>}
|
|
172
|
+
{!isCancel && (
|
|
173
|
+
<div className="absolute top-0 -mt-[2em] text-primary">松开发送,上移取消</div>
|
|
174
|
+
)}
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
<div>按住说话</div>
|
|
178
|
+
)}
|
|
179
|
+
<div className="absolute inset-0" ref={containerRef} />
|
|
180
|
+
{!isRecording && (
|
|
181
|
+
<Button
|
|
182
|
+
type="text"
|
|
183
|
+
shape="circle"
|
|
184
|
+
size="small"
|
|
185
|
+
icon={<Icons component={IconKeyboard} className="!text-[24px]" />}
|
|
186
|
+
onClick={() => {
|
|
187
|
+
setType('input');
|
|
188
|
+
}}
|
|
189
|
+
className="absolute left-2"
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export { InputRecordSwitch, RecordAction };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface MSenderRef {
|
|
2
|
+
focus: () => void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface MSenderValue {
|
|
6
|
+
text?: string;
|
|
7
|
+
files?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface MSenderProps {
|
|
11
|
+
value?: MSenderValue;
|
|
12
|
+
onChange: (value?: MSenderValue) => void;
|
|
13
|
+
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
onSubmit: (value?: MSenderValue) => void | Promise<void>;
|
|
16
|
+
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
|
|
19
|
+
/** 是否允许语音输入 */
|
|
20
|
+
allowSpeech?: {
|
|
21
|
+
/** 录音开始时回调,如果没权限,则 reject */
|
|
22
|
+
onRecordStart?: () => Promise<void>;
|
|
23
|
+
/** 录音结束时回调, isSend 为 true 则发送,否则取消 */
|
|
24
|
+
onRecordEnd?: (isSend: boolean) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
defaultType: 'input' | 'record';
|
|
28
|
+
statement?: string | false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type { MSenderProps, MSenderRef, MSenderValue };
|
package/src/sender/actions.tsx
CHANGED
|
@@ -5,7 +5,6 @@ import { useCallback, type RefObject } from 'react';
|
|
|
5
5
|
import SendIcon from '../svgs/send.svg?react';
|
|
6
6
|
import { FileAction } from './files';
|
|
7
7
|
import { RecordAction } from './record';
|
|
8
|
-
import './style.scss';
|
|
9
8
|
import type { SenderProps } from './types';
|
|
10
9
|
|
|
11
10
|
function Actions(
|
package/src/sender/files.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { App, Button, Dropdown, Input, Modal, Upload } from 'antd';
|
|
|
5
5
|
import type { RefObject } from 'react';
|
|
6
6
|
import { useState } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { FileView } from '../files';
|
|
8
9
|
import FilesIcon from '../svgs/files.svg?react';
|
|
9
10
|
import type { SenderProps } from './types';
|
|
10
11
|
|
|
@@ -136,16 +137,9 @@ function UploadFileItem({ file, onDelete }: { file: UploadFile; onDelete: () =>
|
|
|
136
137
|
return (
|
|
137
138
|
<div className="group relative">
|
|
138
139
|
{isImage ? (
|
|
139
|
-
<
|
|
140
|
-
src={file.originFileObj && URL.createObjectURL(file.originFileObj)}
|
|
141
|
-
className="h-[53px] w-[53px] rounded-lg border border-01 bg-01 object-cover"
|
|
142
|
-
/>
|
|
140
|
+
<FileView url={URL.createObjectURL(file.originFileObj!)} isImage={isImage} />
|
|
143
141
|
) : (
|
|
144
|
-
<
|
|
145
|
-
<div className="min-w-0">
|
|
146
|
-
<FileCard name={file.name} size={file.size} />
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
142
|
+
<FileView url={file.name} />
|
|
149
143
|
)}
|
|
150
144
|
{!isDone && (
|
|
151
145
|
<div className="absolute inset-0 flex items-center justify-center bg-01/80">
|
|
@@ -163,7 +157,7 @@ function UploadFileItem({ file, onDelete }: { file: UploadFile; onDelete: () =>
|
|
|
163
157
|
function UrlFileItem({ url, onDelete }: { url: string; onDelete: () => void }) {
|
|
164
158
|
return (
|
|
165
159
|
<div className="group relative">
|
|
166
|
-
<div className="flex h-[
|
|
160
|
+
<div className="flex h-[60px] w-[250px] items-center rounded bg-01 px-2">
|
|
167
161
|
<div className="line-clamp-2">{url}</div>
|
|
168
162
|
</div>
|
|
169
163
|
<CloseOutlined
|
|
@@ -186,7 +180,7 @@ function Files(
|
|
|
186
180
|
|
|
187
181
|
return (
|
|
188
182
|
<>
|
|
189
|
-
{fileList && fileList.length > 0 && (
|
|
183
|
+
{((fileList && fileList.length > 0) || (fileUrls && fileUrls.length > 0)) && (
|
|
190
184
|
<div className="scrollbar-hide mb-2 flex gap-2 overflow-x-auto">
|
|
191
185
|
{fileList.map((file) => (
|
|
192
186
|
<UploadFileItem
|
package/src/sender/record.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AudioOutlined } from '@fe-free/icons';
|
|
2
2
|
import { Button } from 'antd';
|
|
3
|
+
import { RecordLoading } from '../helper';
|
|
3
4
|
import type { SenderProps } from './types';
|
|
4
5
|
|
|
5
6
|
function RecordAction(props: SenderProps) {
|
|
@@ -9,12 +10,7 @@ function RecordAction(props: SenderProps) {
|
|
|
9
10
|
if (recording) {
|
|
10
11
|
return (
|
|
11
12
|
<Button type="text" shape="circle" onClick={() => onRecordingChange?.(false)}>
|
|
12
|
-
<
|
|
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>
|
|
13
|
+
<RecordLoading count={4} color="primary" />
|
|
18
14
|
</Button>
|
|
19
15
|
);
|
|
20
16
|
}
|
package/src/sender/style.scss
CHANGED
|
@@ -23,53 +23,4 @@
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
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
26
|
}
|
package/src/style.scss
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@keyframes sender-rectangle-white {
|
|
2
|
+
0%,
|
|
3
|
+
80%,
|
|
4
|
+
100% {
|
|
5
|
+
height: 4px;
|
|
6
|
+
box-shadow: 0 0 theme('colors.white');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
40% {
|
|
10
|
+
height: 6px;
|
|
11
|
+
box-shadow: 0 -6px theme('colors.white');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@keyframes sender-rectangle-primary {
|
|
16
|
+
0%,
|
|
17
|
+
80%,
|
|
18
|
+
100% {
|
|
19
|
+
height: 4px;
|
|
20
|
+
box-shadow: 0 0 theme('colors.primary');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
40% {
|
|
24
|
+
height: 6px;
|
|
25
|
+
box-shadow: 0 -6px theme('colors.primary');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20.70703125" height="20.70703125" viewBox="0 0 20.70703125 20.70703125"><g><path d="M4.66015625,7.7204833984375L4.66015625,7.1204833984375Q4.66015625,7.0712376784375,4.6697636088,7.0229382184375Q4.679370968,6.9746387584375,4.698216479,6.9291416384375Q4.717061989,6.8836445484375,4.744421437,6.8426982584375Q4.77178088,6.8017519684375,4.80660285,6.7669299984375Q4.84142482,6.7321080284375,4.88237113,6.7047485854375Q4.9233174,6.6773891374375,4.96881449,6.6585436274375Q5.01431161,6.6396981164375,5.06261107,6.6300907572375Q5.11091053,6.6204833984375,5.16015625,6.6204833984375L5.76015625,6.6204833984375Q5.80940185,6.6204833984375,5.85770135,6.6300907572375Q5.9060007500000005,6.6396981164375,5.95149775,6.6585436274375Q5.9969949499999995,6.6773891374375,6.03794125,6.7047485854375Q6.07888745,6.7321080284375,6.11370955,6.7669299984375Q6.14853145,6.8017519684375,6.17589095,6.8426982784375Q6.20325025,6.8836445484375,6.2220958500000005,6.9291416384375Q6.24094145,6.9746387584375,6.2505488499999995,7.0229382184375Q6.26015615,7.0712376784375,6.26015625,7.1204833984375L6.26015625,7.7204833984375Q6.26015615,7.7697289984375,6.2505488499999995,7.8180284984375Q6.24094145,7.8663278984375005,6.2220958500000005,7.9118248984375Q6.20325025,7.9573220984374995,6.17589095,7.9982683984375Q6.14853145,8.0392145984375,6.11370955,8.0740366984375Q6.07888745,8.1088585984375,6.03794125,8.1362180984375Q5.9969949499999995,8.1635773984375,5.95149795,8.1824229984375Q5.90600085,8.2012685984375,5.85770135,8.2108759984375Q5.80940185,8.2204832984375,5.76015625,8.2204833984375L5.16015625,8.2204833984375Q5.11091053,8.2204832984375,5.06261107,8.2108759984375Q5.01431161,8.2012685984375,4.96881449,8.1824229984375Q4.9233174,8.1635773984375,4.88237111,8.1362180984375Q4.84142482,8.1088585984375,4.80660285,8.0740366984375Q4.77178088,8.0392145984375,4.744421437,7.9982683984375Q4.717061989,7.9573220984374995,4.698216479,7.9118250984375Q4.679370968,7.8663279984375,4.6697636088,7.8180284984375Q4.66015625,7.7697289984375,4.66015625,7.7204833984375ZM4.66015625,10.8207640984375L4.66015625,10.2207641984375Q4.66015625,10.1715182984375,4.6697636088,10.1232187984375Q4.679370968,10.0749189984375,4.698216479,10.0294217984375Q4.717061989,9.9839245984375,4.744421437,9.9429783984375Q4.77178088,9.9020323984375,4.80660285,9.8672103984375Q4.84142482,9.8323883984375,4.88237113,9.805029398437501Q4.9233174,9.7776698984375,4.96881449,9.7588243984375Q5.01431161,9.7399787984375,5.06261107,9.7303714984375Q5.11091053,9.7207641984375,5.16015625,9.7207641984375L5.76015625,9.7207641984375Q5.80940185,9.7207641984375,5.85770135,9.7303714984375Q5.9060007500000005,9.7399787984375,5.95149775,9.7588240984375Q5.9969949499999995,9.7776696984375,6.03794125,9.8050291984375Q6.07888745,9.8323883984375,6.11370955,9.8672103984375Q6.14853145,9.9020323984375,6.17589095,9.9429783984375Q6.20325025,9.9839245984375,6.2220958500000005,10.0294217984375Q6.24094145,10.0749189984375,6.2505488499999995,10.1232184984375Q6.26015615,10.1715182984375,6.26015625,10.2207641984375L6.26015625,10.8207640984375Q6.26015615,10.8700093984375,6.2505488499999995,10.9183086984375Q6.24094145,10.9666079984375,6.2220958500000005,11.0121054984375Q6.20325025,11.0576028984375,6.17589095,11.0985488984375Q6.14853145,11.1394953984375,6.11370955,11.1743173984375Q6.07888745,11.2091388984375,6.03794125,11.2364983984375Q5.9969949499999995,11.263857798437499,5.95149795,11.282702898437499Q5.90600085,11.3015489984375,5.85770135,11.3111562984375Q5.80940185,11.3207635984375,5.76015625,11.3207640984375L5.16015625,11.3207640984375Q5.11091053,11.3207635984375,5.06261107,11.3111562984375Q5.01431161,11.3015489984375,4.96881449,11.282702898437499Q4.9233174,11.263857798437499,4.88237111,11.2364983984375Q4.84142482,11.2091388984375,4.80660285,11.1743173984375Q4.77178088,11.1394953984375,4.744421437,11.0985488984375Q4.717061989,11.0576028984375,4.698216479,11.0121054984375Q4.679370968,10.9666079984375,4.6697636088,10.9183086984375Q4.66015625,10.8700093984375,4.66015625,10.8207640984375ZM7.92285155,7.7204833984375L7.92285155,7.1204833984375Q7.92285155,7.0712376784375,7.93245885,7.0229382184375Q7.9420661500000005,6.9746387584375,7.96091175,6.9291416384375Q7.97975735,6.8836445484375,8.00711655,6.8426982584375Q8.03447585,6.8017519684375,8.06929775,6.7669299984375Q8.10411975,6.7321080284375,8.14506575,6.7047485854375Q8.186012250000001,6.6773891374375,8.23150945,6.6585436274375Q8.27700665,6.6396981164375,8.32530615,6.6300907572375Q8.37360575,6.6204833984375,8.42285155,6.6204833984375L9.022851450000001,6.6204833984375Q9.072096850000001,6.6204833984375,9.120396150000001,6.6300907572375Q9.168695450000001,6.6396981164375,9.21419285,6.6585436274375Q9.25968985,6.6773891374375,9.30063625,6.7047485854375Q9.34158225,6.7321080284375,9.376404749999999,6.7669299984375Q9.41122625,6.8017519684375,9.43858625,6.8426982784375Q9.46594525,6.8836445484375,9.48479085,6.9291416384375Q9.50363635,6.9746387584375,9.51324365,7.0229382184375Q9.522850949999999,7.0712376784375,9.522851450000001,7.1204833984375L9.522851450000001,7.7204833984375Q9.522850949999999,7.7697289984375,9.51324365,7.8180284984375Q9.50363635,7.8663278984375005,9.48479085,7.9118248984375Q9.46594525,7.9573220984374995,9.43858575,7.9982683984375Q9.41122625,8.0392145984375,9.376404749999999,8.0740366984375Q9.34158225,8.1088585984375,9.30063625,8.1362180984375Q9.25968935,8.1635773984375,9.214192350000001,8.1824229984375Q9.168695450000001,8.2012685984375,9.120396150000001,8.2108759984375Q9.072096850000001,8.2204832984375,9.022851450000001,8.2204833984375L8.42285155,8.2204833984375Q8.37360575,8.2204832984375,8.32530615,8.2108759984375Q8.27700665,8.2012685984375,8.23150925,8.1824229984375Q8.186012250000001,8.1635773984375,8.14506605,8.1362180984375Q8.10411975,8.1088585984375,8.06929775,8.0740366984375Q8.03447585,8.0392145984375,8.00711655,7.9982683984375Q7.97975705,7.9573220984374995,7.9609115500000005,7.9118250984375Q7.9420661500000005,7.8663279984375,7.93245885,7.8180284984375Q7.92285155,7.7697289984375,7.92285155,7.7204833984375ZM7.92285155,10.8207640984375L7.92285155,10.2207641984375Q7.92285155,10.1715182984375,7.93245885,10.1232187984375Q7.9420661500000005,10.0749189984375,7.96091175,10.0294217984375Q7.97975735,9.9839245984375,8.00711655,9.9429783984375Q8.03447585,9.9020323984375,8.06929775,9.8672103984375Q8.10411975,9.8323883984375,8.14506575,9.805029398437501Q8.186012250000001,9.7776698984375,8.23150945,9.7588243984375Q8.27700665,9.7399787984375,8.32530615,9.7303714984375Q8.37360575,9.7207641984375,8.42285155,9.7207641984375L9.022851450000001,9.7207641984375Q9.072096850000001,9.7207641984375,9.120396150000001,9.7303714984375Q9.168695450000001,9.7399787984375,9.21419285,9.7588240984375Q9.25968985,9.7776696984375,9.30063625,9.8050291984375Q9.34158225,9.8323883984375,9.376404749999999,9.8672103984375Q9.41122625,9.9020323984375,9.43858625,9.9429783984375Q9.46594525,9.9839245984375,9.48479085,10.0294217984375Q9.50363635,10.0749189984375,9.51324365,10.1232184984375Q9.522850949999999,10.1715182984375,9.522851450000001,10.2207641984375L9.522851450000001,10.8207640984375Q9.522850949999999,10.8700093984375,9.51324365,10.9183086984375Q9.50363635,10.9666079984375,9.48479085,11.0121054984375Q9.46594525,11.0576028984375,9.43858575,11.0985488984375Q9.41122625,11.1394953984375,9.376404749999999,11.1743173984375Q9.34158225,11.2091388984375,9.30063625,11.2364983984375Q9.25968935,11.263857798437499,9.214192350000001,11.282702898437499Q9.168695450000001,11.3015489984375,9.120396150000001,11.3111562984375Q9.072096850000001,11.3207635984375,9.022851450000001,11.3207640984375L8.42285155,11.3207640984375Q8.37360575,11.3207635984375,8.32530615,11.3111562984375Q8.27700665,11.3015489984375,8.23150925,11.282702898437499Q8.186012250000001,11.263857798437499,8.14506605,11.2364983984375Q8.10411975,11.2091388984375,8.06929775,11.1743173984375Q8.03447585,11.1394953984375,8.00711655,11.0985488984375Q7.97975705,11.0576028984375,7.9609115500000005,11.0121054984375Q7.9420661500000005,10.9666079984375,7.93245885,10.9183086984375Q7.92285155,10.8700093984375,7.92285155,10.8207640984375ZM11.18554685,7.7204833984375L11.18554685,7.1204833984375Q11.18554685,7.0712376784375,11.19515415,7.0229382184375Q11.20476155,6.9746387584375,11.22360705,6.9291416384375Q11.24245265,6.8836445484375,11.269811650000001,6.8426982584375Q11.29717115,6.8017519684375,11.331993149999999,6.7669299984375Q11.36681505,6.7321080284375,11.40776155,6.7047485854375Q11.44870755,6.6773891374375,11.49420455,6.6585436274375Q11.539701449999999,6.6396981164375,11.58800125,6.6300907572375Q11.63630105,6.6204833984375,11.68554685,6.6204833984375L12.28554675,6.6204833984375Q12.33479215,6.6204833984375,12.38309195,6.6300907572375Q12.43139075,6.6396981164375,12.47688775,6.6585436274375Q12.52238465,6.6773891374375,12.56333165,6.7047485854375Q12.60427765,6.7321080284375,12.63910005,6.7669299984375Q12.67392155,6.8017519684375,12.70128055,6.8426982784375Q12.72864055,6.8836445484375,12.74748615,6.9291416384375Q12.76633265,6.9746387584375,12.77593995,7.0229382184375Q12.78554725,7.0712376784375,12.78554725,7.1204833984375L12.78554725,7.7204833984375Q12.78554725,7.7697289984375,12.77593995,7.8180284984375Q12.76633265,7.8663278984375005,12.74748615,7.9118248984375Q12.72864055,7.9573220984374995,12.70128155,7.9982683984375Q12.67392155,8.0392145984375,12.63910005,8.0740366984375Q12.60427765,8.1088585984375,12.56333165,8.1362180984375Q12.52238465,8.1635773984375,12.47688775,8.1824229984375Q12.43139075,8.2012685984375,12.38309095,8.2108759984375Q12.33479215,8.2204832984375,12.28554675,8.2204833984375L11.68554685,8.2204833984375Q11.63630105,8.2204832984375,11.58800125,8.2108759984375Q11.539701449999999,8.2012685984375,11.49420455,8.1824229984375Q11.44870755,8.1635773984375,11.407761050000001,8.1362180984375Q11.36681505,8.1088585984375,11.331993149999999,8.0740366984375Q11.29717115,8.0392145984375,11.269811650000001,7.9982683984375Q11.24245215,7.9573220984374995,11.22360655,7.9118250984375Q11.20476155,7.8663279984375,11.19515415,7.8180284984375Q11.18554685,7.7697289984375,11.18554685,7.7204833984375ZM14.44726565,7.7204833984375L14.44726565,7.1204833984375Q14.44726565,7.0712376784375,14.45687295,7.0229382184375Q14.46648025,6.9746387584375,14.48532485,6.9291416384375Q14.50417045,6.8836445484375,14.53153035,6.8426982584375Q14.55888935,6.8017519684375,14.59371185,6.7669299984375Q14.62853435,6.7321080284375,14.66948025,6.7047485854375Q14.71042625,6.6773891374375,14.75592325,6.6585436274375Q14.80142025,6.6396981164375,14.84972025,6.6300907572375Q14.89802025,6.6204833984375,14.94726525,6.6204833984375L15.54726625,6.6204833984375Q15.59651125,6.6204833984375,15.64480925,6.6300907572375Q15.69310825,6.6396981164375,15.73860525,6.6585436274375Q15.78410325,6.6773891374375,15.82505025,6.7047485854375Q15.86599625,6.7321080284375,15.90081925,6.7669299984375Q15.93564025,6.8017519684375,15.96299925,6.8426982784375Q15.99035825,6.8836445484375,16.00920525,6.9291416384375Q16.028049250000002,6.9746387584375,16.037657250000002,7.0229382184375Q16.047265250000002,7.0712376784375,16.04726625,7.1204833984375L16.04726625,7.7204833984375Q16.047265250000002,7.7697289984375,16.037657250000002,7.8180284984375Q16.028049250000002,7.8663278984375005,16.00920525,7.9118248984375Q15.99035825,7.9573220984374995,15.96299925,7.9982683984375Q15.93564025,8.0392145984375,15.90081925,8.0740366984375Q15.86599625,8.1088585984375,15.82504925,8.1362180984375Q15.78410325,8.1635773984375,15.73860725,8.1824229984375Q15.69310925,8.2012685984375,15.64481025,8.2108759984375Q15.59651125,8.2204832984375,15.54726625,8.2204833984375L14.94726525,8.2204833984375Q14.89802025,8.2204832984375,14.84972025,8.2108759984375Q14.80142025,8.2012685984375,14.75592325,8.1824229984375Q14.71042625,8.1635773984375,14.66947925,8.1362180984375Q14.62853435,8.1088585984375,14.59371185,8.0740366984375Q14.55888935,8.0392145984375,14.53153035,7.9982683984375Q14.50417045,7.9573220984374995,14.48532485,7.9118250984375Q14.46648025,7.8663279984375,14.45687295,7.8180284984375Q14.44726565,7.7697289984375,14.44726565,7.7204833984375ZM11.18554685,10.8207640984375L11.18554685,10.2207641984375Q11.18554685,10.1715182984375,11.19515415,10.1232187984375Q11.20476155,10.0749189984375,11.22360705,10.0294217984375Q11.24245265,9.9839245984375,11.269811650000001,9.9429783984375Q11.29717115,9.9020323984375,11.331993149999999,9.8672103984375Q11.36681505,9.8323883984375,11.40776155,9.805029398437501Q11.44870755,9.7776698984375,11.49420455,9.7588243984375Q11.539701449999999,9.7399787984375,11.58800125,9.7303714984375Q11.63630105,9.7207641984375,11.68554685,9.7207641984375L12.28554675,9.7207641984375Q12.33479215,9.7207641984375,12.38309195,9.7303714984375Q12.43139075,9.7399787984375,12.47688775,9.7588240984375Q12.52238465,9.7776696984375,12.56333165,9.8050291984375Q12.60427765,9.8323883984375,12.63910005,9.8672103984375Q12.67392155,9.9020323984375,12.70128055,9.9429783984375Q12.72864055,9.9839245984375,12.74748615,10.0294217984375Q12.76633265,10.0749189984375,12.77593995,10.1232184984375Q12.78554725,10.1715182984375,12.78554725,10.2207641984375L12.78554725,10.8207640984375Q12.78554725,10.8700093984375,12.77593995,10.9183086984375Q12.76633265,10.9666079984375,12.74748615,11.0121054984375Q12.72864055,11.0576028984375,12.70128155,11.0985488984375Q12.67392155,11.1394953984375,12.63910005,11.1743173984375Q12.60427765,11.2091388984375,12.56333165,11.2364983984375Q12.52238465,11.263857798437499,12.47688775,11.282702898437499Q12.43139075,11.3015489984375,12.38309095,11.3111562984375Q12.33479215,11.3207635984375,12.28554675,11.3207640984375L11.68554685,11.3207640984375Q11.63630105,11.3207635984375,11.58800125,11.3111562984375Q11.539701449999999,11.3015489984375,11.49420455,11.282702898437499Q11.44870755,11.263857798437499,11.407761050000001,11.2364983984375Q11.36681505,11.2091388984375,11.331993149999999,11.1743173984375Q11.29717115,11.1394953984375,11.269811650000001,11.0985488984375Q11.24245215,11.0576028984375,11.22360655,11.0121054984375Q11.20476155,10.9666079984375,11.19515415,10.9183086984375Q11.18554685,10.8700093984375,11.18554685,10.8207640984375ZM14.44726565,10.8207640984375L14.44726565,10.2207641984375Q14.44726565,10.1715182984375,14.45687295,10.1232187984375Q14.46648025,10.0749189984375,14.48532485,10.0294217984375Q14.50417045,9.9839245984375,14.53153035,9.9429783984375Q14.55888935,9.9020323984375,14.59371185,9.8672103984375Q14.62853435,9.8323883984375,14.66948025,9.805029398437501Q14.71042625,9.7776698984375,14.75592325,9.7588243984375Q14.80142025,9.7399787984375,14.84972025,9.7303714984375Q14.89802025,9.7207641984375,14.94726525,9.7207641984375L15.54726625,9.7207641984375Q15.59651125,9.7207641984375,15.64480925,9.7303714984375Q15.69310825,9.7399787984375,15.73860525,9.7588240984375Q15.78410325,9.7776696984375,15.82505025,9.8050291984375Q15.86599625,9.8323883984375,15.90081925,9.8672103984375Q15.93564025,9.9020323984375,15.96299925,9.9429783984375Q15.99035825,9.9839245984375,16.00920525,10.0294217984375Q16.028049250000002,10.0749189984375,16.037657250000002,10.1232184984375Q16.047265250000002,10.1715182984375,16.04726625,10.2207641984375L16.04726625,10.8207640984375Q16.047265250000002,10.8700093984375,16.037657250000002,10.9183086984375Q16.028049250000002,10.9666079984375,16.00920525,11.0121054984375Q15.99035825,11.0576028984375,15.96299925,11.0985488984375Q15.93564025,11.1394953984375,15.90081925,11.1743173984375Q15.86599625,11.2091388984375,15.82504925,11.2364983984375Q15.78410325,11.263857798437499,15.73860725,11.282702898437499Q15.69310925,11.3015489984375,15.64481025,11.3111562984375Q15.59651125,11.3207635984375,15.54726625,11.3207640984375L14.94726525,11.3207640984375Q14.89802025,11.3207635984375,14.84972025,11.3111562984375Q14.80142025,11.3015489984375,14.75592325,11.282702898437499Q14.71042625,11.263857798437499,14.66947925,11.2364983984375Q14.62853435,11.2091388984375,14.59371185,11.1743173984375Q14.55888935,11.1394953984375,14.53153035,11.0985488984375Q14.50417045,11.0576028984375,14.48532485,11.0121054984375Q14.46648025,10.9666079984375,14.45687295,10.9183086984375Q14.44726565,10.8700093984375,14.44726565,10.8207640984375Z" fill="#222222" fill-opacity="1"/></g><rect x="6.4091796875" y="13.35443115234375" width="7.887460708618164" height="1.399999976158142" rx="0.5" fill="#222222" fill-opacity="1"/><ellipse cx="10.353515625" cy="10.353515625" rx="9.228515625" ry="9.228515625" fill-opacity="0" stroke-opacity="1" stroke="#222222" fill="none" stroke-width="1.5" stroke-linecap="ROUND" stroke-linejoin="round"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20.70703125" height="20.70703125" viewBox="0 0 20.70703125 20.70703125"><g transform="matrix(0,1,-1,0,20.70703125,-19.20703125)"><g transform="matrix(0,-1,1,0,0,39.9140625)"><ellipse cx="29.560546875" cy="29.560546875" rx="9.228515625" ry="9.228515625" fill-opacity="0" stroke-opacity="1" stroke="#222222" fill="none" stroke-width="1.5" stroke-linecap="ROUND" stroke-linejoin="round"/></g><path d="M26.49383952421875,6.692684515Q25.07902628421875,7.284493835,23.98794138421875,8.363533925L23.98804682421875,8.363640525000001Q23.881225644218752,8.469282625,23.82334768421875,8.607922825Q23.76546979421875,8.746563125,23.76546979421875,8.896799325Q23.76546979421875,8.970653025,23.77987802421875,9.043087725Q23.79428613421875,9.115522425,23.82254874421875,9.183754225Q23.85081136421875,9.251986025,23.89184236421875,9.313393325Q23.93287330421875,9.374800725,23.98509579421875,9.427023125Q24.03731831421875,9.479245625,24.09872549421875,9.520276525Q24.16013264421875,9.561307424999999,24.22836464421875,9.589570325Q24.29659664421875,9.617832925,24.36903134421875,9.632241225Q24.44146604121875,9.646649325,24.51531982421875,9.646649325Q24.66339293421875,9.646649325,24.80035138421875,9.590364025Q24.937309834218752,9.534078825,25.04259282421875,9.429958125L25.04269826421875,9.430064725Q25.92663822421875,8.555882825,27.07268192421875,8.076498525Q28.25945302421875,7.580078125,29.56052212421875,7.580078125Q30.86159232421875,7.580078125,32.04836272421875,8.076498525Q33.19440652421875,8.555882725,34.07834622421875,9.430064725L34.07845212421875,9.429957824999999Q34.183734924218754,9.534078625,34.32069392421875,9.590364025Q34.457652124218754,9.646649325,34.605724824218754,9.646649325Q34.67957882421875,9.646649325,34.75201282421875,9.632241225Q34.82444782421875,9.617832625,34.89267882421875,9.589570025Q34.96091082421875,9.561307424999999,35.02231882421875,9.520276525Q35.083725824218746,9.479245625,35.13594882421875,9.427023125Q35.18817182421875,9.374800725,35.22920182421875,9.313393625Q35.27023282421875,9.251986225,35.29849482421875,9.183754425Q35.32675882421875,9.115522425,35.341166824218746,9.043087925Q35.35557582421875,8.970653025,35.35557582421875,8.896799325Q35.35557582421875,8.746562925,35.29769782421875,8.607922525Q35.239820824218754,8.469282225,35.13299882421875,8.363640325L35.13310382421875,8.363533925Q34.04201892421875,7.284493715,32.62720582421875,6.692684515Q31.16267392421875,6.080078125,29.56052212421875,6.080078125Q27.95837112421875,6.080078125,26.49383952421875,6.692684515Z" fill-rule="evenodd" fill="#222222" fill-opacity="1"/><path d="M30.88091327109375,11.87667225Q31.48886827109375,12.13097655,31.95781417109375,12.59474595L31.95791957109375,12.594639149999999Q32.06320287109375,12.69876005,32.20016097109375,12.75504545Q32.33712007109375,12.81133075,32.48519327109375,12.81133075Q32.55904677109375,12.81133075,32.63148117109375,12.79692245Q32.70391557109375,12.78251435,32.77214767109375,12.75425175Q32.84037967109375,12.72598915,32.901786771093754,12.68495815Q32.96319437109375,12.64392725,33.01541707109375,12.59170475Q33.06763937109375,12.53948225,33.10867027109375,12.47807505Q33.14970107109375,12.41666785,33.17796377109375,12.34843575Q33.20622637109375,12.28020385,33.22063447109375,12.20776915Q33.23504307109375,12.13533445,33.23504357109375,12.06148075Q33.23504397109375,11.91124435,33.17716597109375,11.77260423Q33.11928847109375,11.63396394,33.01246647109375,11.52832204L33.01257177109375,11.52821547Q32.33648017109375,10.8595876407,31.45975537109375,10.49285811Q30.55231717109375,10.11328143,29.56019207109375,10.11328137Q28.56806807109375,10.11328131,27.66062997109375,10.49285814Q26.78390618109375,10.8595873907,26.10781443109375,11.52821541L26.10791987109375,11.52832204Q26.001098691093752,11.63396394,25.94322073109375,11.77260423Q25.88534284109375,11.91124435,25.88534284109375,12.06148075Q25.88534284109375,12.13533445,25.89975107109375,12.20776915Q25.91415918109375,12.28020385,25.94242179109375,12.34843585Q25.97068441109375,12.41666795,26.01171541109375,12.47807505Q26.05274635109375,12.53948225,26.10496884109375,12.59170475Q26.15719136109375,12.64392725,26.21859854109375,12.68495825Q26.28000569109375,12.72598915,26.34823769109375,12.75425175Q26.41646969109375,12.78251435,26.48890439109375,12.79692245Q26.56133908809375,12.81133075,26.63519287109375,12.81133075Q26.78326601109375,12.81133075,26.92022449109375,12.75504565Q27.05718294109375,12.69876025,27.16246593109375,12.59463945L27.16257131109375,12.59474595Q27.63151717109375,12.13097695,28.23947267109375,11.876672150000001Q28.86915017109375,11.61328119,29.56019207109375,11.61328131Q30.25123577109375,11.61328131,30.88091327109375,11.87667225Z" fill-rule="evenodd" fill="#222222" fill-opacity="1"/><path d="M28.2385588909375,14.4939614825C28.1481483279375,14.567035732499999,28.1570268349375,14.6987590825,28.2446259009375,14.7751811125L29.4286651609375,15.8081450125C29.5040001609375,15.8738679125,29.6162943609375,15.8738665125,29.6916277609375,15.8081418125L30.8756027609375,14.7751816525C30.963198460937498,14.6987587225,30.9720709609375,14.5670344825,30.8816618609375,14.4939614525C30.5215592609375,14.2029087725,30.0614510609375,14.0283203125,29.5601104609375,14.0283203125C29.0587695809375,14.0283203125,28.5986614809375,14.2029087725,28.2385588909375,14.4939614825Z" fill="#222222" fill-opacity="1"/></g></svg>
|