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