@adminforth/agent 1.37.0 → 1.38.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/agent/languageDetect.ts +0 -8
- package/agent/simpleAgent.ts +5 -5
- package/agent/systemPrompt.ts +35 -4
- package/agent/toolCallEvents.ts +31 -2
- package/agent/tools/apiTool.ts +1 -1
- package/agentResponseEvents.ts +197 -0
- package/apiBasedTools.ts +118 -284
- package/build.log +12 -2
- package/custom/ChatSurface.vue +31 -21
- package/custom/composables/agentAudio/agent-processing.mp3 +0 -0
- package/custom/composables/agentStore/constants.ts +8 -1
- package/custom/composables/agentStore/useAgentSessions.ts +85 -12
- package/custom/composables/useAgentAudio.ts +392 -0
- package/custom/composables/useAgentStore.ts +52 -5
- package/custom/conversation_area/ConversationArea.vue +1 -1
- package/custom/conversation_area/MessageRenderer.vue +12 -1
- package/custom/conversation_area/SystemMessageRenderer.vue +28 -0
- package/custom/conversation_area/TextRenderer.vue +4 -3
- package/custom/conversation_area/ToolRenderer.vue +1 -1
- package/custom/package.json +2 -1
- package/custom/pnpm-lock.yaml +29 -0
- package/custom/speech_recognition_frontend/AudioLines.vue +97 -0
- package/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
- package/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
- package/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
- package/custom/types.ts +52 -2
- package/dist/agent/languageDetect.js +0 -6
- package/dist/agent/simpleAgent.js +4 -3
- package/dist/agent/systemPrompt.js +24 -3
- package/dist/agent/toolCallEvents.js +24 -2
- package/dist/agent/tools/apiTool.js +1 -1
- package/dist/agentResponseEvents.js +141 -0
- package/dist/apiBasedTools.js +95 -211
- package/dist/custom/ChatSurface.vue +31 -21
- package/dist/custom/composables/agentAudio/agent-processing.mp3 +0 -0
- package/dist/custom/composables/agentStore/constants.ts +8 -1
- package/dist/custom/composables/agentStore/useAgentSessions.ts +85 -12
- package/dist/custom/composables/useAgentAudio.ts +392 -0
- package/dist/custom/composables/useAgentStore.ts +52 -5
- package/dist/custom/conversation_area/ConversationArea.vue +1 -1
- package/dist/custom/conversation_area/MessageRenderer.vue +12 -1
- package/dist/custom/conversation_area/SystemMessageRenderer.vue +28 -0
- package/dist/custom/conversation_area/TextRenderer.vue +4 -3
- package/dist/custom/conversation_area/ToolRenderer.vue +1 -1
- package/dist/custom/package.json +2 -1
- package/dist/custom/pnpm-lock.yaml +29 -0
- package/dist/custom/speech_recognition_frontend/AudioLines.vue +97 -0
- package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
- package/dist/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
- package/dist/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
- package/dist/custom/types.ts +52 -2
- package/dist/index.js +290 -400
- package/index.ts +318 -492
- package/package.json +3 -2
- package/types.ts +1 -1
|
@@ -9,4 +9,11 @@ export const MIN_WIDTH = 25;
|
|
|
9
9
|
export const DEFAULT_TEXTAREA_PLACEHOLDER = 'Type a message...';
|
|
10
10
|
export const PLACEHOLDER_TYPING_DELAY_MS = 60;
|
|
11
11
|
export const PLACEHOLDER_DELETING_DELAY_MS = 35;
|
|
12
|
-
export const PLACEHOLDER_HOLD_DELAY_MS = 3000;
|
|
12
|
+
export const PLACEHOLDER_HOLD_DELAY_MS = 3000;
|
|
13
|
+
export const PRE_SESSION_ID = 'pre-session';
|
|
14
|
+
|
|
15
|
+
export enum RESERVED_SYSTEM_MESSAGE_CONTENT {
|
|
16
|
+
START_AUDIO_CHAT = 'START_AUDIO_CHAT',
|
|
17
|
+
END_AUDIO_CHAT = 'END_AUDIO_CHAT',
|
|
18
|
+
AGENT_RESPONSE_ABORTED = 'AGENT_RESPONSE_ABORTED'
|
|
19
|
+
}
|
|
@@ -2,6 +2,8 @@ import type { ComputedRef, Ref, ShallowRef } from 'vue';
|
|
|
2
2
|
import { callAdminForthApi } from '@/utils';
|
|
3
3
|
import type { Chat } from '../../chat';
|
|
4
4
|
import type { IAgentSession, ISessionsListItem, IPart } from '../../types';
|
|
5
|
+
import { PRE_SESSION_ID } from './constants';
|
|
6
|
+
import { ChatStatus } from 'ai';
|
|
5
7
|
import { useI18n } from 'vue-i18n';
|
|
6
8
|
|
|
7
9
|
type AdminforthLike = {
|
|
@@ -91,8 +93,8 @@ export function createAgentSessionManager({
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
async function deletePreSession() {
|
|
94
|
-
sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !==
|
|
95
|
-
if (activeSessionId.value ===
|
|
96
|
+
sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== PRE_SESSION_ID);
|
|
97
|
+
if (activeSessionId.value === PRE_SESSION_ID) {
|
|
96
98
|
activeSessionId.value = null;
|
|
97
99
|
currentSession.value = null;
|
|
98
100
|
}
|
|
@@ -129,7 +131,7 @@ export function createAgentSessionManager({
|
|
|
129
131
|
if (!message || isResponseInProgress.value) {
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
132
|
-
if (!currentSession.value || currentSession.value.sessionId ===
|
|
134
|
+
if (!currentSession.value || currentSession.value.sessionId === PRE_SESSION_ID) {
|
|
133
135
|
await createNewSession(message);
|
|
134
136
|
}
|
|
135
137
|
currentSession.value!.timestamp = new Date().toISOString();
|
|
@@ -146,27 +148,27 @@ export function createAgentSessionManager({
|
|
|
146
148
|
|
|
147
149
|
async function createPreSession() {
|
|
148
150
|
saveCurrentSessionInCache();
|
|
149
|
-
if (!sessionList.value.some((s: ISessionsListItem) => s.sessionId ===
|
|
151
|
+
if (!sessionList.value.some((s: ISessionsListItem) => s.sessionId === PRE_SESSION_ID)) {
|
|
150
152
|
sessionList.value.unshift({
|
|
151
|
-
sessionId:
|
|
153
|
+
sessionId: PRE_SESSION_ID,
|
|
152
154
|
title: 'New Session',
|
|
153
155
|
timestamp: new Date().toISOString(),
|
|
154
156
|
});
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
activeSessionId.value =
|
|
159
|
+
activeSessionId.value = PRE_SESSION_ID;
|
|
158
160
|
currentSession.value = {
|
|
159
|
-
sessionId:
|
|
161
|
+
sessionId: PRE_SESSION_ID,
|
|
160
162
|
title: 'New Session',
|
|
161
163
|
timestamp: new Date().toISOString(),
|
|
162
164
|
messages: [],
|
|
163
165
|
};
|
|
164
|
-
sessions.value[
|
|
165
|
-
setCurrentChat(
|
|
166
|
+
sessions.value[PRE_SESSION_ID] = currentSession.value;
|
|
167
|
+
setCurrentChat(PRE_SESSION_ID);
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
async function deleteSession(sessionId: string) {
|
|
169
|
-
if (sessionId ===
|
|
171
|
+
if (sessionId === PRE_SESSION_ID) {
|
|
170
172
|
deletePreSession();
|
|
171
173
|
return;
|
|
172
174
|
}
|
|
@@ -236,7 +238,10 @@ export function createAgentSessionManager({
|
|
|
236
238
|
currentChat.value?.messages.push(debugMessage);
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
function addSystemMessage(message: string) {
|
|
241
|
+
async function addSystemMessage(message: string) {
|
|
242
|
+
if (!currentSession.value || currentSession.value.sessionId === PRE_SESSION_ID) {
|
|
243
|
+
await createNewSession('Audio chat');
|
|
244
|
+
}
|
|
240
245
|
const systemMessage = {
|
|
241
246
|
role: 'system',
|
|
242
247
|
parts: [{
|
|
@@ -247,7 +252,7 @@ export function createAgentSessionManager({
|
|
|
247
252
|
};
|
|
248
253
|
currentChat.value?.messages.push(systemMessage);
|
|
249
254
|
try {
|
|
250
|
-
const res = callAdminForthApi({
|
|
255
|
+
const res = await callAdminForthApi({
|
|
251
256
|
method: 'POST',
|
|
252
257
|
path: '/agent/add-system-message-to-turns',
|
|
253
258
|
body: {
|
|
@@ -260,6 +265,69 @@ export function createAgentSessionManager({
|
|
|
260
265
|
}
|
|
261
266
|
}
|
|
262
267
|
|
|
268
|
+
function addAgentMessage(message: string) {
|
|
269
|
+
const agentMessage = {
|
|
270
|
+
role: 'assistant',
|
|
271
|
+
parts: [{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: message,
|
|
274
|
+
state: 'done',
|
|
275
|
+
}]
|
|
276
|
+
};
|
|
277
|
+
currentChat.value?.messages.push(agentMessage);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function updateLastAgentMessage(message: string) {
|
|
281
|
+
const lastMsg = currentChat.value?.lastMessage;
|
|
282
|
+
if (lastMsg && lastMsg.role === 'assistant') {
|
|
283
|
+
lastMsg.parts = [{
|
|
284
|
+
type: 'text',
|
|
285
|
+
text: message,
|
|
286
|
+
state: 'done',
|
|
287
|
+
}];
|
|
288
|
+
currentChat.value?.messages.splice(currentChat.value.messages.length - 1, 1, lastMsg);
|
|
289
|
+
} else {
|
|
290
|
+
addAgentMessage(message);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function addUserMessage(message: string) {
|
|
295
|
+
const userMessage = {
|
|
296
|
+
role: 'user',
|
|
297
|
+
parts: [{
|
|
298
|
+
type: 'text',
|
|
299
|
+
text: message,
|
|
300
|
+
state: 'done',
|
|
301
|
+
}]
|
|
302
|
+
};
|
|
303
|
+
currentChat.value?.messages.push(userMessage);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
function addDataToolCallMessage(data: any) {
|
|
308
|
+
const lastMessage = currentChat.value?.lastMessage;
|
|
309
|
+
if (lastMessage.role === 'assistant') {
|
|
310
|
+
lastMessage.parts.push({
|
|
311
|
+
type: 'data-tool-call',
|
|
312
|
+
data,
|
|
313
|
+
});
|
|
314
|
+
currentChat.value?.messages.splice(currentChat.value.messages.length - 1, 1, lastMessage);
|
|
315
|
+
} else {
|
|
316
|
+
const toolCallMessage = {
|
|
317
|
+
role: 'assistant',
|
|
318
|
+
parts: [{
|
|
319
|
+
type: 'data-tool-call',
|
|
320
|
+
data,
|
|
321
|
+
}]
|
|
322
|
+
};
|
|
323
|
+
currentChat.value?.messages.push(toolCallMessage);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function setCurrentChatStatus(status: ChatStatus) {
|
|
328
|
+
(currentChat.value as any)?.setStatus({status});
|
|
329
|
+
}
|
|
330
|
+
|
|
263
331
|
return {
|
|
264
332
|
sendMessage,
|
|
265
333
|
createPreSession,
|
|
@@ -268,5 +336,10 @@ export function createAgentSessionManager({
|
|
|
268
336
|
deleteSession,
|
|
269
337
|
addDebugMessage,
|
|
270
338
|
addSystemMessage,
|
|
339
|
+
addAgentMessage,
|
|
340
|
+
addUserMessage,
|
|
341
|
+
addDataToolCallMessage,
|
|
342
|
+
setCurrentChatStatus,
|
|
343
|
+
updateLastAgentMessage
|
|
271
344
|
};
|
|
272
345
|
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import adminforth from "@/adminforth";
|
|
2
|
+
import { useAgentStore } from "./useAgentStore";
|
|
3
|
+
import { defineStore } from 'pinia';
|
|
4
|
+
import type { SpeechStreamEvent } from '../types';
|
|
5
|
+
import { ref } from 'vue';
|
|
6
|
+
import { getCurrentPageContext } from './agentStore/pageContext';
|
|
7
|
+
|
|
8
|
+
type StreamingAudioState = {
|
|
9
|
+
mimeType: string;
|
|
10
|
+
mediaSource: MediaSource;
|
|
11
|
+
sourceBuffer: SourceBuffer | null;
|
|
12
|
+
pendingChunks: ArrayBuffer[];
|
|
13
|
+
hasStartedPlayback: boolean;
|
|
14
|
+
isDone: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const standByAudio = new Audio('./agentAudio/agent-processing.mp3');
|
|
18
|
+
|
|
19
|
+
function playStandByAudio() {
|
|
20
|
+
standByAudio.currentTime = 0;
|
|
21
|
+
standByAudio.play()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stopStandByAudio() {
|
|
25
|
+
standByAudio.pause();
|
|
26
|
+
standByAudio.currentTime = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function restartStandByAudio() {
|
|
30
|
+
standByAudio.currentTime = 0;
|
|
31
|
+
playStandByAudio();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
standByAudio.addEventListener('ended', () => {
|
|
35
|
+
if (!standByAudio.paused) {
|
|
36
|
+
restartStandByAudio();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const useAgentAudio = defineStore('agentAudio', () => {
|
|
41
|
+
const agentStore = useAgentStore();
|
|
42
|
+
const agentAudioMode = ref<'transcribing' | 'streaming' | 'fetchingAudio' | 'playingAgentResponse' | null>(null);
|
|
43
|
+
const isStreamingResponse = ref(false);
|
|
44
|
+
|
|
45
|
+
let currentAbortController: AbortController | null = null;
|
|
46
|
+
let isPlaying = false;
|
|
47
|
+
let currentAudio: HTMLAudioElement | null = null;
|
|
48
|
+
let currentAudioObjectUrl: string | null = null;
|
|
49
|
+
let currentStreamingAudio: StreamingAudioState | null = null;
|
|
50
|
+
let bufferedAudioChunks: ArrayBuffer[] = [];
|
|
51
|
+
let bufferedAudioMimeType = 'audio/mpeg';
|
|
52
|
+
|
|
53
|
+
function stopGenerationAndAudio() {
|
|
54
|
+
agentAudioMode.value = null;
|
|
55
|
+
stopCurrentAudioPlayback();
|
|
56
|
+
currentAbortController?.abort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function sendAudioToServerAndHandleResponse(blob: Blob) {
|
|
60
|
+
currentAbortController = new AbortController();
|
|
61
|
+
const formData = new FormData();
|
|
62
|
+
formData.append('file', blob, 'user_prompt.webm');
|
|
63
|
+
formData.append('sessionId', agentStore.activeSessionId);
|
|
64
|
+
formData.append('mode', agentStore.activeModeName ?? '');
|
|
65
|
+
formData.append('timeZone', Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
66
|
+
formData.append('currentPage', JSON.stringify(getCurrentPageContext()));
|
|
67
|
+
const fullPath = `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/agent/speech-response`;
|
|
68
|
+
try {
|
|
69
|
+
agentAudioMode.value = 'transcribing';
|
|
70
|
+
const res = await fetch(fullPath, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
body: formData,
|
|
73
|
+
headers: {
|
|
74
|
+
Accept: 'text/event-stream',
|
|
75
|
+
},
|
|
76
|
+
signal: currentAbortController!.signal,
|
|
77
|
+
});
|
|
78
|
+
isStreamingResponse.value = true;
|
|
79
|
+
if (res.ok) {
|
|
80
|
+
agentAudioMode.value = 'streaming';
|
|
81
|
+
await readSpeechResponseStream(res);
|
|
82
|
+
} else {
|
|
83
|
+
console.error('Failed to transcribe audio:', res.statusText);
|
|
84
|
+
adminforth.alert({message: 'Failed to transcribe audio', variant: 'danger'});
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
88
|
+
//
|
|
89
|
+
} else {
|
|
90
|
+
console.error('Error sending audio to server:', error);
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
isStreamingResponse.value = false;
|
|
94
|
+
agentAudioMode.value = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readSpeechResponseStream(res: Response) {
|
|
99
|
+
const reader = res.body?.getReader();
|
|
100
|
+
if (!reader) {
|
|
101
|
+
adminforth.alert({message: 'Speech response stream is not available', variant: 'danger'});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const decoder = new TextDecoder();
|
|
106
|
+
let buffer = '';
|
|
107
|
+
|
|
108
|
+
agentStore.setCurrentChatStatus('streaming');
|
|
109
|
+
try {
|
|
110
|
+
while (true) {
|
|
111
|
+
const { value, done } = await reader.read();
|
|
112
|
+
|
|
113
|
+
if (done) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
buffer += decoder.decode(value, { stream: true });
|
|
118
|
+
const eventBlocks = buffer.split('\n\n');
|
|
119
|
+
buffer = eventBlocks.pop() ?? '';
|
|
120
|
+
|
|
121
|
+
for (const eventBlock of eventBlocks) {
|
|
122
|
+
await handleSpeechStreamEvent(eventBlock);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
buffer += decoder.decode();
|
|
127
|
+
|
|
128
|
+
if (buffer.trim()) {
|
|
129
|
+
await handleSpeechStreamEvent(buffer);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
finishAudioStream();
|
|
133
|
+
} finally {
|
|
134
|
+
reader.releaseLock();
|
|
135
|
+
agentStore.setCurrentChatStatus('ready');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function handleSpeechStreamEvent(eventBlock: string) {
|
|
140
|
+
if (currentAbortController?.signal.aborted) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const data = eventBlock
|
|
144
|
+
.split('\n')
|
|
145
|
+
.filter((line) => line.startsWith('data:'))
|
|
146
|
+
.map((line) => line.slice(5).trimStart())
|
|
147
|
+
.join('\n');
|
|
148
|
+
|
|
149
|
+
if (!data || data === '[DONE]') {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const event = JSON.parse(data) as SpeechStreamEvent;
|
|
154
|
+
|
|
155
|
+
if (event.type === 'error') {
|
|
156
|
+
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (event.type === 'transcript') {
|
|
161
|
+
agentStore.addUserMessage(event.data.text);
|
|
162
|
+
agentStore.updateLastAgentMessage('');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (event.type === 'speech-response') {
|
|
167
|
+
// stopStandByAudio();
|
|
168
|
+
agentStore.setCurrentChatStatus('ready');
|
|
169
|
+
agentStore.addAgentMessage(event.data.response.text);
|
|
170
|
+
agentAudioMode.value = 'playingAgentResponse';
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (event.type === 'audio-start') {
|
|
175
|
+
isStreamingResponse.value = false;
|
|
176
|
+
agentAudioMode.value = 'fetchingAudio';
|
|
177
|
+
initializeAudioStream(event.data.mimeType);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (event.type === 'audio-delta') {
|
|
182
|
+
appendAudioChunk(event.data.base64);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (event.type === 'audio-done') {
|
|
187
|
+
finishAudioStream();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (event.type === 'data-tool-call') {
|
|
192
|
+
// playStandByAudio();
|
|
193
|
+
agentStore.addDataToolCallMessage(event.data);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function setIsPlaying(value: boolean) {
|
|
198
|
+
isPlaying = value;
|
|
199
|
+
|
|
200
|
+
if (!currentAudio) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!isPlaying) {
|
|
205
|
+
currentAudio.pause();
|
|
206
|
+
currentAudio.currentTime = 0;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
agentAudioMode.value = 'playingAgentResponse';
|
|
210
|
+
void currentAudio.play().catch((error) => {
|
|
211
|
+
console.error('Failed to play audio:', error);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function initializeAudioStream(mimeType: string) {
|
|
216
|
+
stopCurrentAudioPlayback();
|
|
217
|
+
bufferedAudioMimeType = mimeType;
|
|
218
|
+
|
|
219
|
+
if (typeof MediaSource === 'undefined' || !MediaSource.isTypeSupported(mimeType)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const mediaSource = new MediaSource();
|
|
224
|
+
currentAudioObjectUrl = URL.createObjectURL(mediaSource);
|
|
225
|
+
currentAudio = new Audio(currentAudioObjectUrl);
|
|
226
|
+
currentAudio.addEventListener('ended', handleAudioEnded, { once: true });
|
|
227
|
+
currentStreamingAudio = {
|
|
228
|
+
mimeType,
|
|
229
|
+
mediaSource,
|
|
230
|
+
sourceBuffer: null,
|
|
231
|
+
pendingChunks: [],
|
|
232
|
+
hasStartedPlayback: false,
|
|
233
|
+
isDone: false,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
mediaSource.addEventListener('sourceopen', handleMediaSourceOpen, { once: true });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function handleMediaSourceOpen() {
|
|
240
|
+
if (!currentStreamingAudio) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
currentStreamingAudio.sourceBuffer = currentStreamingAudio.mediaSource.addSourceBuffer(currentStreamingAudio.mimeType);
|
|
246
|
+
currentStreamingAudio.sourceBuffer.mode = 'sequence';
|
|
247
|
+
currentStreamingAudio.sourceBuffer.addEventListener('updateend', flushStreamingAudioQueue);
|
|
248
|
+
flushStreamingAudioQueue();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('Failed to initialize streaming audio playback:', error);
|
|
251
|
+
bufferedAudioChunks.push(...currentStreamingAudio.pendingChunks);
|
|
252
|
+
detachStreamingAudio();
|
|
253
|
+
destroyCurrentAudioElement();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function appendAudioChunk(base64: string) {
|
|
258
|
+
const chunk = base64ToArrayBuffer(base64);
|
|
259
|
+
|
|
260
|
+
if (!currentStreamingAudio) {
|
|
261
|
+
bufferedAudioChunks.push(chunk);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
currentStreamingAudio.pendingChunks.push(chunk);
|
|
266
|
+
flushStreamingAudioQueue();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function flushStreamingAudioQueue() {
|
|
270
|
+
if (!currentStreamingAudio?.sourceBuffer || currentStreamingAudio.sourceBuffer.updating) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const nextChunk = currentStreamingAudio.pendingChunks.shift();
|
|
275
|
+
|
|
276
|
+
if (nextChunk) {
|
|
277
|
+
currentStreamingAudio.sourceBuffer.appendBuffer(nextChunk);
|
|
278
|
+
|
|
279
|
+
if (!currentStreamingAudio.hasStartedPlayback) {
|
|
280
|
+
currentStreamingAudio.hasStartedPlayback = true;
|
|
281
|
+
setIsPlaying(true);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (currentStreamingAudio.isDone && currentStreamingAudio.mediaSource.readyState === 'open') {
|
|
288
|
+
currentStreamingAudio.mediaSource.endOfStream();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function finishAudioStream() {
|
|
293
|
+
if (currentStreamingAudio) {
|
|
294
|
+
currentStreamingAudio.isDone = true;
|
|
295
|
+
flushStreamingAudioQueue();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!bufferedAudioChunks.length) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
playAudioChunks(bufferedAudioChunks, bufferedAudioMimeType);
|
|
304
|
+
bufferedAudioChunks = [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function detachStreamingAudio() {
|
|
308
|
+
if (currentStreamingAudio?.sourceBuffer) {
|
|
309
|
+
currentStreamingAudio.sourceBuffer.removeEventListener('updateend', flushStreamingAudioQueue);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
currentStreamingAudio = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function destroyCurrentAudioElement() {
|
|
316
|
+
if (currentAudio) {
|
|
317
|
+
currentAudio.pause();
|
|
318
|
+
currentAudio.currentTime = 0;
|
|
319
|
+
currentAudio.src = '';
|
|
320
|
+
currentAudio.load();
|
|
321
|
+
currentAudio = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (currentAudioObjectUrl) {
|
|
325
|
+
URL.revokeObjectURL(currentAudioObjectUrl);
|
|
326
|
+
currentAudioObjectUrl = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
isPlaying = false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function stopCurrentAudioPlayback(dontResetMode = false) {
|
|
333
|
+
bufferedAudioChunks = [];
|
|
334
|
+
bufferedAudioMimeType = 'audio/mpeg';
|
|
335
|
+
detachStreamingAudio();
|
|
336
|
+
destroyCurrentAudioElement();
|
|
337
|
+
if (!dontResetMode) {
|
|
338
|
+
agentAudioMode.value = null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function handleAudioEnded() {
|
|
343
|
+
agentAudioMode.value = null;
|
|
344
|
+
stopCurrentAudioPlayback();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function playAudioChunks(chunks: ArrayBuffer[], mimeType: string) {
|
|
348
|
+
currentAudioObjectUrl = URL.createObjectURL(new Blob(chunks, { type: mimeType }));
|
|
349
|
+
currentAudio = new Audio(currentAudioObjectUrl);
|
|
350
|
+
currentAudio.addEventListener('ended', handleAudioEnded, { once: true });
|
|
351
|
+
setIsPlaying(true);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function base64ToArrayBuffer(base64: string) {
|
|
355
|
+
const binary = atob(base64);
|
|
356
|
+
const bytes = new Uint8Array(binary.length);
|
|
357
|
+
|
|
358
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
359
|
+
bytes[i] = binary.charCodeAt(i);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return bytes.buffer;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function playBeep(freq = 800, duration = 0.05) {
|
|
366
|
+
const ctx = new AudioContext();
|
|
367
|
+
const osc = ctx.createOscillator();
|
|
368
|
+
const gain = ctx.createGain();
|
|
369
|
+
|
|
370
|
+
osc.frequency.value = freq;
|
|
371
|
+
osc.type = 'sine';
|
|
372
|
+
|
|
373
|
+
osc.connect(gain);
|
|
374
|
+
gain.connect(ctx.destination);
|
|
375
|
+
|
|
376
|
+
osc.start();
|
|
377
|
+
|
|
378
|
+
gain.gain.setValueAtTime(1, ctx.currentTime);
|
|
379
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
|
|
380
|
+
|
|
381
|
+
osc.stop(ctx.currentTime + duration);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
sendAudioToServerAndHandleResponse,
|
|
386
|
+
stopGenerationAndAudio,
|
|
387
|
+
stopCurrentAudioPlayback,
|
|
388
|
+
playBeep,
|
|
389
|
+
agentAudioMode
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
});
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
DEFAULT_CHAT_WIDTH,
|
|
12
12
|
MAX_WIDTH,
|
|
13
13
|
MIN_WIDTH,
|
|
14
|
+
RESERVED_SYSTEM_MESSAGE_CONTENT,
|
|
15
|
+
PRE_SESSION_ID
|
|
14
16
|
} from './agentStore/constants';
|
|
15
17
|
import { createAgentChatManager } from './agentStore/useAgentChat';
|
|
16
18
|
import { createAgentPlaceholderController } from './agentStore/useAgentPlaceholder';
|
|
@@ -67,6 +69,36 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
67
69
|
return window.localStorage.getItem(`${coreStore.config.brandName || 'adminforth'}-${key}`);
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
const isAudioChatMode = ref(false);
|
|
73
|
+
|
|
74
|
+
const onBeforeChatCloseCallbacks: Array<() => Promise<void>> = [];
|
|
75
|
+
function registerOnBeforeChatCloseCallback(hook: () => Promise<void>) {
|
|
76
|
+
onBeforeChatCloseCallbacks.push(hook);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function executeOnBeforeChatCloseCallbacks() {
|
|
80
|
+
for(const hook of onBeforeChatCloseCallbacks) {
|
|
81
|
+
try {
|
|
82
|
+
await hook();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error executing onBeforeChatClose callback:', error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setIsAudioChatMode(isAudioChat: boolean) {
|
|
90
|
+
isAudioChatMode.value = isAudioChat;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
watch(isAudioChatMode, (newVal: boolean) => {
|
|
94
|
+
if (newVal) {
|
|
95
|
+
addSystemMessage(RESERVED_SYSTEM_MESSAGE_CONTENT.START_AUDIO_CHAT);
|
|
96
|
+
} else {
|
|
97
|
+
addSystemMessage(RESERVED_SYSTEM_MESSAGE_CONTENT.END_AUDIO_CHAT);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
|
|
70
102
|
const isResponseInProgress = computed( () => {
|
|
71
103
|
return currentChat.value?.status === 'streaming';
|
|
72
104
|
});
|
|
@@ -79,6 +111,11 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
79
111
|
deleteSession,
|
|
80
112
|
addDebugMessage,
|
|
81
113
|
addSystemMessage,
|
|
114
|
+
addAgentMessage,
|
|
115
|
+
addUserMessage,
|
|
116
|
+
addDataToolCallMessage,
|
|
117
|
+
setCurrentChatStatus,
|
|
118
|
+
updateLastAgentMessage
|
|
82
119
|
} = createAgentSessionManager({
|
|
83
120
|
activeSessionId,
|
|
84
121
|
currentSession,
|
|
@@ -144,7 +181,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
144
181
|
}
|
|
145
182
|
}
|
|
146
183
|
lastSessionId.value = getLocalStorageItem('lastSessionId');
|
|
147
|
-
if (lastSessionId.value && lastSessionId.value !==
|
|
184
|
+
if (lastSessionId.value && lastSessionId.value !== PRE_SESSION_ID) {
|
|
148
185
|
setActiveSession(lastSessionId.value);
|
|
149
186
|
}
|
|
150
187
|
if (coreStore.isMobile) {
|
|
@@ -231,7 +268,8 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
231
268
|
activeModeName.value = modeName;
|
|
232
269
|
}
|
|
233
270
|
|
|
234
|
-
function closeChat() {
|
|
271
|
+
async function closeChat() {
|
|
272
|
+
await executeOnBeforeChatCloseCallbacks();
|
|
235
273
|
if(isFullScreen.value) {
|
|
236
274
|
document.body.style.overflow = '';
|
|
237
275
|
}
|
|
@@ -273,7 +311,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
273
311
|
|
|
274
312
|
function abortCurrentChatRequestAndAddSystemMessage() {
|
|
275
313
|
abortCurrentChatRequest();
|
|
276
|
-
addSystemMessage(
|
|
314
|
+
addSystemMessage(RESERVED_SYSTEM_MESSAGE_CONTENT.AGENT_RESPONSE_ABORTED);
|
|
277
315
|
}
|
|
278
316
|
|
|
279
317
|
return {
|
|
@@ -315,9 +353,18 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
315
353
|
DEFAULT_CHAT_WIDTH,
|
|
316
354
|
MAX_WIDTH,
|
|
317
355
|
MIN_WIDTH,
|
|
356
|
+
RESERVED_SYSTEM_MESSAGE_CONTENT,
|
|
318
357
|
getLocalStorageItem,
|
|
319
358
|
addDebugMessage,
|
|
320
359
|
abortCurrentChatRequestAndAddSystemMessage,
|
|
321
|
-
addSystemMessage
|
|
360
|
+
addSystemMessage,
|
|
361
|
+
isAudioChatMode,
|
|
362
|
+
setIsAudioChatMode,
|
|
363
|
+
registerOnBeforeChatCloseCallback,
|
|
364
|
+
addAgentMessage,
|
|
365
|
+
addUserMessage,
|
|
366
|
+
addDataToolCallMessage,
|
|
367
|
+
setCurrentChatStatus,
|
|
368
|
+
updateLastAgentMessage
|
|
322
369
|
}
|
|
323
|
-
})
|
|
370
|
+
})
|