ai-input-react 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,633 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/hooks/useAiInput.ts
7
+ var DEFAULT_OPTIONS = {
8
+ cooldownMs: 1e3,
9
+ maxRequests: 10,
10
+ windowMs: 6e4
11
+ };
12
+ function useRateLimiter(options = {}) {
13
+ const config = { ...DEFAULT_OPTIONS, ...options };
14
+ const requestTimestamps = react.useRef([]);
15
+ const [cooldownEnd, setCooldownEnd] = react.useState(0);
16
+ const [cooldownRemaining, setCooldownRemaining] = react.useState(0);
17
+ const [, forceUpdate] = react.useState({});
18
+ const cleanupAndCount = react.useCallback(() => {
19
+ const now = Date.now();
20
+ const windowStart = now - config.windowMs;
21
+ requestTimestamps.current = requestTimestamps.current.filter(
22
+ (ts) => ts > windowStart
23
+ );
24
+ return config.maxRequests - requestTimestamps.current.length;
25
+ }, [config.windowMs, config.maxRequests]);
26
+ react.useEffect(() => {
27
+ if (cooldownEnd <= Date.now()) {
28
+ setCooldownRemaining(0);
29
+ return;
30
+ }
31
+ const interval = setInterval(() => {
32
+ const remaining = Math.max(0, cooldownEnd - Date.now());
33
+ setCooldownRemaining(remaining);
34
+ if (remaining === 0) {
35
+ clearInterval(interval);
36
+ }
37
+ }, 100);
38
+ return () => clearInterval(interval);
39
+ }, [cooldownEnd]);
40
+ const canRequest = react.useCallback(() => {
41
+ const now = Date.now();
42
+ if (now < cooldownEnd) {
43
+ return false;
44
+ }
45
+ return cleanupAndCount() > 0;
46
+ }, [cooldownEnd, cleanupAndCount]);
47
+ const recordRequest = react.useCallback(() => {
48
+ const now = Date.now();
49
+ requestTimestamps.current.push(now);
50
+ const newCooldownEnd = now + config.cooldownMs;
51
+ setCooldownEnd(newCooldownEnd);
52
+ setCooldownRemaining(config.cooldownMs);
53
+ forceUpdate({});
54
+ }, [config.cooldownMs]);
55
+ const reset = react.useCallback(() => {
56
+ requestTimestamps.current = [];
57
+ setCooldownEnd(0);
58
+ setCooldownRemaining(0);
59
+ forceUpdate({});
60
+ }, []);
61
+ return {
62
+ canRequest: canRequest(),
63
+ cooldownRemaining,
64
+ requestsRemaining: cleanupAndCount(),
65
+ recordRequest,
66
+ reset
67
+ };
68
+ }
69
+ var DEFAULT_OPTIONS2 = {
70
+ maxDurationMs: 6e4,
71
+ // 1 minute
72
+ mimeTypes: ["audio/webm", "audio/mp4", "audio/ogg", "audio/wav"]
73
+ };
74
+ function getSupportedMimeType(preferredTypes) {
75
+ if (typeof MediaRecorder === "undefined") {
76
+ return null;
77
+ }
78
+ for (const mimeType of preferredTypes) {
79
+ if (MediaRecorder.isTypeSupported(mimeType)) {
80
+ return mimeType;
81
+ }
82
+ }
83
+ return "";
84
+ }
85
+ function useAudioRecorder(options = {}) {
86
+ const config = { ...DEFAULT_OPTIONS2, ...options };
87
+ const [isRecording, setIsRecording] = react.useState(false);
88
+ const [duration, setDuration] = react.useState(0);
89
+ const [audioBlob, setAudioBlob] = react.useState(null);
90
+ const [error, setError] = react.useState(null);
91
+ const [audioLevels, setAudioLevels] = react.useState([]);
92
+ const mediaRecorderRef = react.useRef(null);
93
+ const streamRef = react.useRef(null);
94
+ const chunksRef = react.useRef([]);
95
+ const startTimeRef = react.useRef(0);
96
+ const durationIntervalRef = react.useRef(null);
97
+ const maxDurationTimeoutRef = react.useRef(null);
98
+ const audioContextRef = react.useRef(null);
99
+ const analyserRef = react.useRef(null);
100
+ const animationFrameRef = react.useRef(null);
101
+ const isSupported = typeof navigator !== "undefined" && "mediaDevices" in navigator && "getUserMedia" in navigator.mediaDevices && typeof MediaRecorder !== "undefined";
102
+ const updateAudioLevels = react.useCallback(() => {
103
+ if (!analyserRef.current) return;
104
+ const analyser = analyserRef.current;
105
+ const bufferLength = analyser.frequencyBinCount;
106
+ const dataArray = new Uint8Array(bufferLength);
107
+ analyser.getByteFrequencyData(dataArray);
108
+ const bars = 12;
109
+ const step = Math.floor(bufferLength / bars);
110
+ const levels = [];
111
+ for (let i = 0; i < bars; i++) {
112
+ let sum = 0;
113
+ for (let j = 0; j < step; j++) {
114
+ sum += dataArray[i * step + j];
115
+ }
116
+ levels.push(sum / step / 255);
117
+ }
118
+ setAudioLevels(levels);
119
+ animationFrameRef.current = requestAnimationFrame(updateAudioLevels);
120
+ }, []);
121
+ const cleanup = react.useCallback(() => {
122
+ if (durationIntervalRef.current) {
123
+ clearInterval(durationIntervalRef.current);
124
+ durationIntervalRef.current = null;
125
+ }
126
+ if (maxDurationTimeoutRef.current) {
127
+ clearTimeout(maxDurationTimeoutRef.current);
128
+ maxDurationTimeoutRef.current = null;
129
+ }
130
+ if (animationFrameRef.current) {
131
+ cancelAnimationFrame(animationFrameRef.current);
132
+ animationFrameRef.current = null;
133
+ }
134
+ if (audioContextRef.current) {
135
+ audioContextRef.current.close();
136
+ audioContextRef.current = null;
137
+ }
138
+ if (streamRef.current) {
139
+ streamRef.current.getTracks().forEach((track) => track.stop());
140
+ streamRef.current = null;
141
+ }
142
+ analyserRef.current = null;
143
+ mediaRecorderRef.current = null;
144
+ chunksRef.current = [];
145
+ setAudioLevels([]);
146
+ }, []);
147
+ const stopRecording = react.useCallback(() => {
148
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
149
+ mediaRecorderRef.current.stop();
150
+ }
151
+ setIsRecording(false);
152
+ }, []);
153
+ const startRecording = react.useCallback(async () => {
154
+ if (!isSupported) {
155
+ setError(new Error("Audio recording is not supported in this browser"));
156
+ return;
157
+ }
158
+ setError(null);
159
+ setAudioBlob(null);
160
+ setDuration(0);
161
+ setAudioLevels([]);
162
+ chunksRef.current = [];
163
+ try {
164
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
165
+ streamRef.current = stream;
166
+ const audioContext = new AudioContext();
167
+ audioContextRef.current = audioContext;
168
+ const source = audioContext.createMediaStreamSource(stream);
169
+ const analyser = audioContext.createAnalyser();
170
+ analyser.fftSize = 256;
171
+ analyser.smoothingTimeConstant = 0.8;
172
+ source.connect(analyser);
173
+ analyserRef.current = analyser;
174
+ const mimeType = getSupportedMimeType(config.mimeTypes);
175
+ const mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
176
+ mediaRecorderRef.current = mediaRecorder;
177
+ mediaRecorder.ondataavailable = (event) => {
178
+ if (event.data.size > 0) {
179
+ chunksRef.current.push(event.data);
180
+ }
181
+ };
182
+ mediaRecorder.onstop = () => {
183
+ const blob = new Blob(chunksRef.current, {
184
+ type: mimeType || "audio/webm"
185
+ });
186
+ setAudioBlob(blob);
187
+ if (config.onRecordingComplete) {
188
+ config.onRecordingComplete(blob);
189
+ }
190
+ cleanup();
191
+ };
192
+ mediaRecorder.onerror = () => {
193
+ setError(new Error("Recording error occurred"));
194
+ setIsRecording(false);
195
+ cleanup();
196
+ };
197
+ mediaRecorder.start(100);
198
+ startTimeRef.current = Date.now();
199
+ setIsRecording(true);
200
+ updateAudioLevels();
201
+ durationIntervalRef.current = setInterval(() => {
202
+ setDuration(Date.now() - startTimeRef.current);
203
+ }, 100);
204
+ maxDurationTimeoutRef.current = setTimeout(() => {
205
+ stopRecording();
206
+ }, config.maxDurationMs);
207
+ } catch (err) {
208
+ const errorMessage = err instanceof Error ? err.message : "Failed to access microphone";
209
+ setError(new Error(errorMessage));
210
+ cleanup();
211
+ }
212
+ }, [isSupported, config.mimeTypes, config.maxDurationMs, config.onRecordingComplete, cleanup, stopRecording, updateAudioLevels]);
213
+ const cancelRecording = react.useCallback(() => {
214
+ cleanup();
215
+ setIsRecording(false);
216
+ setDuration(0);
217
+ setAudioBlob(null);
218
+ }, [cleanup]);
219
+ const reset = react.useCallback(() => {
220
+ cleanup();
221
+ setIsRecording(false);
222
+ setDuration(0);
223
+ setAudioBlob(null);
224
+ setError(null);
225
+ setAudioLevels([]);
226
+ }, [cleanup]);
227
+ react.useEffect(() => {
228
+ return () => {
229
+ cleanup();
230
+ };
231
+ }, [cleanup]);
232
+ return {
233
+ isRecording,
234
+ isSupported,
235
+ duration,
236
+ audioBlob,
237
+ audioLevels,
238
+ error,
239
+ startRecording,
240
+ stopRecording,
241
+ cancelRecording,
242
+ reset
243
+ };
244
+ }
245
+
246
+ // src/hooks/useAiInput.ts
247
+ var DEFAULT_RATE_LIMIT = {
248
+ cooldownMs: 1e3,
249
+ maxRequests: 10,
250
+ windowMs: 6e4
251
+ };
252
+ var DEFAULT_AUDIO_CONFIG = {
253
+ maxDurationMs: 6e4,
254
+ mimeTypes: ["audio/webm", "audio/mp4", "audio/ogg", "audio/wav"]
255
+ };
256
+ function useAiInput(options) {
257
+ const {
258
+ send,
259
+ sendAudio,
260
+ rateLimit = {},
261
+ audioConfig = {},
262
+ onSuccess,
263
+ onError,
264
+ onTranscription
265
+ } = options;
266
+ const rateLimitConfig = { ...DEFAULT_RATE_LIMIT, ...rateLimit };
267
+ const audioConfigMerged = { ...DEFAULT_AUDIO_CONFIG, ...audioConfig };
268
+ const [state, setState] = react.useState("idle");
269
+ const [text, setText] = react.useState("");
270
+ const [error, setError] = react.useState(null);
271
+ const [result, setResult] = react.useState(null);
272
+ const pendingAudioSubmitRef = react.useRef(false);
273
+ const rateLimiter = useRateLimiter(rateLimitConfig);
274
+ const audioRecorder = useAudioRecorder({
275
+ ...audioConfigMerged
276
+ });
277
+ react.useEffect(() => {
278
+ if (!rateLimiter.canRequest && state === "idle") {
279
+ setState("rate-limited");
280
+ } else if (rateLimiter.canRequest && state === "rate-limited") {
281
+ setState("idle");
282
+ }
283
+ }, [rateLimiter.canRequest, state]);
284
+ react.useEffect(() => {
285
+ if (audioRecorder.isRecording && state !== "recording") {
286
+ setState("recording");
287
+ }
288
+ }, [audioRecorder.isRecording, state]);
289
+ react.useEffect(() => {
290
+ if (audioRecorder.error) {
291
+ setError(audioRecorder.error);
292
+ setState("error");
293
+ onError?.(audioRecorder.error);
294
+ }
295
+ }, [audioRecorder.error, onError]);
296
+ const submitText = react.useCallback(async () => {
297
+ if (!text.trim() || !rateLimiter.canRequest) {
298
+ return;
299
+ }
300
+ setState("loading");
301
+ setError(null);
302
+ rateLimiter.recordRequest();
303
+ try {
304
+ const response = await send(text);
305
+ setResult(response);
306
+ setState("success");
307
+ onSuccess?.(response);
308
+ setText("");
309
+ } catch (err) {
310
+ const error2 = err instanceof Error ? err : new Error("Request failed");
311
+ setError(error2);
312
+ setState("error");
313
+ onError?.(error2);
314
+ }
315
+ }, [text, rateLimiter, send, onSuccess, onError]);
316
+ const submitAudio = react.useCallback(async (blob) => {
317
+ if (!rateLimiter.canRequest) {
318
+ return;
319
+ }
320
+ setState("loading");
321
+ setError(null);
322
+ rateLimiter.recordRequest();
323
+ try {
324
+ const sendFn = sendAudio || send;
325
+ const response = await sendFn(blob);
326
+ setResult(response);
327
+ setState("success");
328
+ onSuccess?.(response);
329
+ if (onTranscription && response && typeof response === "object") {
330
+ const res = response;
331
+ const transcriptionText = res.text || res.transcription || res.transcript;
332
+ if (typeof transcriptionText === "string") {
333
+ setText(transcriptionText);
334
+ onTranscription(transcriptionText);
335
+ }
336
+ }
337
+ } catch (err) {
338
+ const error2 = err instanceof Error ? err : new Error("Request failed");
339
+ setError(error2);
340
+ setState("error");
341
+ onError?.(error2);
342
+ }
343
+ }, [rateLimiter, send, sendAudio, onSuccess, onError, onTranscription]);
344
+ react.useEffect(() => {
345
+ if (pendingAudioSubmitRef.current && audioRecorder.audioBlob && !audioRecorder.isRecording) {
346
+ pendingAudioSubmitRef.current = false;
347
+ submitAudio(audioRecorder.audioBlob);
348
+ }
349
+ }, [audioRecorder.audioBlob, audioRecorder.isRecording, submitAudio]);
350
+ const startRecording = react.useCallback(async () => {
351
+ if (!rateLimiter.canRequest) {
352
+ return;
353
+ }
354
+ await audioRecorder.startRecording();
355
+ }, [rateLimiter.canRequest, audioRecorder]);
356
+ const stopRecording = react.useCallback(() => {
357
+ pendingAudioSubmitRef.current = true;
358
+ audioRecorder.stopRecording();
359
+ }, [audioRecorder]);
360
+ const cancelRecording = react.useCallback(() => {
361
+ audioRecorder.cancelRecording();
362
+ setState("idle");
363
+ }, [audioRecorder]);
364
+ const submit = react.useCallback(() => {
365
+ if (audioRecorder.isRecording) {
366
+ stopRecording();
367
+ } else if (text.trim()) {
368
+ submitText();
369
+ }
370
+ }, [audioRecorder.isRecording, text, stopRecording, submitText]);
371
+ const reset = react.useCallback(() => {
372
+ setState("idle");
373
+ setText("");
374
+ setError(null);
375
+ setResult(null);
376
+ rateLimiter.reset();
377
+ audioRecorder.reset();
378
+ }, [rateLimiter, audioRecorder]);
379
+ const canSubmit = rateLimiter.canRequest && state !== "loading" && (audioRecorder.isRecording || text.trim().length > 0);
380
+ return {
381
+ // State
382
+ state,
383
+ error,
384
+ result,
385
+ // Text
386
+ text,
387
+ setText,
388
+ submit,
389
+ canSubmit,
390
+ // Audio
391
+ isRecording: audioRecorder.isRecording,
392
+ startRecording,
393
+ stopRecording,
394
+ cancelRecording,
395
+ recordingDuration: audioRecorder.duration,
396
+ maxRecordingDuration: audioConfigMerged.maxDurationMs,
397
+ audioLevels: audioRecorder.audioLevels,
398
+ // Rate limiting
399
+ cooldownRemaining: rateLimiter.cooldownRemaining,
400
+ requestsRemaining: rateLimiter.requestsRemaining,
401
+ // Utils
402
+ reset
403
+ };
404
+ }
405
+ function formatDuration(ms) {
406
+ const seconds = Math.floor(ms / 1e3);
407
+ const minutes = Math.floor(seconds / 60);
408
+ const remainingSeconds = seconds % 60;
409
+ return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
410
+ }
411
+ function Waveform({ levels, className = "" }) {
412
+ const bars = levels.length > 0 ? levels : Array(16).fill(0.1);
413
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex items-center justify-center gap-1 h-10 ${className}`, children: bars.map((level, i) => /* @__PURE__ */ jsxRuntime.jsx(
414
+ "div",
415
+ {
416
+ className: "w-1.5 bg-amber-500 rounded-full transition-all duration-75",
417
+ style: {
418
+ height: `${Math.max(6, level * 40)}px`
419
+ }
420
+ },
421
+ i
422
+ )) });
423
+ }
424
+ function MicIcon({ className = "" }) {
425
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 256 256", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M128,176a48.05,48.05,0,0,0,48-48V64a48,48,0,0,0-96,0v64A48.05,48.05,0,0,0,128,176ZM96,64a32,32,0,0,1,64,0v64a32,32,0,0,1-64,0Zm40,143.6V232a8,8,0,0,1-16,0V207.6A80.11,80.11,0,0,1,48,128a8,8,0,0,1,16,0,64,64,0,0,0,128,0,8,8,0,0,1,16,0A80.11,80.11,0,0,1,136,207.6Z" }) });
426
+ }
427
+ function ArrowUpIcon({ className = "" }) {
428
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 256 256", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M205.66,117.66a8,8,0,0,1-11.32,0L136,59.31V216a8,8,0,0,1-16,0V59.31L61.66,117.66a8,8,0,0,1-11.32-11.32l72-72a8,8,0,0,1,11.32,0l72,72A8,8,0,0,1,205.66,117.66Z" }) });
429
+ }
430
+ function StopIcon({ className = "" }) {
431
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 256 256", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,160H56V56H200V200Z" }) });
432
+ }
433
+ function XIcon({ className = "" }) {
434
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 256 256", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" }) });
435
+ }
436
+ function Spinner({ className = "" }) {
437
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: `animate-spin ${className}`, viewBox: "0 0 24 24", fill: "none", children: [
438
+ /* @__PURE__ */ jsxRuntime.jsx(
439
+ "circle",
440
+ {
441
+ className: "opacity-25",
442
+ cx: "12",
443
+ cy: "12",
444
+ r: "10",
445
+ stroke: "currentColor",
446
+ strokeWidth: "4"
447
+ }
448
+ ),
449
+ /* @__PURE__ */ jsxRuntime.jsx(
450
+ "path",
451
+ {
452
+ className: "opacity-75",
453
+ fill: "currentColor",
454
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
455
+ }
456
+ )
457
+ ] });
458
+ }
459
+ function DefaultUI({
460
+ text,
461
+ setText,
462
+ submit,
463
+ canSubmit,
464
+ state,
465
+ error,
466
+ isRecording,
467
+ startRecording,
468
+ stopRecording,
469
+ cancelRecording,
470
+ recordingDuration,
471
+ maxRecordingDuration,
472
+ audioLevels,
473
+ cooldownRemaining,
474
+ placeholder = "Ask anything...",
475
+ disabled = false
476
+ }) {
477
+ const isLoading = state === "loading";
478
+ const isRateLimited = state === "rate-limited";
479
+ const hasError = state === "error";
480
+ const handleKeyDown = (e) => {
481
+ if (e.key === "Enter" && !e.shiftKey && canSubmit && !isRecording) {
482
+ e.preventDefault();
483
+ submit();
484
+ }
485
+ };
486
+ const handleInput = (e) => {
487
+ setText(e.target.value);
488
+ e.target.style.height = "auto";
489
+ e.target.style.height = `${Math.min(Math.max(e.target.scrollHeight, 56), 200)}px`;
490
+ };
491
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full", children: /* @__PURE__ */ jsxRuntime.jsxs(
492
+ "div",
493
+ {
494
+ className: `
495
+ bg-zinc-900 border border-zinc-800 rounded-xl
496
+ focus-within:ring-1 focus-within:ring-amber-500/50 focus-within:border-amber-500/50
497
+ transition-all duration-200
498
+ ${disabled ? "opacity-50" : ""}
499
+ `,
500
+ children: [
501
+ /* @__PURE__ */ jsxRuntime.jsx(
502
+ "textarea",
503
+ {
504
+ value: text,
505
+ onChange: handleInput,
506
+ onKeyDown: handleKeyDown,
507
+ placeholder: isRecording ? "Listening..." : placeholder,
508
+ disabled: disabled || isLoading || isRateLimited,
509
+ rows: 1,
510
+ className: `
511
+ w-full px-4 pt-4 pb-2
512
+ bg-transparent text-zinc-100 placeholder:text-zinc-500
513
+ focus:outline-none
514
+ disabled:cursor-not-allowed
515
+ resize-none
516
+ min-h-[56px]
517
+ `,
518
+ style: { height: "56px" }
519
+ }
520
+ ),
521
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-3 pb-3 pt-1", children: [
522
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-3", children: isRecording ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
523
+ /* @__PURE__ */ jsxRuntime.jsx(
524
+ "button",
525
+ {
526
+ onClick: cancelRecording,
527
+ disabled,
528
+ className: "p-2 text-zinc-400 hover:text-zinc-200 transition-colors",
529
+ "aria-label": "Cancel recording",
530
+ children: /* @__PURE__ */ jsxRuntime.jsx(XIcon, { className: "h-5 w-5" })
531
+ }
532
+ ),
533
+ /* @__PURE__ */ jsxRuntime.jsx(Waveform, { levels: audioLevels }),
534
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-zinc-400 font-mono", children: formatDuration(recordingDuration) })
535
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm", children: [
536
+ hasError && error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-400", children: error.message }),
537
+ isRateLimited && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-amber-400", children: [
538
+ "Wait ",
539
+ formatDuration(cooldownRemaining)
540
+ ] })
541
+ ] }) }),
542
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: isRecording ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
543
+ "button",
544
+ {
545
+ onClick: stopRecording,
546
+ disabled,
547
+ className: `
548
+ p-2.5 rounded-full
549
+ bg-red-500 hover:bg-red-600
550
+ text-white
551
+ transition-colors
552
+ disabled:opacity-50 disabled:cursor-not-allowed
553
+ `,
554
+ "aria-label": "Stop recording",
555
+ children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: "h-5 w-5" })
556
+ }
557
+ ) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
558
+ /* @__PURE__ */ jsxRuntime.jsx(
559
+ "button",
560
+ {
561
+ onClick: startRecording,
562
+ disabled: disabled || isLoading || isRateLimited,
563
+ className: `
564
+ p-2 text-zinc-400 hover:text-zinc-200
565
+ transition-colors
566
+ disabled:opacity-50 disabled:cursor-not-allowed
567
+ `,
568
+ "aria-label": "Start recording",
569
+ children: /* @__PURE__ */ jsxRuntime.jsx(MicIcon, { className: "h-5 w-5" })
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsxRuntime.jsx(
573
+ "button",
574
+ {
575
+ onClick: submit,
576
+ disabled: !canSubmit || disabled,
577
+ className: `
578
+ p-2.5 rounded-full
579
+ transition-colors
580
+ disabled:opacity-50 disabled:cursor-not-allowed
581
+ ${canSubmit ? "bg-amber-500 hover:bg-amber-600 text-zinc-900" : "bg-zinc-700 text-zinc-500"}
582
+ `,
583
+ "aria-label": "Send message",
584
+ children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Spinner, { className: "h-5 w-5" }) : /* @__PURE__ */ jsxRuntime.jsx(ArrowUpIcon, { className: "h-5 w-5" })
585
+ }
586
+ )
587
+ ] }) })
588
+ ] })
589
+ ]
590
+ }
591
+ ) });
592
+ }
593
+ function AiInput({
594
+ send,
595
+ sendAudio,
596
+ rateLimit,
597
+ audioConfig,
598
+ onSuccess,
599
+ onError,
600
+ onTranscription,
601
+ children,
602
+ placeholder,
603
+ className,
604
+ disabled = false
605
+ }) {
606
+ const inputState = useAiInput({
607
+ send,
608
+ sendAudio,
609
+ rateLimit,
610
+ audioConfig,
611
+ onSuccess,
612
+ onError,
613
+ onTranscription
614
+ });
615
+ if (children) {
616
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children(inputState) });
617
+ }
618
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-full ${className || ""}`, children: /* @__PURE__ */ jsxRuntime.jsx(
619
+ DefaultUI,
620
+ {
621
+ ...inputState,
622
+ placeholder,
623
+ disabled
624
+ }
625
+ ) });
626
+ }
627
+
628
+ exports.AiInput = AiInput;
629
+ exports.useAiInput = useAiInput;
630
+ exports.useAudioRecorder = useAudioRecorder;
631
+ exports.useRateLimiter = useRateLimiter;
632
+ //# sourceMappingURL=index.js.map
633
+ //# sourceMappingURL=index.js.map