@fe-free/ai 4.1.7 → 4.1.9
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 +6 -4
- package/src/ai.stories.tsx +155 -0
- package/src/chat/index.tsx +26 -0
- package/src/helper.tsx +6 -1
- package/src/index.ts +9 -1
- package/src/m_sender/index.tsx +1 -1
- package/src/m_sender/record.tsx +1 -1
- package/src/m_sender/types.ts +3 -3
- package/src/messages/index.tsx +68 -0
- package/src/sender/index.tsx +1 -1
- package/src/sender/types.ts +2 -2
- package/src/store/index.ts +70 -0
- package/src/store/types.ts +32 -0
- package/src/voice/index.ts +33 -0
- package/src/sender/helper.tsx +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @fe-free/ai
|
|
2
2
|
|
|
3
|
+
## 4.1.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat: ai
|
|
8
|
+
- @fe-free/core@4.1.9
|
|
9
|
+
- @fe-free/icons@4.1.9
|
|
10
|
+
- @fe-free/tool@4.1.9
|
|
11
|
+
|
|
12
|
+
## 4.1.8
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- feat: ai
|
|
17
|
+
- @fe-free/core@4.1.8
|
|
18
|
+
- @fe-free/icons@4.1.8
|
|
19
|
+
- @fe-free/tool@4.1.8
|
|
20
|
+
|
|
3
21
|
## 4.1.7
|
|
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.9",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"author": "",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"ahooks": "^3.7.8",
|
|
14
14
|
"classnames": "^2.5.1",
|
|
15
15
|
"lodash-es": "^4.17.21",
|
|
16
|
-
"
|
|
16
|
+
"uuid": "^13.0.0",
|
|
17
|
+
"zustand": "^4.5.7",
|
|
18
|
+
"@fe-free/core": "4.1.9"
|
|
17
19
|
},
|
|
18
20
|
"peerDependencies": {
|
|
19
21
|
"@ant-design/x-sdk": "^2.1.3",
|
|
@@ -24,8 +26,8 @@
|
|
|
24
26
|
"i18next-icu": "^2.4.1",
|
|
25
27
|
"react": "^19.2.0",
|
|
26
28
|
"react-i18next": "^16.4.0",
|
|
27
|
-
"@fe-free/tool": "4.1.
|
|
28
|
-
"@fe-free/icons": "4.1.
|
|
29
|
+
"@fe-free/tool": "4.1.9",
|
|
30
|
+
"@fe-free/icons": "4.1.9"
|
|
29
31
|
},
|
|
30
32
|
"scripts": {
|
|
31
33
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ChatMessage, MSenderProps } from '@fe-free/ai';
|
|
2
|
+
import {
|
|
3
|
+
Chat,
|
|
4
|
+
createChatStore,
|
|
5
|
+
EnumChatMessageStatus,
|
|
6
|
+
generateUUID,
|
|
7
|
+
Messages,
|
|
8
|
+
MSender,
|
|
9
|
+
} from '@fe-free/ai';
|
|
10
|
+
import { sleep } from '@fe-free/tool';
|
|
11
|
+
import type { Meta } from '@storybook/react-vite';
|
|
12
|
+
import { set } from 'lodash-es';
|
|
13
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
14
|
+
|
|
15
|
+
const meta: Meta = {
|
|
16
|
+
title: '@fe-free/ai/Example',
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface AIData {
|
|
21
|
+
text?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function fakeFetchStream(v: MSenderProps['value'], { onUpdate }) {
|
|
25
|
+
// 这里模拟 fetchStream
|
|
26
|
+
await sleep(1000);
|
|
27
|
+
|
|
28
|
+
let count = 0;
|
|
29
|
+
const timer = setInterval(() => {
|
|
30
|
+
if (count > 50) {
|
|
31
|
+
clearInterval(timer);
|
|
32
|
+
onUpdate({ event: 'done', data: '' });
|
|
33
|
+
} else {
|
|
34
|
+
onUpdate({ event: 'message', data: '回复' + count });
|
|
35
|
+
count++;
|
|
36
|
+
}
|
|
37
|
+
}, 300);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Component() {
|
|
41
|
+
const { useChatStore, useChatStoreComputed } = useMemo(() => {
|
|
42
|
+
return createChatStore<MSenderProps['value'], AIData>();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const senderValue = useChatStore((state) => state.senderValue);
|
|
46
|
+
const setSenderValue = useChatStore((state) => state.setSenderValue);
|
|
47
|
+
const messages = useChatStore((state) => state.messages);
|
|
48
|
+
const addMessage = useChatStore((state) => state.addMessage);
|
|
49
|
+
const updateMessage = useChatStore((state) => state.updateMessage);
|
|
50
|
+
const { chatStatus } = useChatStoreComputed();
|
|
51
|
+
|
|
52
|
+
const loading =
|
|
53
|
+
chatStatus === EnumChatMessageStatus.PENDING || chatStatus === EnumChatMessageStatus.STREAMING;
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
addMessage({
|
|
57
|
+
uuid: generateUUID(),
|
|
58
|
+
user: {
|
|
59
|
+
text: 'hello',
|
|
60
|
+
},
|
|
61
|
+
ai: {
|
|
62
|
+
data: {
|
|
63
|
+
text: '你好,\n我是AI,很高兴认识你',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
addMessage({
|
|
68
|
+
uuid: generateUUID(),
|
|
69
|
+
user: {
|
|
70
|
+
text: 'hello',
|
|
71
|
+
},
|
|
72
|
+
ai: {
|
|
73
|
+
data: {
|
|
74
|
+
text: '你\n好,\n我\n是\nAI,\n很\n高\n兴\n认\n识\n你\n你\n好,\n我\n是\nAI,\n很\n高\n兴\n认\n识\n你\n很\n高\n兴\n认\n识\n你\n',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const handleSubmit = useCallback((v) => {
|
|
81
|
+
console.log('onSubmit', v);
|
|
82
|
+
|
|
83
|
+
const message: ChatMessage<AIData> = {
|
|
84
|
+
uuid: generateUUID(),
|
|
85
|
+
status: EnumChatMessageStatus.PENDING,
|
|
86
|
+
user: {
|
|
87
|
+
...v,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
addMessage(message);
|
|
92
|
+
|
|
93
|
+
// fake
|
|
94
|
+
fakeFetchStream(v, {
|
|
95
|
+
onUpdate: ({ event, data }) => {
|
|
96
|
+
if (event === 'message') {
|
|
97
|
+
message.status = EnumChatMessageStatus.STREAMING;
|
|
98
|
+
|
|
99
|
+
const preText = message.ai?.data?.text || '';
|
|
100
|
+
set(message, 'ai.data.text', preText + data);
|
|
101
|
+
|
|
102
|
+
updateMessage({
|
|
103
|
+
...message,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (event === 'done') {
|
|
107
|
+
message.status = EnumChatMessageStatus.DONE;
|
|
108
|
+
updateMessage({
|
|
109
|
+
...message,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="h-[800px] w-[500px] border border-red-500">
|
|
118
|
+
<Chat
|
|
119
|
+
end={
|
|
120
|
+
<div className="p-2">
|
|
121
|
+
<MSender
|
|
122
|
+
value={senderValue}
|
|
123
|
+
onChange={(v) => setSenderValue(v)}
|
|
124
|
+
loading={loading}
|
|
125
|
+
onSubmit={handleSubmit}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
<Messages
|
|
131
|
+
messages={messages}
|
|
132
|
+
renderMessageOfUser={({ message }) => (
|
|
133
|
+
<div className="p-2">
|
|
134
|
+
<div className="rounded-xl bg-primary p-2 text-white">{message.user?.text}</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
renderMessageOfAI={({ message }) => (
|
|
138
|
+
<div className="p-2">
|
|
139
|
+
<div>{message.status}</div>
|
|
140
|
+
<pre className="whitespace-pre-wrap">{message.ai?.data?.text}</pre>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
/>
|
|
144
|
+
</Chat>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const Default = {
|
|
150
|
+
render: () => {
|
|
151
|
+
return <Component />;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default meta;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PageLayout } from '@fe-free/core';
|
|
2
|
+
|
|
3
|
+
function Chat({
|
|
4
|
+
end,
|
|
5
|
+
endClassName,
|
|
6
|
+
children,
|
|
7
|
+
childrenClassName,
|
|
8
|
+
}: {
|
|
9
|
+
end?: React.ReactNode;
|
|
10
|
+
endClassName?: string;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
childrenClassName?: string;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<PageLayout
|
|
16
|
+
direction="vertical"
|
|
17
|
+
end={end}
|
|
18
|
+
endClassName={endClassName}
|
|
19
|
+
childrenClassName={childrenClassName}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</PageLayout>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Chat };
|
package/src/helper.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import { range } from 'lodash-es';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
4
|
|
|
4
5
|
function RecordLoading({
|
|
5
6
|
count = 4,
|
|
@@ -28,4 +29,8 @@ function RecordLoading({
|
|
|
28
29
|
);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
function generateUUID() {
|
|
33
|
+
return uuidv4();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { generateUUID, RecordLoading };
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import './style.scss';
|
|
2
|
+
export { Chat } from './chat';
|
|
1
3
|
export { FileView, FileViewList } from './files';
|
|
4
|
+
export { generateUUID } from './helper';
|
|
2
5
|
export { MSender } from './m_sender';
|
|
3
6
|
export type { MSenderProps, MSenderRef } from './m_sender';
|
|
7
|
+
export { Messages } from './messages';
|
|
8
|
+
export type { MessagesProps } from './messages';
|
|
4
9
|
export { Sender } from './sender';
|
|
5
10
|
export type { SenderProps, SenderRef } from './sender';
|
|
11
|
+
export { createChatStore } from './store';
|
|
12
|
+
export { EnumChatMessageStatus, EnumChatMessageType } from './store/types';
|
|
13
|
+
export type { ChatMessage, ChatMessageOfAI, ChatMessageOfUser } from './store/types';
|
|
6
14
|
export { fetchStream } from './stream';
|
|
7
15
|
export { Tip } from './tip';
|
|
8
|
-
|
|
16
|
+
export { recordAudioOfPCM } from './voice';
|
package/src/m_sender/index.tsx
CHANGED
|
@@ -58,7 +58,7 @@ function MSender(originProps: MSenderProps) {
|
|
|
58
58
|
<div
|
|
59
59
|
ref={refContainer}
|
|
60
60
|
className={classNames(
|
|
61
|
-
'fea-m-sender relative flex items-end rounded-
|
|
61
|
+
'fea-m-sender relative flex items-end rounded-xl border border-01 bg-white p-2',
|
|
62
62
|
)}
|
|
63
63
|
>
|
|
64
64
|
{allowSpeech && !value?.text && (
|
package/src/m_sender/record.tsx
CHANGED
|
@@ -159,7 +159,7 @@ function RecordAction(props: MSenderProps & { setType }) {
|
|
|
159
159
|
|
|
160
160
|
return (
|
|
161
161
|
<div
|
|
162
|
-
className={classNames('absolute inset-0 flex items-center justify-center rounded-
|
|
162
|
+
className={classNames('absolute inset-0 flex items-center justify-center rounded-xl', {
|
|
163
163
|
'bg-white': !isRecording,
|
|
164
164
|
'bg-red-500': isRecording && isCancel,
|
|
165
165
|
'bg-primary': isRecording && !isCancel,
|
package/src/m_sender/types.ts
CHANGED
|
@@ -9,10 +9,10 @@ interface MSenderValue {
|
|
|
9
9
|
|
|
10
10
|
interface MSenderProps {
|
|
11
11
|
value?: MSenderValue;
|
|
12
|
-
onChange
|
|
12
|
+
onChange?: (value?: MSenderValue) => void;
|
|
13
13
|
|
|
14
14
|
loading?: boolean;
|
|
15
|
-
onSubmit
|
|
15
|
+
onSubmit?: (value?: MSenderValue) => void | Promise<void>;
|
|
16
16
|
|
|
17
17
|
placeholder?: string;
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ interface MSenderProps {
|
|
|
24
24
|
onRecordEnd?: (isSend: boolean) => Promise<void>;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
defaultType
|
|
27
|
+
defaultType?: 'input' | 'record';
|
|
28
28
|
statement?: string | false;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { PageLayout } from '@fe-free/core';
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import type { ChatMessage } from '../store/types';
|
|
4
|
+
|
|
5
|
+
interface MessagesProps<AIData> {
|
|
6
|
+
messages?: ChatMessage<AIData>[];
|
|
7
|
+
renderMessageOfUser?: (props: { message: ChatMessage<AIData> }) => React.ReactNode;
|
|
8
|
+
renderMessageOfAI?: (props: { message: ChatMessage<AIData> }) => React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function Messages<AIData>(props: MessagesProps<AIData>) {
|
|
12
|
+
const { messages, renderMessageOfUser, renderMessageOfAI } = props;
|
|
13
|
+
|
|
14
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
const lastMessage = useMemo(() => {
|
|
17
|
+
return messages?.[messages.length - 1];
|
|
18
|
+
}, [messages]);
|
|
19
|
+
|
|
20
|
+
// 首次和更新时滚动到最新消息
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!lastMessage) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const element = document.querySelector(`[data-uuid="${lastMessage.uuid}"]`);
|
|
27
|
+
if (element) {
|
|
28
|
+
element.scrollIntoView({ behavior: 'smooth' });
|
|
29
|
+
}
|
|
30
|
+
}, [lastMessage?.uuid]);
|
|
31
|
+
|
|
32
|
+
// 数据更新是,如果 dom 处于可视区域,则滚动
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!lastMessage || !ref.current) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const element = document.querySelector(`[data-uuid="${lastMessage.uuid}"]`);
|
|
39
|
+
if (!element) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { top: listTop, bottom: listBottom } = ref.current!.getBoundingClientRect();
|
|
44
|
+
const { top, bottom } = element.getBoundingClientRect();
|
|
45
|
+
|
|
46
|
+
// 如果最后一个元素可见,则滚动到底部
|
|
47
|
+
const isVisible = top < listBottom && bottom > listTop;
|
|
48
|
+
if (isVisible) {
|
|
49
|
+
element.scrollIntoView({ behavior: 'smooth' });
|
|
50
|
+
}
|
|
51
|
+
}, [lastMessage?.updatedAt]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<PageLayout>
|
|
55
|
+
<div ref={ref} className="flex h-full flex-col overflow-y-auto">
|
|
56
|
+
{messages?.map((message) => (
|
|
57
|
+
<div key={message.uuid} data-uuid={message.uuid} className="flex flex-col">
|
|
58
|
+
<div className="flex justify-end">{renderMessageOfUser?.({ message })}</div>
|
|
59
|
+
<div className="flex justify-start">{renderMessageOfAI?.({ message })}</div>
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</PageLayout>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Messages };
|
|
68
|
+
export type { MessagesProps };
|
package/src/sender/index.tsx
CHANGED
|
@@ -100,7 +100,7 @@ function Sender(originProps: SenderProps) {
|
|
|
100
100
|
<div
|
|
101
101
|
ref={refContainer}
|
|
102
102
|
className={classNames(
|
|
103
|
-
'fea-sender relative flex flex-col rounded-
|
|
103
|
+
'fea-sender relative flex flex-col rounded-xl border border-01 bg-white p-2',
|
|
104
104
|
{
|
|
105
105
|
'fea-sender-drag-hover': dragHover,
|
|
106
106
|
},
|
package/src/sender/types.ts
CHANGED
|
@@ -9,10 +9,10 @@ interface SenderValue {
|
|
|
9
9
|
|
|
10
10
|
interface SenderProps {
|
|
11
11
|
value?: SenderValue;
|
|
12
|
-
onChange
|
|
12
|
+
onChange?: (value?: SenderValue) => void;
|
|
13
13
|
|
|
14
14
|
loading?: boolean;
|
|
15
|
-
onSubmit
|
|
15
|
+
onSubmit?: (value?: SenderValue) => void | Promise<void>;
|
|
16
16
|
|
|
17
17
|
placeholder?: string;
|
|
18
18
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import type { ChatMessage } from './types';
|
|
4
|
+
|
|
5
|
+
interface ChatStore<Value, AIData> {
|
|
6
|
+
senderValue?: Value;
|
|
7
|
+
setSenderValue: (senderValue?: Value) => void;
|
|
8
|
+
|
|
9
|
+
messages: ChatMessage<AIData>[];
|
|
10
|
+
addMessage: (message: ChatMessage<AIData>) => void;
|
|
11
|
+
updateMessage: (message: ChatMessage<AIData>) => void;
|
|
12
|
+
|
|
13
|
+
reset: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createChatStore<Value, AIData>() {
|
|
17
|
+
const useChatStore = create<ChatStore<Value, AIData>>((set, get, store) => ({
|
|
18
|
+
senderValue: undefined,
|
|
19
|
+
setSenderValue: (senderValue) => {
|
|
20
|
+
set(() => ({ senderValue }));
|
|
21
|
+
},
|
|
22
|
+
messages: [],
|
|
23
|
+
addMessage: (message) => {
|
|
24
|
+
set((state) => ({
|
|
25
|
+
messages: [
|
|
26
|
+
...state.messages,
|
|
27
|
+
{
|
|
28
|
+
...message,
|
|
29
|
+
updatedAt: Date.now(),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}));
|
|
33
|
+
},
|
|
34
|
+
updateMessage: (message) => {
|
|
35
|
+
set((state) => ({
|
|
36
|
+
messages: state.messages.map((m) => {
|
|
37
|
+
if (m.uuid === message.uuid) {
|
|
38
|
+
return {
|
|
39
|
+
...message,
|
|
40
|
+
updatedAt: Date.now(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return m;
|
|
44
|
+
}),
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
reset: () => {
|
|
48
|
+
set(store.getInitialState());
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const useChatStoreComputed = () => {
|
|
53
|
+
const messages = useChatStore((state) => state.messages);
|
|
54
|
+
|
|
55
|
+
const chatStatus = useMemo(() => {
|
|
56
|
+
return messages[messages.length - 1]?.status;
|
|
57
|
+
}, [messages]);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
chatStatus,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
useChatStore,
|
|
66
|
+
useChatStoreComputed,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { createChatStore };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
enum EnumChatMessageType {
|
|
2
|
+
SYSTEM = 'system',
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
enum EnumChatMessageStatus {
|
|
6
|
+
PENDING = 'pending',
|
|
7
|
+
STREAMING = 'streaming',
|
|
8
|
+
DONE = 'done',
|
|
9
|
+
ERROR = 'error',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ChatMessageOfUser {
|
|
13
|
+
text?: string;
|
|
14
|
+
files?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ChatMessageOfAI<AIData> {
|
|
18
|
+
data?: AIData;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ChatMessage<AIData> {
|
|
22
|
+
uuid: string;
|
|
23
|
+
updatedAt?: number;
|
|
24
|
+
type?: EnumChatMessageType;
|
|
25
|
+
status?: EnumChatMessageStatus;
|
|
26
|
+
user?: ChatMessageOfUser;
|
|
27
|
+
ai?: ChatMessageOfAI<AIData>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { EnumChatMessageStatus, EnumChatMessageType };
|
|
31
|
+
|
|
32
|
+
export type { ChatMessage, ChatMessageOfAI, ChatMessageOfUser };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
async function recordAudioOfPCM({ onAudio }: { onAudio: (data: ArrayBufferLike) => void }) {
|
|
2
|
+
// --- 初始化音频 ---
|
|
3
|
+
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
4
|
+
const audioContext = new AudioContext({ sampleRate: 16000 });
|
|
5
|
+
const sourceNode = audioContext.createMediaStreamSource(micStream);
|
|
6
|
+
// ScriptProcessorNode(4096 是稳定 buffer)
|
|
7
|
+
const processorNode = audioContext.createScriptProcessor(4096, 1, 1);
|
|
8
|
+
|
|
9
|
+
processorNode.onaudioprocess = function (event) {
|
|
10
|
+
const float32Data = event.inputBuffer.getChannelData(0); // float32
|
|
11
|
+
|
|
12
|
+
// === 转成 Int16 PCM ===
|
|
13
|
+
const pcm16 = new Int16Array(float32Data.length);
|
|
14
|
+
for (let i = 0; i < float32Data.length; i++) {
|
|
15
|
+
const s = Math.max(-1, Math.min(1, float32Data[i]));
|
|
16
|
+
pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onAudio(pcm16.buffer);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
sourceNode.connect(processorNode);
|
|
23
|
+
processorNode.connect(audioContext.destination);
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
if (processorNode) processorNode.disconnect();
|
|
27
|
+
if (sourceNode) sourceNode.disconnect();
|
|
28
|
+
if (audioContext) audioContext.close();
|
|
29
|
+
if (micStream) micStream.getTracks().forEach((track) => track.stop());
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { recordAudioOfPCM };
|
package/src/sender/helper.tsx
DELETED
|
File without changes
|