vox-ai-react 1.3.4 → 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 +61 -97
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +61 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,16 +41,18 @@ async function decodeAudioData(arrayBuffer, ctx, sampleRate, numChannels) {
|
|
|
41
41
|
}
|
|
42
42
|
return audioBuffer;
|
|
43
43
|
}
|
|
44
|
-
// Icons
|
|
45
|
-
const MessageCircleIcon = () => (jsxRuntime.jsx("svg", { width: "
|
|
46
|
-
const MicIcon = ({ size =
|
|
47
|
-
const MicOffIcon = ({ size =
|
|
48
|
-
const XIcon = () => (jsxRuntime.jsxs("svg", { width: "
|
|
49
|
-
const PhoneOffIcon = () => (jsxRuntime.jsxs("svg", { width: "
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
44
|
+
// Icons
|
|
45
|
+
const MessageCircleIcon = () => (jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntime.jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }) }));
|
|
46
|
+
const MicIcon = ({ size = 20 }) => (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), jsxRuntime.jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), jsxRuntime.jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }));
|
|
47
|
+
const MicOffIcon = ({ size = 20 }) => (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22" }), jsxRuntime.jsx("path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" }), jsxRuntime.jsx("path", { d: "M5 10v2a7 7 0 0 0 12 5" }), jsxRuntime.jsx("path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33" }), jsxRuntime.jsx("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }), jsxRuntime.jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }));
|
|
48
|
+
const XIcon = () => (jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M18 6 6 18" }), jsxRuntime.jsx("path", { d: "m6 6 12 12" })] }));
|
|
49
|
+
const PhoneOffIcon = () => (jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.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" }), jsxRuntime.jsx("line", { x1: "22", x2: "2", y1: "2", y2: "22" })] }));
|
|
50
|
+
const UserIcon = () => (jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }), jsxRuntime.jsx("circle", { cx: "12", cy: "7", r: "4" })] }));
|
|
51
|
+
const BotIcon = () => (jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M12 8V4H8" }), jsxRuntime.jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsxRuntime.jsx("path", { d: "M2 14h2" }), jsxRuntime.jsx("path", { d: "M20 14h2" }), jsxRuntime.jsx("path", { d: "M15 13v2" }), jsxRuntime.jsx("path", { d: "M9 13v2" })] }));
|
|
52
|
+
const SendIcon = () => (jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), jsxRuntime.jsx("path", { d: "M22 2 11 13" })] }));
|
|
53
|
+
const PlusIcon = () => (jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M5 12h14" }), jsxRuntime.jsx("path", { d: "M12 5v14" })] }));
|
|
54
|
+
const Loader2Icon = () => (jsxRuntime.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: jsxRuntime.jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
|
|
55
|
+
const AlertCircleIcon = () => (jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }), jsxRuntime.jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }), jsxRuntime.jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })] }));
|
|
54
56
|
// Visualizer Component
|
|
55
57
|
const Visualizer = ({ analyser, isActive }) => {
|
|
56
58
|
const canvasRef = react.useRef(null);
|
|
@@ -104,9 +106,9 @@ const Visualizer = ({ analyser, isActive }) => {
|
|
|
104
106
|
return jsxRuntime.jsx("canvas", { ref: canvasRef, width: 300, height: 50, style: { width: '100%', height: '50px', background: 'transparent' } });
|
|
105
107
|
};
|
|
106
108
|
// Text Interface Component
|
|
107
|
-
const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
|
|
109
|
+
const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction, logoUrl }) => {
|
|
108
110
|
const [messages, setMessages] = react.useState([
|
|
109
|
-
{ id: 'welcome', role: 'assistant', text: `
|
|
111
|
+
{ id: 'welcome', role: 'assistant', text: `Hello! I'm ${agentName}. How can I help you today?` }
|
|
110
112
|
]);
|
|
111
113
|
const [input, setInput] = react.useState('');
|
|
112
114
|
const [isLoading, setIsLoading] = react.useState(false);
|
|
@@ -116,7 +118,6 @@ const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
|
|
|
116
118
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
117
119
|
}, [messages]);
|
|
118
120
|
const handleSend = async (e) => {
|
|
119
|
-
var _a;
|
|
120
121
|
e === null || e === void 0 ? void 0 : e.preventDefault();
|
|
121
122
|
if (!input.trim() || isLoading)
|
|
122
123
|
return;
|
|
@@ -137,35 +138,36 @@ const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
|
|
|
137
138
|
throw new Error(errorData.error || 'API request failed');
|
|
138
139
|
}
|
|
139
140
|
const data = await response.json();
|
|
140
|
-
const botMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text:
|
|
141
|
+
const botMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: data.response || 'Error: No response' };
|
|
141
142
|
setMessages(prev => [...prev, botMsg]);
|
|
142
143
|
}
|
|
143
144
|
catch (err) {
|
|
144
145
|
console.error('VOX Chat Error:', err);
|
|
145
|
-
const errorMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: `
|
|
146
|
+
const errorMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: `Error: ${err.message || 'Connection lost. Please try again.'}` };
|
|
146
147
|
setMessages(prev => [...prev, errorMsg]);
|
|
147
148
|
}
|
|
148
149
|
finally {
|
|
149
150
|
setIsLoading(false);
|
|
150
151
|
}
|
|
151
152
|
};
|
|
152
|
-
return (jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: '#000000' }, children: [jsxRuntime.jsxs("div", { ref: scrollRef, style: { flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [messages.map((msg) => (jsxRuntime.jsx("div", { style: { display: 'flex', justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start' }, children: jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: msg.role === 'user' ? 'row-reverse' : 'row', alignItems: 'flex-start', gap: '8px', maxWidth: '85%' }, children: [jsxRuntime.jsx("div", { style: {
|
|
153
|
+
return (jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: '#000000', fontFamily: 'Outfit, sans-serif' }, children: [jsxRuntime.jsxs("div", { ref: scrollRef, style: { flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [messages.map((msg) => (jsxRuntime.jsx("div", { style: { display: 'flex', justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start' }, children: jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: msg.role === 'user' ? 'row-reverse' : 'row', alignItems: 'flex-start', gap: '8px', maxWidth: '85%' }, children: [jsxRuntime.jsx("div", { style: {
|
|
153
154
|
flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
154
|
-
border: msg.role === 'user' ? '1px solid #ffffff' : '
|
|
155
|
+
border: msg.role === 'user' ? '1px solid #ffffff' : 'none',
|
|
155
156
|
background: msg.role === 'user' ? '#ffffff' : 'transparent',
|
|
156
157
|
color: msg.role === 'user' ? '#000000' : '#ffffff'
|
|
157
|
-
}, children: msg.role === 'user' ? jsxRuntime.jsx(UserIcon, {}) : jsxRuntime.jsx(
|
|
158
|
-
padding: '8px 12px', fontSize: '
|
|
159
|
-
border: '1px solid
|
|
158
|
+
}, children: msg.role === 'user' ? jsxRuntime.jsx(UserIcon, {}) : (logoUrl ? jsxRuntime.jsx("img", { src: logoUrl, alt: agentName, style: { width: '24px', height: '24px', objectFit: 'contain' } }) : jsxRuntime.jsx(BotIcon, {})) }), jsxRuntime.jsx("div", { style: {
|
|
159
|
+
padding: '8px 12px', fontSize: '14px', fontWeight: 500, lineHeight: 1.6,
|
|
160
|
+
border: '1px solid', borderRadius: '8px',
|
|
161
|
+
borderColor: msg.role === 'user' ? '#52525b' : '#3f3f46',
|
|
162
|
+
background: msg.role === 'user' ? '#27272a' : '#000000',
|
|
160
163
|
color: msg.role === 'user' ? '#ffffff' : '#d4d4d8'
|
|
161
|
-
}, children: msg.text })] }) }, msg.id))), isLoading && (jsxRuntime.jsx("div", { style: { display: 'flex', justifyContent: 'flex-start' }, children: jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', gap: '8px' }, children: [jsxRuntime.jsx("div", { style: { flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}, children: "EXECUTE" })] }) })] }));
|
|
164
|
+
}, children: msg.text })] }) }, msg.id))), isLoading && (jsxRuntime.jsx("div", { style: { display: 'flex', justifyContent: 'flex-start' }, children: jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', gap: '8px' }, children: [jsxRuntime.jsx("div", { style: { flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ffffff', opacity: 0.2 }, children: logoUrl ? jsxRuntime.jsx("img", { src: logoUrl, alt: agentName, style: { width: '24px', height: '24px', objectFit: 'contain' } }) : jsxRuntime.jsx(BotIcon, {}) }), jsxRuntime.jsx("div", { style: { padding: '12px 16px', border: '1px solid #3f3f46', background: '#000000', borderRadius: '8px' }, children: jsxRuntime.jsx(Loader2Icon, {}) })] }) }))] }), jsxRuntime.jsx("div", { style: { padding: '16px' }, children: jsxRuntime.jsx("form", { onSubmit: handleSend, children: jsxRuntime.jsxs("div", { style: {
|
|
165
|
+
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',
|
|
166
|
+
display: 'grid', gridTemplateColumns: 'auto 1fr auto', gridTemplateRows: 'auto', gap: '4px', alignItems: 'center', background: 'rgba(0,0,0,0.5)'
|
|
167
|
+
}, children: [jsxRuntime.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: jsxRuntime.jsx(PlusIcon, {}) }), jsxRuntime.jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter' && !e.shiftKey) {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
handleSend();
|
|
170
|
+
} }, 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' } }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '4px' }, children: [jsxRuntime.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: jsxRuntime.jsx(MicIcon, { size: 16 }) }), input.trim() && (jsxRuntime.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: jsxRuntime.jsx(SendIcon, {}) }))] })] }) }) })] }));
|
|
169
171
|
};
|
|
170
172
|
// Voice Interface Component
|
|
171
173
|
const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall }) => {
|
|
@@ -173,30 +175,20 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
173
175
|
const [errorMsg, setErrorMsg] = react.useState('');
|
|
174
176
|
const [isMuted, setIsMuted] = react.useState(false);
|
|
175
177
|
const [callDuration, setCallDuration] = react.useState(0);
|
|
176
|
-
const [isSpeaking, setIsSpeaking] = react.useState(false);
|
|
177
|
-
const [speakingIntensity, setSpeakingIntensity] = react.useState(0);
|
|
178
178
|
const callStartTimeRef = react.useRef(null);
|
|
179
179
|
const durationIntervalRef = react.useRef(null);
|
|
180
180
|
const inputAudioContextRef = react.useRef(null);
|
|
181
181
|
const outputAudioContextRef = react.useRef(null);
|
|
182
182
|
const inputAnalyserRef = react.useRef(null);
|
|
183
|
-
const outputAnalyserRef = react.useRef(null);
|
|
184
183
|
const outputGainRef = react.useRef(null);
|
|
185
184
|
const nextStartTimeRef = react.useRef(0);
|
|
186
185
|
const sourcesRef = react.useRef(new Set());
|
|
187
|
-
const speakingAnimationRef = react.useRef(null);
|
|
188
186
|
const sessionRef = react.useRef(null);
|
|
189
187
|
const streamRef = react.useRef(null);
|
|
190
188
|
const scriptProcessorRef = react.useRef(null);
|
|
191
189
|
const sourceNodeRef = react.useRef(null);
|
|
192
190
|
const cleanupAudio = react.useCallback(() => {
|
|
193
191
|
var _a, _b, _c, _d;
|
|
194
|
-
if (speakingAnimationRef.current) {
|
|
195
|
-
cancelAnimationFrame(speakingAnimationRef.current);
|
|
196
|
-
speakingAnimationRef.current = null;
|
|
197
|
-
}
|
|
198
|
-
setIsSpeaking(false);
|
|
199
|
-
setSpeakingIntensity(0);
|
|
200
192
|
sourcesRef.current.forEach(source => { try {
|
|
201
193
|
source.stop();
|
|
202
194
|
}
|
|
@@ -227,13 +219,6 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
227
219
|
catch (e) { }
|
|
228
220
|
outputGainRef.current = null;
|
|
229
221
|
}
|
|
230
|
-
if (outputAnalyserRef.current) {
|
|
231
|
-
try {
|
|
232
|
-
outputAnalyserRef.current.disconnect();
|
|
233
|
-
}
|
|
234
|
-
catch (e) { }
|
|
235
|
-
outputAnalyserRef.current = null;
|
|
236
|
-
}
|
|
237
222
|
if (((_a = inputAudioContextRef.current) === null || _a === void 0 ? void 0 : _a.state) !== 'closed') {
|
|
238
223
|
try {
|
|
239
224
|
(_b = inputAudioContextRef.current) === null || _b === void 0 ? void 0 : _b.close();
|
|
@@ -268,34 +253,16 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
268
253
|
const analyser = inputCtx.createAnalyser();
|
|
269
254
|
analyser.fftSize = 256;
|
|
270
255
|
inputAnalyserRef.current = analyser;
|
|
271
|
-
const outputAnalyser = outputCtx.createAnalyser();
|
|
272
|
-
outputAnalyser.fftSize = 256;
|
|
273
|
-
outputAnalyserRef.current = outputAnalyser;
|
|
274
256
|
const outputGain = outputCtx.createGain();
|
|
275
257
|
outputGain.gain.value = 1;
|
|
276
258
|
outputGainRef.current = outputGain;
|
|
277
|
-
outputGain.connect(
|
|
278
|
-
outputAnalyser.connect(outputCtx.destination);
|
|
279
|
-
const monitorSpeaking = () => {
|
|
280
|
-
if (!outputAnalyserRef.current)
|
|
281
|
-
return;
|
|
282
|
-
const dataArray = new Uint8Array(outputAnalyserRef.current.frequencyBinCount);
|
|
283
|
-
outputAnalyserRef.current.getByteFrequencyData(dataArray);
|
|
284
|
-
const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
|
|
285
|
-
const intensity = Math.min(average / 128, 1);
|
|
286
|
-
setIsSpeaking(intensity > 0.05);
|
|
287
|
-
setSpeakingIntensity(intensity);
|
|
288
|
-
speakingAnimationRef.current = requestAnimationFrame(monitorSpeaking);
|
|
289
|
-
};
|
|
290
|
-
monitorSpeaking();
|
|
259
|
+
outputGain.connect(outputCtx.destination);
|
|
291
260
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
292
261
|
streamRef.current = stream;
|
|
293
262
|
const wsUrl = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${geminiApiKey}`;
|
|
294
|
-
console.log('[VOX] Connecting to Gemini Live API...');
|
|
295
263
|
const ws = new WebSocket(wsUrl);
|
|
296
264
|
sessionRef.current = ws;
|
|
297
265
|
ws.onopen = () => {
|
|
298
|
-
console.log('[VOX] WebSocket connected, sending setup...');
|
|
299
266
|
const setupMsg = {
|
|
300
267
|
setup: {
|
|
301
268
|
model: 'models/gemini-2.5-flash-native-audio-preview-09-2025',
|
|
@@ -325,9 +292,7 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
325
292
|
return;
|
|
326
293
|
}
|
|
327
294
|
}
|
|
328
|
-
console.log('[VOX] Received:', data);
|
|
329
295
|
if (data.setupComplete) {
|
|
330
|
-
console.log('[VOX] Setup complete, voice ready!');
|
|
331
296
|
setConnectionState(ConnectionState.CONNECTED);
|
|
332
297
|
const source = inputCtx.createMediaStreamSource(stream);
|
|
333
298
|
sourceNodeRef.current = source;
|
|
@@ -368,19 +333,14 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
368
333
|
nextStartTimeRef.current = 0;
|
|
369
334
|
}
|
|
370
335
|
};
|
|
371
|
-
ws.onclose = (
|
|
372
|
-
|
|
373
|
-
setConnectionState(ConnectionState.DISCONNECTED);
|
|
374
|
-
};
|
|
375
|
-
ws.onerror = (error) => {
|
|
376
|
-
console.error('[VOX] WebSocket error:', error);
|
|
336
|
+
ws.onclose = () => setConnectionState(ConnectionState.DISCONNECTED);
|
|
337
|
+
ws.onerror = () => {
|
|
377
338
|
setConnectionState(ConnectionState.ERROR);
|
|
378
339
|
setErrorMsg('Connection failed. Check your Gemini API key.');
|
|
379
340
|
cleanupAudio();
|
|
380
341
|
};
|
|
381
342
|
}
|
|
382
343
|
catch (err) {
|
|
383
|
-
console.error('[VOX] Connection error:', err);
|
|
384
344
|
setConnectionState(ConnectionState.ERROR);
|
|
385
345
|
setErrorMsg(err.message || 'Failed to access microphone or connect.');
|
|
386
346
|
cleanupAudio();
|
|
@@ -406,15 +366,24 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
406
366
|
connect();
|
|
407
367
|
return () => disconnect();
|
|
408
368
|
}, []);
|
|
409
|
-
|
|
369
|
+
const toggleMute = () => {
|
|
370
|
+
setIsMuted(!isMuted);
|
|
371
|
+
if (streamRef.current)
|
|
372
|
+
streamRef.current.getAudioTracks().forEach(track => { track.enabled = isMuted; });
|
|
373
|
+
};
|
|
374
|
+
return (jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '32px', height: '100%', padding: '24px', fontFamily: 'Outfit, sans-serif' }, children: [jsxRuntime.jsxs("div", { style: { position: 'relative' }, children: [jsxRuntime.jsx("div", { style: {
|
|
410
375
|
width: '112px', height: '112px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
411
376
|
border: `1px solid ${connectionState === ConnectionState.CONNECTED ? '#ffffff' : '#52525b'}`,
|
|
412
|
-
boxShadow: connectionState === ConnectionState.CONNECTED ?
|
|
413
|
-
|
|
414
|
-
}, children: connectionState === ConnectionState.CONNECTING ? (jsxRuntime.jsx("div", { style: { width: '40px', height: '40px', border: '2px solid #ffffff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'vox-spin 1s linear infinite' } })) : (jsxRuntime.jsx("div", { style: { padding: '20px', color: isMuted ? '#52525b' : '#ffffff' }, children: isMuted ? jsxRuntime.jsx(MicOffIcon, {}) : jsxRuntime.jsx(MicIcon, {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
377
|
+
boxShadow: connectionState === ConnectionState.CONNECTED ? '0 0 30px rgba(255,255,255,0.15)' : 'none',
|
|
378
|
+
transition: 'all 0.3s'
|
|
379
|
+
}, children: connectionState === ConnectionState.CONNECTING ? (jsxRuntime.jsx("div", { style: { width: '40px', height: '40px', border: '2px solid #ffffff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'vox-spin 1s linear infinite' } })) : (jsxRuntime.jsx("div", { style: { padding: '20px', color: isMuted ? '#52525b' : '#ffffff' }, children: isMuted ? jsxRuntime.jsx(MicOffIcon, {}) : jsxRuntime.jsx(MicIcon, {}) })) }), connectionState === ConnectionState.CONNECTED && (jsxRuntime.jsxs("span", { style: { position: 'absolute', bottom: '-4px', right: '-4px', display: 'flex', width: '12px', height: '12px' }, children: [jsxRuntime.jsx("span", { style: { position: 'absolute', display: 'inline-flex', width: '100%', height: '100%', borderRadius: '50%', background: '#ffffff', opacity: 0.75 } }), jsxRuntime.jsx("span", { style: { position: 'relative', display: 'inline-flex', borderRadius: '50%', width: '12px', height: '12px', background: '#ffffff' } })] }))] }), jsxRuntime.jsxs("div", { style: { width: '100%', maxWidth: '280px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [jsxRuntime.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 && (jsxRuntime.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')] })), jsxRuntime.jsx(Visualizer, { analyser: inputAnalyserRef.current, isActive: connectionState === ConnectionState.CONNECTED && !isMuted })] }), connectionState === ConnectionState.ERROR && (jsxRuntime.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: [jsxRuntime.jsx(AlertCircleIcon, {}), jsxRuntime.jsx("span", { children: errorMsg })] })), connectionState === ConnectionState.CONNECTED && (jsxRuntime.jsxs("div", { style: { display: 'flex', gap: '16px' }, children: [jsxRuntime.jsx("button", { onClick: toggleMute, style: {
|
|
380
|
+
padding: '16px', borderRadius: '50%', border: 'none', cursor: 'pointer', transition: 'all 0.2s',
|
|
381
|
+
background: isMuted ? '#27272a' : '#ffffff',
|
|
382
|
+
color: isMuted ? '#71717a' : '#000000'
|
|
383
|
+
}, children: isMuted ? jsxRuntime.jsx(MicOffIcon, {}) : jsxRuntime.jsx(MicIcon, {}) }), jsxRuntime.jsx("button", { onClick: () => { disconnect(); onEndCall === null || onEndCall === void 0 ? void 0 : onEndCall(); }, style: {
|
|
384
|
+
padding: '16px', borderRadius: '50%', border: 'none', cursor: 'pointer', transition: 'all 0.2s',
|
|
385
|
+
background: '#dc2626', color: '#ffffff'
|
|
386
|
+
}, children: jsxRuntime.jsx(PhoneOffIcon, {}) })] }))] }));
|
|
418
387
|
};
|
|
419
388
|
// Main VoxChat Component
|
|
420
389
|
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 }) => {
|
|
@@ -422,11 +391,9 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
422
391
|
const [isOpen, setIsOpen] = react.useState(false);
|
|
423
392
|
const [activeTab, setActiveTab] = react.useState('text');
|
|
424
393
|
const [isVoiceActive, setIsVoiceActive] = react.useState(false);
|
|
425
|
-
// Server config state
|
|
426
394
|
const [serverConfig, setServerConfig] = react.useState(null);
|
|
427
395
|
const [configLoaded, setConfigLoaded] = react.useState(!useServerConfig);
|
|
428
396
|
const [isInitialLoad, setIsInitialLoad] = react.useState(true);
|
|
429
|
-
// Fetch config from server
|
|
430
397
|
const fetchConfig = react.useCallback(() => {
|
|
431
398
|
if (useServerConfig && apiKey && apiUrl) {
|
|
432
399
|
fetch(`${apiUrl}/api/v1/config`, {
|
|
@@ -447,22 +414,16 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
447
414
|
});
|
|
448
415
|
}
|
|
449
416
|
}, [useServerConfig, apiKey, apiUrl]);
|
|
450
|
-
// Initial fetch and real-time polling (every 10 seconds)
|
|
451
417
|
react.useEffect(() => {
|
|
452
418
|
if (useServerConfig && apiKey && apiUrl) {
|
|
453
|
-
// Fetch immediately
|
|
454
419
|
fetchConfig();
|
|
455
|
-
// Poll at specified interval for real-time updates
|
|
456
420
|
const intervalId = setInterval(fetchConfig, configPollInterval);
|
|
457
|
-
// Cleanup interval on unmount
|
|
458
421
|
return () => clearInterval(intervalId);
|
|
459
422
|
}
|
|
460
423
|
}, [useServerConfig, apiKey, apiUrl, fetchConfig, configPollInterval]);
|
|
461
|
-
// Use server config if available, otherwise use props
|
|
462
424
|
const agentName = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.name) || propAgentName;
|
|
463
425
|
const voiceName = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.voiceName) || propVoiceName;
|
|
464
426
|
const systemInstruction = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.systemInstruction) || propSystemInstruction;
|
|
465
|
-
// Widget style from server or props
|
|
466
427
|
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');
|
|
467
428
|
const finalButtonSize = ((_b = serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.widgetStyle) === null || _b === void 0 ? void 0 : _b.buttonSize) || buttonSize || 56;
|
|
468
429
|
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);
|
|
@@ -488,7 +449,7 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
488
449
|
const positionStyles = finalPosition === 'bottom-left'
|
|
489
450
|
? { bottom: '16px', left: '16px' }
|
|
490
451
|
: { bottom: '16px', right: '16px' };
|
|
491
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("style", { children: `
|
|
452
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("link", { href: "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;900&display=swap", rel: "stylesheet" }), jsxRuntime.jsx("style", { children: `
|
|
492
453
|
@keyframes vox-fade-in { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
493
454
|
@keyframes vox-spin { to { transform: rotate(360deg); } }
|
|
494
455
|
@keyframes vox-ping { 0% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.15); opacity: 0; } 100% { transform: scale(1); opacity: 0; } }
|
|
@@ -496,14 +457,17 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
496
457
|
` }), jsxRuntime.jsxs("div", { style: { position: 'fixed', zIndex: 10000, display: 'flex', flexDirection: 'column', alignItems: finalPosition === 'bottom-left' ? 'flex-start' : 'flex-end', gap: '24px', ...positionStyles }, children: [isOpen && (jsxRuntime.jsxs("div", { className: "vox-widget", style: {
|
|
497
458
|
width: 'min(calc(100vw - 32px), 360px)', height: '550px', maxHeight: '550px',
|
|
498
459
|
background: '#000000', overflow: 'hidden', display: 'flex', flexDirection: 'column',
|
|
499
|
-
border: '1px solid #3f3f46', boxShadow: '0 20px 40px -10px rgba(0,0,0,0.5)', borderRadius: '12px'
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
460
|
+
border: '1px solid #3f3f46', boxShadow: '0 20px 40px -10px rgba(0,0,0,0.5)', borderRadius: '12px',
|
|
461
|
+
transition: 'all 0.3s ease'
|
|
462
|
+
}, children: [jsxRuntime.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: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '10px' }, children: [jsxRuntime.jsx("div", { style: { width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.3s ease' }, children: finalLogoUrl ? (jsxRuntime.jsx("img", { src: finalLogoUrl, alt: agentName, style: { width: '28px', height: '28px', objectFit: 'contain', transition: 'opacity 0.3s ease' } }, finalLogoUrl)) : (activeTab === 'voice' ? jsxRuntime.jsx(MicIcon, {}) : jsxRuntime.jsx(MessageCircleIcon, {})) }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("h2", { style: { color: '#ffffff', fontWeight: 700, fontSize: '14px', margin: 0, transition: 'all 0.3s ease' }, children: agentName }), jsxRuntime.jsxs("p", { style: { color: '#a1a1aa', fontSize: '12px', fontWeight: 500, margin: 0, display: 'flex', alignItems: 'center' }, children: [jsxRuntime.jsx("span", { style: { width: '6px', height: '6px', background: '#ffffff', borderRadius: '50%', marginRight: '6px' } }), "Active"] })] })] }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '2px', padding: '2px', borderRadius: '8px', background: '#18181b' }, children: [jsxRuntime.jsx("button", { onClick: () => handleTabChange('text'), style: {
|
|
463
|
+
padding: '6px 12px', fontSize: '12px', fontWeight: 600, borderRadius: '6px',
|
|
464
|
+
background: activeTab === 'text' ? '#27272a' : 'transparent', border: 'none', cursor: 'pointer', color: activeTab === 'text' ? '#ffffff' : '#71717a', transition: 'all 0.2s',
|
|
465
|
+
boxShadow: activeTab === 'text' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
|
|
466
|
+
}, children: "Text" }), jsxRuntime.jsx("button", { onClick: () => handleTabChange('voice'), style: {
|
|
467
|
+
padding: '6px 12px', fontSize: '12px', fontWeight: 600, borderRadius: '6px',
|
|
468
|
+
background: activeTab === 'voice' ? '#27272a' : 'transparent', border: 'none', cursor: 'pointer', color: activeTab === 'voice' ? '#ffffff' : '#71717a', transition: 'all 0.2s',
|
|
469
|
+
boxShadow: activeTab === 'voice' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
|
|
470
|
+
}, children: "Voice" })] }), jsxRuntime.jsx("button", { onClick: handleClose, style: { background: 'none', border: 'none', color: '#a1a1aa', cursor: 'pointer', padding: '4px', transition: 'color 0.2s' }, children: jsxRuntime.jsx(XIcon, {}) })] }), jsxRuntime.jsx("div", { style: { flex: 1, overflow: 'hidden', position: 'relative', background: '#000000' }, children: activeTab === 'text' ? (jsxRuntime.jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction, logoUrl: finalLogoUrl })) : (isVoiceActive ? (jsxRuntime.jsx(VoiceInterface, { geminiApiKey: geminiApiKey, voiceName: voiceName, systemInstruction: systemInstruction, onEndCall: handleEndCall })) : (jsxRuntime.jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction, logoUrl: finalLogoUrl }))) })] })), !isOpen && (!useServerConfig || configLoaded) && (jsxRuntime.jsx("button", { onClick: handleOpen, style: {
|
|
507
471
|
width: `${finalButtonSize}px`, height: `${finalButtonSize}px`,
|
|
508
472
|
borderRadius: `${finalBorderRadius}px`,
|
|
509
473
|
background: finalButtonColor, color: finalIconColor, border: 'none', cursor: 'pointer',
|