@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,166 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const FADE_DURATION = 300; // milliseconds
|
|
4
|
+
const DISPLAY_DURATION = 10000; // milliseconds (10 seconds)
|
|
5
|
+
|
|
6
|
+
type ImageOverlayProps = {
|
|
7
|
+
imageUrls: string[];
|
|
8
|
+
onComplete?: () => void;
|
|
9
|
+
isAudioPlaying?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function ImageOverlay({ imageUrls, onComplete, isAudioPlaying = false }: ImageOverlayProps) {
|
|
13
|
+
const [currentIndex, setCurrentIndex] = useState(-1);
|
|
14
|
+
const [shownImages, setShownImages] = useState<Set<string>>(new Set());
|
|
15
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
16
|
+
const [showControls, setShowControls] = useState(false);
|
|
17
|
+
const previousAudioPlaying = useRef(isAudioPlaying);
|
|
18
|
+
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
19
|
+
const displayStartTimeRef = useRef<number | null>(null);
|
|
20
|
+
|
|
21
|
+
const clearTimer = useCallback(() => {
|
|
22
|
+
if (timerRef.current) {
|
|
23
|
+
clearTimeout(timerRef.current);
|
|
24
|
+
timerRef.current = null;
|
|
25
|
+
}
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
// Start display timer when image becomes visible
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (isVisible && currentIndex >= 0) {
|
|
31
|
+
displayStartTimeRef.current = Date.now();
|
|
32
|
+
}
|
|
33
|
+
}, [isVisible, currentIndex]);
|
|
34
|
+
|
|
35
|
+
// Handle image transitions when audio stops and minimum time has passed
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (previousAudioPlaying.current && !isAudioPlaying && currentIndex >= 0) {
|
|
38
|
+
const timeElapsed = displayStartTimeRef.current ? Date.now() - displayStartTimeRef.current : 0;
|
|
39
|
+
const remainingTime = Math.max(0, DISPLAY_DURATION - timeElapsed);
|
|
40
|
+
|
|
41
|
+
clearTimer();
|
|
42
|
+
|
|
43
|
+
if (remainingTime > 0) {
|
|
44
|
+
// If minimum display time hasn't elapsed, wait for the remaining time
|
|
45
|
+
timerRef.current = setTimeout(() => {
|
|
46
|
+
const currentUrl = imageUrls[currentIndex];
|
|
47
|
+
setIsVisible(false);
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
setShownImages(prev => new Set(Array.from(prev).concat([currentUrl])));
|
|
50
|
+
setCurrentIndex(-1);
|
|
51
|
+
}, FADE_DURATION);
|
|
52
|
+
}, remainingTime);
|
|
53
|
+
} else {
|
|
54
|
+
// If minimum time has elapsed, start fade immediately
|
|
55
|
+
const currentUrl = imageUrls[currentIndex];
|
|
56
|
+
setIsVisible(false);
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
setShownImages(prev => new Set(Array.from(prev).concat([currentUrl])));
|
|
59
|
+
setCurrentIndex(-1);
|
|
60
|
+
}, FADE_DURATION);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
previousAudioPlaying.current = isAudioPlaying;
|
|
64
|
+
}, [isAudioPlaying, currentIndex, imageUrls, clearTimer]);
|
|
65
|
+
|
|
66
|
+
// Start timer when new image is shown
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (currentIndex >= 0 && !isAudioPlaying) {
|
|
69
|
+
clearTimer();
|
|
70
|
+
timerRef.current = setTimeout(() => {
|
|
71
|
+
const currentUrl = imageUrls[currentIndex];
|
|
72
|
+
setIsVisible(false);
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
setShownImages(prev => new Set(Array.from(prev).concat([currentUrl])));
|
|
75
|
+
setCurrentIndex(-1);
|
|
76
|
+
}, FADE_DURATION);
|
|
77
|
+
}, DISPLAY_DURATION);
|
|
78
|
+
|
|
79
|
+
return () => clearTimer();
|
|
80
|
+
}
|
|
81
|
+
}, [currentIndex, imageUrls, isAudioPlaying, clearTimer]);
|
|
82
|
+
|
|
83
|
+
// Handle image transitions
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (currentIndex === -1) {
|
|
86
|
+
const nextIndex = imageUrls.findIndex(url => !shownImages.has(url));
|
|
87
|
+
if (nextIndex !== -1) {
|
|
88
|
+
setCurrentIndex(nextIndex);
|
|
89
|
+
setIsVisible(true);
|
|
90
|
+
} else if (shownImages.size > 0) {
|
|
91
|
+
onComplete?.();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, [imageUrls, shownImages, currentIndex, onComplete]);
|
|
95
|
+
|
|
96
|
+
const handleImageSelect = (index: number) => {
|
|
97
|
+
clearTimer();
|
|
98
|
+
displayStartTimeRef.current = Date.now();
|
|
99
|
+
setCurrentIndex(index);
|
|
100
|
+
// Start with image invisible for manual selection too
|
|
101
|
+
setIsVisible(false);
|
|
102
|
+
setTimeout(() => setIsVisible(true), 50);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Cleanup on unmount
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
return () => clearTimer();
|
|
108
|
+
}, [clearTimer]);
|
|
109
|
+
|
|
110
|
+
if (!imageUrls.length) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className="absolute inset-0 flex items-center justify-center"
|
|
117
|
+
onMouseEnter={() => {
|
|
118
|
+
setShowControls(true);
|
|
119
|
+
}}
|
|
120
|
+
onMouseLeave={() => {
|
|
121
|
+
setShowControls(false);
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{currentIndex !== -1 && (
|
|
125
|
+
<img
|
|
126
|
+
src={imageUrls[currentIndex]}
|
|
127
|
+
alt="Generated content"
|
|
128
|
+
style={{
|
|
129
|
+
opacity: isVisible ? 1 : 0,
|
|
130
|
+
transition: 'opacity 500ms ease-in-out'
|
|
131
|
+
}}
|
|
132
|
+
className="max-w-full max-h-full object-contain"
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{/* Image selection controls */}
|
|
137
|
+
<div
|
|
138
|
+
className={`absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2 p-2 bg-black/50 rounded-full transition-opacity duration-200 ${
|
|
139
|
+
showControls ? 'opacity-100' : 'opacity-0'
|
|
140
|
+
}`}
|
|
141
|
+
>
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => {
|
|
144
|
+
clearTimer();
|
|
145
|
+
setCurrentIndex(-1);
|
|
146
|
+
setIsVisible(false);
|
|
147
|
+
}}
|
|
148
|
+
className={`w-3 h-3 rounded-full transition-all ${
|
|
149
|
+
currentIndex === -1 ? 'bg-white scale-110' : 'bg-white/50 hover:bg-white/75'
|
|
150
|
+
}`}
|
|
151
|
+
title="Hide all images"
|
|
152
|
+
/>
|
|
153
|
+
{imageUrls.map((_, index) => (
|
|
154
|
+
<button
|
|
155
|
+
key={index}
|
|
156
|
+
onClick={() => handleImageSelect(index)}
|
|
157
|
+
className={`w-3 h-3 rounded-full transition-all ${
|
|
158
|
+
currentIndex === index ? 'bg-white scale-110' : 'bg-white/50 hover:bg-white/75'
|
|
159
|
+
}`}
|
|
160
|
+
title={`Show image ${index + 1}`}
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/MicrophoneVisualizer.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
type MicrophoneVisualizerProps = {
|
|
4
|
+
audioContext: AudioContext | null;
|
|
5
|
+
sourceNode: MediaStreamAudioSourceNode | null;
|
|
6
|
+
size?: 'small' | 'large';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const MicrophoneVisualizer = ({
|
|
10
|
+
audioContext,
|
|
11
|
+
sourceNode,
|
|
12
|
+
size = 'large'
|
|
13
|
+
}: MicrophoneVisualizerProps) => {
|
|
14
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
15
|
+
const analyzerRef = useRef<AnalyserNode | null>(null);
|
|
16
|
+
const animationRef = useRef<number>();
|
|
17
|
+
|
|
18
|
+
const dimensions = size === 'small' ? 48 : 64;
|
|
19
|
+
const ringRadius = size === 'small' ? 21 : 28;
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!audioContext || !sourceNode || !canvasRef.current) return;
|
|
23
|
+
|
|
24
|
+
const analyzer = audioContext.createAnalyser();
|
|
25
|
+
analyzerRef.current = analyzer;
|
|
26
|
+
|
|
27
|
+
analyzer.fftSize = 256;
|
|
28
|
+
analyzer.smoothingTimeConstant = 0.7;
|
|
29
|
+
|
|
30
|
+
sourceNode.connect(analyzer);
|
|
31
|
+
|
|
32
|
+
const draw = () => {
|
|
33
|
+
const canvas = canvasRef.current;
|
|
34
|
+
if (!canvas || !analyzer) return;
|
|
35
|
+
|
|
36
|
+
const ctx = canvas.getContext('2d');
|
|
37
|
+
if (!ctx) return;
|
|
38
|
+
|
|
39
|
+
const bufferLength = analyzer.frequencyBinCount;
|
|
40
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
41
|
+
analyzer.getByteFrequencyData(dataArray);
|
|
42
|
+
|
|
43
|
+
// Calculate average volume
|
|
44
|
+
const average = dataArray.reduce((a, b) => a + b) / bufferLength;
|
|
45
|
+
const normalizedVolume = Math.min(average / 128, 1);
|
|
46
|
+
|
|
47
|
+
// Clear canvas
|
|
48
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
49
|
+
|
|
50
|
+
// Draw background ring
|
|
51
|
+
ctx.beginPath();
|
|
52
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
|
53
|
+
ctx.lineWidth = size === 'small' ? 3 : 4;
|
|
54
|
+
ctx.arc(canvas.width / 2, canvas.height / 2, ringRadius, 0, Math.PI * 2);
|
|
55
|
+
ctx.stroke();
|
|
56
|
+
|
|
57
|
+
// Draw volume indicator with increased brightness
|
|
58
|
+
ctx.beginPath();
|
|
59
|
+
ctx.strokeStyle = `hsla(${210 + normalizedVolume * 30}, 100%, 90%, 1.0)`;
|
|
60
|
+
ctx.lineWidth = size === 'small' ? 3 : 4;
|
|
61
|
+
ctx.arc(canvas.width / 2, canvas.height / 2, ringRadius, 0, Math.PI * 2 * normalizedVolume);
|
|
62
|
+
ctx.stroke();
|
|
63
|
+
|
|
64
|
+
// Subtle glow effect
|
|
65
|
+
ctx.shadowBlur = 10;
|
|
66
|
+
ctx.shadowColor = `hsla(${210 + normalizedVolume * 30}, 100%, 70%, ${0.6})`;
|
|
67
|
+
|
|
68
|
+
// Simpler second pass
|
|
69
|
+
ctx.beginPath();
|
|
70
|
+
ctx.strokeStyle = `hsla(${210 + normalizedVolume * 30}, 100%, 95%, ${0.4 + normalizedVolume * 0.4})`;
|
|
71
|
+
ctx.lineWidth = 2;
|
|
72
|
+
ctx.arc(canvas.width / 2, canvas.height / 2, ringRadius + 1, 0, Math.PI * 2 * normalizedVolume);
|
|
73
|
+
ctx.stroke();
|
|
74
|
+
|
|
75
|
+
animationRef.current = requestAnimationFrame(draw);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
draw();
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
if (animationRef.current) {
|
|
82
|
+
cancelAnimationFrame(animationRef.current);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}, [audioContext, sourceNode, size, ringRadius]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<canvas
|
|
89
|
+
ref={canvasRef}
|
|
90
|
+
width={dimensions}
|
|
91
|
+
height={dimensions}
|
|
92
|
+
className={size === 'small' ? 'w-12 h-12' : 'w-16 h-16'}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
};
|
package/helper-apps/cortex-realtime-voice-server/client/src/chat/components/ScreenshotCapture.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { Socket } from 'socket.io-client';
|
|
3
|
+
import { ClientToServerEvents, ServerToClientEvents } from '../../../../src/realtime/socket';
|
|
4
|
+
import { logger } from '../../utils/logger';
|
|
5
|
+
|
|
6
|
+
type ScreenshotCaptureProps = {
|
|
7
|
+
socket: Socket<ServerToClientEvents, ClientToServerEvents>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ScreenshotCapture = ({ socket }: ScreenshotCaptureProps) => {
|
|
11
|
+
const activeStreamRef = useRef<MediaStream | null>(null);
|
|
12
|
+
|
|
13
|
+
const startScreenCapture = useCallback(async () => {
|
|
14
|
+
logger.log('Starting screen capture...');
|
|
15
|
+
try {
|
|
16
|
+
// Request screen capture with friendly message and whole screen preference
|
|
17
|
+
logger.log('Requesting display media...');
|
|
18
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
19
|
+
video: {
|
|
20
|
+
frameRate: 1,
|
|
21
|
+
displaySurface: 'monitor', // Prefer whole screen
|
|
22
|
+
cursor: 'never' // Don't need cursor
|
|
23
|
+
} as MediaTrackConstraints
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Store the stream reference
|
|
27
|
+
activeStreamRef.current = stream;
|
|
28
|
+
|
|
29
|
+
// Handle stream ending (user clicks "Stop Sharing")
|
|
30
|
+
stream.getVideoTracks()[0].onended = () => {
|
|
31
|
+
logger.log('Screen sharing stopped by user');
|
|
32
|
+
activeStreamRef.current = null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return stream;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logger.error('Error starting screen capture:', error);
|
|
38
|
+
activeStreamRef.current = null;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const captureFrame = useCallback(async (stream: MediaStream) => {
|
|
44
|
+
logger.log('Capturing frame from stream...');
|
|
45
|
+
const track = stream.getVideoTracks()[0];
|
|
46
|
+
|
|
47
|
+
// Create video element to capture frame
|
|
48
|
+
const video = document.createElement('video');
|
|
49
|
+
video.srcObject = stream;
|
|
50
|
+
|
|
51
|
+
// Wait for video to load
|
|
52
|
+
await new Promise<void>((resolve) => {
|
|
53
|
+
video.onloadedmetadata = () => {
|
|
54
|
+
logger.log('Video metadata loaded, playing...');
|
|
55
|
+
video.play();
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Create canvas and draw video frame
|
|
61
|
+
const canvas = document.createElement('canvas');
|
|
62
|
+
canvas.width = video.videoWidth;
|
|
63
|
+
canvas.height = video.videoHeight;
|
|
64
|
+
const ctx = canvas.getContext('2d');
|
|
65
|
+
|
|
66
|
+
if (!ctx) {
|
|
67
|
+
throw new Error('Could not get canvas context');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Draw the video frame
|
|
71
|
+
ctx.drawImage(video, 0, 0);
|
|
72
|
+
|
|
73
|
+
// Convert to base64
|
|
74
|
+
const imageData = canvas.toDataURL('image/png');
|
|
75
|
+
|
|
76
|
+
// Clean up
|
|
77
|
+
video.remove();
|
|
78
|
+
canvas.remove();
|
|
79
|
+
|
|
80
|
+
return imageData;
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const handleScreenshotRequest = useCallback(async () => {
|
|
84
|
+
try {
|
|
85
|
+
// Use existing stream or request new one
|
|
86
|
+
const stream = activeStreamRef.current || await startScreenCapture();
|
|
87
|
+
|
|
88
|
+
// Capture frame from stream
|
|
89
|
+
const imageData = await captureFrame(stream);
|
|
90
|
+
|
|
91
|
+
logger.log('Sending screenshot data to server...');
|
|
92
|
+
socket.emit('screenshotCaptured', imageData);
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger.error('Error handling screenshot request:', error);
|
|
96
|
+
socket.emit('screenshotError', error instanceof Error ? error.message : 'Failed to capture screenshot');
|
|
97
|
+
}
|
|
98
|
+
}, [socket, startScreenCapture, captureFrame]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
logger.log('Setting up screenshot request listener');
|
|
102
|
+
socket.on('requestScreenshot', handleScreenshotRequest);
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
logger.log('Cleaning up screenshot request listener');
|
|
106
|
+
socket.off('requestScreenshot');
|
|
107
|
+
// Clean up stream if component unmounts
|
|
108
|
+
if (activeStreamRef.current) {
|
|
109
|
+
activeStreamRef.current.getTracks().forEach(track => track.stop());
|
|
110
|
+
activeStreamRef.current = null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}, [socket, handleScreenshotRequest]);
|
|
114
|
+
|
|
115
|
+
return null; // This is a non-visual component
|
|
116
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export const useWindowResize = () => {
|
|
4
|
+
const [size, setSize] = useState({
|
|
5
|
+
width: 0,
|
|
6
|
+
height: 0,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const handleResize = () => {
|
|
11
|
+
setSize({
|
|
12
|
+
width: window.innerWidth,
|
|
13
|
+
height: window.innerHeight,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
handleResize();
|
|
18
|
+
|
|
19
|
+
window.addEventListener("resize", handleResize);
|
|
20
|
+
|
|
21
|
+
return () => {
|
|
22
|
+
window.removeEventListener("resize", handleResize);
|
|
23
|
+
};
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return size;
|
|
27
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function arrayBufferToBase64(
|
|
2
|
+
arrayBuffer: ArrayBuffer | Int16Array,
|
|
3
|
+
): string {
|
|
4
|
+
let buffer: ArrayBuffer;
|
|
5
|
+
if (arrayBuffer instanceof ArrayBuffer) {
|
|
6
|
+
buffer = arrayBuffer;
|
|
7
|
+
} else {
|
|
8
|
+
buffer = arrayBuffer.buffer as ArrayBuffer;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const bytes = new Uint8Array(buffer);
|
|
12
|
+
const chunkSize = 0x80_00; // 32KB chunk size
|
|
13
|
+
let binary = '';
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
16
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
17
|
+
binary += String.fromCharCode.apply(null, chunk as any);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return btoa(binary);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
24
|
+
const binaryString = atob(base64)
|
|
25
|
+
const len = binaryString.length
|
|
26
|
+
const bytes = new Uint8Array(len)
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < len; i++) {
|
|
29
|
+
bytes[i] = binaryString.charCodeAt(i)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return bytes.buffer as ArrayBuffer;
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
html, body {
|
|
6
|
+
background: rgb(17, 24, 39); /* matches from-gray-900 */
|
|
7
|
+
min-height: 100%;
|
|
8
|
+
overscroll-behavior: none; /* prevents bounce on some browsers */
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* For Safari/iOS */
|
|
12
|
+
@supports (-webkit-overflow-scrolling: touch) {
|
|
13
|
+
body {
|
|
14
|
+
position: fixed;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
overflow-y: auto;
|
|
18
|
+
-webkit-overflow-scrolling: touch;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import './index.css';
|
|
4
|
+
import App from './App';
|
|
5
|
+
import reportWebVitals from './reportWebVitals';
|
|
6
|
+
|
|
7
|
+
const root = ReactDOM.createRoot(
|
|
8
|
+
document.getElementById('root') as HTMLElement
|
|
9
|
+
);
|
|
10
|
+
root.render(
|
|
11
|
+
<React.StrictMode>
|
|
12
|
+
<App />
|
|
13
|
+
</React.StrictMode>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// If you want to start measuring performance in your app, pass a function
|
|
17
|
+
// to log results (for example: reportWebVitals(console.log))
|
|
18
|
+
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
19
|
+
reportWebVitals();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="react-scripts" />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ReportHandler } from 'web-vitals';
|
|
2
|
+
|
|
3
|
+
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
4
|
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
5
|
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
6
|
+
getCLS(onPerfEntry);
|
|
7
|
+
getFID(onPerfEntry);
|
|
8
|
+
getFCP(onPerfEntry);
|
|
9
|
+
getLCP(onPerfEntry);
|
|
10
|
+
getTTFB(onPerfEntry);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default reportWebVitals;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Logger utility for centralized logging control
|
|
2
|
+
|
|
3
|
+
// Environment-based logging control
|
|
4
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
5
|
+
let isLoggingEnabled = !isProduction;
|
|
6
|
+
|
|
7
|
+
export const logger = {
|
|
8
|
+
enable: () => {
|
|
9
|
+
isLoggingEnabled = true;
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
disable: () => {
|
|
13
|
+
isLoggingEnabled = false;
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
log: (...args: any[]) => {
|
|
17
|
+
if (isLoggingEnabled) {
|
|
18
|
+
console.log(...args);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// Additional logging levels if needed
|
|
23
|
+
debug: (...args: any[]) => {
|
|
24
|
+
if (isLoggingEnabled) {
|
|
25
|
+
console.debug(...args);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
error: (...args: any[]) => {
|
|
30
|
+
// Always log errors, even in production
|
|
31
|
+
console.error(...args);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
warn: (...args: any[]) => {
|
|
35
|
+
if (isLoggingEnabled) {
|
|
36
|
+
console.warn(...args);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
info: (...args: any[]) => {
|
|
41
|
+
if (isLoggingEnabled) {
|
|
42
|
+
console.info(...args);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"module": "esnext",
|
|
17
|
+
"moduleResolution": "node",
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"noEmit": true,
|
|
21
|
+
"jsx": "react-jsx"
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"exclude": [
|
|
27
|
+
"node_modules",
|
|
28
|
+
"dist"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
server: {
|
|
7
|
+
watch: {
|
|
8
|
+
usePolling: true
|
|
9
|
+
},
|
|
10
|
+
host: true,
|
|
11
|
+
proxy: {
|
|
12
|
+
'/api': {
|
|
13
|
+
target: 'http://localhost:8081',
|
|
14
|
+
changeOrigin: true
|
|
15
|
+
},
|
|
16
|
+
'/socket.io': {
|
|
17
|
+
target: 'http://localhost:8081',
|
|
18
|
+
ws: true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {SocketServer} from './src/SocketServer';
|
|
2
|
+
import {ApiServer} from "./src/ApiServer";
|
|
3
|
+
|
|
4
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
5
|
+
const CORS_HOSTS = process.env.CORS_HOSTS ? JSON.parse(process.env.CORS_HOSTS) : 'http://localhost:5173';
|
|
6
|
+
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 8081;
|
|
7
|
+
|
|
8
|
+
if (!OPENAI_API_KEY) {
|
|
9
|
+
console.error(
|
|
10
|
+
`Environment variable "OPENAI_API_KEY" is required.\n` +
|
|
11
|
+
`Please set it in your .env file.`
|
|
12
|
+
);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const apiServer = new ApiServer(OPENAI_API_KEY, CORS_HOSTS);
|
|
17
|
+
apiServer.initServer();
|
|
18
|
+
const server = new SocketServer(OPENAI_API_KEY, CORS_HOSTS);
|
|
19
|
+
server.listen(apiServer.getServer(), PORT);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cortex-realtime-voice",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "concurrently \"cd client && bun run dev\" \"bun --watch run index.ts\"",
|
|
7
|
+
"dev:server": "bun --watch run index.ts",
|
|
8
|
+
"dev:client": "cd client && bun run dev",
|
|
9
|
+
"start": "bun run index.ts",
|
|
10
|
+
"start:test": "NODE_ENV=test bun run index.ts",
|
|
11
|
+
"start:prod": "NODE_ENV=production bun run index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@hono/node-server": "1.13.7",
|
|
15
|
+
"@paralleldrive/cuid2": "2.2.2",
|
|
16
|
+
"hono": "4.6.13",
|
|
17
|
+
"socket.io": "4.8.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/bun": "1.1.14",
|
|
21
|
+
"@types/node": "22.10.1",
|
|
22
|
+
"bun-types": "^1.1.38",
|
|
23
|
+
"concurrently": "^8.2.2"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"typescript": "5.7.2"
|
|
27
|
+
}
|
|
28
|
+
}
|