vox-ai-react 1.0.2 → 1.0.3

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 CHANGED
@@ -1,229 +1,457 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
- import { useState, useRef, useEffect } from 'react';
2
+ import { useState, useRef, useEffect, useCallback } from 'react';
3
3
 
4
- const VoxChat = ({ apiKey, apiUrl = 'https://api.vox.ai', agentName = 'VOX-01', position = 'bottom-right', theme = 'dark', buttonColor, buttonSize = 48, borderRadius = 0, iconColor, onOpen, onClose, onMessage, }) => {
5
- const [isOpen, setIsOpen] = useState(false);
6
- const [messages, setMessages] = useState([]);
4
+ var ConnectionState;
5
+ (function (ConnectionState) {
6
+ ConnectionState["DISCONNECTED"] = "DISCONNECTED";
7
+ ConnectionState["CONNECTING"] = "CONNECTING";
8
+ ConnectionState["CONNECTED"] = "CONNECTED";
9
+ ConnectionState["ERROR"] = "ERROR";
10
+ })(ConnectionState || (ConnectionState = {}));
11
+ // Audio utilities
12
+ function createBlob(inputData) {
13
+ const buffer = new ArrayBuffer(inputData.length * 2);
14
+ const view = new DataView(buffer);
15
+ for (let i = 0; i < inputData.length; i++) {
16
+ const s = Math.max(-1, Math.min(1, inputData[i]));
17
+ view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true);
18
+ }
19
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
20
+ return { mimeType: 'audio/pcm;rate=16000', data: base64 };
21
+ }
22
+ function decode(base64) {
23
+ const binaryString = atob(base64);
24
+ const bytes = new Uint8Array(binaryString.length);
25
+ for (let i = 0; i < binaryString.length; i++) {
26
+ bytes[i] = binaryString.charCodeAt(i);
27
+ }
28
+ return bytes.buffer;
29
+ }
30
+ async function decodeAudioData(arrayBuffer, ctx, sampleRate, numChannels) {
31
+ const dataView = new DataView(arrayBuffer);
32
+ const numSamples = arrayBuffer.byteLength / 2;
33
+ const audioBuffer = ctx.createBuffer(numChannels, numSamples, sampleRate);
34
+ const channelData = audioBuffer.getChannelData(0);
35
+ for (let i = 0; i < numSamples; i++) {
36
+ const sample = dataView.getInt16(i * 2, true);
37
+ channelData[i] = sample / 32768;
38
+ }
39
+ return audioBuffer;
40
+ }
41
+ // Icons
42
+ const MessageIcon = () => (jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }) }));
43
+ const MicIcon = ({ size = 14 }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", 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" })] }));
44
+ const MicOffIcon = ({ size = 40 }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", 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" })] }));
45
+ const XIcon = () => (jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M18 6L6 18M6 6l12 12" }) }));
46
+ const PhoneOffIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", 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
+ const BotIcon = () => (jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", 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 UserIcon = () => (jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", 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" })] }));
49
+ const LoaderIcon = () => (jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "vox-spin", children: jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
50
+ const AlertIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }), jsx("path", { d: "M12 9v4" }), jsx("path", { d: "M12 17h.01" })] }));
51
+ // Visualizer Component
52
+ const Visualizer = ({ analyser, isActive, theme }) => {
53
+ const canvasRef = useRef(null);
54
+ const animationRef = useRef();
55
+ useEffect(() => {
56
+ const canvas = canvasRef.current;
57
+ if (!canvas)
58
+ return;
59
+ const ctx = canvas.getContext('2d');
60
+ if (!ctx)
61
+ return;
62
+ const draw = () => {
63
+ const width = canvas.width;
64
+ const height = canvas.height;
65
+ ctx.clearRect(0, 0, width, height);
66
+ if (!analyser || !isActive) {
67
+ ctx.strokeStyle = theme === 'light' ? '#a1a1aa' : '#3f3f46';
68
+ ctx.lineWidth = 1;
69
+ ctx.beginPath();
70
+ ctx.moveTo(0, height / 2);
71
+ ctx.lineTo(width, height / 2);
72
+ ctx.stroke();
73
+ animationRef.current = requestAnimationFrame(draw);
74
+ return;
75
+ }
76
+ const bufferLength = analyser.frequencyBinCount;
77
+ const dataArray = new Uint8Array(bufferLength);
78
+ analyser.getByteTimeDomainData(dataArray);
79
+ ctx.strokeStyle = theme === 'light' ? '#000000' : '#ffffff';
80
+ ctx.lineWidth = 1;
81
+ ctx.beginPath();
82
+ const sliceWidth = width / bufferLength;
83
+ let x = 0;
84
+ for (let i = 0; i < bufferLength; i++) {
85
+ const v = dataArray[i] / 128.0;
86
+ const y = (v * height) / 2;
87
+ if (i === 0)
88
+ ctx.moveTo(x, y);
89
+ else
90
+ ctx.lineTo(x, y);
91
+ x += sliceWidth;
92
+ }
93
+ ctx.lineTo(width, height / 2);
94
+ ctx.stroke();
95
+ animationRef.current = requestAnimationFrame(draw);
96
+ };
97
+ draw();
98
+ return () => {
99
+ if (animationRef.current)
100
+ cancelAnimationFrame(animationRef.current);
101
+ };
102
+ }, [analyser, isActive, theme]);
103
+ return jsx("canvas", { ref: canvasRef, width: 300, height: 50, className: "vox-visualizer" });
104
+ };
105
+ // Text Interface Component
106
+ const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction, theme }) => {
107
+ const [messages, setMessages] = useState([
108
+ { id: 'welcome', role: 'assistant', text: `VOX INITIALIZED. GREETINGS. I AM ${agentName}. HOW MAY I ASSIST YOUR INQUIRY?` }
109
+ ]);
7
110
  const [input, setInput] = useState('');
8
111
  const [isLoading, setIsLoading] = useState(false);
9
112
  const scrollRef = useRef(null);
10
- // Determine actual theme
11
- const actualTheme = theme === 'auto'
12
- ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
13
- : theme;
14
- // Default colors based on theme
15
- const defaultButtonColor = actualTheme === 'dark' ? '#000000' : '#ffffff';
16
- const defaultIconColor = actualTheme === 'dark' ? '#ffffff' : '#000000';
17
- const finalButtonColor = buttonColor || defaultButtonColor;
18
- const finalIconColor = iconColor || defaultIconColor;
19
113
  useEffect(() => {
20
- if (scrollRef.current) {
114
+ if (scrollRef.current)
21
115
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
22
- }
23
116
  }, [messages]);
24
- useEffect(() => {
25
- // Add welcome message
26
- setMessages([{
27
- id: 'welcome',
28
- role: 'assistant',
29
- text: `Hello! I'm ${agentName}. How can I help you today?`,
30
- timestamp: new Date()
31
- }]);
32
- }, [agentName]);
33
- const handleOpen = () => {
34
- setIsOpen(true);
35
- onOpen === null || onOpen === void 0 ? void 0 : onOpen();
36
- };
37
- const handleClose = () => {
38
- setIsOpen(false);
39
- onClose === null || onClose === void 0 ? void 0 : onClose();
40
- };
41
117
  const handleSend = async (e) => {
118
+ var _a;
42
119
  e === null || e === void 0 ? void 0 : e.preventDefault();
43
120
  if (!input.trim() || isLoading)
44
121
  return;
45
- const userMessage = {
46
- id: Date.now().toString(),
47
- role: 'user',
48
- text: input,
49
- timestamp: new Date()
50
- };
51
- setMessages(prev => [...prev, userMessage]);
52
- onMessage === null || onMessage === void 0 ? void 0 : onMessage(input, 'user');
122
+ const userMsg = { id: Date.now().toString(), role: 'user', text: input };
123
+ setMessages(prev => [...prev, userMsg]);
53
124
  setInput('');
54
125
  setIsLoading(true);
55
126
  try {
127
+ const history = messages.map(m => ({ role: m.role === 'assistant' ? 'model' : 'user', text: m.text }));
56
128
  const response = await fetch(`${apiUrl}/api/v1/chat`, {
57
129
  method: 'POST',
58
- headers: {
59
- 'Content-Type': 'application/json',
60
- 'Authorization': `Bearer ${apiKey}`
61
- },
62
- body: JSON.stringify({
63
- message: userMessage.text,
64
- history: messages.map(m => ({ role: m.role, content: m.text }))
65
- })
130
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
131
+ body: JSON.stringify({ message: input, history, systemInstruction })
66
132
  });
133
+ if (!response.ok)
134
+ throw new Error('API request failed');
67
135
  const data = await response.json();
68
- const assistantMessage = {
69
- id: (Date.now() + 1).toString(),
70
- role: 'assistant',
71
- text: data.response || 'Sorry, I encountered an error.',
72
- timestamp: new Date()
73
- };
74
- setMessages(prev => [...prev, assistantMessage]);
75
- onMessage === null || onMessage === void 0 ? void 0 : onMessage(assistantMessage.text, 'assistant');
76
- }
77
- catch (error) {
78
- const errorMessage = {
79
- id: (Date.now() + 1).toString(),
80
- role: 'assistant',
81
- text: 'Sorry, I encountered an error. Please try again.',
82
- timestamp: new Date()
83
- };
84
- setMessages(prev => [...prev, errorMessage]);
136
+ const botMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: ((_a = data.response) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'ERROR: NO RESPONSE' };
137
+ setMessages(prev => [...prev, botMsg]);
138
+ }
139
+ catch (err) {
140
+ const errorMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: 'ERROR: SIGNAL LOST. RETRY.' };
141
+ setMessages(prev => [...prev, errorMsg]);
85
142
  }
86
143
  finally {
87
144
  setIsLoading(false);
88
145
  }
89
146
  };
