@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.
Files changed (94) hide show
  1. package/helper-apps/cortex-autogen/agents.py +31 -2
  2. package/helper-apps/cortex-realtime-voice-server/.env.sample +6 -0
  3. package/helper-apps/cortex-realtime-voice-server/README.md +22 -0
  4. package/helper-apps/cortex-realtime-voice-server/bun.lockb +0 -0
  5. package/helper-apps/cortex-realtime-voice-server/client/bun.lockb +0 -0
  6. package/helper-apps/cortex-realtime-voice-server/client/index.html +12 -0
  7. package/helper-apps/cortex-realtime-voice-server/client/package.json +65 -0
  8. package/helper-apps/cortex-realtime-voice-server/client/postcss.config.js +6 -0
  9. package/helper-apps/cortex-realtime-voice-server/client/public/favicon.ico +0 -0
  10. package/helper-apps/cortex-realtime-voice-server/client/public/index.html +43 -0
  11. package/helper-apps/cortex-realtime-voice-server/client/public/logo192.png +0 -0
  12. package/helper-apps/cortex-realtime-voice-server/client/public/logo512.png +0 -0
  13. package/helper-apps/cortex-realtime-voice-server/client/public/manifest.json +25 -0
  14. package/helper-apps/cortex-realtime-voice-server/client/public/robots.txt +3 -0
  15. package/helper-apps/cortex-realtime-voice-server/client/public/sounds/connect.mp3 +0 -0
  16. package/helper-apps/cortex-realtime-voice-server/client/public/sounds/disconnect.mp3 +0 -0
  17. package/helper-apps/cortex-realtime-voice-server/client/src/App.test.tsx +9 -0
  18. package/helper-apps/cortex-realtime-voice-server/client/src/App.tsx +126 -0
  19. package/helper-apps/cortex-realtime-voice-server/client/src/SettingsModal.tsx +207 -0
  20. package/helper-apps/cortex-realtime-voice-server/client/src/chat/Chat.tsx +553 -0
  21. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubble.tsx +22 -0
  22. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleLeft.tsx +22 -0
  23. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatBubbleRight.tsx +21 -0
  24. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessage.tsx +27 -0
  25. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatMessageInput.tsx +74 -0
  26. package/helper-apps/cortex-realtime-voice-server/client/src/chat/ChatTile.tsx +211 -0
  27. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/SoundEffects.ts +56 -0
  28. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavPacker.ts +112 -0
  29. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavRecorder.ts +571 -0
  30. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/WavStreamPlayer.ts +290 -0
  31. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/AudioAnalysis.ts +186 -0
  32. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/analysis/constants.ts +59 -0
  33. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/AudioProcessor.ts +214 -0
  34. package/helper-apps/cortex-realtime-voice-server/client/src/chat/audio/worklets/StreamProcessor.ts +183 -0
  35. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/AudioVisualizer.tsx +151 -0
  36. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/CopyButton.tsx +32 -0
  37. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ImageOverlay.tsx +166 -0
  38. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/MicrophoneVisualizer.tsx +95 -0
  39. package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ScreenshotCapture.tsx +116 -0
  40. package/helper-apps/cortex-realtime-voice-server/client/src/chat/hooks/useWindowResize.ts +27 -0
  41. package/helper-apps/cortex-realtime-voice-server/client/src/chat/utils/audio.ts +33 -0
  42. package/helper-apps/cortex-realtime-voice-server/client/src/index.css +20 -0
  43. package/helper-apps/cortex-realtime-voice-server/client/src/index.tsx +19 -0
  44. package/helper-apps/cortex-realtime-voice-server/client/src/logo.svg +1 -0
  45. package/helper-apps/cortex-realtime-voice-server/client/src/react-app-env.d.ts +1 -0
  46. package/helper-apps/cortex-realtime-voice-server/client/src/reportWebVitals.ts +15 -0
  47. package/helper-apps/cortex-realtime-voice-server/client/src/setupTests.ts +5 -0
  48. package/helper-apps/cortex-realtime-voice-server/client/src/utils/logger.ts +45 -0
  49. package/helper-apps/cortex-realtime-voice-server/client/tailwind.config.js +14 -0
  50. package/helper-apps/cortex-realtime-voice-server/client/tsconfig.json +30 -0
  51. package/helper-apps/cortex-realtime-voice-server/client/vite.config.ts +22 -0
  52. package/helper-apps/cortex-realtime-voice-server/index.ts +19 -0
  53. package/helper-apps/cortex-realtime-voice-server/package.json +28 -0
  54. package/helper-apps/cortex-realtime-voice-server/src/ApiServer.ts +35 -0
  55. package/helper-apps/cortex-realtime-voice-server/src/SocketServer.ts +737 -0
  56. package/helper-apps/cortex-realtime-voice-server/src/Tools.ts +520 -0
  57. package/helper-apps/cortex-realtime-voice-server/src/cortex/expert.ts +29 -0
  58. package/helper-apps/cortex-realtime-voice-server/src/cortex/image.ts +29 -0
  59. package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +91 -0
  60. package/helper-apps/cortex-realtime-voice-server/src/cortex/reason.ts +29 -0
  61. package/helper-apps/cortex-realtime-voice-server/src/cortex/search.ts +30 -0
  62. package/helper-apps/cortex-realtime-voice-server/src/cortex/style.ts +31 -0
  63. package/helper-apps/cortex-realtime-voice-server/src/cortex/utils.ts +95 -0
  64. package/helper-apps/cortex-realtime-voice-server/src/cortex/vision.ts +34 -0
  65. package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +499 -0
  66. package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +279 -0
  67. package/helper-apps/cortex-realtime-voice-server/src/realtime/socket.ts +27 -0
  68. package/helper-apps/cortex-realtime-voice-server/src/realtime/transcription.ts +75 -0
  69. package/helper-apps/cortex-realtime-voice-server/src/realtime/utils.ts +33 -0
  70. package/helper-apps/cortex-realtime-voice-server/src/utils/logger.ts +45 -0
  71. package/helper-apps/cortex-realtime-voice-server/src/utils/prompt.ts +81 -0
  72. package/helper-apps/cortex-realtime-voice-server/tsconfig.json +28 -0
  73. package/package.json +1 -1
  74. package/pathways/basePathway.js +3 -1
  75. package/pathways/system/entity/memory/sys_memory_manager.js +3 -0
  76. package/pathways/system/entity/memory/sys_memory_update.js +44 -45
  77. package/pathways/system/entity/memory/sys_read_memory.js +86 -6
  78. package/pathways/system/entity/memory/sys_search_memory.js +66 -0
  79. package/pathways/system/entity/shared/sys_entity_constants.js +2 -2
  80. package/pathways/system/entity/sys_entity_continue.js +2 -1
  81. package/pathways/system/entity/sys_entity_start.js +10 -0
  82. package/pathways/system/entity/sys_generator_expert.js +0 -2
  83. package/pathways/system/entity/sys_generator_memory.js +31 -0
  84. package/pathways/system/entity/sys_generator_voice_sample.js +36 -0
  85. package/pathways/system/entity/sys_router_tool.js +13 -10
  86. package/pathways/system/sys_parse_numbered_object_list.js +1 -1
  87. package/server/pathwayResolver.js +41 -31
  88. package/server/plugins/azureVideoTranslatePlugin.js +28 -16
  89. package/server/plugins/claude3VertexPlugin.js +0 -9
  90. package/server/plugins/gemini15ChatPlugin.js +18 -5
  91. package/server/plugins/modelPlugin.js +27 -6
  92. package/server/plugins/openAiChatPlugin.js +10 -8
  93. package/server/plugins/openAiVisionPlugin.js +56 -0
  94. 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
+ }