@fe-free/ai 5.0.0 → 6.0.2
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 +486 -4
- package/package.json +10 -4
- package/src/ai.stories.tsx +228 -0
- package/src/chat/index.tsx +32 -0
- package/src/files/files.stories.tsx +22 -0
- package/src/files/index.tsx +56 -0
- package/src/files/style.scss +7 -0
- package/src/helper/complete.ts +90 -0
- package/src/helper/index.tsx +45 -0
- package/src/index.ts +17 -0
- package/src/m_sender/actions.tsx +65 -0
- package/src/m_sender/index.tsx +74 -0
- package/src/m_sender/m_sender.stories.tsx +231 -0
- package/src/m_sender/record.tsx +173 -0
- package/src/m_sender/types.ts +32 -0
- package/src/messages/index.tsx +5 -0
- package/src/messages/message_actions.tsx +166 -0
- package/src/messages/message_think.tsx +69 -0
- package/src/messages/messages.stories.tsx +44 -0
- package/src/messages/messages.tsx +169 -0
- package/src/sender/actions.tsx +10 -47
- package/src/sender/files.tsx +7 -13
- package/src/sender/index.tsx +78 -20
- package/src/sender/record.tsx +3 -7
- package/src/sender/sender.stories.tsx +7 -0
- package/src/sender/types.ts +4 -2
- package/src/store/index.ts +166 -0
- package/src/store/types.ts +46 -0
- package/src/stream/index.ts +41 -0
- package/src/style.scss +100 -0
- package/src/svgs/keyboard.svg +3 -0
- package/src/svgs/record.svg +3 -0
- package/src/svgs/think.svg +3 -0
- package/src/voice/index.ts +188 -0
- package/src/sender/style.scss +0 -75
- /package/src/{sender → m_sender}/helper.tsx +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { ChatMessage, MSenderProps } from '@fe-free/ai';
|
|
2
|
+
import {
|
|
3
|
+
Chat,
|
|
4
|
+
createChatStore,
|
|
5
|
+
EnumChatMessageStatus,
|
|
6
|
+
EnumChatMessageType,
|
|
7
|
+
generateUUID,
|
|
8
|
+
getRecordAudioOfPCM,
|
|
9
|
+
Markdown,
|
|
10
|
+
MessageActions,
|
|
11
|
+
Messages,
|
|
12
|
+
MSender,
|
|
13
|
+
} from '@fe-free/ai';
|
|
14
|
+
import { sleep } from '@fe-free/tool';
|
|
15
|
+
import type { Meta } from '@storybook/react-vite';
|
|
16
|
+
import { App, Button, Divider } from 'antd';
|
|
17
|
+
import { set } from 'lodash-es';
|
|
18
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
19
|
+
|
|
20
|
+
const meta: Meta = {
|
|
21
|
+
title: '@fe-free/ai/Example',
|
|
22
|
+
tags: ['autodocs'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface AIData {
|
|
26
|
+
text?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fakeFetchStream(v: MSenderProps['value'], { onUpdate }) {
|
|
30
|
+
// 这里模拟 fetchStream
|
|
31
|
+
await sleep(1000);
|
|
32
|
+
|
|
33
|
+
let count = 0;
|
|
34
|
+
const timer = setInterval(() => {
|
|
35
|
+
if (count > 50) {
|
|
36
|
+
clearInterval(timer);
|
|
37
|
+
onUpdate({ event: 'done', data: '' });
|
|
38
|
+
} else {
|
|
39
|
+
onUpdate({ event: 'message', data: '回复' + count });
|
|
40
|
+
count++;
|
|
41
|
+
}
|
|
42
|
+
}, 300);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Component() {
|
|
46
|
+
const { useChatStore, useChatStoreComputed } = useMemo(() => {
|
|
47
|
+
return createChatStore<MSenderProps['value'], AIData>();
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const senderValue = useChatStore((state) => state.senderValue);
|
|
51
|
+
const setSenderValue = useChatStore((state) => state.setSenderValue);
|
|
52
|
+
const messages = useChatStore((state) => state.messages);
|
|
53
|
+
const setMessages = useChatStore((state) => state.setMessages);
|
|
54
|
+
const addMessage = useChatStore((state) => state.addMessage);
|
|
55
|
+
const updateMessage = useChatStore((state) => state.updateMessage);
|
|
56
|
+
const { chatStatus } = useChatStoreComputed();
|
|
57
|
+
|
|
58
|
+
const { message } = App.useApp();
|
|
59
|
+
|
|
60
|
+
// init from cache
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const cacheMessages = localStorage.getItem('chatMessages');
|
|
63
|
+
if (cacheMessages) {
|
|
64
|
+
setMessages(JSON.parse(cacheMessages));
|
|
65
|
+
}
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const loading =
|
|
69
|
+
chatStatus === EnumChatMessageStatus.PENDING || chatStatus === EnumChatMessageStatus.STREAMING;
|
|
70
|
+
|
|
71
|
+
const handleSubmit = useCallback(
|
|
72
|
+
(v) => {
|
|
73
|
+
console.log('onSubmit', v);
|
|
74
|
+
|
|
75
|
+
const message: ChatMessage<MSenderProps['value'], AIData> = {
|
|
76
|
+
uuid: generateUUID(),
|
|
77
|
+
status: EnumChatMessageStatus.PENDING,
|
|
78
|
+
user: {
|
|
79
|
+
...v,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
addMessage(message);
|
|
84
|
+
|
|
85
|
+
// fake
|
|
86
|
+
fakeFetchStream(v, {
|
|
87
|
+
onUpdate: ({ event, data }) => {
|
|
88
|
+
if (event === 'message') {
|
|
89
|
+
message.status = EnumChatMessageStatus.STREAMING;
|
|
90
|
+
|
|
91
|
+
const preText = message.ai?.data?.text || '';
|
|
92
|
+
set(message, 'ai.data.text', preText + data);
|
|
93
|
+
|
|
94
|
+
// 假设有 session_id
|
|
95
|
+
set(message, 'ai.session_id', '123');
|
|
96
|
+
|
|
97
|
+
updateMessage(message);
|
|
98
|
+
}
|
|
99
|
+
if (event === 'done') {
|
|
100
|
+
message.status = EnumChatMessageStatus.DONE;
|
|
101
|
+
updateMessage(message);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
[addMessage, updateMessage],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const { start: startRecord, stop: stopRecord } = useMemo(() => {
|
|
110
|
+
return getRecordAudioOfPCM();
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div>
|
|
115
|
+
<div>
|
|
116
|
+
<Button
|
|
117
|
+
onClick={() => {
|
|
118
|
+
addMessage({
|
|
119
|
+
uuid: generateUUID(),
|
|
120
|
+
type: EnumChatMessageType.SYSTEM,
|
|
121
|
+
system: {
|
|
122
|
+
data: {
|
|
123
|
+
type: 'new_session',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
Add New Session
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="h-[500px] w-[500px] max-w-full border border-red-500">
|
|
133
|
+
<Chat
|
|
134
|
+
end={
|
|
135
|
+
<div
|
|
136
|
+
className="p-2"
|
|
137
|
+
onFocus={() => {
|
|
138
|
+
console.log('onFocus');
|
|
139
|
+
}}
|
|
140
|
+
onBlur={() => {
|
|
141
|
+
console.log('onBlur');
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<div>这里是 suggestion 区域这里是 suggestion 区域</div>
|
|
145
|
+
<MSender
|
|
146
|
+
value={senderValue}
|
|
147
|
+
onChange={(v) => setSenderValue(v)}
|
|
148
|
+
loading={loading}
|
|
149
|
+
onSubmit={handleSubmit}
|
|
150
|
+
allowSpeech={{
|
|
151
|
+
onRecordStart: async () => {
|
|
152
|
+
console.log('onRecordStart');
|
|
153
|
+
try {
|
|
154
|
+
await startRecord({
|
|
155
|
+
onAudio: (data) => {
|
|
156
|
+
console.log('onAudio', data);
|
|
157
|
+
},
|
|
158
|
+
onError: (err) => {
|
|
159
|
+
message.error(err.message);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(err);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
onRecordEnd: async (isSend) => {
|
|
167
|
+
console.log('onRecordEnd', isSend);
|
|
168
|
+
const voiceData = await stopRecord();
|
|
169
|
+
console.log('voiceData', voiceData);
|
|
170
|
+
},
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
}
|
|
175
|
+
>
|
|
176
|
+
<Messages
|
|
177
|
+
messages={messages}
|
|
178
|
+
renderMessageOfSystem={({ message }) => {
|
|
179
|
+
if (message.system?.data?.type === 'new_session') {
|
|
180
|
+
return <Divider>让我们聊点新内容吧</Divider>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}}
|
|
185
|
+
renderMessageOfUser={({ message }) => {
|
|
186
|
+
return (
|
|
187
|
+
<div className="p-2">
|
|
188
|
+
<div className="rounded-xl bg-primary p-2 text-white">{message.user?.text}</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}}
|
|
192
|
+
renderMessageOfAI={({ message }) => {
|
|
193
|
+
return (
|
|
194
|
+
<div className="max-w-full p-2">
|
|
195
|
+
<div>
|
|
196
|
+
status: {message.status} session_id: {message.ai?.session_id}
|
|
197
|
+
</div>
|
|
198
|
+
<Markdown content={message.ai?.data?.text || ''} />
|
|
199
|
+
<div className="flex gap-2">
|
|
200
|
+
<MessageActions.Copy value={message.ai?.data?.text || ''} />
|
|
201
|
+
<MessageActions.Like
|
|
202
|
+
onClick={async () => {
|
|
203
|
+
// some thing
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
<MessageActions.Dislike
|
|
207
|
+
onClick={async () => {
|
|
208
|
+
// some thing
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
</Chat>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const Default = {
|
|
223
|
+
render: () => {
|
|
224
|
+
return <Component />;
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export default meta;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PageLayout } from '@fe-free/core';
|
|
2
|
+
|
|
3
|
+
function Chat({
|
|
4
|
+
start,
|
|
5
|
+
startClassName,
|
|
6
|
+
end,
|
|
7
|
+
endClassName,
|
|
8
|
+
children,
|
|
9
|
+
childrenClassName,
|
|
10
|
+
}: {
|
|
11
|
+
start?: React.ReactNode;
|
|
12
|
+
startClassName?: string;
|
|
13
|
+
end?: React.ReactNode;
|
|
14
|
+
endClassName?: string;
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
childrenClassName?: string;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<PageLayout
|
|
20
|
+
direction="vertical"
|
|
21
|
+
start={start}
|
|
22
|
+
startClassName={startClassName}
|
|
23
|
+
end={end}
|
|
24
|
+
endClassName={endClassName}
|
|
25
|
+
childrenClassName={childrenClassName}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</PageLayout>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { Chat };
|
|
@@ -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,56 @@
|
|
|
1
|
+
import { FileCard } from '@fe-free/core';
|
|
2
|
+
import { Image, Typography } from 'antd';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import './style.scss';
|
|
5
|
+
|
|
6
|
+
function isUrl(url: string) {
|
|
7
|
+
return url.startsWith('http') || url.startsWith('https');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function FileView({
|
|
11
|
+
url,
|
|
12
|
+
isImage: propsIsImage,
|
|
13
|
+
size,
|
|
14
|
+
className,
|
|
15
|
+
}: {
|
|
16
|
+
url: string;
|
|
17
|
+
size?: number;
|
|
18
|
+
isImage?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
}) {
|
|
21
|
+
const isImage = propsIsImage ?? FileCard.isImage(url);
|
|
22
|
+
|
|
23
|
+
// 判断是 url 才 decodeURIComponent
|
|
24
|
+
const decodedUrl = isUrl(url) ? decodeURIComponent(url) : url;
|
|
25
|
+
const name = decodedUrl.split('/').pop() || decodedUrl;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={classNames('fea-file-view bg-01', className)}>
|
|
29
|
+
{isImage ? (
|
|
30
|
+
<Image width={60} height={60} src={url} />
|
|
31
|
+
) : (
|
|
32
|
+
<div className="flex h-[60px] w-[250px] items-center rounded px-1">
|
|
33
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
34
|
+
<FileCard.FileIcon name={name} className="text-4xl" />
|
|
35
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
36
|
+
{name && <Typography.Text ellipsis={{ tooltip: name }}>{name}</Typography.Text>}
|
|
37
|
+
{size && <div className="text-sm text-03">{FileCard.getFileSize(size)}</div>}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function FileViewList({ urls }: { urls: string[] }) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex flex-wrap gap-2">
|
|
49
|
+
{urls.map((url) => (
|
|
50
|
+
<FileView key={url} url={url} />
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { FileView, FileViewList };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function completeHtml(partialHtml?: string) {
|
|
2
|
+
if (!partialHtml) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// 创建一个临时容器来解析 HTML
|
|
7
|
+
const tempDiv = document.createElement('div');
|
|
8
|
+
tempDiv.innerHTML = partialHtml;
|
|
9
|
+
|
|
10
|
+
// 浏览器会自动修正不匹配的标签(如自动闭合、忽略无效标签等)
|
|
11
|
+
// 但 innerHTML 不会包含 <html>, <head>, <body> 等顶层结构
|
|
12
|
+
// 所以我们手动构建一个完整文档
|
|
13
|
+
|
|
14
|
+
// 提取 head 内容(如果有 <head> 标签,通常不会出现在片段中,但以防万一)
|
|
15
|
+
let headContent = '';
|
|
16
|
+
const headMatch = partialHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
17
|
+
if (headMatch) {
|
|
18
|
+
headContent = headMatch[1] || '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 使用 tempDiv.innerHTML 获取浏览器修正后的 body 内容
|
|
22
|
+
const bodyContent = tempDiv.innerHTML;
|
|
23
|
+
|
|
24
|
+
// 构建完整 HTML
|
|
25
|
+
const fullHtml = `<!DOCTYPE html>
|
|
26
|
+
<html>
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="utf-8">
|
|
29
|
+
${headContent}
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
${bodyContent}
|
|
33
|
+
</body>
|
|
34
|
+
</html>`;
|
|
35
|
+
|
|
36
|
+
return fullHtml;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function completeJson(partialJson?: string): string {
|
|
40
|
+
if (!partialJson) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let result = partialJson.trim();
|
|
45
|
+
|
|
46
|
+
const stack: string[] = [];
|
|
47
|
+
let inString = false;
|
|
48
|
+
let escape = false;
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < result.length; i++) {
|
|
51
|
+
const char = result[i];
|
|
52
|
+
|
|
53
|
+
if (inString) {
|
|
54
|
+
if (escape) {
|
|
55
|
+
escape = false;
|
|
56
|
+
} else if (char === '\\') {
|
|
57
|
+
escape = true;
|
|
58
|
+
} else if (char === '"') {
|
|
59
|
+
inString = false;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (char === '"') {
|
|
65
|
+
inString = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (char === '{') stack.push('}');
|
|
70
|
+
else if (char === '[') stack.push(']');
|
|
71
|
+
else if (char === '}' || char === ']') stack.pop();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 1️⃣ 如果字符串没闭合,补一个 "
|
|
75
|
+
if (inString) {
|
|
76
|
+
result += '"';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2️⃣ 去掉末尾非法逗号
|
|
80
|
+
result = result.replace(/,\s*([}\]])/g, '$1');
|
|
81
|
+
|
|
82
|
+
// 3️⃣ 补齐所有缺失的 } 或 ]
|
|
83
|
+
while (stack.length) {
|
|
84
|
+
result += stack.pop();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { completeHtml, completeJson };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { range } from 'lodash-es';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
function getAnimationDelay(index: number) {
|
|
6
|
+
const arr = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9];
|
|
7
|
+
return arr[index % arr.length];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function RecordLoading({
|
|
11
|
+
count = 4,
|
|
12
|
+
gap = 2,
|
|
13
|
+
color = 'white',
|
|
14
|
+
}: {
|
|
15
|
+
count?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
width?: number;
|
|
18
|
+
gap?: number;
|
|
19
|
+
color?: 'white' | 'primary';
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex h-0 gap-[2px]" style={{ gap: `${gap}px` }}>
|
|
23
|
+
{range(count).map((index) => (
|
|
24
|
+
<div
|
|
25
|
+
key={index}
|
|
26
|
+
className={classNames({
|
|
27
|
+
'bg-white': color === 'white',
|
|
28
|
+
'bg-primary': color === 'primary',
|
|
29
|
+
})}
|
|
30
|
+
style={{
|
|
31
|
+
height: '4px',
|
|
32
|
+
width: '2px',
|
|
33
|
+
animation: `fea-sender-rectangle-${color} infinite 1s ease-in-out ${getAnimationDelay(index)}s`,
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function generateUUID() {
|
|
42
|
+
return uuidv4();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { generateUUID, RecordLoading };
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
import './style.scss';
|
|
2
|
+
|
|
3
|
+
export { CustomMarkdown, Markdown } from '@fe-free/core';
|
|
4
|
+
export type { CustomMarkdownProps, MarkdownProps } from '@fe-free/core';
|
|
5
|
+
export { Chat } from './chat';
|
|
6
|
+
export { FileView, FileViewList } from './files';
|
|
7
|
+
export { generateUUID } from './helper';
|
|
8
|
+
export { completeHtml, completeJson } from './helper/complete';
|
|
9
|
+
export { MSender } from './m_sender';
|
|
10
|
+
export type { MSenderProps, MSenderRef } from './m_sender';
|
|
11
|
+
export { MessageActions, MessageThink, MessageThinkOfDeepSeek, Messages } from './messages';
|
|
12
|
+
export type { MessageThinkProps, MessagesProps } from './messages';
|
|
1
13
|
export { Sender } from './sender';
|
|
2
14
|
export type { SenderProps, SenderRef } from './sender';
|
|
15
|
+
export { createChatStore } from './store';
|
|
16
|
+
export { EnumChatMessageStatus, EnumChatMessageType } from './store/types';
|
|
17
|
+
export type { ChatMessage, ChatMessageOfAI, ChatMessageOfUser } from './store/types';
|
|
18
|
+
export { fetchStream } from './stream';
|
|
3
19
|
export { Tip } from './tip';
|
|
20
|
+
export { getRecordAudioOfBlob, getRecordAudioOfPCM } from './voice';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Icons from '@fe-free/icons';
|
|
2
|
+
import { Button } from 'antd';
|
|
3
|
+
import { useCallback, type RefObject } from 'react';
|
|
4
|
+
import IconRecord from '../svgs/record.svg?react';
|
|
5
|
+
import SendIcon from '../svgs/send.svg?react';
|
|
6
|
+
import type { MSenderProps } from './types';
|
|
7
|
+
|
|
8
|
+
function Actions(
|
|
9
|
+
props: MSenderProps & {
|
|
10
|
+
refText: RefObject<HTMLTextAreaElement | null>;
|
|
11
|
+
type: 'input' | 'record';
|
|
12
|
+
setType: (type: 'input' | 'record') => void;
|
|
13
|
+
},
|
|
14
|
+
) {
|
|
15
|
+
const { loading, onSubmit, value, onChange, setType, allowSpeech } = props;
|
|
16
|
+
|
|
17
|
+
const isLoading = loading;
|
|
18
|
+
|
|
19
|
+
const handleSubmit = useCallback(async () => {
|
|
20
|
+
if (isLoading || value?.text?.trim() === '') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const newValue = {
|
|
25
|
+
...value,
|
|
26
|
+
text: value?.text?.trim(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 有内容才提交
|
|
30
|
+
if (newValue.text || (newValue.files && newValue.files.length > 0)) {
|
|
31
|
+
await Promise.resolve(onSubmit?.(newValue));
|
|
32
|
+
|
|
33
|
+
// reset
|
|
34
|
+
onChange?.({});
|
|
35
|
+
|
|
36
|
+
// 移动端 不 focus
|
|
37
|
+
// refText.current?.focus();
|
|
38
|
+
}
|
|
39
|
+
}, [isLoading, value, onSubmit, onChange]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="mr-1 flex items-center gap-2">
|
|
43
|
+
{allowSpeech && !value?.text ? (
|
|
44
|
+
<Button
|
|
45
|
+
type="primary"
|
|
46
|
+
shape="circle"
|
|
47
|
+
icon={<Icons component={IconRecord} className="h-[28px]! text-lg!" />}
|
|
48
|
+
onClick={() => {
|
|
49
|
+
setType('record');
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
) : (
|
|
53
|
+
<Button
|
|
54
|
+
type="primary"
|
|
55
|
+
shape="circle"
|
|
56
|
+
icon={<Icons component={SendIcon} className="h-[28px]! text-lg!" />}
|
|
57
|
+
loading={isLoading}
|
|
58
|
+
onClick={handleSubmit}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { Actions };
|
|
@@ -0,0 +1,74 @@
|
|
|
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 { RecordAction } from './record';
|
|
7
|
+
import type { MSenderProps, MSenderRef } from './types';
|
|
8
|
+
|
|
9
|
+
function Text(props: MSenderProps & { refText: RefObject<HTMLTextAreaElement | null> }) {
|
|
10
|
+
const { value, onChange, placeholder, refText, autoFocus } = props;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Input.TextArea
|
|
14
|
+
ref={refText}
|
|
15
|
+
value={value?.text}
|
|
16
|
+
autoFocus={autoFocus}
|
|
17
|
+
onChange={(e) => {
|
|
18
|
+
onChange?.({ ...value, text: e.target.value });
|
|
19
|
+
}}
|
|
20
|
+
placeholder={placeholder}
|
|
21
|
+
autoSize={{ minRows: 1, maxRows: 3 }}
|
|
22
|
+
className="px-1 text-[15px]"
|
|
23
|
+
variant="borderless"
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function useProps(originProps: MSenderProps) {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
|
|
31
|
+
return useMemo(() => {
|
|
32
|
+
return {
|
|
33
|
+
...originProps,
|
|
34
|
+
placeholder:
|
|
35
|
+
originProps.placeholder ?? t('@fe-free/ai.sender.describeYourQuestion', '描述你的问题'),
|
|
36
|
+
defaultType: originProps.defaultType ?? 'input',
|
|
37
|
+
};
|
|
38
|
+
}, [originProps, t]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function MSender(originProps: MSenderProps) {
|
|
42
|
+
const refText = useRef<HTMLTextAreaElement>(null);
|
|
43
|
+
|
|
44
|
+
const props = useProps(originProps);
|
|
45
|
+
const { statement, defaultType } = props;
|
|
46
|
+
|
|
47
|
+
const [type, setType] = useState<'input' | 'record'>(defaultType);
|
|
48
|
+
|
|
49
|
+
const refContainer = useRef<HTMLDivElement>(null);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="fea-m-sender-wrap">
|
|
53
|
+
<div
|
|
54
|
+
ref={refContainer}
|
|
55
|
+
className={classNames(
|
|
56
|
+
'fea-m-sender relative flex items-end rounded-xl border border-01 bg-white p-2.5',
|
|
57
|
+
)}
|
|
58
|
+
style={{
|
|
59
|
+
boxShadow: '0px 2px 12px 0px #00000014',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex flex-1">
|
|
63
|
+
<Text {...props} refText={refText} />
|
|
64
|
+
</div>
|
|
65
|
+
<Actions {...props} refText={refText} type={type} setType={setType} />
|
|
66
|
+
{type === 'record' && <RecordAction {...props} refText={refText} setType={setType} />}
|
|
67
|
+
</div>
|
|
68
|
+
{statement && <div className="mt-1 text-center text-xs text-04">*{statement}</div>}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { MSender };
|
|
74
|
+
export type { MSenderProps, MSenderRef };
|