@adminforth/agent 1.36.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 +88 -13
- 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 +88 -13
- 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 +4 -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,9 @@ 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';
|
|
7
|
+
import { useI18n } from 'vue-i18n';
|
|
5
8
|
|
|
6
9
|
type AdminforthLike = {
|
|
7
10
|
confirm(options: { message: string; yes: string; no: string }): Promise<boolean>;
|
|
@@ -40,6 +43,7 @@ export function createAgentSessionManager({
|
|
|
40
43
|
return [...sessionsListToSort].sort((a: ISessionsListItem, b: ISessionsListItem) => b.timestamp.localeCompare(a.timestamp));
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
const { t } = useI18n();
|
|
43
47
|
function saveCurrentSessionInCache() {
|
|
44
48
|
if (currentSession.value) {
|
|
45
49
|
currentSession.value.messages = currentChat.value?.messages.map((m: any) => ({
|
|
@@ -89,8 +93,8 @@ export function createAgentSessionManager({
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
async function deletePreSession() {
|
|
92
|
-
sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !==
|
|
93
|
-
if (activeSessionId.value ===
|
|
96
|
+
sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== PRE_SESSION_ID);
|
|
97
|
+
if (activeSessionId.value === PRE_SESSION_ID) {
|
|
94
98
|
activeSessionId.value = null;
|
|
95
99
|
currentSession.value = null;
|
|
96
100
|
}
|
|
@@ -127,7 +131,7 @@ export function createAgentSessionManager({
|
|
|
127
131
|
if (!message || isResponseInProgress.value) {
|
|
128
132
|
return;
|
|
129
133
|
}
|
|
130
|
-
if (!currentSession.value || currentSession.value.sessionId ===
|
|
134
|
+
if (!currentSession.value || currentSession.value.sessionId === PRE_SESSION_ID) {
|
|
131
135
|
await createNewSession(message);
|
|
132
136
|
}
|
|
133
137
|
currentSession.value!.timestamp = new Date().toISOString();
|
|
@@ -144,32 +148,32 @@ export function createAgentSessionManager({
|
|
|
144
148
|
|
|
145
149
|
async function createPreSession() {
|
|
146
150
|
saveCurrentSessionInCache();
|
|
147
|
-
if (!sessionList.value.some((s: ISessionsListItem) => s.sessionId ===
|
|
151
|
+
if (!sessionList.value.some((s: ISessionsListItem) => s.sessionId === PRE_SESSION_ID)) {
|
|
148
152
|
sessionList.value.unshift({
|
|
149
|
-
sessionId:
|
|
153
|
+
sessionId: PRE_SESSION_ID,
|
|
150
154
|
title: 'New Session',
|
|
151
155
|
timestamp: new Date().toISOString(),
|
|
152
156
|
});
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
activeSessionId.value =
|
|
159
|
+
activeSessionId.value = PRE_SESSION_ID;
|
|
156
160
|
currentSession.value = {
|
|
157
|
-
sessionId:
|
|
161
|
+
sessionId: PRE_SESSION_ID,
|
|
158
162
|
title: 'New Session',
|
|
159
163
|
timestamp: new Date().toISOString(),
|
|
160
164
|
messages: [],
|
|
161
165
|
};
|
|
162
|
-
sessions.value[
|
|
163
|
-
setCurrentChat(
|
|
166
|
+
sessions.value[PRE_SESSION_ID] = currentSession.value;
|
|
167
|
+
setCurrentChat(PRE_SESSION_ID);
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
async function deleteSession(sessionId: string) {
|
|
167
|
-
if (sessionId ===
|
|
171
|
+
if (sessionId === PRE_SESSION_ID) {
|
|
168
172
|
deletePreSession();
|
|
169
173
|
return;
|
|
170
174
|
}
|
|
171
175
|
blockCloseOfChat.value = true;
|
|
172
|
-
const isConfirmed = await adminforth.confirm({
|
|
176
|
+
const isConfirmed = await adminforth.confirm({title: t('Are you sure, that you want to delete this session?'), message: t('This process is irreversible.'), yes: 'Yes', no: 'No'});
|
|
173
177
|
blockCloseOfChat.value = false;
|
|
174
178
|
if (!isConfirmed) {
|
|
175
179
|
return;
|
|
@@ -234,7 +238,10 @@ export function createAgentSessionManager({
|
|
|
234
238
|
currentChat.value?.messages.push(debugMessage);
|
|
235
239
|
}
|
|
236
240
|
|
|
237
|
-
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
|
+
}
|
|
238
245
|
const systemMessage = {
|
|
239
246
|
role: 'system',
|
|
240
247
|
parts: [{
|
|
@@ -245,7 +252,7 @@ export function createAgentSessionManager({
|
|
|
245
252
|
};
|
|
246
253
|
currentChat.value?.messages.push(systemMessage);
|
|
247
254
|
try {
|
|
248
|
-
const res = callAdminForthApi({
|
|
255
|
+
const res = await callAdminForthApi({
|
|
249
256
|
method: 'POST',
|
|
250
257
|
path: '/agent/add-system-message-to-turns',
|
|
251
258
|
body: {
|
|
@@ -258,6 +265,69 @@ export function createAgentSessionManager({
|
|
|
258
265
|
}
|
|
259
266
|
}
|
|
260
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
|
+
|
|
261
331
|
return {
|
|
262
332
|
sendMessage,
|
|
263
333
|
createPreSession,
|
|
@@ -266,5 +336,10 @@ export function createAgentSessionManager({
|
|
|
266
336
|
deleteSession,
|
|
267
337
|
addDebugMessage,
|
|
268
338
|
addSystemMessage,
|
|
339
|
+
addAgentMessage,
|
|
340
|
+
addUserMessage,
|
|
341
|
+
addDataToolCallMessage,
|
|
342
|
+
setCurrentChatStatus,
|
|
343
|
+
updateLastAgentMessage
|
|
269
344
|
};
|
|
270
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
|
+
})
|