vox-ai-react 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.js +59 -96
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +59 -96
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -37,16 +37,18 @@ async function decodeAudioData(arrayBuffer, ctx, sampleRate, numChannels) {
|
|
|
37
37
|
}
|
|
38
38
|
return audioBuffer;
|
|
39
39
|
}
|
|
40
|
-
// Icons
|
|
40
|
+
// Icons
|
|
41
41
|
const MessageCircleIcon = () => (jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }) }));
|
|
42
|
-
const MicIcon = ({ size =
|
|
43
|
-
const MicOffIcon = ({ size =
|
|
44
|
-
const XIcon = () => (jsxs("svg", { width: "
|
|
45
|
-
const PhoneOffIcon = () => (jsxs("svg", { width: "
|
|
46
|
-
const VoxLogoIcon = () => (jsxs("svg", { width: "18", height: "18", viewBox: "0 0 48 48", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M28.764 25.966c0-4.489-1.936-5.99-4.764-5.99s-4.764 1.501-4.764 5.99m-.55 0h.55v5.324h0h-.55a1.826 1.826 0 0 1-1.826-1.826v-1.672a1.826 1.826 0 0 1 1.826-1.826m10.628 5.324h-.55h0v-5.324h.55a1.826 1.826 0 0 1 1.826 1.826v1.672a1.826 1.826 0 0 1-1.826 1.826" }), jsx("path", { d: "M24 42.476L12.938 33.58V5.524L24 16.351" }), jsx("path", { d: "M24 42.476L9.125 35.557V7.501L24 16.351" }), jsx("path", { d: "M24 42.476L5.5 38.004V9.948L24 16.351m0 26.125l11.062-8.896V5.524L24 16.351" }), jsx("path", { d: "m24 42.476l14.875-6.919V7.501L24 16.351" }), jsx("path", { d: "m24 42.476l18.5-4.472V9.948L24 16.351" })] }));
|
|
42
|
+
const MicIcon = ({ size = 20 }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }));
|
|
43
|
+
const MicOffIcon = ({ size = 20 }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22" }), jsx("path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" }), jsx("path", { d: "M5 10v2a7 7 0 0 0 12 5" }), jsx("path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33" }), jsx("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }), jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }));
|
|
44
|
+
const XIcon = () => (jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M18 6 6 18" }), jsx("path", { d: "m6 6 12 12" })] }));
|
|
45
|
+
const PhoneOffIcon = () => (jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91" }), jsx("line", { x1: "22", x2: "2", y1: "2", y2: "22" })] }));
|
|
47
46
|
const UserIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }), jsx("circle", { cx: "12", cy: "7", r: "4" })] }));
|
|
48
|
-
const
|
|
49
|
-
const
|
|
47
|
+
const BotIcon = () => (jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M12 8V4H8" }), jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsx("path", { d: "M2 14h2" }), jsx("path", { d: "M20 14h2" }), jsx("path", { d: "M15 13v2" }), jsx("path", { d: "M9 13v2" })] }));
|
|
48
|
+
const SendIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), jsx("path", { d: "M22 2 11 13" })] }));
|
|
49
|
+
const PlusIcon = () => (jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M5 12h14" }), jsx("path", { d: "M12 5v14" })] }));
|
|
50
|
+
const Loader2Icon = () => (jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { animation: 'vox-spin 1s linear infinite' }, children: jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
|
|
51
|
+
const AlertCircleIcon = () => (jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("circle", { cx: "12", cy: "12", r: "10" }), jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }), jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })] }));
|
|
50
52
|
// Visualizer Component
|
|
51
53
|
const Visualizer = ({ analyser, isActive }) => {
|
|
52
54
|
const canvasRef = useRef(null);
|
|
@@ -100,9 +102,9 @@ const Visualizer = ({ analyser, isActive }) => {
|
|
|
100
102
|
return jsx("canvas", { ref: canvasRef, width: 300, height: 50, style: { width: '100%', height: '50px', background: 'transparent' } });
|
|
101
103
|
};
|
|
102
104
|
// Text Interface Component
|
|
103
|
-
const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
|
|
105
|
+
const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction, logoUrl }) => {
|
|
104
106
|
const [messages, setMessages] = useState([
|
|
105
|
-
{ id: 'welcome', role: 'assistant', text: `
|
|
107
|
+
{ id: 'welcome', role: 'assistant', text: `Hello! I'm ${agentName}. How can I help you today?` }
|
|
106
108
|
]);
|
|
107
109
|
const [input, setInput] = useState('');
|
|
108
110
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -112,7 +114,6 @@ const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
|
|
|
112
114
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
113
115
|
}, [messages]);
|
|
114
116
|
const handleSend = async (e) => {
|
|
115
|
-
var _a;
|
|
116
117
|
e === null || e === void 0 ? void 0 : e.preventDefault();
|
|
117
118
|
if (!input.trim() || isLoading)
|
|
118
119
|
return;
|
|
@@ -133,36 +134,36 @@ const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
|
|
|
133
134
|
throw new Error(errorData.error || 'API request failed');
|
|
134
135
|
}
|
|
135
136
|
const data = await response.json();
|
|
136
|
-
const botMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text:
|
|
137
|
+
const botMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: data.response || 'Error: No response' };
|
|
137
138
|
setMessages(prev => [...prev, botMsg]);
|
|
138
139
|
}
|
|
139
140
|
catch (err) {
|
|
140
141
|
console.error('VOX Chat Error:', err);
|
|
141
|
-
const errorMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: `
|
|
142
|
+
const errorMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: `Error: ${err.message || 'Connection lost. Please try again.'}` };
|
|
142
143
|
setMessages(prev => [...prev, errorMsg]);
|
|
143
144
|
}
|
|
144
145
|
finally {
|
|
145
146
|
setIsLoading(false);
|
|
146
147
|
}
|
|
147
148
|
};
|
|
148
|
-
return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: '#000000' }, children: [jsxs("div", { ref: scrollRef, style: { flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [messages.map((msg) => (jsx("div", { style: { display: 'flex', justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start' }, children: jsxs("div", { style: { display: 'flex', flexDirection: msg.role === 'user' ? 'row-reverse' : 'row', alignItems: 'flex-start', gap: '8px', maxWidth: '85%' }, children: [jsx("div", { style: {
|
|
149
|
+
return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: '#000000', fontFamily: 'Outfit, sans-serif' }, children: [jsxs("div", { ref: scrollRef, style: { flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [messages.map((msg) => (jsx("div", { style: { display: 'flex', justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start' }, children: jsxs("div", { style: { display: 'flex', flexDirection: msg.role === 'user' ? 'row-reverse' : 'row', alignItems: 'flex-start', gap: '8px', maxWidth: '85%' }, children: [jsx("div", { style: {
|
|
149
150
|
flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
150
151
|
border: msg.role === 'user' ? '1px solid #ffffff' : 'none',
|
|
151
152
|
background: msg.role === 'user' ? '#ffffff' : 'transparent',
|
|
152
153
|
color: msg.role === 'user' ? '#000000' : '#ffffff'
|
|
153
|
-
}, children: msg.role === 'user' ? jsx(UserIcon, {}) : jsx(
|
|
154
|
-
padding: '8px 12px', fontSize: '
|
|
155
|
-
border: '1px solid
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
154
|
+
}, children: msg.role === 'user' ? jsx(UserIcon, {}) : (logoUrl ? jsx("img", { src: logoUrl, alt: agentName, style: { width: '24px', height: '24px', objectFit: 'contain' } }) : jsx(BotIcon, {})) }), jsx("div", { style: {
|
|
155
|
+
padding: '8px 12px', fontSize: '14px', fontWeight: 500, lineHeight: 1.6,
|
|
156
|
+
border: '1px solid', borderRadius: '8px',
|
|
157
|
+
borderColor: msg.role === 'user' ? '#52525b' : '#3f3f46',
|
|
158
|
+
background: msg.role === 'user' ? '#27272a' : '#000000',
|
|
159
|
+
color: msg.role === 'user' ? '#ffffff' : '#d4d4d8'
|
|
160
|
+
}, children: msg.text })] }) }, msg.id))), isLoading && (jsx("div", { style: { display: 'flex', justifyContent: 'flex-start' }, children: jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', gap: '8px' }, children: [jsx("div", { style: { flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ffffff', opacity: 0.2 }, children: logoUrl ? jsx("img", { src: logoUrl, alt: agentName, style: { width: '24px', height: '24px', objectFit: 'contain' } }) : jsx(BotIcon, {}) }), jsx("div", { style: { padding: '12px 16px', border: '1px solid #3f3f46', background: '#000000', borderRadius: '8px' }, children: jsx(Loader2Icon, {}) })] }) }))] }), jsx("div", { style: { padding: '16px' }, children: jsx("form", { onSubmit: handleSend, children: jsxs("div", { style: {
|
|
161
|
+
width: '100%', cursor: 'text', overflow: 'clip', padding: '6px', boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1)', border: '1px solid #3f3f46', borderRadius: '9999px',
|
|
162
|
+
display: 'grid', gridTemplateColumns: 'auto 1fr auto', gridTemplateRows: 'auto', gap: '4px', alignItems: 'center', background: 'rgba(0,0,0,0.5)'
|
|
163
|
+
}, children: [jsx("button", { type: "button", style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', borderRadius: '9999px', outline: 'none', border: 'none', background: 'transparent', cursor: 'pointer', color: '#71717a' }, children: jsx(PlusIcon, {}) }), jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter' && !e.shiftKey) {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
handleSend();
|
|
166
|
+
} }, placeholder: "Type your message...", disabled: isLoading, style: { width: '100%', minHeight: '32px', resize: 'none', border: 0, padding: 0, fontSize: '14px', outline: 'none', background: 'transparent', color: '#ffffff' } }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '4px' }, children: [jsx("button", { type: "button", style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', borderRadius: '9999px', outline: 'none', border: 'none', background: 'transparent', cursor: 'pointer', color: '#71717a' }, children: jsx(MicIcon, { size: 16 }) }), input.trim() && (jsx("button", { type: "submit", disabled: isLoading, style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', borderRadius: '9999px', border: 'none', background: '#ffffff', color: '#000000', cursor: 'pointer' }, children: jsx(SendIcon, {}) }))] })] }) }) })] }));
|
|
166
167
|
};
|
|
167
168
|
// Voice Interface Component
|
|
168
169
|
const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall }) => {
|
|
@@ -170,30 +171,20 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
170
171
|
const [errorMsg, setErrorMsg] = useState('');
|
|
171
172
|
const [isMuted, setIsMuted] = useState(false);
|
|
172
173
|
const [callDuration, setCallDuration] = useState(0);
|
|
173
|
-
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
174
|
-
const [speakingIntensity, setSpeakingIntensity] = useState(0);
|
|
175
174
|
const callStartTimeRef = useRef(null);
|
|
176
175
|
const durationIntervalRef = useRef(null);
|
|
177
176
|
const inputAudioContextRef = useRef(null);
|
|
178
177
|
const outputAudioContextRef = useRef(null);
|
|
179
178
|
const inputAnalyserRef = useRef(null);
|
|
180
|
-
const outputAnalyserRef = useRef(null);
|
|
181
179
|
const outputGainRef = useRef(null);
|
|
182
180
|
const nextStartTimeRef = useRef(0);
|
|
183
181
|
const sourcesRef = useRef(new Set());
|
|
184
|
-
const speakingAnimationRef = useRef(null);
|
|
185
182
|
const sessionRef = useRef(null);
|
|
186
183
|
const streamRef = useRef(null);
|
|
187
184
|
const scriptProcessorRef = useRef(null);
|
|
188
185
|
const sourceNodeRef = useRef(null);
|
|
189
186
|
const cleanupAudio = useCallback(() => {
|
|
190
187
|
var _a, _b, _c, _d;
|
|
191
|
-
if (speakingAnimationRef.current) {
|
|
192
|
-
cancelAnimationFrame(speakingAnimationRef.current);
|
|
193
|
-
speakingAnimationRef.current = null;
|
|
194
|
-
}
|
|
195
|
-
setIsSpeaking(false);
|
|
196
|
-
setSpeakingIntensity(0);
|
|
197
188
|
sourcesRef.current.forEach(source => { try {
|
|
198
189
|
source.stop();
|
|
199
190
|
}
|
|
@@ -224,13 +215,6 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
224
215
|
catch (e) { }
|
|
225
216
|
outputGainRef.current = null;
|
|
226
217
|
}
|
|
227
|
-
if (outputAnalyserRef.current) {
|
|
228
|
-
try {
|
|
229
|
-
outputAnalyserRef.current.disconnect();
|
|
230
|
-
}
|
|
231
|
-
catch (e) { }
|
|
232
|
-
outputAnalyserRef.current = null;
|
|
233
|
-
}
|
|
234
218
|
if (((_a = inputAudioContextRef.current) === null || _a === void 0 ? void 0 : _a.state) !== 'closed') {
|
|
235
219
|
try {
|
|
236
220
|
(_b = inputAudioContextRef.current) === null || _b === void 0 ? void 0 : _b.close();
|
|
@@ -265,34 +249,16 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
265
249
|
const analyser = inputCtx.createAnalyser();
|
|
266
250
|
analyser.fftSize = 256;
|
|
267
251
|
inputAnalyserRef.current = analyser;
|
|
268
|
-
const outputAnalyser = outputCtx.createAnalyser();
|
|
269
|
-
outputAnalyser.fftSize = 256;
|
|
270
|
-
outputAnalyserRef.current = outputAnalyser;
|
|
271
252
|
const outputGain = outputCtx.createGain();
|
|
272
253
|
outputGain.gain.value = 1;
|
|
273
254
|
outputGainRef.current = outputGain;
|
|
274
|
-
outputGain.connect(
|
|
275
|
-
outputAnalyser.connect(outputCtx.destination);
|
|
276
|
-
const monitorSpeaking = () => {
|
|
277
|
-
if (!outputAnalyserRef.current)
|
|
278
|
-
return;
|
|
279
|
-
const dataArray = new Uint8Array(outputAnalyserRef.current.frequencyBinCount);
|
|
280
|
-
outputAnalyserRef.current.getByteFrequencyData(dataArray);
|
|
281
|
-
const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
|
|
282
|
-
const intensity = Math.min(average / 128, 1);
|
|
283
|
-
setIsSpeaking(intensity > 0.05);
|
|
284
|
-
setSpeakingIntensity(intensity);
|
|
285
|
-
speakingAnimationRef.current = requestAnimationFrame(monitorSpeaking);
|
|
286
|
-
};
|
|
287
|
-
monitorSpeaking();
|
|
255
|
+
outputGain.connect(outputCtx.destination);
|
|
288
256
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
289
257
|
streamRef.current = stream;
|
|
290
258
|
const wsUrl = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${geminiApiKey}`;
|
|
291
|
-
console.log('[VOX] Connecting to Gemini Live API...');
|
|
292
259
|
const ws = new WebSocket(wsUrl);
|
|
293
260
|
sessionRef.current = ws;
|
|
294
261
|
ws.onopen = () => {
|
|
295
|
-
console.log('[VOX] WebSocket connected, sending setup...');
|
|
296
262
|
const setupMsg = {
|
|
297
263
|
setup: {
|
|
298
264
|
model: 'models/gemini-2.5-flash-native-audio-preview-09-2025',
|
|
@@ -322,9 +288,7 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
322
288
|
return;
|
|
323
289
|
}
|
|
324
290
|
}
|
|
325
|
-
console.log('[VOX] Received:', data);
|
|
326
291
|
if (data.setupComplete) {
|
|
327
|
-
console.log('[VOX] Setup complete, voice ready!');
|
|
328
292
|
setConnectionState(ConnectionState.CONNECTED);
|
|
329
293
|
const source = inputCtx.createMediaStreamSource(stream);
|
|
330
294
|
sourceNodeRef.current = source;
|
|
@@ -365,19 +329,14 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
365
329
|
nextStartTimeRef.current = 0;
|
|
366
330
|
}
|
|
367
331
|
};
|
|
368
|
-
ws.onclose = (
|
|
369
|
-
|
|
370
|
-
setConnectionState(ConnectionState.DISCONNECTED);
|
|
371
|
-
};
|
|
372
|
-
ws.onerror = (error) => {
|
|
373
|
-
console.error('[VOX] WebSocket error:', error);
|
|
332
|
+
ws.onclose = () => setConnectionState(ConnectionState.DISCONNECTED);
|
|
333
|
+
ws.onerror = () => {
|
|
374
334
|
setConnectionState(ConnectionState.ERROR);
|
|
375
335
|
setErrorMsg('Connection failed. Check your Gemini API key.');
|
|
376
336
|
cleanupAudio();
|
|
377
337
|
};
|
|
378
338
|
}
|
|
379
339
|
catch (err) {
|
|
380
|
-
console.error('[VOX] Connection error:', err);
|
|
381
340
|
setConnectionState(ConnectionState.ERROR);
|
|
382
341
|
setErrorMsg(err.message || 'Failed to access microphone or connect.');
|
|
383
342
|
cleanupAudio();
|
|
@@ -403,15 +362,24 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
403
362
|
connect();
|
|
404
363
|
return () => disconnect();
|
|
405
364
|
}, []);
|
|
406
|
-
|
|
365
|
+
const toggleMute = () => {
|
|
366
|
+
setIsMuted(!isMuted);
|
|
367
|
+
if (streamRef.current)
|
|
368
|
+
streamRef.current.getAudioTracks().forEach(track => { track.enabled = isMuted; });
|
|
369
|
+
};
|
|
370
|
+
return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '32px', height: '100%', padding: '24px', fontFamily: 'Outfit, sans-serif' }, children: [jsxs("div", { style: { position: 'relative' }, children: [jsx("div", { style: {
|
|
407
371
|
width: '112px', height: '112px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
408
372
|
border: `1px solid ${connectionState === ConnectionState.CONNECTED ? '#ffffff' : '#52525b'}`,
|
|
409
|
-
boxShadow: connectionState === ConnectionState.CONNECTED ?
|
|
410
|
-
|
|
411
|
-
}, children: connectionState === ConnectionState.CONNECTING ? (jsx("div", { style: { width: '40px', height: '40px', border: '2px solid #ffffff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'vox-spin 1s linear infinite' } })) : (jsx("div", { style: { padding: '20px', color: isMuted ? '#52525b' : '#ffffff' }, children: isMuted ? jsx(MicOffIcon, {}) : jsx(MicIcon, {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
373
|
+
boxShadow: connectionState === ConnectionState.CONNECTED ? '0 0 30px rgba(255,255,255,0.15)' : 'none',
|
|
374
|
+
transition: 'all 0.3s'
|
|
375
|
+
}, children: connectionState === ConnectionState.CONNECTING ? (jsx("div", { style: { width: '40px', height: '40px', border: '2px solid #ffffff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'vox-spin 1s linear infinite' } })) : (jsx("div", { style: { padding: '20px', color: isMuted ? '#52525b' : '#ffffff' }, children: isMuted ? jsx(MicOffIcon, {}) : jsx(MicIcon, {}) })) }), connectionState === ConnectionState.CONNECTED && (jsxs("span", { style: { position: 'absolute', bottom: '-4px', right: '-4px', display: 'flex', width: '12px', height: '12px' }, children: [jsx("span", { style: { position: 'absolute', display: 'inline-flex', width: '100%', height: '100%', borderRadius: '50%', background: '#ffffff', opacity: 0.75 } }), jsx("span", { style: { position: 'relative', display: 'inline-flex', borderRadius: '50%', width: '12px', height: '12px', background: '#ffffff' } })] }))] }), jsxs("div", { style: { width: '100%', maxWidth: '280px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [jsxs("p", { style: { textAlign: 'center', fontSize: '14px', fontWeight: 600, color: '#71717a', height: '20px' }, children: [connectionState === ConnectionState.CONNECTING && 'Initializing...', connectionState === ConnectionState.CONNECTED && (isMuted ? 'Muted' : 'Listening...'), connectionState === ConnectionState.ERROR && 'Connection error', connectionState === ConnectionState.DISCONNECTED && 'Disconnected'] }), connectionState === ConnectionState.CONNECTED && callDuration > 0 && (jsxs("p", { style: { textAlign: 'center', fontSize: '11px', fontFamily: 'monospace', color: '#ffffff' }, children: [Math.floor(callDuration / 60).toString().padStart(2, '0'), ":", (callDuration % 60).toString().padStart(2, '0')] })), jsx(Visualizer, { analyser: inputAnalyserRef.current, isActive: connectionState === ConnectionState.CONNECTED && !isMuted })] }), connectionState === ConnectionState.ERROR && (jsxs("div", { style: { display: 'flex', alignItems: 'center', fontSize: '14px', fontWeight: 600, padding: '12px 16px', maxWidth: '280px', textAlign: 'center', color: '#71717a', background: '#18181b', border: '1px solid #3f3f46', borderRadius: '8px', gap: '12px' }, children: [jsx(AlertCircleIcon, {}), jsx("span", { children: errorMsg })] })), connectionState === ConnectionState.CONNECTED && (jsxs("div", { style: { display: 'flex', gap: '16px' }, children: [jsx("button", { onClick: toggleMute, style: {
|
|
376
|
+
padding: '16px', borderRadius: '50%', border: 'none', cursor: 'pointer', transition: 'all 0.2s',
|
|
377
|
+
background: isMuted ? '#27272a' : '#ffffff',
|
|
378
|
+
color: isMuted ? '#71717a' : '#000000'
|
|
379
|
+
}, children: isMuted ? jsx(MicOffIcon, {}) : jsx(MicIcon, {}) }), jsx("button", { onClick: () => { disconnect(); onEndCall === null || onEndCall === void 0 ? void 0 : onEndCall(); }, style: {
|
|
380
|
+
padding: '16px', borderRadius: '50%', border: 'none', cursor: 'pointer', transition: 'all 0.2s',
|
|
381
|
+
background: '#dc2626', color: '#ffffff'
|
|
382
|
+
}, children: jsx(PhoneOffIcon, {}) })] }))] }));
|
|
415
383
|
};
|
|
416
384
|
// Main VoxChat Component
|
|
417
385
|
const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', agentName: propAgentName = 'VOX-01', voiceName: propVoiceName = 'Kore', systemInstruction: propSystemInstruction = 'You are a helpful AI assistant.', position = 'bottom-right', theme = 'dark', buttonColor, buttonSize = 56, borderRadius = 0, iconColor, onOpen, onClose, useServerConfig = false, configPollInterval = 10000 }) => {
|
|
@@ -419,11 +387,9 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
419
387
|
const [isOpen, setIsOpen] = useState(false);
|
|
420
388
|
const [activeTab, setActiveTab] = useState('text');
|
|
421
389
|
const [isVoiceActive, setIsVoiceActive] = useState(false);
|
|
422
|
-
// Server config state
|
|
423
390
|
const [serverConfig, setServerConfig] = useState(null);
|
|
424
391
|
const [configLoaded, setConfigLoaded] = useState(!useServerConfig);
|
|
425
392
|
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
|
426
|
-
// Fetch config from server
|
|
427
393
|
const fetchConfig = useCallback(() => {
|
|
428
394
|
if (useServerConfig && apiKey && apiUrl) {
|
|
429
395
|
fetch(`${apiUrl}/api/v1/config`, {
|
|
@@ -444,22 +410,16 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
444
410
|
});
|
|
445
411
|
}
|
|
446
412
|
}, [useServerConfig, apiKey, apiUrl]);
|
|
447
|
-
// Initial fetch and real-time polling (every 10 seconds)
|
|
448
413
|
useEffect(() => {
|
|
449
414
|
if (useServerConfig && apiKey && apiUrl) {
|
|
450
|
-
// Fetch immediately
|
|
451
415
|
fetchConfig();
|
|
452
|
-
// Poll at specified interval for real-time updates
|
|
453
416
|
const intervalId = setInterval(fetchConfig, configPollInterval);
|
|
454
|
-
// Cleanup interval on unmount
|
|
455
417
|
return () => clearInterval(intervalId);
|
|
456
418
|
}
|
|
457
419
|
}, [useServerConfig, apiKey, apiUrl, fetchConfig, configPollInterval]);
|
|
458
|
-
// Use server config if available, otherwise use props
|
|
459
420
|
const agentName = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.name) || propAgentName;
|
|
460
421
|
const voiceName = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.voiceName) || propVoiceName;
|
|
461
422
|
const systemInstruction = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.systemInstruction) || propSystemInstruction;
|
|
462
|
-
// Widget style from server or props
|
|
463
423
|
const finalButtonColor = ((_a = serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.widgetStyle) === null || _a === void 0 ? void 0 : _a.buttonColor) || buttonColor || (theme === 'light' ? '#000000' : '#ffffff');
|
|
464
424
|
const finalButtonSize = ((_b = serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.widgetStyle) === null || _b === void 0 ? void 0 : _b.buttonSize) || buttonSize || 56;
|
|
465
425
|
const finalBorderRadius = ((_c = serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.widgetStyle) === null || _c === void 0 ? void 0 : _c.borderRadius) !== undefined ? serverConfig.widgetStyle.borderRadius : (borderRadius !== undefined ? borderRadius : 50);
|
|
@@ -485,7 +445,7 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
485
445
|
const positionStyles = finalPosition === 'bottom-left'
|
|
486
446
|
? { bottom: '16px', left: '16px' }
|
|
487
447
|
: { bottom: '16px', right: '16px' };
|
|
488
|
-
return (jsxs(Fragment, { children: [jsx("style", { children: `
|
|
448
|
+
return (jsxs(Fragment, { children: [jsx("link", { href: "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;900&display=swap", rel: "stylesheet" }), jsx("style", { children: `
|
|
489
449
|
@keyframes vox-fade-in { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
490
450
|
@keyframes vox-spin { to { transform: rotate(360deg); } }
|
|
491
451
|
@keyframes vox-ping { 0% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.15); opacity: 0; } 100% { transform: scale(1); opacity: 0; } }
|
|
@@ -493,14 +453,17 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
493
453
|
` }), jsxs("div", { style: { position: 'fixed', zIndex: 10000, display: 'flex', flexDirection: 'column', alignItems: finalPosition === 'bottom-left' ? 'flex-start' : 'flex-end', gap: '24px', ...positionStyles }, children: [isOpen && (jsxs("div", { className: "vox-widget", style: {
|
|
494
454
|
width: 'min(calc(100vw - 32px), 360px)', height: '550px', maxHeight: '550px',
|
|
495
455
|
background: '#000000', overflow: 'hidden', display: 'flex', flexDirection: 'column',
|
|
496
|
-
border: '1px solid #3f3f46', boxShadow: '0 20px 40px -10px rgba(0,0,0,0.5)', borderRadius: '12px'
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
456
|
+
border: '1px solid #3f3f46', boxShadow: '0 20px 40px -10px rgba(0,0,0,0.5)', borderRadius: '12px',
|
|
457
|
+
transition: 'all 0.3s ease'
|
|
458
|
+
}, children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 16px', flexShrink: 0, borderBottom: '1px solid #3f3f46', background: '#000000', fontFamily: 'Outfit, sans-serif' }, children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '10px' }, children: [jsx("div", { style: { width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.3s ease' }, children: finalLogoUrl ? (jsx("img", { src: finalLogoUrl, alt: agentName, style: { width: '28px', height: '28px', objectFit: 'contain', transition: 'opacity 0.3s ease' } }, finalLogoUrl)) : (activeTab === 'voice' ? jsx(MicIcon, {}) : jsx(MessageCircleIcon, {})) }), jsxs("div", { children: [jsx("h2", { style: { color: '#ffffff', fontWeight: 700, fontSize: '14px', margin: 0, transition: 'all 0.3s ease' }, children: agentName }), jsxs("p", { style: { color: '#a1a1aa', fontSize: '12px', fontWeight: 500, margin: 0, display: 'flex', alignItems: 'center' }, children: [jsx("span", { style: { width: '6px', height: '6px', background: '#ffffff', borderRadius: '50%', marginRight: '6px' } }), "Active"] })] })] }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '2px', padding: '2px', borderRadius: '8px', background: '#18181b' }, children: [jsx("button", { onClick: () => handleTabChange('text'), style: {
|
|
459
|
+
padding: '6px 12px', fontSize: '12px', fontWeight: 600, borderRadius: '6px',
|
|
460
|
+
background: activeTab === 'text' ? '#27272a' : 'transparent', border: 'none', cursor: 'pointer', color: activeTab === 'text' ? '#ffffff' : '#71717a', transition: 'all 0.2s',
|
|
461
|
+
boxShadow: activeTab === 'text' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
|
|
462
|
+
}, children: "Text" }), jsx("button", { onClick: () => handleTabChange('voice'), style: {
|
|
463
|
+
padding: '6px 12px', fontSize: '12px', fontWeight: 600, borderRadius: '6px',
|
|
464
|
+
background: activeTab === 'voice' ? '#27272a' : 'transparent', border: 'none', cursor: 'pointer', color: activeTab === 'voice' ? '#ffffff' : '#71717a', transition: 'all 0.2s',
|
|
465
|
+
boxShadow: activeTab === 'voice' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
|
|
466
|
+
}, children: "Voice" })] }), jsx("button", { onClick: handleClose, style: { background: 'none', border: 'none', color: '#a1a1aa', cursor: 'pointer', padding: '4px', transition: 'color 0.2s' }, children: jsx(XIcon, {}) })] }), jsx("div", { style: { flex: 1, overflow: 'hidden', position: 'relative', background: '#000000' }, children: activeTab === 'text' ? (jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction, logoUrl: finalLogoUrl })) : (isVoiceActive ? (jsx(VoiceInterface, { geminiApiKey: geminiApiKey, voiceName: voiceName, systemInstruction: systemInstruction, onEndCall: handleEndCall })) : (jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction, logoUrl: finalLogoUrl }))) })] })), !isOpen && (!useServerConfig || configLoaded) && (jsx("button", { onClick: handleOpen, style: {
|
|
504
467
|
width: `${finalButtonSize}px`, height: `${finalButtonSize}px`,
|
|
505
468
|
borderRadius: `${finalBorderRadius}px`,
|
|
506
469
|
background: finalButtonColor, color: finalIconColor, border: 'none', cursor: 'pointer',
|