90
- const positionStyles = position === 'bottom-left'
91
- ? { left: '16px' }
92
- : { right: '16px' };
93
- const colors = actualTheme === 'dark' ? {
94
- bg: '#000000',
95
- bgSecondary: '#18181b',
96
- border: '#27272a',
97
- text: '#ffffff',
98
- textSecondary: '#a1a1aa',
99
- userBubble: '#27272a',
100
- assistantBubble: '#000000',
101
- } : {
102
- bg: '#ffffff',
103
- bgSecondary: '#f4f4f5',
104
- border: '#e4e4e7',
105
- text: '#000000',
106
- textSecondary: '#71717a',
107
- userBubble: '#f4f4f5',
108
- assistantBubble: '#ffffff',
147
+ const bgColor = theme === 'light' ? '#ffffff' : '#000000';
148
+ const textColor = theme === 'light' ? '#000000' : '#ffffff';
149
+ const borderColor = theme === 'light' ? '#a1a1aa' : '#52525b';
150
+ const mutedColor = theme === 'light' ? '#71717a' : '#a1a1aa';
151
+ return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: bgColor }, children: [jsxs("div", { ref: scrollRef, style: { flex: 1, overflowY: 'auto', padding: '24px', 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: '12px', maxWidth: '85%' }, children: [jsx("div", { style: {
152
+ width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center',
153
+ border: `1px solid ${msg.role === 'user' ? textColor : borderColor}`,
154
+ background: msg.role === 'user' ? textColor : 'transparent', color: msg.role === 'user' ? bgColor : textColor
155
+ }, children: msg.role === 'user' ? jsx(UserIcon, {}) : jsx(BotIcon, {}) }), jsx("div", { style: {
156
+ padding: '12px 16px', fontSize: '11px', fontWeight: 700, letterSpacing: '0.05em', lineHeight: 1.6,
157
+ border: `1px solid ${borderColor}`, background: msg.role === 'user' ? (theme === 'light' ? '#f4f4f5' : '#27272a') : bgColor,
158
+ color: msg.role === 'user' ? textColor : mutedColor
159
+ }, children: msg.text })] }) }, msg.id))), isLoading && (jsx("div", { style: { display: 'flex', justifyContent: 'flex-start' }, children: jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '12px' }, children: [jsx("div", { style: { width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: `1px solid ${borderColor}`, color: textColor, opacity: 0.2 }, children: jsx(BotIcon, {}) }), jsx("div", { style: { padding: '16px 24px', border: `1px solid ${borderColor}`, background: bgColor, color: mutedColor }, children: jsx(LoaderIcon, {}) })] }) }))] }), jsx("div", { style: { padding: '24px' }, children: jsxs("form", { onSubmit: handleSend, style: { display: 'flex', gap: '16px' }, children: [jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), placeholder: "INPUT COMMAND...", disabled: isLoading, style: {
160
+ flex: 1, padding: '16px 24px', border: `1px solid ${borderColor}`, background: bgColor, color: textColor,
161
+ outline: 'none', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em'
162
+ } }), jsx("button", { type: "submit", disabled: !input.trim() || isLoading, style: {
163
+ padding: '16px 24px', background: textColor, color: bgColor, border: 'none', cursor: 'pointer',
164
+ fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em',
165
+ opacity: !input.trim() || isLoading ? 0.2 : 1
166
+ }, children: "EXECUTE" })] }) })] }));
167
+ };
168
+ // Voice Interface Component
169
+ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, theme, onEndCall }) => {
170
+ const [connectionState, setConnectionState] = useState(ConnectionState.DISCONNECTED);
171
+ const [errorMsg, setErrorMsg] = useState('');
172
+ const [isMuted, setIsMuted] = useState(false);
173
+ const [callDuration, setCallDuration] = useState(0);
174
+ const [isSpeaking, setIsSpeaking] = useState(false);
175
+ const [speakingIntensity, setSpeakingIntensity] = useState(0);
176
+ const callStartTimeRef = useRef(null);
177
+ const durationIntervalRef = useRef(null);
178
+ const inputAudioContextRef = useRef(null);
179
+ const outputAudioContextRef = useRef(null);
180
+ const inputAnalyserRef = useRef(null);
181
+ const outputAnalyserRef = useRef(null);
182
+ const outputGainRef = useRef(null);
183
+ const nextStartTimeRef = useRef(0);
184
+ const sourcesRef = useRef(new Set());
185
+ const speakingAnimationRef = useRef(null);
186
+ const sessionRef = useRef(null);
187
+ const streamRef = useRef(null);
188
+ const scriptProcessorRef = useRef(null);
189
+ const sourceNodeRef = useRef(null);
190
+ const cleanupAudio = useCallback(() => {
191
+ var _a, _b, _c, _d;
192
+ if (speakingAnimationRef.current) {
193
+ cancelAnimationFrame(speakingAnimationRef.current);
194
+ speakingAnimationRef.current = null;
195
+ }
196
+ setIsSpeaking(false);
197
+ setSpeakingIntensity(0);
198
+ sourcesRef.current.forEach(source => { try {
199
+ source.stop();
200
+ }
201
+ catch (e) { } });
202
+ sourcesRef.current.clear();
203
+ if (streamRef.current) {
204
+ streamRef.current.getTracks().forEach(track => { track.stop(); track.enabled = false; });
205
+ streamRef.current = null;
206
+ }
207
+ if (scriptProcessorRef.current) {
208
+ try {
209
+ scriptProcessorRef.current.disconnect();
210
+ }
211
+ catch (e) { }
212
+ scriptProcessorRef.current = null;
213
+ }
214
+ if (sourceNodeRef.current) {
215
+ try {
216
+ sourceNodeRef.current.disconnect();
217
+ }
218
+ catch (e) { }
219
+ sourceNodeRef.current = null;
220
+ }
221
+ if (outputGainRef.current) {
222
+ try {
223
+ outputGainRef.current.disconnect();
224
+ }
225
+ catch (e) { }
226
+ outputGainRef.current = null;
227
+ }
228
+ if (outputAnalyserRef.current) {
229
+ try {
230
+ outputAnalyserRef.current.disconnect();
231
+ }
232
+ catch (e) { }
233
+ outputAnalyserRef.current = null;
234
+ }
235
+ if (((_a = inputAudioContextRef.current) === null || _a === void 0 ? void 0 : _a.state) !== 'closed') {
236
+ try {
237
+ (_b = inputAudioContextRef.current) === null || _b === void 0 ? void 0 : _b.close();
238
+ }
239
+ catch (e) { }
240
+ }
241
+ if (((_c = outputAudioContextRef.current) === null || _c === void 0 ? void 0 : _c.state) !== 'closed') {
242
+ try {
243
+ (_d = outputAudioContextRef.current) === null || _d === void 0 ? void 0 : _d.close();
244
+ }
245
+ catch (e) { }
246
+ }
247
+ inputAudioContextRef.current = null;
248
+ outputAudioContextRef.current = null;
249
+ inputAnalyserRef.current = null;
250
+ sessionRef.current = null;
251
+ }, []);
252
+ const connect = async () => {
253
+ try {
254
+ setConnectionState(ConnectionState.CONNECTING);
255
+ setErrorMsg('');
256
+ callStartTimeRef.current = Date.now();
257
+ durationIntervalRef.current = setInterval(() => {
258
+ if (callStartTimeRef.current)
259
+ setCallDuration(Math.floor((Date.now() - callStartTimeRef.current) / 1000));
260
+ }, 1000);
261
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
262
+ const inputCtx = new AudioContextClass({ sampleRate: 16000 });
263
+ const outputCtx = new AudioContextClass({ sampleRate: 24000 });
264
+ inputAudioContextRef.current = inputCtx;
265
+ outputAudioContextRef.current = outputCtx;
266
+ const analyser = inputCtx.createAnalyser();
267
+ analyser.fftSize = 256;
268
+ inputAnalyserRef.current = analyser;
269
+ const outputAnalyser = outputCtx.createAnalyser();
270
+ outputAnalyser.fftSize = 256;
271
+ outputAnalyserRef.current = outputAnalyser;
272
+ const outputGain = outputCtx.createGain();
273
+ outputGain.gain.value = 1;
274
+ outputGainRef.current = outputGain;
275
+ outputGain.connect(outputAnalyser);
276
+ outputAnalyser.connect(outputCtx.destination);
277
+ const monitorSpeaking = () => {
278
+ if (!outputAnalyserRef.current)
279
+ return;
280
+ const dataArray = new Uint8Array(outputAnalyserRef.current.frequencyBinCount);
281
+ outputAnalyserRef.current.getByteFrequencyData(dataArray);
282
+ const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
283
+ const intensity = Math.min(average / 128, 1);
284
+ setIsSpeaking(intensity > 0.05);
285
+ setSpeakingIntensity(intensity);
286
+ speakingAnimationRef.current = requestAnimationFrame(monitorSpeaking);
287
+ };
288
+ monitorSpeaking();
289
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
290
+ streamRef.current = stream;
291
+ // Connect to Gemini Live API via WebSocket
292
+ const wsUrl = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${geminiApiKey}`;
293
+ const ws = new WebSocket(wsUrl);
294
+ sessionRef.current = ws;
295
+ ws.onopen = () => {
296
+ // Send setup message
297
+ const setupMsg = {
298
+ setup: {
299
+ model: 'models/gemini-2.5-flash-preview-native-audio-dialog',
300
+ generationConfig: { responseModalities: ['AUDIO'], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName } } } },
301
+ systemInstruction: { parts: [{ text: systemInstruction }] }
302
+ }
303
+ };
304
+ ws.send(JSON.stringify(setupMsg));
305
+ };
306
+ ws.onmessage = async (event) => {
307
+ var _a, _b, _c, _d, _e, _f;
308
+ const data = JSON.parse(event.data);
309
+ if (data.setupComplete) {
310
+ setConnectionState(ConnectionState.CONNECTED);
311
+ // Setup audio input pipeline
312
+ const source = inputCtx.createMediaStreamSource(stream);
313
+ sourceNodeRef.current = source;
314
+ const processor = inputCtx.createScriptProcessor(2048, 1, 1);
315
+ scriptProcessorRef.current = processor;
316
+ processor.onaudioprocess = (e) => {
317
+ if (isMuted || ws.readyState !== WebSocket.OPEN)
318
+ return;
319
+ const inputData = e.inputBuffer.getChannelData(0);
320
+ const pcmBlob = createBlob(inputData);
321
+ ws.send(JSON.stringify({ realtimeInput: { mediaChunks: [pcmBlob] } }));
322
+ };
323
+ source.connect(analyser);
324
+ source.connect(processor);
325
+ processor.connect(inputCtx.destination);
326
+ }
327
+ // Handle audio output
328
+ const audioData = (_e = (_d = (_c = (_b = (_a = data.serverContent) === null || _a === void 0 ? void 0 : _a.modelTurn) === null || _b === void 0 ? void 0 : _b.parts) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.inlineData) === null || _e === void 0 ? void 0 : _e.data;
329
+ if (audioData) {
330
+ const ctx = outputAudioContextRef.current;
331
+ if (!ctx)
332
+ return;
333
+ nextStartTimeRef.current = Math.max(nextStartTimeRef.current, ctx.currentTime);
334
+ const audioBuffer = await decodeAudioData(decode(audioData), ctx, 24000, 1);
335
+ const source = ctx.createBufferSource();
336
+ source.buffer = audioBuffer;
337
+ if (outputGainRef.current)
338
+ source.connect(outputGainRef.current);
339
+ else
340
+ source.connect(ctx.destination);
341
+ source.addEventListener('ended', () => sourcesRef.current.delete(source));
342
+ source.start(nextStartTimeRef.current);
343
+ nextStartTimeRef.current += audioBuffer.duration;
344
+ sourcesRef.current.add(source);
345
+ }
346
+ if ((_f = data.serverContent) === null || _f === void 0 ? void 0 : _f.interrupted) {
347
+ sourcesRef.current.forEach(s => s.stop());
348
+ sourcesRef.current.clear();
349
+ nextStartTimeRef.current = 0;
350
+ }
351
+ };
352
+ ws.onclose = () => setConnectionState(ConnectionState.DISCONNECTED);
353
+ ws.onerror = () => {
354
+ setConnectionState(ConnectionState.ERROR);
355
+ setErrorMsg('Connection lost. Please try again.');
356
+ cleanupAudio();
357
+ };
358
+ }
359
+ catch (err) {
360
+ setConnectionState(ConnectionState.ERROR);
361
+ setErrorMsg(err.message || 'Failed to access microphone or connect.');
362
+ cleanupAudio();
363
+ }
364
+ };
365
+ const disconnect = useCallback(() => {
366
+ callStartTimeRef.current = null;
367
+ if (durationIntervalRef.current) {
368
+ clearInterval(durationIntervalRef.current);
369
+ durationIntervalRef.current = null;
370
+ }
371
+ setCallDuration(0);
372
+ if (sessionRef.current) {
373
+ try {
374
+ sessionRef.current.close();
375
+ }
376
+ catch (e) { }
377
+ }
378
+ cleanupAudio();
379
+ setConnectionState(ConnectionState.DISCONNECTED);
380
+ }, [cleanupAudio]);
381
+ useEffect(() => {
382
+ connect();
383
+ return () => disconnect();
384
+ }, []);
385
+ const bgColor = theme === 'light' ? '#ffffff' : '#000000';
386
+ const textColor = theme === 'light' ? '#000000' : '#ffffff';
387
+ const borderColor = theme === 'light' ? '#a1a1aa' : '#52525b';
388
+ const mutedColor = '#71717a';
389
+ 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: [connectionState === ConnectionState.CONNECTED && isSpeaking && (jsx("div", { style: {
390
+ position: 'absolute', inset: 0, borderRadius: '50%', border: `1px solid ${textColor}`,
391
+ transform: `scale(${1 + speakingIntensity * 0.3})`, opacity: 0.2, animation: 'vox-ping 1s infinite'
392
+ } })), jsx("div", { style: {
393
+ width: '112px', height: '112px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
394
+ border: `1px solid ${connectionState === ConnectionState.CONNECTED ? textColor : borderColor}`,
395
+ boxShadow: connectionState === ConnectionState.CONNECTED ? `0 0 ${30 + speakingIntensity * 40}px ${theme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.15)'}` : 'none',
396
+ transform: isSpeaking ? `scale(${1 + speakingIntensity * 0.08})` : 'scale(1)', transition: 'all 0.3s'
397
+ }, children: connectionState === ConnectionState.CONNECTING ? (jsx("div", { style: { width: '40px', height: '40px', border: `2px solid ${textColor}`, borderTopColor: 'transparent', borderRadius: '50%', animation: 'vox-spin 1s linear infinite' } })) : (jsx("div", { style: { padding: '20px', color: isMuted ? mutedColor : textColor }, children: isMuted ? jsx(MicOffIcon, {}) : jsx(MicIcon, { size: 40 }) })) }), connectionState === ConnectionState.CONNECTED && (jsx("span", { style: { position: 'absolute', bottom: '-4px', right: '-4px', width: '12px', height: '12px', borderRadius: '50%', background: textColor } }))] }), jsxs("div", { style: { width: '100%', maxWidth: '280px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [jsxs("p", { style: { textAlign: 'center', fontSize: '10px', fontWeight: 900, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.2em', height: '20px' }, children: [connectionState === ConnectionState.CONNECTING && 'Initializing...', connectionState === ConnectionState.CONNECTED && (isSpeaking ? 'Agent Speaking...' : isMuted ? 'Signal 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: textColor }, 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, theme: theme })] }), connectionState === ConnectionState.ERROR && (jsxs("div", { style: { display: 'flex', alignItems: 'center', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.1em', padding: '12px 16px', maxWidth: '280px', textAlign: 'center', color: mutedColor, background: theme === 'light' ? '#f4f4f5' : '#18181b', border: `1px solid ${borderColor}` }, children: [jsx(AlertIcon, {}), jsx("span", { style: { marginLeft: '12px' }, children: errorMsg })] })), connectionState === ConnectionState.CONNECTED && (jsxs("button", { onClick: () => { disconnect(); onEndCall === null || onEndCall === void 0 ? void 0 : onEndCall(); }, style: {
398
+ padding: '12px 24px', border: `1px solid ${borderColor}`, background: bgColor, color: mutedColor, cursor: 'pointer',
399
+ fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em', display: 'flex', alignItems: 'center', gap: '8px'
400
+ }, children: [jsx(PhoneOffIcon, {}), "End Call"] }))] }));
401
+ };
402
+ // Main VoxChat Component
403
+ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', agentName = 'VOX-01', voiceName = 'Kore', systemInstruction = 'You are a helpful AI assistant.', position = 'bottom-right', theme = 'dark', buttonColor, buttonSize = 56, borderRadius = 0, iconColor, onOpen, onClose }) => {
404
+ const [isOpen, setIsOpen] = useState(false);
405
+ const [activeTab, setActiveTab] = useState('text');
406
+ const [isVoiceActive, setIsVoiceActive] = useState(false);
407
+ // Invert theme for widget (black in light mode, white in dark mode)
408
+ const widgetTheme = theme === 'light' ? 'dark' : 'light';
409
+ const bgColor = widgetTheme === 'light' ? '#ffffff' : '#000000';
410
+ const textColor = widgetTheme === 'light' ? '#000000' : '#ffffff';
411
+ const borderColor = widgetTheme === 'light' ? '#a1a1aa' : '#52525b';
412
+ const mutedColor = '#71717a';
413
+ const handleOpen = () => { setIsOpen(true); onOpen === null || onOpen === void 0 ? void 0 : onOpen(); };
414
+ const handleClose = () => { setIsOpen(false); setIsVoiceActive(false); onClose === null || onClose === void 0 ? void 0 : onClose(); };
415
+ const handleTabChange = (tab) => {
416
+ if (tab === 'voice' && activeTab !== 'voice') {
417
+ setActiveTab('voice');
418
+ setIsVoiceActive(true);
419
+ }
420
+ else if (tab === 'text') {
421
+ setIsVoiceActive(false);
422
+ setActiveTab('text');
423
+ }
109
424
  };
