vox-ai-react 1.4.0 → 1.6.1
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 +92 -100
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +92 -100
- package/dist/index.js.map +1 -1
- package/package.json +4 -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 = ({ size = 20 }) => (jsxs("svg", { width: size, height: size, 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,58 @@ 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: {
|
|
149
|
+
return (jsxs("div", { style: {
|
|
150
|
+
display: 'flex', flexDirection: 'column', height: '100%', background: '#000000', fontFamily: '"Plus Jakarta Sans", sans-serif',
|
|
151
|
+
WebkitFontSmoothing: 'antialiased', MozOsxFontSmoothing: 'grayscale'
|
|
152
|
+
}, 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
153
|
flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
150
154
|
border: msg.role === 'user' ? '1px solid #ffffff' : 'none',
|
|
151
155
|
background: msg.role === 'user' ? '#ffffff' : 'transparent',
|
|
152
156
|
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
|
-
|
|
157
|
+
}, 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: {
|
|
158
|
+
padding: '8px 12px', fontSize: '12px', fontWeight: 400, lineHeight: 1.625,
|
|
159
|
+
border: '1px solid', borderRadius: '8px',
|
|
160
|
+
borderColor: msg.role === 'user' ? '#52525b' : '#3f3f46',
|
|
161
|
+
background: msg.role === 'user' ? '#27272a' : '#000000',
|
|
162
|
+
color: msg.role === 'user' ? '#ffffff' : '#d4d4d8'
|
|
163
|
+
}, 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, style: { width: '100%' }, children: jsxs("div", { style: {
|
|
164
|
+
width: '100%', cursor: 'text', overflow: 'clip', backgroundClip: 'padding-box',
|
|
165
|
+
padding: '6px', boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)',
|
|
166
|
+
border: '1px solid #3f3f46', transition: 'border-radius 200ms ease-out',
|
|
167
|
+
borderRadius: '9999px', display: 'grid',
|
|
168
|
+
gridTemplateColumns: 'auto 1fr auto', gridTemplateRows: 'auto 1fr auto',
|
|
169
|
+
gridTemplateAreas: '"header header header" "leading primary trailing" ". footer ."',
|
|
170
|
+
background: 'rgba(0,0,0,0.5)', boxSizing: 'border-box'
|
|
171
|
+
}, children: [jsx("div", { style: { gridArea: 'primary', display: 'flex', minHeight: '32px', alignItems: 'center', overflowX: 'hidden', paddingLeft: '4px', paddingRight: '4px', marginTop: '-6px', marginBottom: '-6px' }, children: jsx("div", { style: { flex: '1 1 0%', overflow: 'auto', maxHeight: '13rem' }, children: jsx("input", { type: "text", placeholder: "Type your message...", value: input, onChange: (e) => setInput(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter' && !e.shiftKey) {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
handleSend();
|
|
174
|
+
} }, disabled: isLoading, style: {
|
|
175
|
+
width: '100%', minHeight: 0, resize: 'none', border: 'none', padding: 0,
|
|
176
|
+
fontSize: '12px', outline: 'none', background: 'transparent',
|
|
177
|
+
color: '#ffffff', margin: 0, fontFamily: 'inherit', lineHeight: 'normal'
|
|
178
|
+
} }) }) }), jsx("div", { style: { gridArea: 'leading', display: 'flex' }, children: jsx("button", { type: "button", "aria-label": "Add attachments", style: {
|
|
179
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
180
|
+
width: '32px', height: '32px', borderRadius: '9999px', outline: 'none',
|
|
181
|
+
border: 'none', background: 'transparent', cursor: 'pointer', color: '#71717a',
|
|
182
|
+
transition: 'all 0.15s ease'
|
|
183
|
+
}, children: jsx(PlusIcon, { size: 20 }) }) }), jsx("div", { style: { gridArea: 'trailing', display: 'flex', alignItems: 'center', gap: '4px' }, children: jsxs("div", { style: { marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '4px' }, children: [jsx("button", { type: "button", "aria-label": "Record audio message", style: {
|
|
184
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
185
|
+
width: '32px', height: '32px', borderRadius: '9999px', outline: 'none',
|
|
186
|
+
border: 'none', background: 'transparent', cursor: 'pointer', color: '#71717a',
|
|
187
|
+
transition: 'all 0.15s ease'
|
|
188
|
+
}, 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
189
|
};
|
|
167
190
|
// Voice Interface Component
|
|
168
191
|
const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall }) => {
|
|
@@ -170,30 +193,20 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
170
193
|
const [errorMsg, setErrorMsg] = useState('');
|
|
171
194
|
const [isMuted, setIsMuted] = useState(false);
|
|
172
195
|
const [callDuration, setCallDuration] = useState(0);
|
|
173
|
-
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
174
|
-
const [speakingIntensity, setSpeakingIntensity] = useState(0);
|
|
175
196
|
const callStartTimeRef = useRef(null);
|
|
176
197
|
const durationIntervalRef = useRef(null);
|
|
177
198
|
const inputAudioContextRef = useRef(null);
|
|
178
199
|
const outputAudioContextRef = useRef(null);
|
|
179
200
|
const inputAnalyserRef = useRef(null);
|
|
180
|
-
const outputAnalyserRef = useRef(null);
|
|
181
201
|
const outputGainRef = useRef(null);
|
|
182
202
|
const nextStartTimeRef = useRef(0);
|
|
183
203
|
const sourcesRef = useRef(new Set());
|
|
184
|
-
const speakingAnimationRef = useRef(null);
|
|
185
204
|
const sessionRef = useRef(null);
|
|
186
205
|
const streamRef = useRef(null);
|
|
187
206
|
const scriptProcessorRef = useRef(null);
|
|
188
207
|
const sourceNodeRef = useRef(null);
|
|
189
208
|
const cleanupAudio = useCallback(() => {
|
|
190
209
|
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
210
|
sourcesRef.current.forEach(source => { try {
|
|
198
211
|
source.stop();
|
|
199
212
|
}
|
|
@@ -224,13 +237,6 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
224
237
|
catch (e) { }
|
|
225
238
|
outputGainRef.current = null;
|
|
226
239
|
}
|
|
227
|
-
if (outputAnalyserRef.current) {
|
|
228
|
-
try {
|
|
229
|
-
outputAnalyserRef.current.disconnect();
|
|
230
|
-
}
|
|
231
|
-
catch (e) { }
|
|
232
|
-
outputAnalyserRef.current = null;
|
|
233
|
-
}
|
|
234
240
|
if (((_a = inputAudioContextRef.current) === null || _a === void 0 ? void 0 : _a.state) !== 'closed') {
|
|
235
241
|
try {
|
|
236
242
|
(_b = inputAudioContextRef.current) === null || _b === void 0 ? void 0 : _b.close();
|
|
@@ -265,34 +271,16 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
265
271
|
const analyser = inputCtx.createAnalyser();
|
|
266
272
|
analyser.fftSize = 256;
|
|
267
273
|
inputAnalyserRef.current = analyser;
|
|
268
|
-
const outputAnalyser = outputCtx.createAnalyser();
|
|
269
|
-
outputAnalyser.fftSize = 256;
|
|
270
|
-
outputAnalyserRef.current = outputAnalyser;
|
|
271
274
|
const outputGain = outputCtx.createGain();
|
|
272
275
|
outputGain.gain.value = 1;
|
|
273
276
|
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();
|
|
277
|
+
outputGain.connect(outputCtx.destination);
|
|
288
278
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
289
279
|
streamRef.current = stream;
|
|
290
280
|
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
281
|
const ws = new WebSocket(wsUrl);
|
|
293
282
|
sessionRef.current = ws;
|
|
294
283
|
ws.onopen = () => {
|
|
295
|
-
console.log('[VOX] WebSocket connected, sending setup...');
|
|
296
284
|
const setupMsg = {
|
|
297
285
|
setup: {
|
|
298
286
|
model: 'models/gemini-2.5-flash-native-audio-preview-09-2025',
|
|
@@ -322,9 +310,7 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
322
310
|
return;
|
|
323
311
|
}
|
|
324
312
|
}
|
|
325
|
-
console.log('[VOX] Received:', data);
|
|
326
313
|
if (data.setupComplete) {
|
|
327
|
-
console.log('[VOX] Setup complete, voice ready!');
|
|
328
314
|
setConnectionState(ConnectionState.CONNECTED);
|
|
329
315
|
const source = inputCtx.createMediaStreamSource(stream);
|
|
330
316
|
sourceNodeRef.current = source;
|
|
@@ -365,19 +351,14 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
365
351
|
nextStartTimeRef.current = 0;
|
|
366
352
|
}
|
|
367
353
|
};
|
|
368
|
-
ws.onclose = (
|
|
369
|
-
|
|
370
|
-
setConnectionState(ConnectionState.DISCONNECTED);
|
|
371
|
-
};
|
|
372
|
-
ws.onerror = (error) => {
|
|
373
|
-
console.error('[VOX] WebSocket error:', error);
|
|
354
|
+
ws.onclose = () => setConnectionState(ConnectionState.DISCONNECTED);
|
|
355
|
+
ws.onerror = () => {
|
|
374
356
|
setConnectionState(ConnectionState.ERROR);
|
|
375
357
|
setErrorMsg('Connection failed. Check your Gemini API key.');
|
|
376
358
|
cleanupAudio();
|
|
377
359
|
};
|
|
378
360
|
}
|
|
379
361
|
catch (err) {
|
|
380
|
-
console.error('[VOX] Connection error:', err);
|
|
381
362
|
setConnectionState(ConnectionState.ERROR);
|
|
382
363
|
setErrorMsg(err.message || 'Failed to access microphone or connect.');
|
|
383
364
|
cleanupAudio();
|
|
@@ -403,15 +384,24 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
|
|
|
403
384
|
connect();
|
|
404
385
|
return () => disconnect();
|
|
405
386
|
}, []);
|
|
406
|
-
|
|
387
|
+
const toggleMute = () => {
|
|
388
|
+
setIsMuted(!isMuted);
|
|
389
|
+
if (streamRef.current)
|
|
390
|
+
streamRef.current.getAudioTracks().forEach(track => { track.enabled = isMuted; });
|
|
391
|
+
};
|
|
392
|
+
return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '32px', height: '100%', padding: '24px' }, children: [jsxs("div", { style: { position: 'relative' }, children: [jsx("div", { style: {
|
|
407
393
|
width: '112px', height: '112px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
408
394
|
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
|
-
|
|
395
|
+
boxShadow: connectionState === ConnectionState.CONNECTED ? '0 0 30px rgba(255,255,255,0.15)' : 'none',
|
|
396
|
+
transition: 'all 0.3s'
|
|
397
|
+
}, 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: {
|
|
398
|
+
padding: '16px', borderRadius: '50%', border: 'none', cursor: 'pointer', transition: 'all 0.2s',
|
|
399
|
+
background: isMuted ? '#27272a' : '#ffffff',
|
|
400
|
+
color: isMuted ? '#71717a' : '#000000'
|
|
401
|
+
}, children: isMuted ? jsx(MicOffIcon, {}) : jsx(MicIcon, {}) }), jsx("button", { onClick: () => { disconnect(); onEndCall === null || onEndCall === void 0 ? void 0 : onEndCall(); }, style: {
|
|
402
|
+
padding: '16px', borderRadius: '50%', border: 'none', cursor: 'pointer', transition: 'all 0.2s',
|
|
403
|
+
background: '#dc2626', color: '#ffffff'
|
|
404
|
+
}, children: jsx(PhoneOffIcon, {}) })] }))] }));
|
|
415
405
|
};
|
|
416
406
|
// Main VoxChat Component
|
|
417
407
|
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 +409,10 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
419
409
|
const [isOpen, setIsOpen] = useState(false);
|
|
420
410
|
const [activeTab, setActiveTab] = useState('text');
|
|
421
411
|
const [isVoiceActive, setIsVoiceActive] = useState(false);
|
|
422
|
-
// Server config state
|
|
423
412
|
const [serverConfig, setServerConfig] = useState(null);
|
|
413
|
+
const [serverGeminiApiKey, setServerGeminiApiKey] = useState();
|
|
424
414
|
const [configLoaded, setConfigLoaded] = useState(!useServerConfig);
|
|
425
415
|
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
|
426
|
-
// Fetch config from server
|
|
427
416
|
const fetchConfig = useCallback(() => {
|
|
428
417
|
if (useServerConfig && apiKey && apiUrl) {
|
|
429
418
|
fetch(`${apiUrl}/api/v1/config`, {
|
|
@@ -434,6 +423,9 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
434
423
|
if (data.agentConfig) {
|
|
435
424
|
setServerConfig(data.agentConfig);
|
|
436
425
|
}
|
|
426
|
+
if (data.geminiApiKey) {
|
|
427
|
+
setServerGeminiApiKey(data.geminiApiKey);
|
|
428
|
+
}
|
|
437
429
|
setConfigLoaded(true);
|
|
438
430
|
setIsInitialLoad(false);
|
|
439
431
|
})
|
|
@@ -444,22 +436,16 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
444
436
|
});
|
|
445
437
|
}
|
|
446
438
|
}, [useServerConfig, apiKey, apiUrl]);
|
|
447
|
-
// Initial fetch and real-time polling (every 10 seconds)
|
|
448
439
|
useEffect(() => {
|
|
449
440
|
if (useServerConfig && apiKey && apiUrl) {
|
|
450
|
-
// Fetch immediately
|
|
451
441
|
fetchConfig();
|
|
452
|
-
// Poll at specified interval for real-time updates
|
|
453
442
|
const intervalId = setInterval(fetchConfig, configPollInterval);
|
|
454
|
-
// Cleanup interval on unmount
|
|
455
443
|
return () => clearInterval(intervalId);
|
|
456
444
|
}
|
|
457
445
|
}, [useServerConfig, apiKey, apiUrl, fetchConfig, configPollInterval]);
|
|
458
|
-
// Use server config if available, otherwise use props
|
|
459
446
|
const agentName = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.name) || propAgentName;
|
|
460
447
|
const voiceName = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.voiceName) || propVoiceName;
|
|
461
448
|
const systemInstruction = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.systemInstruction) || propSystemInstruction;
|
|
462
|
-
// Widget style from server or props
|
|
463
449
|
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
450
|
const finalButtonSize = ((_b = serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.widgetStyle) === null || _b === void 0 ? void 0 : _b.buttonSize) || buttonSize || 56;
|
|
465
451
|
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,22 +471,28 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
|
|
|
485
471
|
const positionStyles = finalPosition === 'bottom-left'
|
|
486
472
|
? { bottom: '16px', left: '16px' }
|
|
487
473
|
: { bottom: '16px', right: '16px' };
|
|
488
|
-
return (jsxs(Fragment, { children: [jsx("style", { children: `
|
|
489
|
-
@keyframes vox-fade-in { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
490
|
-
@keyframes vox-spin { to { transform: rotate(360deg); } }
|
|
491
|
-
@keyframes vox-ping { 0% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.15); opacity: 0; } 100% { transform: scale(1); opacity: 0; } }
|
|
492
|
-
.vox-widget { animation: vox-fade-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
|
474
|
+
return (jsxs(Fragment, { children: [jsx("link", { href: "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap", rel: "stylesheet" }), jsx("style", { children: `
|
|
475
|
+
@keyframes vox-fade-in { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
476
|
+
@keyframes vox-spin { to { transform: rotate(360deg); } }
|
|
477
|
+
@keyframes vox-ping { 0% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.15); opacity: 0; } 100% { transform: scale(1); opacity: 0; } }
|
|
478
|
+
.vox-widget { animation: vox-fade-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
|
493
479
|
` }), 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
480
|
width: 'min(calc(100vw - 32px), 360px)', height: '550px', maxHeight: '550px',
|
|
495
481
|
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: '
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
482
|
+
border: '1px solid #3f3f46', boxShadow: '0 20px 40px -10px rgba(0,0,0,0.5)', borderRadius: '8px',
|
|
483
|
+
transition: 'all 0.3s ease',
|
|
484
|
+
fontFamily: '"Plus Jakarta Sans", sans-serif',
|
|
485
|
+
WebkitFontSmoothing: 'antialiased', MozOsxFontSmoothing: 'grayscale',
|
|
486
|
+
textAlign: 'left', boxSizing: 'border-box'
|
|
487
|
+
}, children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 16px', flexShrink: 0, borderBottom: '1px solid #3f3f46', background: '#000000', fontFamily: '"Plus Jakarta Sans", 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', color: '#ffffff' }, 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: 600, fontSize: '12px', margin: 0, transition: 'all 0.3s ease', lineHeight: '1.2', fontFamily: 'inherit' }, children: agentName }), jsxs("p", { style: { color: '#a1a1aa', fontSize: '11px', fontWeight: 400, margin: 0, display: 'flex', alignItems: 'center', lineHeight: '1.2', fontFamily: 'inherit' }, 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: {
|
|
488
|
+
padding: '6px 12px', fontSize: '11px', fontWeight: 500, borderRadius: '6px',
|
|
489
|
+
background: activeTab === 'text' ? '#27272a' : 'transparent', border: 'none', cursor: 'pointer', color: activeTab === 'text' ? '#ffffff' : '#71717a', transition: 'all 0.2s',
|
|
490
|
+
boxShadow: activeTab === 'text' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
|
|
491
|
+
}, children: "Text" }), jsx("button", { onClick: () => handleTabChange('voice'), style: {
|
|
492
|
+
padding: '6px 12px', fontSize: '11px', fontWeight: 500, borderRadius: '6px',
|
|
493
|
+
background: activeTab === 'voice' ? '#27272a' : 'transparent', border: 'none', cursor: 'pointer', color: activeTab === 'voice' ? '#ffffff' : '#71717a', transition: 'all 0.2s',
|
|
494
|
+
boxShadow: activeTab === 'voice' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
|
|
495
|
+
}, 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 || serverGeminiApiKey || '', 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
496
|
width: `${finalButtonSize}px`, height: `${finalButtonSize}px`,
|
|
505
497
|
borderRadius: `${finalBorderRadius}px`,
|
|
506
498
|
background: finalButtonColor, color: finalIconColor, border: 'none', cursor: 'pointer',
|