@adminforth/agent 1.37.0 → 1.39.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 +13 -2
- package/custom/ChatSurface.vue +31 -21
- package/custom/composables/agentStore/constants.ts +8 -1
- package/custom/composables/agentStore/useAgentSessions.ts +85 -12
- package/custom/composables/useAgentAudio.ts +401 -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/public/agentAudio/agent-processing.mp3 +0 -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/agentStore/constants.ts +8 -1
- package/dist/custom/composables/agentStore/useAgentSessions.ts +85 -12
- package/dist/custom/composables/useAgentAudio.ts +401 -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/public/agentAudio/agent-processing.mp3 +0 -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
|
@@ -0,0 +1,401 @@
|
|
|
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
|
+
let standByAudio: HTMLAudioElement | null = null;
|
|
18
|
+
|
|
19
|
+
function playStandByAudio() {
|
|
20
|
+
if (!standByAudio) {
|
|
21
|
+
standByAudio = new Audio(`/plugins/AdminForthAgentPlugin/agentAudio/agent-processing.mp3`);
|
|
22
|
+
standByAudio.addEventListener('ended', () => {
|
|
23
|
+
if (!standByAudio.paused) {
|
|
24
|
+
restartStandByAudio();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
standByAudio.currentTime = 0;
|
|
29
|
+
standByAudio.play()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stopStandByAudio() {
|
|
33
|
+
if (!standByAudio) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
standByAudio.pause();
|
|
37
|
+
standByAudio.currentTime = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function restartStandByAudio() {
|
|
41
|
+
if (standByAudio) {
|
|
42
|
+
standByAudio.currentTime = 0;
|
|
43
|
+
}
|
|
44
|
+
playStandByAudio();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export const useAgentAudio = defineStore('agentAudio', () => {
|
|
49
|
+
const agentStore = useAgentStore();
|
|
50
|
+
const agentAudioMode = ref<'transcribing' | 'streaming' | 'fetchingAudio' | 'playingAgentResponse' | null>(null);
|
|
51
|
+
const isStreamingResponse = ref(false);
|
|
52
|
+
|
|
53
|
+
let currentAbortController: AbortController | null = null;
|
|
54
|
+
let isPlaying = false;
|
|
55
|
+
let currentAudio: HTMLAudioElement | null = null;
|
|
56
|
+
let currentAudioObjectUrl: string | null = null;
|
|
57
|
+
let currentStreamingAudio: StreamingAudioState | null = null;
|
|
58
|
+
let bufferedAudioChunks: ArrayBuffer[] = [];
|
|
59
|
+
let bufferedAudioMimeType = 'audio/mpeg';
|
|
60
|
+
|
|
61
|
+
function stopGenerationAndAudio() {
|
|
62
|
+
agentAudioMode.value = null;
|
|
63
|
+
stopCurrentAudioPlayback();
|
|
64
|
+
currentAbortController?.abort();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function sendAudioToServerAndHandleResponse(blob: Blob) {
|
|
68
|
+
currentAbortController = new AbortController();
|
|
69
|
+
const formData = new FormData();
|
|
70
|
+
formData.append('file', blob, 'user_prompt.webm');
|
|
71
|
+
formData.append('sessionId', agentStore.activeSessionId);
|
|
72
|
+
formData.append('mode', agentStore.activeModeName ?? '');
|
|
73
|
+
formData.append('timeZone', Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
74
|
+
formData.append('currentPage', JSON.stringify(getCurrentPageContext()));
|
|
75
|
+
const fullPath = `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/agent/speech-response`;
|
|
76
|
+
try {
|
|
77
|
+
agentAudioMode.value = 'transcribing';
|
|
78
|
+
const res = await fetch(fullPath, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: formData,
|
|
81
|
+
headers: {
|
|
82
|
+
Accept: 'text/event-stream',
|
|
83
|
+
},
|
|
84
|
+
signal: currentAbortController!.signal,
|
|
85
|
+
});
|
|
86
|
+
isStreamingResponse.value = true;
|
|
87
|
+
if (res.ok) {
|
|
88
|
+
agentAudioMode.value = 'streaming';
|
|
89
|
+
await readSpeechResponseStream(res);
|
|
90
|
+
} else {
|
|
91
|
+
console.error('Failed to transcribe audio:', res.statusText);
|
|
92
|
+
adminforth.alert({message: 'Failed to transcribe audio', variant: 'danger'});
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
96
|
+
//
|
|
97
|
+
} else {
|
|
98
|
+
console.error('Error sending audio to server:', error);
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
isStreamingResponse.value = false;
|
|
102
|
+
agentAudioMode.value = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function readSpeechResponseStream(res: Response) {
|
|
107
|
+
const reader = res.body?.getReader();
|
|
108
|
+
if (!reader) {
|
|
109
|
+
adminforth.alert({message: 'Speech response stream is not available', variant: 'danger'});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const decoder = new TextDecoder();
|
|
114
|
+
let buffer = '';
|
|
115
|
+
|
|
116
|
+
agentStore.setCurrentChatStatus('streaming');
|
|
117
|
+
try {
|
|
118
|
+
while (true) {
|
|
119
|
+
const { value, done } = await reader.read();
|
|
120
|
+
|
|
121
|
+
if (done) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
buffer += decoder.decode(value, { stream: true });
|
|
126
|
+
const eventBlocks = buffer.split('\n\n');
|
|
127
|
+
buffer = eventBlocks.pop() ?? '';
|
|
128
|
+
|
|
129
|
+
for (const eventBlock of eventBlocks) {
|
|
130
|
+
await handleSpeechStreamEvent(eventBlock);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
buffer += decoder.decode();
|
|
135
|
+
|
|
136
|
+
if (buffer.trim()) {
|
|
137
|
+
await handleSpeechStreamEvent(buffer);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
finishAudioStream();
|
|
141
|
+
} finally {
|
|
142
|
+
reader.releaseLock();
|
|
143
|
+
agentStore.setCurrentChatStatus('ready');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function handleSpeechStreamEvent(eventBlock: string) {
|
|
148
|
+
if (currentAbortController?.signal.aborted) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const data = eventBlock
|
|
152
|
+
.split('\n')
|
|
153
|
+
.filter((line) => line.startsWith('data:'))
|
|
154
|
+
.map((line) => line.slice(5).trimStart())
|
|
155
|
+
.join('\n');
|
|
156
|
+
|
|
157
|
+
if (!data || data === '[DONE]') {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const event = JSON.parse(data) as SpeechStreamEvent;
|
|
162
|
+
|
|
163
|
+
if (event.type === 'error') {
|
|
164
|
+
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (event.type === 'transcript') {
|
|
169
|
+
agentStore.addUserMessage(event.data.text);
|
|
170
|
+
agentStore.updateLastAgentMessage('');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (event.type === 'speech-response') {
|
|
175
|
+
stopStandByAudio();
|
|
176
|
+
agentStore.setCurrentChatStatus('ready');
|
|
177
|
+
agentStore.addAgentMessage(event.data.response.text);
|
|
178
|
+
agentAudioMode.value = 'playingAgentResponse';
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (event.type === 'audio-start') {
|
|
183
|
+
isStreamingResponse.value = false;
|
|
184
|
+
agentAudioMode.value = 'fetchingAudio';
|
|
185
|
+
initializeAudioStream(event.data.mimeType);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (event.type === 'audio-delta') {
|
|
190
|
+
appendAudioChunk(event.data.base64);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (event.type === 'audio-done') {
|
|
195
|
+
finishAudioStream();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (event.type === 'data-tool-call') {
|
|
200
|
+
playStandByAudio();
|
|
201
|
+
agentStore.addDataToolCallMessage(event.data);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function setIsPlaying(value: boolean) {
|
|
206
|
+
isPlaying = value;
|
|
207
|
+
|
|
208
|
+
if (!currentAudio) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!isPlaying) {
|
|
213
|
+
currentAudio.pause();
|
|
214
|
+
currentAudio.currentTime = 0;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
agentAudioMode.value = 'playingAgentResponse';
|
|
218
|
+
void currentAudio.play().catch((error) => {
|
|
219
|
+
console.error('Failed to play audio:', error);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function initializeAudioStream(mimeType: string) {
|
|
224
|
+
stopCurrentAudioPlayback();
|
|
225
|
+
bufferedAudioMimeType = mimeType;
|
|
226
|
+
|
|
227
|
+
if (typeof MediaSource === 'undefined' || !MediaSource.isTypeSupported(mimeType)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const mediaSource = new MediaSource();
|
|
232
|
+
currentAudioObjectUrl = URL.createObjectURL(mediaSource);
|
|
233
|
+
currentAudio = new Audio(currentAudioObjectUrl);
|
|
234
|
+
currentAudio.addEventListener('ended', handleAudioEnded, { once: true });
|
|
235
|
+
currentStreamingAudio = {
|
|
236
|
+
mimeType,
|
|
237
|
+
mediaSource,
|
|
238
|
+
sourceBuffer: null,
|
|
239
|
+
pendingChunks: [],
|
|
240
|
+
hasStartedPlayback: false,
|
|
241
|
+
isDone: false,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
mediaSource.addEventListener('sourceopen', handleMediaSourceOpen, { once: true });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function handleMediaSourceOpen() {
|
|
248
|
+
if (!currentStreamingAudio) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
currentStreamingAudio.sourceBuffer = currentStreamingAudio.mediaSource.addSourceBuffer(currentStreamingAudio.mimeType);
|
|
254
|
+
currentStreamingAudio.sourceBuffer.mode = 'sequence';
|
|
255
|
+
currentStreamingAudio.sourceBuffer.addEventListener('updateend', flushStreamingAudioQueue);
|
|
256
|
+
flushStreamingAudioQueue();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('Failed to initialize streaming audio playback:', error);
|
|
259
|
+
bufferedAudioChunks.push(...currentStreamingAudio.pendingChunks);
|
|
260
|
+
detachStreamingAudio();
|
|
261
|
+
destroyCurrentAudioElement();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function appendAudioChunk(base64: string) {
|
|
266
|
+
const chunk = base64ToArrayBuffer(base64);
|
|
267
|
+
|
|
268
|
+
if (!currentStreamingAudio) {
|
|
269
|
+
bufferedAudioChunks.push(chunk);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
currentStreamingAudio.pendingChunks.push(chunk);
|
|
274
|
+
flushStreamingAudioQueue();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function flushStreamingAudioQueue() {
|
|
278
|
+
if (!currentStreamingAudio?.sourceBuffer || currentStreamingAudio.sourceBuffer.updating) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const nextChunk = currentStreamingAudio.pendingChunks.shift();
|
|
283
|
+
|
|
284
|
+
if (nextChunk) {
|
|
285
|
+
currentStreamingAudio.sourceBuffer.appendBuffer(nextChunk);
|
|
286
|
+
|
|
287
|
+
if (!currentStreamingAudio.hasStartedPlayback) {
|
|
288
|
+
currentStreamingAudio.hasStartedPlayback = true;
|
|
289
|
+
setIsPlaying(true);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (currentStreamingAudio.isDone && currentStreamingAudio.mediaSource.readyState === 'open') {
|
|
296
|
+
currentStreamingAudio.mediaSource.endOfStream();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function finishAudioStream() {
|
|
301
|
+
if (currentStreamingAudio) {
|
|
302
|
+
currentStreamingAudio.isDone = true;
|
|
303
|
+
flushStreamingAudioQueue();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!bufferedAudioChunks.length) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
playAudioChunks(bufferedAudioChunks, bufferedAudioMimeType);
|
|
312
|
+
bufferedAudioChunks = [];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function detachStreamingAudio() {
|
|
316
|
+
if (currentStreamingAudio?.sourceBuffer) {
|
|
317
|
+
currentStreamingAudio.sourceBuffer.removeEventListener('updateend', flushStreamingAudioQueue);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
currentStreamingAudio = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function destroyCurrentAudioElement() {
|
|
324
|
+
if (currentAudio) {
|
|
325
|
+
currentAudio.pause();
|
|
326
|
+
currentAudio.currentTime = 0;
|
|
327
|
+
currentAudio.src = '';
|
|
328
|
+
currentAudio.load();
|
|
329
|
+
currentAudio = null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (currentAudioObjectUrl) {
|
|
333
|
+
URL.revokeObjectURL(currentAudioObjectUrl);
|
|
334
|
+
currentAudioObjectUrl = null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
isPlaying = false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function stopCurrentAudioPlayback(dontResetMode = false) {
|
|
341
|
+
stopStandByAudio();
|
|
342
|
+
bufferedAudioChunks = [];
|
|
343
|
+
bufferedAudioMimeType = 'audio/mpeg';
|
|
344
|
+
detachStreamingAudio();
|
|
345
|
+
destroyCurrentAudioElement();
|
|
346
|
+
if (!dontResetMode) {
|
|
347
|
+
agentAudioMode.value = null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function handleAudioEnded() {
|
|
352
|
+
agentAudioMode.value = null;
|
|
353
|
+
stopCurrentAudioPlayback();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function playAudioChunks(chunks: ArrayBuffer[], mimeType: string) {
|
|
357
|
+
currentAudioObjectUrl = URL.createObjectURL(new Blob(chunks, { type: mimeType }));
|
|
358
|
+
currentAudio = new Audio(currentAudioObjectUrl);
|
|
359
|
+
currentAudio.addEventListener('ended', handleAudioEnded, { once: true });
|
|
360
|
+
setIsPlaying(true);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function base64ToArrayBuffer(base64: string) {
|
|
364
|
+
const binary = atob(base64);
|
|
365
|
+
const bytes = new Uint8Array(binary.length);
|
|
366
|
+
|
|
367
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
368
|
+
bytes[i] = binary.charCodeAt(i);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return bytes.buffer;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function playBeep(freq = 800, duration = 0.05) {
|
|
375
|
+
const ctx = new AudioContext();
|
|
376
|
+
const osc = ctx.createOscillator();
|
|
377
|
+
const gain = ctx.createGain();
|
|
378
|
+
|
|
379
|
+
osc.frequency.value = freq;
|
|
380
|
+
osc.type = 'sine';
|
|
381
|
+
|
|
382
|
+
osc.connect(gain);
|
|
383
|
+
gain.connect(ctx.destination);
|
|
384
|
+
|
|
385
|
+
osc.start();
|
|
386
|
+
|
|
387
|
+
gain.gain.setValueAtTime(1, ctx.currentTime);
|
|
388
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
|
|
389
|
+
|
|
390
|
+
osc.stop(ctx.currentTime + duration);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
sendAudioToServerAndHandleResponse,
|
|
395
|
+
stopGenerationAndAudio,
|
|
396
|
+
stopCurrentAudioPlayback,
|
|
397
|
+
playBeep,
|
|
398
|
+
agentAudioMode
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
});
|
|
@@ -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
|
+
})
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
:key="part.type"
|
|
9
9
|
>
|
|
10
10
|
<TextRenderer
|
|
11
|
-
v-if="part.type === 'text'"
|
|
11
|
+
v-if="part.type === 'text' && !checkIfMessageSystemMessage(part.text ?? '')"
|
|
12
12
|
:message="part.text"
|
|
13
13
|
:role="props.message.role"
|
|
14
14
|
:state="part.state ?? (props.message.role === 'user' ? 'done' : undefined)"
|
|
15
15
|
/>
|
|
16
|
+
<SystemMessageRenderer
|
|
17
|
+
v-else
|
|
18
|
+
:message="part.text"
|
|
19
|
+
/>
|
|
16
20
|
</template>
|
|
17
21
|
|
|
18
22
|
</template>
|
|
@@ -25,9 +29,16 @@
|
|
|
25
29
|
import type { IMessage } from '../types';
|
|
26
30
|
import { getMessageParts } from '../utils';
|
|
27
31
|
import ProcessingTimeline from './ProcessingTimeline.vue';
|
|
32
|
+
import SystemMessageRenderer from './SystemMessageRenderer.vue';
|
|
33
|
+
import { RESERVED_SYSTEM_MESSAGE_CONTENT } from '../composables/agentStore/constants';
|
|
28
34
|
|
|
29
35
|
const props = defineProps<{
|
|
30
36
|
message: IMessage
|
|
31
37
|
isLastMessageInChat: boolean
|
|
32
38
|
}>();
|
|
39
|
+
|
|
40
|
+
function checkIfMessageSystemMessage(message: IMessage): boolean {
|
|
41
|
+
const isReserved = Object.values(RESERVED_SYSTEM_MESSAGE_CONTENT).includes(message as RESERVED_SYSTEM_MESSAGE_CONTENT);
|
|
42
|
+
return isReserved;
|
|
43
|
+
}
|
|
33
44
|
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<p :class="`${TEXT_CLASSES}`" v-if="message === RESERVED_SYSTEM_MESSAGE_CONTENT.START_AUDIO_CHAT">
|
|
3
|
+
<IconPhoneSolid class="inline-block w-4 h-4 mr-1" />
|
|
4
|
+
{{$t('Audio chat started')}}
|
|
5
|
+
</p>
|
|
6
|
+
<p :class="`${TEXT_CLASSES}`" v-else-if="message === RESERVED_SYSTEM_MESSAGE_CONTENT.END_AUDIO_CHAT">
|
|
7
|
+
<IconPhoneHangupSolid class="inline-block w-4 h-4 mr-1" />
|
|
8
|
+
{{$t('Audio chat ended')}}
|
|
9
|
+
</p>
|
|
10
|
+
<p :class="`${TEXT_CLASSES}`" v-else-if="message === RESERVED_SYSTEM_MESSAGE_CONTENT.AGENT_RESPONSE_ABORTED">
|
|
11
|
+
{{$t('Agent response aborted')}}
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { RESERVED_SYSTEM_MESSAGE_CONTENT } from '../composables/agentStore/constants';
|
|
20
|
+
import { IconPhoneSolid, IconPhoneHangupSolid } from '@iconify-prerendered/vue-flowbite';
|
|
21
|
+
|
|
22
|
+
const TEXT_CLASSES = 'text-center italic text-lightListTableHeadingText dark:text-darkListTableHeadingText flex items-center justify-center';
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{
|
|
25
|
+
message: string
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
</script>
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
:components="incremarkComponents"
|
|
17
17
|
:incremark-options="incremarkOptions"
|
|
18
18
|
/>
|
|
19
|
-
<p v-else class="text-red-500 py-2">
|
|
19
|
+
<!-- <p v-else class="text-red-500 py-2">
|
|
20
20
|
{{ $t('No content to render') }}
|
|
21
|
-
</p>
|
|
21
|
+
</p> -->
|
|
22
22
|
</div>
|
|
23
23
|
</template>
|
|
24
24
|
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
import { useRouter } from 'vue-router';
|
|
29
29
|
import { useAgentStore } from '../composables/useAgentStore';
|
|
30
30
|
import { useCoreStore } from '@/stores/core';
|
|
31
|
+
import type { IMessage } from '../types';
|
|
31
32
|
|
|
32
33
|
const props = defineProps<{
|
|
33
34
|
message: string | undefined,
|
|
34
35
|
state: string | undefined,
|
|
35
|
-
role: '
|
|
36
|
+
role: IMessage['role']
|
|
36
37
|
}>();
|
|
37
38
|
|
|
38
39
|
const emit = defineEmits(['toggle-thoughts']);
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
const hasToolSections = computed(() => toolSections.value.length > 0);
|
|
110
110
|
|
|
111
111
|
onMounted(() => {
|
|
112
|
-
if (toolRendererRef.value) {
|
|
112
|
+
if (toolRendererRef.value && props.data.toolInfo) {
|
|
113
113
|
toolRendererInitialWidth.value = toolRendererRef.value.offsetWidth;
|
|
114
114
|
}
|
|
115
115
|
});
|