110
- return (jsxs(Fragment, { children: [isOpen && (jsxs("div", { style: {
111
- position: 'fixed',
112
- bottom: '80px',
113
- ...positionStyles,
114
- width: '380px',
115
- maxWidth: 'calc(100vw - 32px)',
116
- height: '500px',
117
- maxHeight: 'calc(100vh - 120px)',
118
- backgroundColor: colors.bg,
119
- border: `1px solid ${colors.border}`,
120
- boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
121
- display: 'flex',
122
- flexDirection: 'column',
123
- zIndex: 9999,
124
- fontFamily: 'system-ui, -apple-system, sans-serif',
125
- }, children: [jsxs("div", { style: {
126
- padding: '16px',
127
- borderBottom: `1px solid ${colors.border}`,
128
- display: 'flex',
129
- justifyContent: 'space-between',
130
- alignItems: 'center',
131
- }, children: [jsxs("div", { children: [jsx("div", { style: {
132
- fontSize: '10px',
133
- fontWeight: 900,
134
- color: colors.text,
135
- textTransform: 'uppercase',
136
- letterSpacing: '0.1em'
137
- }, children: agentName }), jsxs("div", { style: {
138
- fontSize: '9px',
139
- color: colors.textSecondary,
140
- textTransform: 'uppercase',
141
- letterSpacing: '0.1em',
142
- display: 'flex',
143
- alignItems: 'center',
144
- gap: '6px'
145
- }, children: [jsx("span", { style: {
146
- width: '6px',
147
- height: '6px',
148
- backgroundColor: '#22c55e',
149
- borderRadius: '50%'
150
- } }), "Online"] })] }), jsx("button", { onClick: handleClose, style: {
151
- background: 'none',
152
- border: 'none',
153
- color: colors.textSecondary,
154
- cursor: 'pointer',
155
- padding: '4px',
156
- }, children: jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] }), jsxs("div", { ref: scrollRef, style: {
157
- flex: 1,
158
- overflowY: 'auto',
159
- padding: '16px',
160
- display: 'flex',
161
- flexDirection: 'column',
162
- gap: '12px',
163
- }, children: [messages.map((msg) => (jsx("div", { style: {
164
- display: 'flex',
165
- justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
166
- }, children: jsx("div", { style: {
167
- maxWidth: '80%',
168
- padding: '10px 14px',
169
- backgroundColor: msg.role === 'user' ? colors.userBubble : colors.assistantBubble,
170
- border: `1px solid ${colors.border}`,
171
- color: colors.text,
172
- fontSize: '13px',
173
- lineHeight: 1.5,
174
- }, children: msg.text }) }, msg.id))), isLoading && (jsx("div", { style: { display: 'flex', justifyContent: 'flex-start' }, children: jsx("div", { style: {
175
- padding: '10px 14px',
176
- backgroundColor: colors.assistantBubble,
177
- border: `1px solid ${colors.border}`,
178
- color: colors.textSecondary,
179
- fontSize: '13px',
180
- }, children: "Typing..." }) }))] }), jsxs("form", { onSubmit: handleSend, style: {
181
- padding: '16px',
182
- borderTop: `1px solid ${colors.border}`,
183
- display: 'flex',
184
- gap: '8px',
185
- }, children: [jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), placeholder: "Type a message...", style: {
186
- flex: 1,
187
- padding: '10px 14px',
188
- backgroundColor: colors.bgSecondary,
189
- border: `1px solid ${colors.border}`,
190
- color: colors.text,
191
- fontSize: '13px',
192
- outline: 'none',
193
- } }), jsx("button", { type: "submit", disabled: !input.trim() || isLoading, style: {
194
- padding: '10px 16px',
195
- backgroundColor: colors.text,
196
- color: colors.bg,
197
- border: 'none',
198
- fontSize: '11px',
199
- fontWeight: 700,
200
- textTransform: 'uppercase',
201
- letterSpacing: '0.05em',
202
- cursor: input.trim() && !isLoading ? 'pointer' : 'not-allowed',
203
- opacity: input.trim() && !isLoading ? 1 : 0.5,
204
- }, children: "Send" })] })] })), !isOpen && (jsx("button", { onClick: handleOpen, style: {
205
- position: 'fixed',
206
- bottom: '16px',
207
- ...positionStyles,
208
- width: `${buttonSize}px`,
209
- height: `${buttonSize}px`,
210
- backgroundColor: finalButtonColor,
211
- borderRadius: `${borderRadius}px`,
212
- border: 'none',
213
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
214
- cursor: 'pointer',
215
- display: 'flex',
216
- alignItems: 'center',
217
- justifyContent: 'center',
218
- zIndex: 9999,
219
- transition: 'transform 0.2s, box-shadow 0.2s',
220
- }, onMouseEnter: (e) => {
221
- e.currentTarget.style.transform = 'scale(1.05)';
222
- e.currentTarget.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.2)';
223
- }, onMouseLeave: (e) => {
224
- e.currentTarget.style.transform = 'scale(1)';
225
- e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
226
- }, children: jsx("svg", { width: buttonSize * 0.4, height: buttonSize * 0.4, viewBox: "0 0 24 24", fill: "none", stroke: finalIconColor, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }) }) }))] }));
425
+ const positionStyles = position === 'bottom-left'
426
+ ? { bottom: '16px', left: '16px' }
427
+ : { bottom: '16px', right: '16px' };
428
+ const btnBg = buttonColor || (theme === 'light' ? '#000000' : '#ffffff');
429
+ const btnIcon = iconColor || (theme === 'light' ? '#ffffff' : '#000000');
430
+ return (jsxs(Fragment, { children: [jsx("style", { children: `
431
+ @keyframes vox-fade-in { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
432
+ @keyframes vox-spin { to { transform: rotate(360deg); } }
433
+ @keyframes vox-ping { 0% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.15); opacity: 0; } 100% { transform: scale(1); opacity: 0; } }
434
+ .vox-widget { animation: vox-fade-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
435
+ .vox-spin { animation: vox-spin 1s linear infinite; }
436
+ .vox-visualizer { width: 100%; height: 50px; background: transparent; }
437
+ .vox-scrollbar::-webkit-scrollbar { display: none; }
438
+ .vox-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
439
+ ` }), jsxs("div", { style: { position: 'fixed', zIndex: 10000, display: 'flex', flexDirection: 'column', alignItems: position === 'bottom-left' ? 'flex-start' : 'flex-end', gap: '24px', ...positionStyles }, children: [isOpen && (jsxs("div", { className: "vox-widget", style: {
440
+ width: 'min(calc(100vw - 32px), 400px)', height: 'min(calc(100vh - 100px), 640px)',
441
+ background: bgColor, overflow: 'hidden', display: 'flex', flexDirection: 'column',
442
+ border: `1px solid ${borderColor}`, boxShadow: '0 30px 60px -15px rgba(0,0,0,0.5)'
443
+ }, children: [jsxs("div", { style: { height: '56px', background: bgColor, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px', flexShrink: 0, borderBottom: `1px solid ${borderColor}` }, children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '12px' }, children: [jsx("div", { style: { width: '32px', height: '32px', border: `1px solid ${borderColor}`, display: 'flex', alignItems: 'center', justifyContent: 'center', color: textColor }, children: jsx(MicIcon, {}) }), jsxs("div", { children: [jsx("h2", { style: { color: textColor, fontWeight: 900, fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.2em', margin: 0 }, children: agentName }), jsxs("p", { style: { color: mutedColor, fontSize: '9px', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', margin: 0, display: 'flex', alignItems: 'center' }, children: [jsx("span", { style: { width: '4px', height: '4px', background: textColor, borderRadius: '50%', marginRight: '8px' } }), "Active"] })] })] }), jsx("button", { onClick: handleClose, style: { background: 'none', border: 'none', color: mutedColor, cursor: 'pointer', padding: '4px' }, children: jsx(XIcon, {}) })] }), jsxs("div", { style: { display: 'flex', padding: '4px', background: widgetTheme === 'light' ? '#f4f4f5' : '#09090b', borderBottom: `1px solid ${borderColor}`, flexShrink: 0 }, children: [jsx("button", { onClick: () => handleTabChange('text'), style: {
444
+ flex: 1, padding: '12px', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.2em',
445
+ background: 'none', border: 'none', cursor: 'pointer', color: activeTab === 'text' ? textColor : mutedColor
446
+ }, children: "Text Interface" }), jsx("div", { style: { width: '1px', background: borderColor } }), jsx("button", { onClick: () => handleTabChange('voice'), style: {
447
+ flex: 1, padding: '12px', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.2em',
448
+ background: 'none', border: 'none', cursor: 'pointer', color: activeTab === 'voice' ? textColor : mutedColor
449
+ }, children: "Voice Protocol" })] }), jsx("div", { style: { flex: 1, overflow: 'hidden', position: 'relative', background: bgColor }, children: activeTab === 'text' ? (jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction, theme: widgetTheme })) : (isVoiceActive && jsx(VoiceInterface, { geminiApiKey: geminiApiKey, voiceName: voiceName, systemInstruction: systemInstruction, theme: widgetTheme, onEndCall: () => setIsVoiceActive(false) })) })] })), jsx("button", { onClick: isOpen ? handleClose : handleOpen, style: {
450
+ width: `${buttonSize}px`, height: `${buttonSize}px`, borderRadius: `${borderRadius}%`,
451
+ background: btnBg, color: btnIcon, border: 'none', cursor: 'pointer',
452
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
453
+ boxShadow: '0 4px 20px rgba(0,0,0,0.3)', transition: 'transform 0.2s'
454
+ }, children: isOpen ? jsx(XIcon, {}) : jsx(MessageIcon, {}) })] })] }));
227
455
  };
228
456
 
229
457
  export { VoxChat, VoxChat as default };