vox-ai-react 1.0.2 → 1.0.4

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,461 @@ 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 messageText = input; // Capture before clearing
127
+ const userMsg = { id: Date.now().toString(), role: 'user', text: messageText };
128
+ setMessages(prev => [...prev, userMsg]);
57
129
  setInput('');
58
130
  setIsLoading(true);
59
131
  try {
132
+ const history = messages.map(m => ({ role: m.role === 'assistant' ? 'model' : 'user', text: m.text }));
60
133
  const response = await fetch(`${apiUrl}/api/v1/chat`, {
61
134
  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
- })
135
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
136
+ body: JSON.stringify({ message: messageText, history, systemInstruction })
70
137
  });
138
+ if (!response.ok) {
139
+ const errorData = await response.json().catch(() => ({}));
140
+ throw new Error(errorData.error || 'API request failed');
141
+ }
71
142
  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]);
143
+ const botMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: ((_a = data.response) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'ERROR: NO RESPONSE' };
144
+ setMessages(prev => [...prev, botMsg]);
145
+ }
146
+ catch (err) {
147
+ console.error('VOX Chat Error:', err);
148
+ const errorMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: `ERROR: ${err.message || 'SIGNAL LOST. RETRY.'}` };
149
+ setMessages(prev => [...prev, errorMsg]);
89
150
  }
90
151
  finally {
91
152
  setIsLoading(false);
92
153
  }
