vox-ai-react 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -37,16 +37,18 @@ async function decodeAudioData(arrayBuffer, ctx, sampleRate, numChannels) {
37
37
  }
38
38
  return audioBuffer;
39
39
  }
40
- // Icons - matching exact website icons
40
+ // Icons
41
41
  const MessageCircleIcon = () => (jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" }) }));
42
- const MicIcon = ({ size = 14 }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }));
43
- const MicOffIcon = ({ size = 40 }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22" }), jsx("path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" }), jsx("path", { d: "M5 10v2a7 7 0 0 0 12 5" }), jsx("path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33" }), jsx("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }), jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }));
44
- const XIcon = () => (jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M18 6 6 18" }), jsx("path", { d: "m6 6 12 12" })] }));
45
- const PhoneOffIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91" }), jsx("line", { x1: "22", x2: "2", y1: "2", y2: "22" })] }));
46
- const VoxLogoIcon = () => (jsxs("svg", { width: "18", height: "18", viewBox: "0 0 48 48", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M28.764 25.966c0-4.489-1.936-5.99-4.764-5.99s-4.764 1.501-4.764 5.99m-.55 0h.55v5.324h0h-.55a1.826 1.826 0 0 1-1.826-1.826v-1.672a1.826 1.826 0 0 1 1.826-1.826m10.628 5.324h-.55h0v-5.324h.55a1.826 1.826 0 0 1 1.826 1.826v1.672a1.826 1.826 0 0 1-1.826 1.826" }), jsx("path", { d: "M24 42.476L12.938 33.58V5.524L24 16.351" }), jsx("path", { d: "M24 42.476L9.125 35.557V7.501L24 16.351" }), jsx("path", { d: "M24 42.476L5.5 38.004V9.948L24 16.351m0 26.125l11.062-8.896V5.524L24 16.351" }), jsx("path", { d: "m24 42.476l14.875-6.919V7.501L24 16.351" }), jsx("path", { d: "m24 42.476l18.5-4.472V9.948L24 16.351" })] }));
42
+ const MicIcon = ({ size = 20 }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }));
43
+ const MicOffIcon = ({ size = 20 }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22" }), jsx("path", { d: "M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" }), jsx("path", { d: "M5 10v2a7 7 0 0 0 12 5" }), jsx("path", { d: "M15 9.34V5a3 3 0 0 0-5.68-1.33" }), jsx("path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12" }), jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }));
44
+ const XIcon = () => (jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M18 6 6 18" }), jsx("path", { d: "m6 6 12 12" })] }));
45
+ const PhoneOffIcon = () => (jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91" }), jsx("line", { x1: "22", x2: "2", y1: "2", y2: "22" })] }));
47
46
  const UserIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }), jsx("circle", { cx: "12", cy: "7", r: "4" })] }));
48
- const Loader2Icon = () => (jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { animation: 'vox-spin 1s linear infinite' }, children: jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
49
- const AlertCircleIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("circle", { cx: "12", cy: "12", r: "10" }), jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }), jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })] }));
47
+ const BotIcon = () => (jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M12 8V4H8" }), jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsx("path", { d: "M2 14h2" }), jsx("path", { d: "M20 14h2" }), jsx("path", { d: "M15 13v2" }), jsx("path", { d: "M9 13v2" })] }));
48
+ const SendIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), jsx("path", { d: "M22 2 11 13" })] }));
49
+ const PlusIcon = () => (jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M5 12h14" }), jsx("path", { d: "M12 5v14" })] }));
50
+ const Loader2Icon = () => (jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { animation: 'vox-spin 1s linear infinite' }, children: jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
51
+ const AlertCircleIcon = () => (jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("circle", { cx: "12", cy: "12", r: "10" }), jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }), jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })] }));
50
52
  // Visualizer Component
51
53
  const Visualizer = ({ analyser, isActive }) => {
52
54
  const canvasRef = useRef(null);
@@ -100,9 +102,9 @@ const Visualizer = ({ analyser, isActive }) => {
100
102
  return jsx("canvas", { ref: canvasRef, width: 300, height: 50, style: { width: '100%', height: '50px', background: 'transparent' } });
101
103
  };
102
104
  // Text Interface Component
103
- const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
105
+ const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction, logoUrl }) => {
104
106
  const [messages, setMessages] = useState([
105
- { id: 'welcome', role: 'assistant', text: `VOX INITIALIZED. GREETINGS. I AM ${agentName}. HOW MAY I ASSIST YOUR INQUIRY?` }
107
+ { id: 'welcome', role: 'assistant', text: `Hello! I'm ${agentName}. How can I help you today?` }
106
108
  ]);
107
109
  const [input, setInput] = useState('');
108
110
  const [isLoading, setIsLoading] = useState(false);
@@ -112,7 +114,6 @@ const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
112
114
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
113
115
  }, [messages]);
