@aj-archipelago/cortex 1.3.5 → 1.3.7
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/helper-apps/cortex-autogen/agents.py +31 -2
- package/helper-apps/cortex-realtime-voice-server/.env.sample +6 -0
- package/helper-apps/cortex-realtime-voice-server/README.md +22 -0
- package/helper-apps/cortex-realtime-voice-server/bun.lockb +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/bun.lockb +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/index.html +12 -0
- package/helper-apps/cortex-realtime-voice-server/client/package.json +65 -0
- package/helper-apps/cortex-realtime-voice-server/client/postcss.config.js +6 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/favicon.ico +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/index.html +43 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/logo192.png +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/logo512.png +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/manifest.json +25 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/robots.txt +3 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/sounds/connect.mp3 +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/public/sounds/disconnect.mp3 +0 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/App.test.tsx +9 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/App.tsx +126 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/SettingsModal.tsx +207 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/Chat.tsx +553 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubble.tsx +22 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleLeft.tsx +22 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleRight.tsx +21 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessage.tsx +27 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessageInput.tsx +74 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatTile.tsx +211 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/SoundEffects.ts +56 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavPacker.ts +112 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavRecorder.ts +571 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavStreamPlayer.ts +290 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/AudioAnalysis.ts +186 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/constants.ts +59 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/AudioProcessor.ts +214 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/StreamProcessor.ts +183 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/AudioVisualizer.tsx +151 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/CopyButton.tsx +32 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ImageOverlay.tsx +166 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/MicrophoneVisualizer.tsx +95 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ScreenshotCapture.tsx +116 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/hooks/useWindowResize.ts +27 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/chat/utils/audio.ts +33 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/index.css +20 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/index.tsx +19 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/logo.svg +1 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/react-app-env.d.ts +1 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/reportWebVitals.ts +15 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/setupTests.ts +5 -0
- package/helper-apps/cortex-realtime-voice-server/client/src/utils/logger.ts +45 -0
- package/helper-apps/cortex-realtime-voice-server/client/tailwind.config.js +14 -0
- package/helper-apps/cortex-realtime-voice-server/client/tsconfig.json +30 -0
- package/helper-apps/cortex-realtime-voice-server/client/vite.config.ts +22 -0
- package/helper-apps/cortex-realtime-voice-server/index.ts +19 -0
- package/helper-apps/cortex-realtime-voice-server/package.json +28 -0
- package/helper-apps/cortex-realtime-voice-server/src/ApiServer.ts +35 -0
- package/helper-apps/cortex-realtime-voice-server/src/SocketServer.ts +737 -0
- package/helper-apps/cortex-realtime-voice-server/src/Tools.ts +520 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/expert.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/image.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +91 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/reason.ts +29 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/search.ts +30 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/style.ts +31 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/utils.ts +95 -0
- package/helper-apps/cortex-realtime-voice-server/src/cortex/vision.ts +34 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +499 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +279 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/socket.ts +27 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/transcription.ts +75 -0
- package/helper-apps/cortex-realtime-voice-server/src/realtime/utils.ts +33 -0
- package/helper-apps/cortex-realtime-voice-server/src/utils/logger.ts +45 -0
- package/helper-apps/cortex-realtime-voice-server/src/utils/prompt.ts +81 -0
- package/helper-apps/cortex-realtime-voice-server/tsconfig.json +28 -0
- package/package.json +1 -1
- package/pathways/basePathway.js +3 -1
- package/pathways/system/entity/memory/sys_memory_manager.js +3 -0
- package/pathways/system/entity/memory/sys_memory_update.js +44 -45
- package/pathways/system/entity/memory/sys_read_memory.js +86 -6
- package/pathways/system/entity/memory/sys_search_memory.js +66 -0
- package/pathways/system/entity/shared/sys_entity_constants.js +2 -2
- package/pathways/system/entity/sys_entity_continue.js +2 -1
- package/pathways/system/entity/sys_entity_start.js +10 -0
- package/pathways/system/entity/sys_generator_expert.js +0 -2
- package/pathways/system/entity/sys_generator_memory.js +31 -0
- package/pathways/system/entity/sys_generator_voice_sample.js +36 -0
- package/pathways/system/entity/sys_router_tool.js +13 -10
- package/pathways/system/sys_parse_numbered_object_list.js +1 -1
- package/server/pathwayResolver.js +41 -31
- package/server/plugins/azureVideoTranslatePlugin.js +28 -16
- package/server/plugins/claude3VertexPlugin.js +0 -9
- package/server/plugins/gemini15ChatPlugin.js +18 -5
- package/server/plugins/modelPlugin.js +27 -6
- package/server/plugins/openAiChatPlugin.js +10 -8
- package/server/plugins/openAiVisionPlugin.js +56 -0
- package/tests/memoryfunction.test.js +73 -1
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
4
|
+
import {io, Socket} from 'socket.io-client';
|
|
5
|
+
import {WavRecorder} from './audio/WavRecorder';
|
|
6
|
+
import {WavStreamPlayer} from './audio/WavStreamPlayer';
|
|
7
|
+
import {ChatMessage, ChatTile} from './ChatTile';
|
|
8
|
+
import {arrayBufferToBase64, base64ToArrayBuffer} from "./utils/audio";
|
|
9
|
+
import {ClientToServerEvents, ServerToClientEvents} from "../../../src/realtime/socket";
|
|
10
|
+
import { AudioVisualizer } from './components/AudioVisualizer';
|
|
11
|
+
import { MicrophoneVisualizer } from './components/MicrophoneVisualizer';
|
|
12
|
+
import { ImageOverlay } from './components/ImageOverlay';
|
|
13
|
+
import MicIcon from '@mui/icons-material/Mic';
|
|
14
|
+
import MicOffIcon from '@mui/icons-material/MicOff';
|
|
15
|
+
import PhoneEnabledIcon from '@mui/icons-material/PhoneEnabled';
|
|
16
|
+
import CallEndIcon from '@mui/icons-material/CallEnd';
|
|
17
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
18
|
+
import ChatIcon from '@mui/icons-material/Chat';
|
|
19
|
+
import type { Voice } from '../../../src/realtime/realtimeTypes';
|
|
20
|
+
import {SoundEffects} from './audio/SoundEffects';
|
|
21
|
+
import { logger } from '../utils/logger';
|
|
22
|
+
import {ScreenshotCapture} from './components/ScreenshotCapture';
|
|
23
|
+
|
|
24
|
+
type ChatProps = {
|
|
25
|
+
userId: string;
|
|
26
|
+
userName: string;
|
|
27
|
+
aiName: string;
|
|
28
|
+
language: string;
|
|
29
|
+
aiMemorySelfModify: boolean;
|
|
30
|
+
aiStyle: string;
|
|
31
|
+
voice: Voice;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default function Chat({
|
|
35
|
+
userId,
|
|
36
|
+
userName,
|
|
37
|
+
aiName,
|
|
38
|
+
language,
|
|
39
|
+
aiMemorySelfModify,
|
|
40
|
+
aiStyle,
|
|
41
|
+
voice
|
|
42
|
+
}: ChatProps) {
|
|
43
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
44
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
45
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
46
|
+
const cleanupPromiseRef = useRef<Promise<void> | null>(null);
|
|
47
|
+
const [imageUrls, setImageUrls] = useState<string[]>([]);
|
|
48
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
49
|
+
const [overlayKey, setOverlayKey] = useState(0);
|
|
50
|
+
const [isAudioPlaying, setIsAudioPlaying] = useState(false);
|
|
51
|
+
const wavRecorderRef = useRef<WavRecorder>(
|
|
52
|
+
new WavRecorder({sampleRate: 24000}),
|
|
53
|
+
);
|
|
54
|
+
const wavStreamPlayerRef = useRef<WavStreamPlayer>(
|
|
55
|
+
new WavStreamPlayer({sampleRate: 24000}),
|
|
56
|
+
);
|
|
57
|
+
const socketRef =
|
|
58
|
+
useRef<Socket<ServerToClientEvents, ClientToServerEvents>>(
|
|
59
|
+
io(`/?userId=${userId}&userName=${userName}&aiName=${aiName}&voice=${voice}&aiStyle=${aiStyle}&language=${language}`, {autoConnect: false})
|
|
60
|
+
);
|
|
61
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
62
|
+
const sourceNodeRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
|
63
|
+
const outputAnalyserRef = useRef<AnalyserNode | null>(null);
|
|
64
|
+
const [showChat, setShowChat] = useState(false);
|
|
65
|
+
const [mounted, setMounted] = useState(false);
|
|
66
|
+
|
|
67
|
+
// Log on every render
|
|
68
|
+
logger.log('Chat rendering, showChat:', showChat);
|
|
69
|
+
|
|
70
|
+
// Log on mount only
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
logger.log('Chat mounted');
|
|
73
|
+
return () => logger.log('Chat unmounted');
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
// Existing effect to track showChat changes
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
logger.log('showChat changed:', showChat);
|
|
79
|
+
}, [showChat]);
|
|
80
|
+
|
|
81
|
+
const stopConversation = useCallback(async () => {
|
|
82
|
+
logger.log('Stopping conversation');
|
|
83
|
+
const wavRecorder = wavRecorderRef.current;
|
|
84
|
+
const wavStreamPlayer = wavStreamPlayerRef.current;
|
|
85
|
+
|
|
86
|
+
if (wavRecorder.getStatus() === "recording") {
|
|
87
|
+
await wavRecorder.end();
|
|
88
|
+
await wavStreamPlayer.interrupt();
|
|
89
|
+
await SoundEffects.playDisconnect();
|
|
90
|
+
socketRef.current.emit('conversationCompleted');
|
|
91
|
+
socketRef.current.removeAllListeners();
|
|
92
|
+
socketRef.current.disconnect();
|
|
93
|
+
|
|
94
|
+
// Clean up audio nodes
|
|
95
|
+
if (sourceNodeRef.current) {
|
|
96
|
+
sourceNodeRef.current.disconnect();
|
|
97
|
+
sourceNodeRef.current = null;
|
|
98
|
+
}
|
|
99
|
+
if (outputAnalyserRef.current) {
|
|
100
|
+
outputAnalyserRef.current.disconnect();
|
|
101
|
+
outputAnalyserRef.current = null;
|
|
102
|
+
}
|
|
103
|
+
if (audioContextRef.current) {
|
|
104
|
+
await audioContextRef.current.close();
|
|
105
|
+
audioContextRef.current = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Reset recorder and player
|
|
109
|
+
wavRecorderRef.current = new WavRecorder({sampleRate: 24000});
|
|
110
|
+
wavStreamPlayerRef.current = new WavStreamPlayer({sampleRate: 24000});
|
|
111
|
+
|
|
112
|
+
setIsRecording(false);
|
|
113
|
+
setIsMuted(false); // Reset mute state
|
|
114
|
+
setImageUrls([]); // Clear images on disconnect
|
|
115
|
+
}
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const startConversation = useCallback(() => {
|
|
119
|
+
logger.log('Starting conversation');
|
|
120
|
+
|
|
121
|
+
const socket = socketRef.current;
|
|
122
|
+
|
|
123
|
+
// Remove ALL existing listeners before adding new ones
|
|
124
|
+
socket.removeAllListeners();
|
|
125
|
+
|
|
126
|
+
// Create fresh audio context
|
|
127
|
+
if (audioContextRef.current) {
|
|
128
|
+
audioContextRef.current.close();
|
|
129
|
+
audioContextRef.current = null;
|
|
130
|
+
}
|
|
131
|
+
audioContextRef.current = new AudioContext();
|
|
132
|
+
|
|
133
|
+
const wavStreamPlayer = wavStreamPlayerRef.current;
|
|
134
|
+
const wavRecorder = wavRecorderRef.current;
|
|
135
|
+
|
|
136
|
+
const updateOrCreateMessage = (item: any, delta: any, isNewMessage = false) => {
|
|
137
|
+
setMessages((prev) => {
|
|
138
|
+
// Skip messages that start with <INSTRUCTIONS>
|
|
139
|
+
if (item.role === 'user' &&
|
|
140
|
+
(item.content?.[0]?.text?.startsWith('<INSTRUCTIONS>') ||
|
|
141
|
+
delta.text?.startsWith('<INSTRUCTIONS>') ||
|
|
142
|
+
delta.transcript?.startsWith('<INSTRUCTIONS>'))) {
|
|
143
|
+
return prev;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Try to find an existing message from the same turn
|
|
147
|
+
const existingIndex = prev.findIndex(m =>
|
|
148
|
+
m.id === item.id ||
|
|
149
|
+
m.id === `local-${item.timestamp}` || // This matches our local message ID format
|
|
150
|
+
// Match user messages by timestamp (within 1 second) to catch server echoes
|
|
151
|
+
(item.role === 'user' && m.isSelf && Math.abs(m.timestamp - (item.timestamp || Date.now())) < 1000)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (existingIndex !== -1) {
|
|
155
|
+
// Message exists - update it with any new content
|
|
156
|
+
const newList = [...prev];
|
|
157
|
+
const existing = newList[existingIndex];
|
|
158
|
+
let message = existing.message;
|
|
159
|
+
|
|
160
|
+
// Only update if we have new content
|
|
161
|
+
if (delta.transcript || delta.text || item.content?.[0]?.text) {
|
|
162
|
+
if (delta.transcript) {
|
|
163
|
+
message = message + delta.transcript; // Concatenate transcript chunks
|
|
164
|
+
} else if (delta.text) {
|
|
165
|
+
message = message + delta.text; // Concatenate text chunks
|
|
166
|
+
} else if (item.content?.[0]?.text) {
|
|
167
|
+
message = item.content[0].text; // Full message replacement
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
newList[existingIndex] = {
|
|
171
|
+
...existing,
|
|
172
|
+
id: item.id || existing.id,
|
|
173
|
+
message,
|
|
174
|
+
};
|
|
175
|
+
return newList;
|
|
176
|
+
}
|
|
177
|
+
return prev;
|
|
178
|
+
} else if (isNewMessage) {
|
|
179
|
+
// Only create if we don't have a matching message
|
|
180
|
+
const messageContent =
|
|
181
|
+
delta.text ||
|
|
182
|
+
delta.transcript ||
|
|
183
|
+
item.content?.[0]?.text ||
|
|
184
|
+
'';
|
|
185
|
+
|
|
186
|
+
// For user messages, use a timestamp slightly before now
|
|
187
|
+
// For AI messages, use current timestamp
|
|
188
|
+
const timestamp = item.role === 'user' ?
|
|
189
|
+
Date.now() - 500 : // 500ms earlier for user messages
|
|
190
|
+
Date.now();
|
|
191
|
+
|
|
192
|
+
return [...prev, {
|
|
193
|
+
id: item.id || `local-${timestamp}`,
|
|
194
|
+
isSelf: item.role === 'user',
|
|
195
|
+
name: item.role === 'user' ? userName : aiName,
|
|
196
|
+
message: messageContent,
|
|
197
|
+
isImage: false,
|
|
198
|
+
timestamp,
|
|
199
|
+
}];
|
|
200
|
+
}
|
|
201
|
+
return prev;
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
socket.on('connect', () => {
|
|
206
|
+
logger.log('Connected', socket.id);
|
|
207
|
+
SoundEffects.playConnect();
|
|
208
|
+
});
|
|
209
|
+
socket.on('disconnect', () => {
|
|
210
|
+
logger.log('Disconnected', socket.id);
|
|
211
|
+
stopConversation().then(() => {
|
|
212
|
+
logger.log('Conversation stopped by disconnect');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
socket.on('ready', () => {
|
|
216
|
+
wavRecorder.record((data) => {
|
|
217
|
+
socket?.emit('appendAudio', arrayBufferToBase64(data.mono));
|
|
218
|
+
|
|
219
|
+
if (!sourceNodeRef.current && wavRecorder.getStream()) {
|
|
220
|
+
sourceNodeRef.current = audioContextRef.current!.createMediaStreamSource(wavRecorder.getStream()!);
|
|
221
|
+
}
|
|
222
|
+
}).then(() => {
|
|
223
|
+
logger.log('Recording started')
|
|
224
|
+
setIsRecording(true);
|
|
225
|
+
setIsLoading(false); // Clear loading only when fully set up
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
socket.on('conversationInterrupted', async () => {
|
|
229
|
+
logger.log('conversationInterrupted');
|
|
230
|
+
const trackSampleOffset = await wavStreamPlayer.interrupt();
|
|
231
|
+
if (trackSampleOffset?.trackId) {
|
|
232
|
+
socket.emit('cancelResponse');
|
|
233
|
+
if (wavStreamPlayer.currentTrackId) {
|
|
234
|
+
await wavStreamPlayer.fadeOut(150);
|
|
235
|
+
socket.emit('audioPlaybackComplete', trackSampleOffset.trackId);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
socket.on('imageCreated', (imageUrl) => {
|
|
240
|
+
logger.log('imageCreated event received:', imageUrl);
|
|
241
|
+
setImageUrls(prev => {
|
|
242
|
+
logger.log('setImageUrls called with prev:', prev);
|
|
243
|
+
const next = prev.length === 0 ? [imageUrl] : [...prev, imageUrl];
|
|
244
|
+
logger.log('setImageUrls returning next:', next);
|
|
245
|
+
if (prev.length === 0) {
|
|
246
|
+
setOverlayKey(k => k + 1);
|
|
247
|
+
}
|
|
248
|
+
return next;
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
socket.on('conversationUpdated', (item, delta) => {
|
|
252
|
+
logger.log('Conversation updated:', { item, delta });
|
|
253
|
+
|
|
254
|
+
if (delta?.audio) {
|
|
255
|
+
const audio = base64ToArrayBuffer(delta.audio);
|
|
256
|
+
wavStreamPlayer.add16BitPCM(audio, item.id);
|
|
257
|
+
setIsAudioPlaying(true);
|
|
258
|
+
|
|
259
|
+
// Set up track completion callback if not already set
|
|
260
|
+
if (!wavStreamPlayer.onTrackComplete) {
|
|
261
|
+
wavStreamPlayer.setTrackCompleteCallback((trackId) => {
|
|
262
|
+
logger.log('Audio track completed:', trackId);
|
|
263
|
+
setIsAudioPlaying(false);
|
|
264
|
+
socket.emit('audioPlaybackComplete', trackId);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
} else {
|
|
269
|
+
logger.log('Raw delta:', JSON.stringify(delta));
|
|
270
|
+
logger.log('Raw item:', JSON.stringify(item));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// For user messages, filter out audio-only updates
|
|
274
|
+
if (item.role === 'user') {
|
|
275
|
+
const hasUserContent =
|
|
276
|
+
delta.transcript ||
|
|
277
|
+
delta.text ||
|
|
278
|
+
item.content?.[0]?.text ||
|
|
279
|
+
(item.content?.[0]?.type === 'input_text' && item.content[0].text);
|
|
280
|
+
|
|
281
|
+
if (!hasUserContent) {
|
|
282
|
+
logger.log('Filtering audio-only message');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Process message if it has any kind of content
|
|
288
|
+
const hasContent = !!(
|
|
289
|
+
delta.text ||
|
|
290
|
+
delta.transcript ||
|
|
291
|
+
item.content?.[0]?.text
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (hasContent) {
|
|
295
|
+
updateOrCreateMessage(item, delta, true);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
wavRecorder.begin(null).then(() => {
|
|
300
|
+
wavStreamPlayer.connect().then(() => {
|
|
301
|
+
outputAnalyserRef.current = wavStreamPlayer.getAnalyser();
|
|
302
|
+
logger.log('Conversation started, connecting to socket');
|
|
303
|
+
socket.connect();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Add error handler to clear loading state if setup fails
|
|
308
|
+
socket.on('connect_error', () => {
|
|
309
|
+
logger.log('Connection error');
|
|
310
|
+
setIsLoading(false);
|
|
311
|
+
});
|
|
312
|
+
}, [aiName, stopConversation, userName]);
|
|
313
|
+
|
|
314
|
+
const postMessage = useCallback(async (message: string) => {
|
|
315
|
+
if (socketRef.current?.connected) {
|
|
316
|
+
// Just send to socket and let the server response create the message
|
|
317
|
+
socketRef.current?.emit('sendMessage', message);
|
|
318
|
+
}
|
|
319
|
+
}, []);
|
|
320
|
+
|
|
321
|
+
const onStartStop = useCallback(async () => {
|
|
322
|
+
if (isLoading) return; // Prevent any action while loading
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
if (isRecording) {
|
|
326
|
+
setIsLoading(true);
|
|
327
|
+
await stopConversation();
|
|
328
|
+
logger.log('Conversation stopped by user');
|
|
329
|
+
} else {
|
|
330
|
+
startConversation(); // startConversation now handles its own loading state
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
setIsLoading(false);
|
|
334
|
+
}
|
|
335
|
+
}, [isRecording, startConversation, stopConversation, isLoading]);
|
|
336
|
+
|
|
337
|
+
const handleImagesComplete = useCallback(() => {
|
|
338
|
+
// Don't clear the array anymore
|
|
339
|
+
}, []);
|
|
340
|
+
|
|
341
|
+
const handleMicMute = useCallback(() => {
|
|
342
|
+
const wavRecorder = wavRecorderRef.current;
|
|
343
|
+
if (wavRecorder.getStream()) {
|
|
344
|
+
const audioTrack = wavRecorder.getStream()!.getAudioTracks()[0];
|
|
345
|
+
audioTrack.enabled = !audioTrack.enabled;
|
|
346
|
+
setIsMuted(!isMuted);
|
|
347
|
+
}
|
|
348
|
+
}, [isMuted]);
|
|
349
|
+
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
const unloadCallback = (event: BeforeUnloadEvent) => {
|
|
352
|
+
logger.log('Unloading', event);
|
|
353
|
+
if (isRecording) {
|
|
354
|
+
stopConversation().then(() => {
|
|
355
|
+
logger.log('Conversation stopped by unmount');
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return "";
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
window.addEventListener("beforeunload", unloadCallback);
|
|
362
|
+
return () => {
|
|
363
|
+
window.removeEventListener("beforeunload", unloadCallback);
|
|
364
|
+
}
|
|
365
|
+
}, [stopConversation, isRecording]);
|
|
366
|
+
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
return () => {
|
|
369
|
+
stopConversation().then(() => {
|
|
370
|
+
logger.log('Conversation stopped by effect cleanup');
|
|
371
|
+
if (audioContextRef.current) {
|
|
372
|
+
audioContextRef.current.close();
|
|
373
|
+
audioContextRef.current = null;
|
|
374
|
+
}
|
|
375
|
+
sourceNodeRef.current = null;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}, [stopConversation]);
|
|
379
|
+
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
logger.log('imageUrls changed:', imageUrls);
|
|
382
|
+
}, [imageUrls]);
|
|
383
|
+
|
|
384
|
+
// Log in toggle
|
|
385
|
+
const toggleChat = () => {
|
|
386
|
+
logger.log('Toggle clicked, current showChat:', showChat);
|
|
387
|
+
setShowChat(prev => {
|
|
388
|
+
logger.log('Setting showChat from', prev, 'to', !prev);
|
|
389
|
+
return !prev;
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
setMounted(true);
|
|
395
|
+
}, []);
|
|
396
|
+
|
|
397
|
+
// Add a debug log in the render to check values
|
|
398
|
+
const chatPanelClasses = `absolute inset-x-0 bottom-0 h-[66vh] bg-gray-800/95
|
|
399
|
+
backdrop-blur-sm border-t border-gray-700/50 shadow-2xl
|
|
400
|
+
rounded-t-2xl z-50 transition-transform duration-300 ease-in-out
|
|
401
|
+
transform translate-y-full ${showChat ? 'translate-y-0' : ''}`;
|
|
402
|
+
|
|
403
|
+
logger.log('Render state:', {
|
|
404
|
+
showChat,
|
|
405
|
+
mounted,
|
|
406
|
+
classes: chatPanelClasses
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
// Initialize sound effects when component mounts
|
|
411
|
+
SoundEffects.init().catch(console.error);
|
|
412
|
+
}, []);
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<div className="h-screen flex items-center justify-center p-4">
|
|
416
|
+
<div className="w-full max-w-3xl h-full flex flex-col relative gap-[5px]">
|
|
417
|
+
{socketRef.current?.connected && (
|
|
418
|
+
<ScreenshotCapture socket={socketRef.current} />
|
|
419
|
+
)}
|
|
420
|
+
|
|
421
|
+
<div className={`flex flex-col bg-gray-800/50 backdrop-blur-sm rounded-2xl
|
|
422
|
+
shadow-2xl border border-gray-700/50 px-6 pt-6 pb-2
|
|
423
|
+
transition-all duration-300 ease-in-out`}
|
|
424
|
+
style={showChat ? {
|
|
425
|
+
height: '34vh'
|
|
426
|
+
} : { height: '100%' }}>
|
|
427
|
+
<div className="flex flex-col justify-center flex-1">
|
|
428
|
+
<div className={`flex items-center justify-center relative
|
|
429
|
+
transition-all duration-300 ease-in-out
|
|
430
|
+
${showChat ? 'mb-2' : 'mb-8'}`}>
|
|
431
|
+
{isRecording ? (
|
|
432
|
+
<div className={`animate-fadeIn h-full aspect-square flex items-center justify-center
|
|
433
|
+
transition-all duration-300 ease-in-out`}>
|
|
434
|
+
<AudioVisualizer
|
|
435
|
+
audioContext={audioContextRef.current}
|
|
436
|
+
analyserNode={outputAnalyserRef.current}
|
|
437
|
+
width={showChat ? window.innerHeight * (100 - 66) / 100 * 0.6 : window.innerHeight * 0.75}
|
|
438
|
+
height={showChat ? window.innerHeight * (100 - 66) / 100 * 0.6 : window.innerHeight * 0.75}
|
|
439
|
+
/>
|
|
440
|
+
</div>
|
|
441
|
+
) : (
|
|
442
|
+
<div className="h-full aspect-square flex items-center justify-center">
|
|
443
|
+
<div className={`aspect-square flex items-center justify-center
|
|
444
|
+
rounded-lg bg-gray-900/50 border border-gray-800
|
|
445
|
+
animate-pulse transition-all duration-300 ease-in-out`}
|
|
446
|
+
style={{
|
|
447
|
+
width: showChat ?
|
|
448
|
+
window.innerHeight * (100 - 66) / 100 * 0.6 :
|
|
449
|
+
window.innerHeight * 0.75,
|
|
450
|
+
height: showChat ?
|
|
451
|
+
window.innerHeight * (100 - 66) / 100 * 0.6 :
|
|
452
|
+
window.innerHeight * 0.75
|
|
453
|
+
}}>
|
|
454
|
+
<span className={`text-gray-500 transition-all duration-300 ease-in-out
|
|
455
|
+
${showChat ? 'text-sm' : 'text-xl'}`}>
|
|
456
|
+
Awaiting Entity Link...
|
|
457
|
+
</span>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
463
|
+
<ImageOverlay
|
|
464
|
+
key={overlayKey}
|
|
465
|
+
imageUrls={imageUrls}
|
|
466
|
+
onComplete={handleImagesComplete}
|
|
467
|
+
isAudioPlaying={isAudioPlaying}
|
|
468
|
+
/>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div className={`flex-none flex items-center justify-center
|
|
473
|
+
transition-all duration-300 ease-in-out
|
|
474
|
+
${showChat ? 'pb-2' : 'py-4'}
|
|
475
|
+
${showChat ? 'space-x-3' : 'space-x-8'}`}>
|
|
476
|
+
<div className={`relative ${showChat ? 'w-12 h-12' : 'w-16 h-16'}`}>
|
|
477
|
+
{isRecording && !isMuted && (
|
|
478
|
+
<div className="absolute inset-0 pointer-events-none">
|
|
479
|
+
<MicrophoneVisualizer
|
|
480
|
+
audioContext={audioContextRef.current}
|
|
481
|
+
sourceNode={sourceNodeRef.current}
|
|
482
|
+
size={showChat ? 'small' : 'large'}
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
<button
|
|
487
|
+
onClick={handleMicMute}
|
|
488
|
+
className={`absolute inset-0 z-10 flex items-center justify-center rounded-full transition duration-300 shadow-lg ${
|
|
489
|
+
!isRecording
|
|
490
|
+
? 'bg-gray-800/50 cursor-not-allowed opacity-50'
|
|
491
|
+
: !isMuted
|
|
492
|
+
? 'bg-gradient-to-r from-blue-500/70 to-cyan-500/70 hover:from-blue-600/70 hover:to-cyan-600/70 shadow-cyan-500/20'
|
|
493
|
+
: 'bg-gradient-to-r from-slate-600/70 to-gray-700/70 hover:from-slate-700/70 hover:to-gray-800/70 shadow-slate-500/20'
|
|
494
|
+
}`}
|
|
495
|
+
aria-label={isMuted ? 'Unmute microphone' : 'Mute microphone'}
|
|
496
|
+
disabled={!isRecording}
|
|
497
|
+
>
|
|
498
|
+
{!isMuted ? <MicIcon sx={{ fontSize: showChat ? 20 : 28 }} /> : <MicOffIcon sx={{ fontSize: showChat ? 20 : 28 }} />}
|
|
499
|
+
</button>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<button
|
|
503
|
+
onClick={onStartStop}
|
|
504
|
+
disabled={isLoading}
|
|
505
|
+
className={`flex items-center justify-center ${showChat ? 'w-12 h-12' : 'w-16 h-16'} rounded-full transition duration-300 shadow-lg ${
|
|
506
|
+
isLoading ? 'opacity-50 cursor-not-allowed' :
|
|
507
|
+
isRecording
|
|
508
|
+
? 'bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 shadow-red-500/20'
|
|
509
|
+
: 'bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 shadow-cyan-500/20'
|
|
510
|
+
}`}
|
|
511
|
+
aria-label={isRecording ? 'Terminate connection' : 'Initialize connection'}
|
|
512
|
+
>
|
|
513
|
+
{isRecording ? (
|
|
514
|
+
<CallEndIcon sx={{ fontSize: showChat ? 20 : 28 }} />
|
|
515
|
+
) : (
|
|
516
|
+
<PhoneEnabledIcon sx={{ fontSize: showChat ? 20 : 28 }} />
|
|
517
|
+
)}
|
|
518
|
+
</button>
|
|
519
|
+
|
|
520
|
+
<button
|
|
521
|
+
onClick={toggleChat}
|
|
522
|
+
className={`flex items-center justify-center ${showChat ? 'w-12 h-12' : 'w-16 h-16'} rounded-full
|
|
523
|
+
transition duration-300 shadow-lg
|
|
524
|
+
bg-gradient-to-r from-gray-600 to-gray-700
|
|
525
|
+
hover:from-gray-700 hover:to-gray-800`}
|
|
526
|
+
aria-label={showChat ? 'Hide chat' : 'Show chat'}
|
|
527
|
+
>
|
|
528
|
+
{showChat ? <CloseIcon sx={{ fontSize: 20 }} /> : <ChatIcon sx={{ fontSize: 28 }} />}
|
|
529
|
+
</button>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
{mounted && showChat && (
|
|
535
|
+
<div className={`bg-gray-800/95 backdrop-blur-sm border-t border-gray-700/50
|
|
536
|
+
shadow-2xl rounded-t-2xl transition-all duration-300 ease-in-out`}
|
|
537
|
+
style={{
|
|
538
|
+
height: '66vh',
|
|
539
|
+
transform: showChat ? 'none' : 'translateY(100%)',
|
|
540
|
+
}}>
|
|
541
|
+
<div className="h-full">
|
|
542
|
+
<ChatTile
|
|
543
|
+
messages={messages}
|
|
544
|
+
onSend={postMessage}
|
|
545
|
+
isConnected={isRecording}
|
|
546
|
+
/>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ChatBubbleRight } from './ChatBubbleRight';
|
|
2
|
+
import { ChatBubbleLeft } from './ChatBubbleLeft';
|
|
3
|
+
|
|
4
|
+
type ChatBubbleProps = {
|
|
5
|
+
message: string;
|
|
6
|
+
name: string;
|
|
7
|
+
isSelf: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ChatBubble = ({
|
|
11
|
+
name,
|
|
12
|
+
message,
|
|
13
|
+
isSelf,
|
|
14
|
+
}: ChatBubbleProps) => {
|
|
15
|
+
return (
|
|
16
|
+
isSelf ? (
|
|
17
|
+
<ChatBubbleRight name={name} message={message} />
|
|
18
|
+
) : (
|
|
19
|
+
<ChatBubbleLeft name={name} message={message} />
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {ChatMessage} from "./ChatMessage";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
type ChatBubbleLeftProps = {
|
|
5
|
+
name: string;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const ChatBubbleLeft = ({name, message}: ChatBubbleLeftProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex items-start justify-start p-3">
|
|
12
|
+
<div className="flex flex-col leading-1.5 p-4 border-gray-200 bg-gray-900 rounded-e-xl rounded-es-xl mr-20">
|
|
13
|
+
<div className="flex items-start justify-start space-x-2">
|
|
14
|
+
<p className="text-sm font-semibold text-gray-100">{name}</p>
|
|
15
|
+
</div>
|
|
16
|
+
<p className="text-sm font-normal py-2.5 text-gray-100">
|
|
17
|
+
<ChatMessage message={message}/>
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {ChatMessage} from "./ChatMessage";
|
|
2
|
+
|
|
3
|
+
type ChatBubbleRightProps = {
|
|
4
|
+
name: string;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const ChatBubbleRight = ({name, message}: ChatBubbleRightProps) => {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex items-end justify-end p-3">
|
|
11
|
+
<div className="flex flex-col leading-1.5 p-4 border-gray-200 bg-[#2F93FF] rounded-b-xl rounded-tl-xl ml-20">
|
|
12
|
+
<div className="flex items-end justify-end space-x-2">
|
|
13
|
+
<p className="text-sm font-semibold text-gray-100">{name}</p>
|
|
14
|
+
</div>
|
|
15
|
+
<p className="text-sm font-normal py-2.5 text-gray-100">
|
|
16
|
+
<ChatMessage message={message}/>
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Markdown from "react-markdown";
|
|
2
|
+
|
|
3
|
+
type ChatMessageProps = {
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const ChatMessage = ({message}: ChatMessageProps) => {
|
|
8
|
+
return <Markdown
|
|
9
|
+
children={message}
|
|
10
|
+
components={{
|
|
11
|
+
h1: ({children}) =>
|
|
12
|
+
<h1 className="text-xl font-bold text-gray-100">{children}</h1>,
|
|
13
|
+
h2: ({children}) =>
|
|
14
|
+
<h2 className="text-lg font-semi-bold text-gray-100">{children}</h2>,
|
|
15
|
+
p: ({children}) =>
|
|
16
|
+
<p className="text-sm font-normal text-gray-100">{children}</p>,
|
|
17
|
+
ol: ({children}) =>
|
|
18
|
+
<ol className="list-decimal">{children}</ol>,
|
|
19
|
+
ul: ({children}) =>
|
|
20
|
+
<ul className="list-disc">{children}</ul>,
|
|
21
|
+
li: ({children}) =>
|
|
22
|
+
<li className="pt-2 ml-4">{children}</li>,
|
|
23
|
+
a: ({children, href}) =>
|
|
24
|
+
<a className="text-blue-500" target="_blank" rel="noreferrer" href={href}>{children}</a>
|
|
25
|
+
}}
|
|
26
|
+
/>;
|
|
27
|
+
}
|