93
154
  };
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',
155
+ const bgColor = theme === 'light' ? '#ffffff' : '#000000';
156
+ const textColor = theme === 'light' ? '#000000' : '#ffffff';
157
+ const borderColor = theme === 'light' ? '#a1a1aa' : '#52525b';
158
+ const mutedColor = theme === 'light' ? '#71717a' : '#a1a1aa';
159
+ 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: {
160
+ width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center',
161
+ border: `1px solid ${msg.role === 'user' ? textColor : borderColor}`,
162
+ background: msg.role === 'user' ? textColor : 'transparent', color: msg.role === 'user' ? bgColor : textColor
163
+ }, children: msg.role === 'user' ? jsxRuntime.jsx(UserIcon, {}) : jsxRuntime.jsx(BotIcon, {}) }), jsxRuntime.jsx("div", { style: {
164
+ padding: '12px 16px', fontSize: '11px', fontWeight: 700, letterSpacing: '0.05em', lineHeight: 1.6,
165
+ border: `1px solid ${borderColor}`, background: msg.role === 'user' ? (theme === 'light' ? '#f4f4f5' : '#27272a') : bgColor,
166
+ color: msg.role === 'user' ? textColor : mutedColor
167
+ }, 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: {
168
+ flex: 1, padding: '16px 24px', border: `1px solid ${borderColor}`, background: bgColor, color: textColor,
169
+ outline: 'none', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em'
170
+ } }), jsxRuntime.jsx("button", { type: "submit", disabled: !input.trim() || isLoading, style: {
171
+ padding: '16px 24px', background: textColor, color: bgColor, border: 'none', cursor: 'pointer',
172
+ fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em',
173
+ opacity: !input.trim() || isLoading ? 0.2 : 1
174
+ }, children: "EXECUTE" })] }) })] }));
175
+ };
176
+ // Voice Interface Component
177
+ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, theme, onEndCall }) => {
178
+ const [connectionState, setConnectionState] = react.useState(ConnectionState.DISCONNECTED);
179
+ const [errorMsg, setErrorMsg] = react.useState('');
180
+ const [isMuted, setIsMuted] = react.useState(false);
181
+ const [callDuration, setCallDuration] = react.useState(0);
182
+ const [isSpeaking, setIsSpeaking] = react.useState(false);
183
+ const [speakingIntensity, setSpeakingIntensity] = react.useState(0);
184
+ const callStartTimeRef = react.useRef(null);
185
+ const durationIntervalRef = react.useRef(null);
186
+ const inputAudioContextRef = react.useRef(null);
187
+ const outputAudioContextRef = react.useRef(null);
188
+ const inputAnalyserRef = react.useRef(null);
189
+ const outputAnalyserRef = react.useRef(null);
190
+ const outputGainRef = react.useRef(null);
191
+ const nextStartTimeRef = react.useRef(0);
192
+ const sourcesRef = react.useRef(new Set());
193
+ const speakingAnimationRef = react.useRef(null);
194
+ const sessionRef = react.useRef(null);
195
+ const streamRef = react.useRef(null);
196
+ const scriptProcessorRef = react.useRef(null);
197
+ const sourceNodeRef = react.useRef(null);
198
+ const cleanupAudio = react.useCallback(() => {
199
+ var _a, _b, _c, _d;
200
+ if (speakingAnimationRef.current) {
201
+ cancelAnimationFrame(speakingAnimationRef.current);
202
+ speakingAnimationRef.current = null;
203
+ }
204
+ setIsSpeaking(false);
205
+ setSpeakingIntensity(0);
206
+ sourcesRef.current.forEach(source => { try {
207
+ source.stop();
208
+ }
209
+ catch (e) { } });
210
+ sourcesRef.current.clear();
211
+ if (streamRef.current) {
212
+ streamRef.current.getTracks().forEach(track => { track.stop(); track.enabled = false; });
213
+ streamRef.current = null;
214
+ }
215
+ if (scriptProcessorRef.current) {
216
+ try {
217
+ scriptProcessorRef.current.disconnect();
218
+ }
219
+ catch (e) { }
220
+ scriptProcessorRef.current = null;
221
+ }
222
+ if (sourceNodeRef.current) {
223
+ try {
224
+ sourceNodeRef.current.disconnect();
225
+ }
226
+ catch (e) { }
227
+ sourceNodeRef.current = null;
228
+ }
229
+ if (outputGainRef.current) {
230
+ try {
231
+ outputGainRef.current.disconnect();
232
+ }
233
+ catch (e) { }
234
+ outputGainRef.current = null;
235
+ }
236
+ if (outputAnalyserRef.current) {
237
+ try {
238
+ outputAnalyserRef.current.disconnect();
239
+ }
240
+ catch (e) { }
241
+ outputAnalyserRef.current = null;
242
+ }
243
+ if (((_a = inputAudioContextRef.current) === null || _a === void 0 ? void 0 : _a.state) !== 'closed') {
244
+ try {
245
+ (_b = inputAudioContextRef.current) === null || _b === void 0 ? void 0 : _b.close();
246
+ }
247
+ catch (e) { }
248
+ }
249
+ if (((_c = outputAudioContextRef.current) === null || _c === void 0 ? void 0 : _c.state) !== 'closed') {
250
+ try {
251
+ (_d = outputAudioContextRef.current) === null || _d === void 0 ? void 0 : _d.close();
252
+ }
253
+ catch (e) { }
254
+ }
255
+ inputAudioContextRef.current = null;
256
+ outputAudioContextRef.current = null;
257
+ inputAnalyserRef.current = null;
258
+ sessionRef.current = null;
259
+ }, []);
260
+ const connect = async () => {
261
+ try {
262
+ setConnectionState(ConnectionState.CONNECTING);
263
+ setErrorMsg('');
264
+ callStartTimeRef.current = Date.now();
265
+ durationIntervalRef.current = setInterval(() => {
266
+ if (callStartTimeRef.current)
267
+ setCallDuration(Math.floor((Date.now() - callStartTimeRef.current) / 1000));
268
+ }, 1000);
269
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
270
+ const inputCtx = new AudioContextClass({ sampleRate: 16000 });
271
+ const outputCtx = new AudioContextClass({ sampleRate: 24000 });
272
+ inputAudioContextRef.current = inputCtx;
273
+ outputAudioContextRef.current = outputCtx;
274
+ const analyser = inputCtx.createAnalyser();
275
+ analyser.fftSize = 256;
276
+ inputAnalyserRef.current = analyser;
277
+ const outputAnalyser = outputCtx.createAnalyser();
278
+ outputAnalyser.fftSize = 256;
279
+ outputAnalyserRef.current = outputAnalyser;
280
+ const outputGain = outputCtx.createGain();
281
+ outputGain.gain.value = 1;
282
+ outputGainRef.current = outputGain;
283
+ outputGain.connect(outputAnalyser);
284
+ outputAnalyser.connect(outputCtx.destination);
285
+ const monitorSpeaking = () => {
286
+ if (!outputAnalyserRef.current)
287
+ return;
288
+ const dataArray = new Uint8Array(outputAnalyserRef.current.frequencyBinCount);
289
+ outputAnalyserRef.current.getByteFrequencyData(dataArray);
290
+ const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
291
+ const intensity = Math.min(average / 128, 1);
292
+ setIsSpeaking(intensity > 0.05);
293
+ setSpeakingIntensity(intensity);
294
+ speakingAnimationRef.current = requestAnimationFrame(monitorSpeaking);
295
+ };
296
+ monitorSpeaking();
297
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
298
+ streamRef.current = stream;
299
+ // Connect to Gemini Live API via WebSocket
300
+ const wsUrl = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${geminiApiKey}`;
301
+ const ws = new WebSocket(wsUrl);
302
+ sessionRef.current = ws;
303
+ ws.onopen = () => {
304
+ // Send setup message
305
+ const setupMsg = {
306
+ setup: {
307
+ model: 'models/gemini-2.5-flash-preview-native-audio-dialog',
308
+ generationConfig: { responseModalities: ['AUDIO'], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName } } } },
309
+ systemInstruction: { parts: [{ text: systemInstruction }] }
310
+ }
311
+ };
312
+ ws.send(JSON.stringify(setupMsg));
313
+ };
314
+ ws.onmessage = async (event) => {
315
+ var _a, _b, _c, _d, _e, _f;
316
+ const data = JSON.parse(event.data);
317
+ if (data.setupComplete) {
318
+ setConnectionState(ConnectionState.CONNECTED);
319
+ // Setup audio input pipeline
320
+ const source = inputCtx.createMediaStreamSource(stream);
321
+ sourceNodeRef.current = source;
322
+ const processor = inputCtx.createScriptProcessor(2048, 1, 1);
323
+ scriptProcessorRef.current = processor;
324
+ processor.onaudioprocess = (e) => {
325
+ if (isMuted || ws.readyState !== WebSocket.OPEN)
326
+ return;
327
+ const inputData = e.inputBuffer.getChannelData(0);
328
+ const pcmBlob = createBlob(inputData);
329
+ ws.send(JSON.stringify({ realtimeInput: { mediaChunks: [pcmBlob] } }));
330
+ };
331
+ source.connect(analyser);
332
+ source.connect(processor);
333
+ processor.connect(inputCtx.destination);
334
+ }
335
+ // Handle audio output
336
+ 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;
337
+ if (audioData) {
338
+ const ctx = outputAudioContextRef.current;
339
+ if (!ctx)
340
+ return;
341
+ nextStartTimeRef.current = Math.max(nextStartTimeRef.current, ctx.currentTime);
342
+ const audioBuffer = await decodeAudioData(decode(audioData), ctx, 24000, 1);
343
+ const source = ctx.createBufferSource();
344
+ source.buffer = audioBuffer;
345
+ if (outputGainRef.current)
346
+ source.connect(outputGainRef.current);
347
+ else
348
+ source.connect(ctx.destination);
349
+ source.addEventListener('ended', () => sourcesRef.current.delete(source));
350
+ source.start(nextStartTimeRef.current);
351
+ nextStartTimeRef.current += audioBuffer.duration;
352
+ sourcesRef.current.add(source);
353
+ }
354
+ if ((_f = data.serverContent) === null || _f === void 0 ? void 0 : _f.interrupted) {
355
+ sourcesRef.current.forEach(s => s.stop());
356
+ sourcesRef.current.clear();
357
+ nextStartTimeRef.current = 0;
358
+ }
359
+ };
360
+ ws.onclose = () => setConnectionState(ConnectionState.DISCONNECTED);
361
+ ws.onerror = () => {
362
+ setConnectionState(ConnectionState.ERROR);
363
+ setErrorMsg('Connection lost. Please try again.');
364
+ cleanupAudio();
365
+ };
366
+ }
367
+ catch (err) {
368
+ setConnectionState(ConnectionState.ERROR);
369
+ setErrorMsg(err.message || 'Failed to access microphone or connect.');
370
+ cleanupAudio();
371
+ }
372
+ };
373
+ const disconnect = react.useCallback(() => {
374
+ callStartTimeRef.current = null;
375
+ if (durationIntervalRef.current) {
376
+ clearInterval(durationIntervalRef.current);
377
+ durationIntervalRef.current = null;
378
+ }
379
+ setCallDuration(0);
380
+ if (sessionRef.current) {
381
+ try {
382
+ sessionRef.current.close();
383
+ }
384
+ catch (e) { }
385
+ }
386
+ cleanupAudio();
387
+ setConnectionState(ConnectionState.DISCONNECTED);
388
+ }, [cleanupAudio]);
389
+ react.useEffect(() => {
390
+ connect();
391
+ return () => disconnect();
392
+ }, []);
393
+ const bgColor = theme === 'light' ? '#ffffff' : '#000000';
394
+ const textColor = theme === 'light' ? '#000000' : '#ffffff';
395
+ const borderColor = theme === 'light' ? '#a1a1aa' : '#52525b';
396
+ const mutedColor = '#71717a';
397
+ 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: {
398
+ position: 'absolute', inset: 0, borderRadius: '50%', border: `1px solid ${textColor}`,
399
+ transform: `scale(${1 + speakingIntensity * 0.3})`, opacity: 0.2, animation: 'vox-ping 1s infinite'
400
+ } })), jsxRuntime.jsx("div", { style: {
401
+ width: '112px', height: '112px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
402
+ border: `1px solid ${connectionState === ConnectionState.CONNECTED ? textColor : borderColor}`,
403
+ 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',
404
+ transform: isSpeaking ? `scale(${1 + speakingIntensity * 0.08})` : 'scale(1)', transition: 'all 0.3s'
405
+ }, 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: {
406
+ padding: '12px 24px', border: `1px solid ${borderColor}`, background: bgColor, color: mutedColor, cursor: 'pointer',
407
+ fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em', display: 'flex', alignItems: 'center', gap: '8px'
408
+ }, children: [jsxRuntime.jsx(PhoneOffIcon, {}), "End Call"] }))] }));
409
+ };
410
+ // Main VoxChat Component
411
+ 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 }) => {
412
+ const [isOpen, setIsOpen] = react.useState(false);
413
+ const [activeTab, setActiveTab] = react.useState('text');
414
+ const [isVoiceActive, setIsVoiceActive] = react.useState(false);
415
+ // Invert theme for widget (black in light mode, white in dark mode)
416
+ const widgetTheme = theme === 'light' ? 'dark' : 'light';
417
+ const bgColor = widgetTheme === 'light' ? '#ffffff' : '#000000';
418
+ const textColor = widgetTheme === 'light' ? '#000000' : '#ffffff';
419
+ const borderColor = widgetTheme === 'light' ? '#a1a1aa' : '#52525b';
420
+ const mutedColor = '#71717a';
421
+ const handleOpen = () => { setIsOpen(true); onOpen === null || onOpen === void 0 ? void 0 : onOpen(); };
422
+ const handleClose = () => { setIsOpen(false); setIsVoiceActive(false); onClose === null || onClose === void 0 ? void 0 : onClose(); };
423
+ const handleTabChange = (tab) => {
424
+ if (tab === 'voice' && activeTab !== 'voice') {
425
+ setActiveTab('voice');
426
+ setIsVoiceActive(true);
427
+ }
428
+ else if (tab === 'text') {
429
+ setIsVoiceActive(false);
430
+ setActiveTab('text');
431
+ }
113
432
  };
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" }) }) }))] }));
433
+ const positionStyles = position === 'bottom-left'
434
+ ? { bottom: '16px', left: '16px' }
435
+ : { bottom: '16px', right: '16px' };
436
+ const btnBg = buttonColor || (theme === 'light' ? '#000000' : '#ffffff');
437
+ const btnIcon = iconColor || (theme === 'light' ? '#ffffff' : '#000000');
438
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("style", { children: `
439
+ @keyframes vox-fade-in { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
440
+ @keyframes vox-spin { to { transform: rotate(360deg); } }
441
+ @keyframes vox-ping { 0% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.15); opacity: 0; } 100% { transform: scale(1); opacity: 0; } }
442
+ .vox-widget { animation: vox-fade-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
443
+ .vox-spin { animation: vox-spin 1s linear infinite; }
444
+ .vox-visualizer { width: 100%; height: 50px; background: transparent; }
445
+ .vox-scrollbar::-webkit-scrollbar { display: none; }
446
+ .vox-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
447
+ ` }), 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: {
448
+ width: 'min(calc(100vw - 32px), 400px)', height: 'min(calc(100vh - 100px), 640px)',
449
+ background: bgColor, overflow: 'hidden', display: 'flex', flexDirection: 'column',
450
+ border: `1px solid ${borderColor}`, boxShadow: '0 30px 60px -15px rgba(0,0,0,0.5)'
451
+ }, 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: {
452
+ flex: 1, padding: '12px', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.2em',
453
+ background: 'none', border: 'none', cursor: 'pointer', color: activeTab === 'text' ? textColor : mutedColor
454
+ }, children: "Text Interface" }), jsxRuntime.jsx("div", { style: { width: '1px', background: borderColor } }), jsxRuntime.jsx("button", { onClick: () => handleTabChange('voice'), style: {
455
+ flex: 1, padding: '12px', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.2em',
456
+ background: 'none', border: 'none', cursor: 'pointer', color: activeTab === 'voice' ? textColor : mutedColor
457
+ }, 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: {
458
+ width: `${buttonSize}px`, height: `${buttonSize}px`, borderRadius: `${borderRadius}%`,
459
+ background: btnBg, color: btnIcon, border: 'none', cursor: 'pointer',
460
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
461
+ boxShadow: '0 4px 20px rgba(0,0,0,0.3)', transition: 'transform 0.2s'
462
+ }, children: isOpen ? jsxRuntime.jsx(XIcon, {}) : jsxRuntime.jsx(MessageIcon, {}) })] })] }));
231
463
  };
232
464
 
233
465
  exports.VoxChat = VoxChat;