114
116
  const handleSend = async (e) => {
115
- var _a;
116
117
  e === null || e === void 0 ? void 0 : e.preventDefault();
117
118
  if (!input.trim() || isLoading)
118
119
  return;
@@ -133,36 +134,36 @@ const TextInterface = ({ apiKey, apiUrl, agentName, systemInstruction }) => {
133
134
  throw new Error(errorData.error || 'API request failed');
134
135
  }
135
136
  const data = await response.json();
136
- const botMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: ((_a = data.response) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'ERROR: NO RESPONSE' };
137
+ const botMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: data.response || 'Error: No response' };
137
138
  setMessages(prev => [...prev, botMsg]);
138
139
  }
139
140
  catch (err) {
140
141
  console.error('VOX Chat Error:', err);
141
- const errorMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: `ERROR: ${err.message || 'SIGNAL LOST. RETRY.'}` };
142
+ const errorMsg = { id: (Date.now() + 1).toString(), role: 'assistant', text: `Error: ${err.message || 'Connection lost. Please try again.'}` };
142
143
  setMessages(prev => [...prev, errorMsg]);
143
144
  }
144
145
  finally {
145
146
  setIsLoading(false);
146
147
  }
147
148
  };
148
- return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: '#000000' }, children: [jsxs("div", { ref: scrollRef, style: { flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [messages.map((msg) => (jsx("div", { style: { display: 'flex', justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start' }, children: jsxs("div", { style: { display: 'flex', flexDirection: msg.role === 'user' ? 'row-reverse' : 'row', alignItems: 'flex-start', gap: '8px', maxWidth: '85%' }, children: [jsx("div", { style: {
149
+ return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', background: '#000000', fontFamily: 'Outfit, sans-serif' }, children: [jsxs("div", { ref: scrollRef, style: { flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [messages.map((msg) => (jsx("div", { style: { display: 'flex', justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start' }, children: jsxs("div", { style: { display: 'flex', flexDirection: msg.role === 'user' ? 'row-reverse' : 'row', alignItems: 'flex-start', gap: '8px', maxWidth: '85%' }, children: [jsx("div", { style: {
149
150
  flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center',
150
151
  border: msg.role === 'user' ? '1px solid #ffffff' : 'none',
151
152
  background: msg.role === 'user' ? '#ffffff' : 'transparent',
152
153
  color: msg.role === 'user' ? '#000000' : '#ffffff'
153
- }, children: msg.role === 'user' ? jsx(UserIcon, {}) : jsx(VoxLogoIcon, {}) }), jsx("div", { style: {
154
- padding: '8px 12px', fontSize: '10px', fontWeight: 700, letterSpacing: '0.05em', lineHeight: 1.6,
155
- border: '1px solid #3f3f46', background: msg.role === 'user' ? '#27272a' : '#000000',
156
- color: msg.role === 'user' ? '#ffffff' : '#d4d4d8', borderRadius: '8px'
157
- }, children: msg.text })] }) }, msg.id))), isLoading && (jsx("div", { style: { display: 'flex', justifyContent: 'flex-start' }, children: jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', gap: '8px' }, children: [jsx("div", { style: { flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ffffff', opacity: 0.2 }, children: jsx(VoxLogoIcon, {}) }), jsx("div", { style: { padding: '12px 16px', border: '1px solid #3f3f46', background: '#000000', color: '#71717a', borderRadius: '8px' }, children: jsx(Loader2Icon, {}) })] }) }))] }), jsx("div", { style: { padding: '16px' }, children: jsxs("form", { onSubmit: handleSend, style: { display: 'flex', gap: '8px' }, children: [jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), placeholder: "INPUT COMMAND...", disabled: isLoading, style: {
158
- flex: 1, padding: '12px 16px', border: '1px solid #52525b', background: '#000000', color: '#ffffff',
159
- outline: 'none', fontSize: '9px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em',
160
- borderRadius: '8px'
161
- } }), jsx("button", { type: "submit", disabled: !input.trim() || isLoading, style: {
162
- padding: '12px 16px', background: '#ffffff', color: '#000000', border: 'none', cursor: 'pointer',
163
- fontSize: '9px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em',
164
- opacity: !input.trim() || isLoading ? 0.2 : 1, borderRadius: '8px'
165
- }, children: "EXECUTE" })] }) })] }));
154
+ }, children: msg.role === 'user' ? jsx(UserIcon, {}) : (logoUrl ? jsx("img", { src: logoUrl, alt: agentName, style: { width: '24px', height: '24px', objectFit: 'contain' } }) : jsx(BotIcon, {})) }), jsx("div", { style: {
155
+ padding: '8px 12px', fontSize: '14px', fontWeight: 500, lineHeight: 1.6,
156
+ border: '1px solid', borderRadius: '8px',
157
+ borderColor: msg.role === 'user' ? '#52525b' : '#3f3f46',
158
+ background: msg.role === 'user' ? '#27272a' : '#000000',
159
+ color: msg.role === 'user' ? '#ffffff' : '#d4d4d8'
160
+ }, children: msg.text })] }) }, msg.id))), isLoading && (jsx("div", { style: { display: 'flex', justifyContent: 'flex-start' }, children: jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', gap: '8px' }, children: [jsx("div", { style: { flexShrink: 0, width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ffffff', opacity: 0.2 }, children: logoUrl ? jsx("img", { src: logoUrl, alt: agentName, style: { width: '24px', height: '24px', objectFit: 'contain' } }) : jsx(BotIcon, {}) }), jsx("div", { style: { padding: '12px 16px', border: '1px solid #3f3f46', background: '#000000', borderRadius: '8px' }, children: jsx(Loader2Icon, {}) })] }) }))] }), jsx("div", { style: { padding: '16px' }, children: jsx("form", { onSubmit: handleSend, children: jsxs("div", { style: {
161
+ width: '100%', cursor: 'text', overflow: 'clip', padding: '6px', boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1)', border: '1px solid #3f3f46', borderRadius: '9999px',
162
+ display: 'grid', gridTemplateColumns: 'auto 1fr auto', gridTemplateRows: 'auto', gap: '4px', alignItems: 'center', background: 'rgba(0,0,0,0.5)'
163
+ }, children: [jsx("button", { type: "button", style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', borderRadius: '9999px', outline: 'none', border: 'none', background: 'transparent', cursor: 'pointer', color: '#71717a' }, children: jsx(PlusIcon, {}) }), jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter' && !e.shiftKey) {
164
+ e.preventDefault();
165
+ handleSend();
166
+ } }, placeholder: "Type your message...", disabled: isLoading, style: { width: '100%', minHeight: '32px', resize: 'none', border: 0, padding: 0, fontSize: '14px', outline: 'none', background: 'transparent', color: '#ffffff' } }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '4px' }, children: [jsx("button", { type: "button", style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', borderRadius: '9999px', outline: 'none', border: 'none', background: 'transparent', cursor: 'pointer', color: '#71717a' }, children: jsx(MicIcon, { size: 16 }) }), input.trim() && (jsx("button", { type: "submit", disabled: isLoading, style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', borderRadius: '9999px', border: 'none', background: '#ffffff', color: '#000000', cursor: 'pointer' }, children: jsx(SendIcon, {}) }))] })] }) }) })] }));
166
167
  };
167
168
  // Voice Interface Component
168
169
  const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall }) => {
@@ -170,30 +171,20 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
170
171
  const [errorMsg, setErrorMsg] = useState('');
171
172
  const [isMuted, setIsMuted] = useState(false);
172
173
  const [callDuration, setCallDuration] = useState(0);
173
- const [isSpeaking, setIsSpeaking] = useState(false);
174
- const [speakingIntensity, setSpeakingIntensity] = useState(0);
175
174
  const callStartTimeRef = useRef(null);
176
175
  const durationIntervalRef = useRef(null);
177
176
  const inputAudioContextRef = useRef(null);
178
177
  const outputAudioContextRef = useRef(null);
179
178
  const inputAnalyserRef = useRef(null);
180
- const outputAnalyserRef = useRef(null);
181
179
  const outputGainRef = useRef(null);
182
180
  const nextStartTimeRef = useRef(0);
183
181
  const sourcesRef = useRef(new Set());
184
- const speakingAnimationRef = useRef(null);
185
182
  const sessionRef = useRef(null);
186
183
  const streamRef = useRef(null);
187
184
  const scriptProcessorRef = useRef(null);
188
185
  const sourceNodeRef = useRef(null);
189
186
  const cleanupAudio = useCallback(() => {
190
187
  var _a, _b, _c, _d;
191
- if (speakingAnimationRef.current) {
192
- cancelAnimationFrame(speakingAnimationRef.current);
193
- speakingAnimationRef.current = null;
194
- }
195
- setIsSpeaking(false);
196
- setSpeakingIntensity(0);
197
188
  sourcesRef.current.forEach(source => { try {
198
189
  source.stop();
199
190
  }
@@ -224,13 +215,6 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
224
215
  catch (e) { }
225
216
  outputGainRef.current = null;
226
217
  }
227
- if (outputAnalyserRef.current) {
228
- try {
229
- outputAnalyserRef.current.disconnect();
230
- }
231
- catch (e) { }
232
- outputAnalyserRef.current = null;
233
- }
234
218
  if (((_a = inputAudioContextRef.current) === null || _a === void 0 ? void 0 : _a.state) !== 'closed') {
235
219
  try {
236
220
  (_b = inputAudioContextRef.current) === null || _b === void 0 ? void 0 : _b.close();
@@ -265,34 +249,16 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
265
249
  const analyser = inputCtx.createAnalyser();
266
250
  analyser.fftSize = 256;
267
251
  inputAnalyserRef.current = analyser;
268
- const outputAnalyser = outputCtx.createAnalyser();
269
- outputAnalyser.fftSize = 256;
270
- outputAnalyserRef.current = outputAnalyser;
271
252
  const outputGain = outputCtx.createGain();
272
253
  outputGain.gain.value = 1;
273
254
  outputGainRef.current = outputGain;
274
- outputGain.connect(outputAnalyser);
275
- outputAnalyser.connect(outputCtx.destination);
276
- const monitorSpeaking = () => {
277
- if (!outputAnalyserRef.current)
278
- return;
279
- const dataArray = new Uint8Array(outputAnalyserRef.current.frequencyBinCount);
280
- outputAnalyserRef.current.getByteFrequencyData(dataArray);
281
- const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
282
- const intensity = Math.min(average / 128, 1);
283
- setIsSpeaking(intensity > 0.05);
284
- setSpeakingIntensity(intensity);
285
- speakingAnimationRef.current = requestAnimationFrame(monitorSpeaking);
286
- };
287
- monitorSpeaking();
255
+ outputGain.connect(outputCtx.destination);
288
256
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
289
257
  streamRef.current = stream;
290
258
  const wsUrl = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${geminiApiKey}`;
291
- console.log('[VOX] Connecting to Gemini Live API...');
292
259
  const ws = new WebSocket(wsUrl);
293
260
  sessionRef.current = ws;
294
261
  ws.onopen = () => {
295
- console.log('[VOX] WebSocket connected, sending setup...');
296
262
  const setupMsg = {
297
263
  setup: {
298
264
  model: 'models/gemini-2.5-flash-native-audio-preview-09-2025',
@@ -322,9 +288,7 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
322
288
  return;
323
289
  }
324
290
  }
325
- console.log('[VOX] Received:', data);
326
291
  if (data.setupComplete) {
327
- console.log('[VOX] Setup complete, voice ready!');
328
292
  setConnectionState(ConnectionState.CONNECTED);
329
293
  const source = inputCtx.createMediaStreamSource(stream);
330
294
  sourceNodeRef.current = source;
@@ -365,19 +329,14 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
365
329
  nextStartTimeRef.current = 0;
366
330
  }
367
331
  };
368
- ws.onclose = (event) => {
369
- console.log('[VOX] WebSocket closed:', event.code, event.reason);
370
- setConnectionState(ConnectionState.DISCONNECTED);
371
- };
372
- ws.onerror = (error) => {
373
- console.error('[VOX] WebSocket error:', error);
332
+ ws.onclose = () => setConnectionState(ConnectionState.DISCONNECTED);
333
+ ws.onerror = () => {
374
334
  setConnectionState(ConnectionState.ERROR);
375
335
  setErrorMsg('Connection failed. Check your Gemini API key.');
376
336
  cleanupAudio();
377
337
  };
378
338
  }
379
339
  catch (err) {
380
- console.error('[VOX] Connection error:', err);
381
340
  setConnectionState(ConnectionState.ERROR);
382
341
  setErrorMsg(err.message || 'Failed to access microphone or connect.');
383
342
  cleanupAudio();
@@ -403,15 +362,24 @@ const VoiceInterface = ({ geminiApiKey, voiceName, systemInstruction, onEndCall
403
362
  connect();
404
363
  return () => disconnect();
405
364
  }, []);
406
- 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 && (jsxs(Fragment, { children: [jsx("div", { style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1px solid rgba(255,255,255,0.2)', transform: `scale(${1 + speakingIntensity * 0.3})`, animation: 'vox-ping 1s infinite' } }), jsx("div", { style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1px solid rgba(255,255,255,0.1)', transform: `scale(${1 + speakingIntensity * 0.5})`, transition: 'transform 0.1s ease-out' } })] })), jsx("div", { style: {
365
+ const toggleMute = () => {
366
+ setIsMuted(!isMuted);
367
+ if (streamRef.current)
368
+ streamRef.current.getAudioTracks().forEach(track => { track.enabled = isMuted; });
369
+ };
370
+ return (jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '32px', height: '100%', padding: '24px', fontFamily: 'Outfit, sans-serif' }, children: [jsxs("div", { style: { position: 'relative' }, children: [jsx("div", { style: {
407
371
  width: '112px', height: '112px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
408
372
  border: `1px solid ${connectionState === ConnectionState.CONNECTED ? '#ffffff' : '#52525b'}`,
409
- boxShadow: connectionState === ConnectionState.CONNECTED ? `0 0 ${30 + speakingIntensity * 40}px rgba(255,255,255,${0.15 + speakingIntensity * 0.25})` : 'none',
410
- transform: isSpeaking ? `scale(${1 + speakingIntensity * 0.08})` : 'scale(1)', transition: 'all 0.3s'
411
- }, children: connectionState === ConnectionState.CONNECTING ? (jsx("div", { style: { width: '40px', height: '40px', border: '2px solid #ffffff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'vox-spin 1s linear infinite' } })) : (jsx("div", { style: { padding: '20px', color: isMuted ? '#52525b' : '#ffffff' }, children: isMuted ? jsx(MicOffIcon, {}) : jsx(MicIcon, { size: 40 }) })) }), connectionState === ConnectionState.CONNECTED && (jsxs("span", { style: { position: 'absolute', bottom: '-4px', right: '-4px', display: 'flex', width: '12px', height: '12px' }, children: [jsx("span", { style: { position: 'absolute', display: 'inline-flex', width: '100%', height: '100%', borderRadius: '50%', background: '#ffffff', opacity: isSpeaking ? 1 : 0.75, animation: isSpeaking ? 'vox-ping 1s infinite' : 'none' } }), jsx("span", { style: { position: 'relative', display: 'inline-flex', borderRadius: '50%', width: '12px', height: '12px', background: '#ffffff' } })] }))] }), jsxs("div", { style: { width: '100%', maxWidth: '280px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [jsxs("p", { style: { textAlign: 'center', fontSize: '10px', fontWeight: 900, color: '#71717a', 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: '#ffffff' }, children: [Math.floor(callDuration / 60).toString().padStart(2, '0'), ":", (callDuration % 60).toString().padStart(2, '0')] })), jsx(Visualizer, { analyser: inputAnalyserRef.current, isActive: connectionState === ConnectionState.CONNECTED && !isMuted })] }), connectionState === ConnectionState.ERROR && (jsxs("div", { style: { display: 'flex', alignItems: 'center', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.1em', padding: '12px 16px', maxWidth: '280px', textAlign: 'center', color: '#71717a', background: '#18181b', border: '1px solid #3f3f46' }, children: [jsx(AlertCircleIcon, {}), jsx("span", { style: { marginLeft: '12px' }, children: errorMsg })] })), connectionState === ConnectionState.CONNECTED && (jsxs("button", { onClick: () => { disconnect(); onEndCall === null || onEndCall === void 0 ? void 0 : onEndCall(); }, style: {
412
- padding: '12px 24px', border: '1px solid #3f3f46', background: '#000000', color: '#71717a', cursor: 'pointer',
413
- fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.1em', display: 'flex', alignItems: 'center', gap: '8px'
414
- }, children: [jsx(PhoneOffIcon, {}), "End Call"] }))] }));
373
+ boxShadow: connectionState === ConnectionState.CONNECTED ? '0 0 30px rgba(255,255,255,0.15)' : 'none',
374
+ transition: 'all 0.3s'
375
+ }, children: connectionState === ConnectionState.CONNECTING ? (jsx("div", { style: { width: '40px', height: '40px', border: '2px solid #ffffff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'vox-spin 1s linear infinite' } })) : (jsx("div", { style: { padding: '20px', color: isMuted ? '#52525b' : '#ffffff' }, children: isMuted ? jsx(MicOffIcon, {}) : jsx(MicIcon, {}) })) }), connectionState === ConnectionState.CONNECTED && (jsxs("span", { style: { position: 'absolute', bottom: '-4px', right: '-4px', display: 'flex', width: '12px', height: '12px' }, children: [jsx("span", { style: { position: 'absolute', display: 'inline-flex', width: '100%', height: '100%', borderRadius: '50%', background: '#ffffff', opacity: 0.75 } }), jsx("span", { style: { position: 'relative', display: 'inline-flex', borderRadius: '50%', width: '12px', height: '12px', background: '#ffffff' } })] }))] }), jsxs("div", { style: { width: '100%', maxWidth: '280px', display: 'flex', flexDirection: 'column', gap: '16px' }, children: [jsxs("p", { style: { textAlign: 'center', fontSize: '14px', fontWeight: 600, color: '#71717a', height: '20px' }, children: [connectionState === ConnectionState.CONNECTING && 'Initializing...', connectionState === ConnectionState.CONNECTED && (isMuted ? 'Muted' : 'Listening...'), connectionState === ConnectionState.ERROR && 'Connection error', connectionState === ConnectionState.DISCONNECTED && 'Disconnected'] }), connectionState === ConnectionState.CONNECTED && callDuration > 0 && (jsxs("p", { style: { textAlign: 'center', fontSize: '11px', fontFamily: 'monospace', color: '#ffffff' }, children: [Math.floor(callDuration / 60).toString().padStart(2, '0'), ":", (callDuration % 60).toString().padStart(2, '0')] })), jsx(Visualizer, { analyser: inputAnalyserRef.current, isActive: connectionState === ConnectionState.CONNECTED && !isMuted })] }), connectionState === ConnectionState.ERROR && (jsxs("div", { style: { display: 'flex', alignItems: 'center', fontSize: '14px', fontWeight: 600, padding: '12px 16px', maxWidth: '280px', textAlign: 'center', color: '#71717a', background: '#18181b', border: '1px solid #3f3f46', borderRadius: '8px', gap: '12px' }, children: [jsx(AlertCircleIcon, {}), jsx("span", { children: errorMsg })] })), connectionState === ConnectionState.CONNECTED && (jsxs("div", { style: { display: 'flex', gap: '16px' }, children: [jsx("button", { onClick: toggleMute, style: {
376
+ padding: '16px', borderRadius: '50%', border: 'none', cursor: 'pointer', transition: 'all 0.2s',
377
+ background: isMuted ? '#27272a' : '#ffffff',
378
+ color: isMuted ? '#71717a' : '#000000'
379
+ }, children: isMuted ? jsx(MicOffIcon, {}) : jsx(MicIcon, {}) }), jsx("button", { onClick: () => { disconnect(); onEndCall === null || onEndCall === void 0 ? void 0 : onEndCall(); }, style: {
380
+ padding: '16px', borderRadius: '50%', border: 'none', cursor: 'pointer', transition: 'all 0.2s',
381
+ background: '#dc2626', color: '#ffffff'
382
+ }, children: jsx(PhoneOffIcon, {}) })] }))] }));
415
383
  };
416
384
  // Main VoxChat Component
417
385
  const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', agentName: propAgentName = 'VOX-01', voiceName: propVoiceName = 'Kore', systemInstruction: propSystemInstruction = 'You are a helpful AI assistant.', position = 'bottom-right', theme = 'dark', buttonColor, buttonSize = 56, borderRadius = 0, iconColor, onOpen, onClose, useServerConfig = false, configPollInterval = 10000 }) => {
@@ -419,11 +387,9 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
419
387
  const [isOpen, setIsOpen] = useState(false);
420
388
  const [activeTab, setActiveTab] = useState('text');
421
389
  const [isVoiceActive, setIsVoiceActive] = useState(false);
422
- // Server config state
423
390
  const [serverConfig, setServerConfig] = useState(null);
424
391
  const [configLoaded, setConfigLoaded] = useState(!useServerConfig);
425
392
  const [isInitialLoad, setIsInitialLoad] = useState(true);
426
- // Fetch config from server
427
393
  const fetchConfig = useCallback(() => {
428
394
  if (useServerConfig && apiKey && apiUrl) {
429
395
  fetch(`${apiUrl}/api/v1/config`, {
@@ -444,22 +410,16 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
444
410
  });
445
411
  }
446
412
  }, [useServerConfig, apiKey, apiUrl]);
447
- // Initial fetch and real-time polling (every 10 seconds)
448
413
  useEffect(() => {
449
414
  if (useServerConfig && apiKey && apiUrl) {
450
- // Fetch immediately
451
415
  fetchConfig();
452
- // Poll at specified interval for real-time updates
453
416
  const intervalId = setInterval(fetchConfig, configPollInterval);
454
- // Cleanup interval on unmount
455
417
  return () => clearInterval(intervalId);
456
418
  }
457
419
  }, [useServerConfig, apiKey, apiUrl, fetchConfig, configPollInterval]);
458
- // Use server config if available, otherwise use props
459
420
  const agentName = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.name) || propAgentName;
460
421
  const voiceName = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.voiceName) || propVoiceName;
461
422
  const systemInstruction = (serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.systemInstruction) || propSystemInstruction;
462
- // Widget style from server or props
463
423
  const finalButtonColor = ((_a = serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.widgetStyle) === null || _a === void 0 ? void 0 : _a.buttonColor) || buttonColor || (theme === 'light' ? '#000000' : '#ffffff');
464
424
  const finalButtonSize = ((_b = serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.widgetStyle) === null || _b === void 0 ? void 0 : _b.buttonSize) || buttonSize || 56;
465
425
  const finalBorderRadius = ((_c = serverConfig === null || serverConfig === void 0 ? void 0 : serverConfig.widgetStyle) === null || _c === void 0 ? void 0 : _c.borderRadius) !== undefined ? serverConfig.widgetStyle.borderRadius : (borderRadius !== undefined ? borderRadius : 50);
@@ -485,7 +445,7 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
485
445
  const positionStyles = finalPosition === 'bottom-left'
486
446
  ? { bottom: '16px', left: '16px' }
487
447
  : { bottom: '16px', right: '16px' };
488
- return (jsxs(Fragment, { children: [jsx("style", { children: `
448
+ return (jsxs(Fragment, { children: [jsx("link", { href: "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;900&display=swap", rel: "stylesheet" }), jsx("style", { children: `
489
449
  @keyframes vox-fade-in { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
490
450
  @keyframes vox-spin { to { transform: rotate(360deg); } }
491
451
  @keyframes vox-ping { 0% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.15); opacity: 0; } 100% { transform: scale(1); opacity: 0; } }
@@ -493,14 +453,17 @@ const VoxChat = ({ apiKey, geminiApiKey, apiUrl = 'https://your-server.com', age
493
453
  ` }), jsxs("div", { style: { position: 'fixed', zIndex: 10000, display: 'flex', flexDirection: 'column', alignItems: finalPosition === 'bottom-left' ? 'flex-start' : 'flex-end', gap: '24px', ...positionStyles }, children: [isOpen && (jsxs("div", { className: "vox-widget", style: {
494
454
  width: 'min(calc(100vw - 32px), 360px)', height: '550px', maxHeight: '550px',
495
455
  background: '#000000', overflow: 'hidden', display: 'flex', flexDirection: 'column',
496
- border: '1px solid #3f3f46', boxShadow: '0 20px 40px -10px rgba(0,0,0,0.5)', borderRadius: '12px'
497
- }, children: [jsxs("div", { style: { height: '48px', background: '#000000', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', flexShrink: 0, borderBottom: '1px solid #3f3f46' }, children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '10px' }, children: [jsx("div", { style: { width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.3s ease' }, children: finalLogoUrl ? (jsx("img", { src: finalLogoUrl, alt: agentName, style: { width: '28px', height: '28px', objectFit: 'contain', transition: 'opacity 0.3s ease' } }, finalLogoUrl)) : (jsx(MessageCircleIcon, {})) }), jsxs("div", { children: [jsx("h2", { style: { color: '#ffffff', fontWeight: 900, fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.15em', margin: 0, transition: 'all 0.3s ease' }, children: agentName }), jsxs("p", { style: { color: '#a1a1aa', fontSize: '8px', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', margin: 0, display: 'flex', alignItems: 'center' }, children: [jsx("span", { style: { width: '4px', height: '4px', background: '#ffffff', borderRadius: '50%', marginRight: '6px' } }), "Active"] })] })] }), jsx("button", { onClick: handleClose, style: { background: 'none', border: 'none', color: '#a1a1aa', cursor: 'pointer', padding: '4px', transition: 'color 0.2s' }, children: jsx(XIcon, {}) })] }), jsxs("div", { style: { display: 'flex', padding: '2px', background: '#18181b', borderBottom: '1px solid #3f3f46', flexShrink: 0 }, children: [jsx("button", { onClick: () => handleTabChange('text'), style: {
498
- flex: 1, padding: '8px', fontSize: '9px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.15em',
499
- background: 'none', border: 'none', cursor: 'pointer', color: activeTab === 'text' ? '#ffffff' : '#71717a', transition: 'color 0.2s'
500
- }, children: "Text Interface" }), jsx("div", { style: { width: '1px', background: '#3f3f46' } }), jsx("button", { onClick: () => handleTabChange('voice'), style: {
501
- flex: 1, padding: '8px', fontSize: '9px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.15em',
502
- background: 'none', border: 'none', cursor: 'pointer', color: activeTab === 'voice' ? '#ffffff' : '#71717a', transition: 'color 0.2s'
503
- }, children: "Voice Protocol" })] }), jsx("div", { style: { flex: 1, overflow: 'hidden', position: 'relative', background: '#000000' }, children: activeTab === 'text' ? (jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction })) : (isVoiceActive ? (jsx(VoiceInterface, { geminiApiKey: geminiApiKey, voiceName: voiceName, systemInstruction: systemInstruction, onEndCall: handleEndCall })) : (jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction }))) })] })), !isOpen && (!useServerConfig || configLoaded) && (jsx("button", { onClick: handleOpen, style: {
456
+ border: '1px solid #3f3f46', boxShadow: '0 20px 40px -10px rgba(0,0,0,0.5)', borderRadius: '12px',
457
+ transition: 'all 0.3s ease'
458
+ }, children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 16px', flexShrink: 0, borderBottom: '1px solid #3f3f46', background: '#000000', fontFamily: 'Outfit, sans-serif' }, children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '10px' }, children: [jsx("div", { style: { width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.3s ease' }, children: finalLogoUrl ? (jsx("img", { src: finalLogoUrl, alt: agentName, style: { width: '28px', height: '28px', objectFit: 'contain', transition: 'opacity 0.3s ease' } }, finalLogoUrl)) : (activeTab === 'voice' ? jsx(MicIcon, {}) : jsx(MessageCircleIcon, {})) }), jsxs("div", { children: [jsx("h2", { style: { color: '#ffffff', fontWeight: 700, fontSize: '14px', margin: 0, transition: 'all 0.3s ease' }, children: agentName }), jsxs("p", { style: { color: '#a1a1aa', fontSize: '12px', fontWeight: 500, margin: 0, display: 'flex', alignItems: 'center' }, children: [jsx("span", { style: { width: '6px', height: '6px', background: '#ffffff', borderRadius: '50%', marginRight: '6px' } }), "Active"] })] })] }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '2px', padding: '2px', borderRadius: '8px', background: '#18181b' }, children: [jsx("button", { onClick: () => handleTabChange('text'), style: {
459
+ padding: '6px 12px', fontSize: '12px', fontWeight: 600, borderRadius: '6px',
460
+ background: activeTab === 'text' ? '#27272a' : 'transparent', border: 'none', cursor: 'pointer', color: activeTab === 'text' ? '#ffffff' : '#71717a', transition: 'all 0.2s',
461
+ boxShadow: activeTab === 'text' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
462
+ }, children: "Text" }), jsx("button", { onClick: () => handleTabChange('voice'), style: {
463
+ padding: '6px 12px', fontSize: '12px', fontWeight: 600, borderRadius: '6px',
464
+ background: activeTab === 'voice' ? '#27272a' : 'transparent', border: 'none', cursor: 'pointer', color: activeTab === 'voice' ? '#ffffff' : '#71717a', transition: 'all 0.2s',
465
+ boxShadow: activeTab === 'voice' ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
466
+ }, children: "Voice" })] }), jsx("button", { onClick: handleClose, style: { background: 'none', border: 'none', color: '#a1a1aa', cursor: 'pointer', padding: '4px', transition: 'color 0.2s' }, children: jsx(XIcon, {}) })] }), jsx("div", { style: { flex: 1, overflow: 'hidden', position: 'relative', background: '#000000' }, children: activeTab === 'text' ? (jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction, logoUrl: finalLogoUrl })) : (isVoiceActive ? (jsx(VoiceInterface, { geminiApiKey: geminiApiKey, voiceName: voiceName, systemInstruction: systemInstruction, onEndCall: handleEndCall })) : (jsx(TextInterface, { apiKey: apiKey, apiUrl: apiUrl, agentName: agentName, systemInstruction: systemInstruction, logoUrl: finalLogoUrl }))) })] })), !isOpen && (!useServerConfig || configLoaded) && (jsx("button", { onClick: handleOpen, style: {
504
467
  width: `${finalButtonSize}px`, height: `${finalButtonSize}px`,
505
468
  borderRadius: `${finalBorderRadius}px`,
506
469
  background: finalButtonColor, color: finalIconColor, border: 'none', cursor: 'pointer',