@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.
Files changed (55) hide show
  1. package/agent/languageDetect.ts +0 -8
  2. package/agent/simpleAgent.ts +5 -5
  3. package/agent/systemPrompt.ts +35 -4
  4. package/agent/toolCallEvents.ts +31 -2
  5. package/agent/tools/apiTool.ts +1 -1
  6. package/agentResponseEvents.ts +197 -0
  7. package/apiBasedTools.ts +118 -284
  8. package/build.log +12 -2
  9. package/custom/ChatSurface.vue +31 -21
  10. package/custom/composables/agentAudio/agent-processing.mp3 +0 -0
  11. package/custom/composables/agentStore/constants.ts +8 -1
  12. package/custom/composables/agentStore/useAgentSessions.ts +88 -13
  13. package/custom/composables/useAgentAudio.ts +392 -0
  14. package/custom/composables/useAgentStore.ts +52 -5
  15. package/custom/conversation_area/ConversationArea.vue +1 -1
  16. package/custom/conversation_area/MessageRenderer.vue +12 -1
  17. package/custom/conversation_area/SystemMessageRenderer.vue +28 -0
  18. package/custom/conversation_area/TextRenderer.vue +4 -3
  19. package/custom/conversation_area/ToolRenderer.vue +1 -1
  20. package/custom/package.json +2 -1
  21. package/custom/pnpm-lock.yaml +29 -0
  22. package/custom/speech_recognition_frontend/AudioLines.vue +97 -0
  23. package/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
  24. package/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
  25. package/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
  26. package/custom/types.ts +52 -2
  27. package/dist/agent/languageDetect.js +0 -6
  28. package/dist/agent/simpleAgent.js +4 -3
  29. package/dist/agent/systemPrompt.js +24 -3
  30. package/dist/agent/toolCallEvents.js +24 -2
  31. package/dist/agent/tools/apiTool.js +1 -1
  32. package/dist/agentResponseEvents.js +141 -0
  33. package/dist/apiBasedTools.js +95 -211
  34. package/dist/custom/ChatSurface.vue +31 -21
  35. package/dist/custom/composables/agentAudio/agent-processing.mp3 +0 -0
  36. package/dist/custom/composables/agentStore/constants.ts +8 -1
  37. package/dist/custom/composables/agentStore/useAgentSessions.ts +88 -13
  38. package/dist/custom/composables/useAgentAudio.ts +392 -0
  39. package/dist/custom/composables/useAgentStore.ts +52 -5
  40. package/dist/custom/conversation_area/ConversationArea.vue +1 -1
  41. package/dist/custom/conversation_area/MessageRenderer.vue +12 -1
  42. package/dist/custom/conversation_area/SystemMessageRenderer.vue +28 -0
  43. package/dist/custom/conversation_area/TextRenderer.vue +4 -3
  44. package/dist/custom/conversation_area/ToolRenderer.vue +1 -1
  45. package/dist/custom/package.json +2 -1
  46. package/dist/custom/pnpm-lock.yaml +29 -0
  47. package/dist/custom/speech_recognition_frontend/AudioLines.vue +97 -0
  48. package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +157 -0
  49. package/dist/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +22 -0
  50. package/dist/custom/speech_recognition_frontend/voiceActivityDetection.ts +151 -0
  51. package/dist/custom/types.ts +52 -2
  52. package/dist/index.js +290 -400
  53. package/index.ts +318 -492
  54. package/package.json +4 -2
  55. package/types.ts +1 -1
@@ -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 !== 'pre-session');
93
- if (activeSessionId.value === 'pre-session') {
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 === 'pre-session') {
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 === 'pre-session')) {
151
+ if (!sessionList.value.some((s: ISessionsListItem) => s.sessionId === PRE_SESSION_ID)) {
148
152
  sessionList.value.unshift({
149
- sessionId: 'pre-session',
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 = 'pre-session';
159
+ activeSessionId.value = PRE_SESSION_ID;
156
160
  currentSession.value = {
157
- sessionId: 'pre-session',
161
+ sessionId: PRE_SESSION_ID,
158
162
  title: 'New Session',
159
163
  timestamp: new Date().toISOString(),
160
164
  messages: [],
161
165
  };
162
- sessions.value['pre-session'] = currentSession.value;
163
- setCurrentChat('pre-session');
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 === 'pre-session') {
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({message: 'Are you sure, that you want to delete this session?', yes: 'Yes', no: 'No'});
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 !== 'pre-session') {
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('[Response generation aborted]');
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
+ })
@@ -35,7 +35,7 @@
35
35
 
36
36
  <div
37
37
  v-for="(message, index) in props.messages" :key="index"
38
- class="flex flex-col w-full mt-2"
38
+ class="flex flex-col w-full mt-2 pb-2"
39
39
  :class="message.role === 'user' ? 'self-end' : 'self-start'"
40
40
  ref="messagesRefs"
41
41
  >