expo-ai-core 1.0.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.cjs ADDED
@@ -0,0 +1,1345 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactNative = require('react-native');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/hooks/useAIChat.ts
8
+
9
+ // src/utils/helpers.ts
10
+ function createId(prefix = "msg") {
11
+ const randomPart = Math.random().toString(36).slice(2, 10);
12
+ const timePart = Date.now().toString(36);
13
+ if (typeof globalThis.crypto !== "undefined" && "randomUUID" in globalThis.crypto) {
14
+ return `${prefix}_${globalThis.crypto.randomUUID()}`;
15
+ }
16
+ return `${prefix}_${timePart}_${randomPart}`;
17
+ }
18
+ function trimContent(value) {
19
+ return value.trim();
20
+ }
21
+ function isNonEmptyString(value) {
22
+ return typeof value === "string" && value.trim().length > 0;
23
+ }
24
+ function toConversationMessages(messages, systemPrompt) {
25
+ const conversation = [];
26
+ if (systemPrompt && systemPrompt.trim()) {
27
+ conversation.push({ role: "system", content: systemPrompt.trim() });
28
+ }
29
+ for (const message of messages) {
30
+ conversation.push({ role: message.role, content: message.content });
31
+ }
32
+ return conversation;
33
+ }
34
+ function mergeAssistantText(previous, nextChunk) {
35
+ if (!nextChunk) {
36
+ return previous;
37
+ }
38
+ return `${previous}${nextChunk}`;
39
+ }
40
+ function safeJsonParse(input, fallback) {
41
+ try {
42
+ return JSON.parse(input);
43
+ } catch {
44
+ return fallback;
45
+ }
46
+ }
47
+ function clamp(value, min, max) {
48
+ return Math.min(Math.max(value, min), max);
49
+ }
50
+ function createLogger(enabled) {
51
+ const prefix = "[expo-ai-core]";
52
+ return {
53
+ log: (...args) => {
54
+ if (enabled) {
55
+ console.log(prefix, ...args);
56
+ }
57
+ },
58
+ warn: (...args) => {
59
+ if (enabled) {
60
+ console.warn(prefix, ...args);
61
+ }
62
+ },
63
+ error: (...args) => {
64
+ if (enabled) {
65
+ console.error(prefix, ...args);
66
+ }
67
+ }
68
+ };
69
+ }
70
+ function buildDefaultCacheKey(provider, model) {
71
+ return `expo-ai-core:${provider}:${model ?? "default"}`;
72
+ }
73
+ function toPlainText(value) {
74
+ if (typeof value === "string") {
75
+ return value;
76
+ }
77
+ if (Array.isArray(value)) {
78
+ return value.map(toPlainText).join("");
79
+ }
80
+ if (value && typeof value === "object") {
81
+ const record = value;
82
+ if (typeof record.text === "string") {
83
+ return record.text;
84
+ }
85
+ if (typeof record.content === "string") {
86
+ return record.content;
87
+ }
88
+ }
89
+ return "";
90
+ }
91
+
92
+ // src/providers/shared.ts
93
+ function createRequestController(signal, timeoutMs) {
94
+ const controller = new AbortController();
95
+ let timeoutId = null;
96
+ const abortFromExternal = () => {
97
+ if (!controller.signal.aborted) {
98
+ controller.abort(signal?.reason ?? new Error("Request aborted"));
99
+ }
100
+ };
101
+ if (signal) {
102
+ if (signal.aborted) {
103
+ abortFromExternal();
104
+ } else {
105
+ signal.addEventListener("abort", abortFromExternal, { once: true });
106
+ }
107
+ }
108
+ if (typeof timeoutMs === "number" && timeoutMs > 0) {
109
+ timeoutId = setTimeout(() => {
110
+ if (!controller.signal.aborted) {
111
+ controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));
112
+ }
113
+ }, timeoutMs);
114
+ }
115
+ return {
116
+ signal: controller.signal,
117
+ abort: (reason) => controller.abort(reason),
118
+ cleanup: () => {
119
+ if (timeoutId) {
120
+ clearTimeout(timeoutId);
121
+ }
122
+ if (signal) {
123
+ signal.removeEventListener("abort", abortFromExternal);
124
+ }
125
+ }
126
+ };
127
+ }
128
+ function getLogger(debug) {
129
+ return createLogger(debug);
130
+ }
131
+ function buildTextResult(content, raw) {
132
+ return { content, raw };
133
+ }
134
+ function createProvider(provider) {
135
+ return provider;
136
+ }
137
+ function readOpenAIContent(payload) {
138
+ const choice = payload?.choices?.[0];
139
+ return choice?.message?.content ?? choice?.delta?.content ?? "";
140
+ }
141
+ function readGeminiContent(payload) {
142
+ const candidate = payload?.candidates?.[0];
143
+ const parts = candidate?.content?.parts ?? [];
144
+ return parts.map((part) => typeof part?.text === "string" ? part.text : "").join("");
145
+ }
146
+ async function readStreamingBody(response, onChunk, signal) {
147
+ if (response.body && "getReader" in response.body) {
148
+ const reader = response.body.getReader();
149
+ const decoder = new TextDecoder();
150
+ while (true) {
151
+ if (signal?.aborted) {
152
+ await reader.cancel().catch(() => void 0);
153
+ break;
154
+ }
155
+ const { done, value } = await reader.read();
156
+ if (done) {
157
+ break;
158
+ }
159
+ if (value) {
160
+ onChunk(decoder.decode(value, { stream: true }));
161
+ }
162
+ }
163
+ const finalChunk = decoder.decode();
164
+ if (finalChunk) {
165
+ onChunk(finalChunk);
166
+ }
167
+ return;
168
+ }
169
+ const text = await response.text();
170
+ onChunk(text);
171
+ }
172
+ function consumeSseLines(chunk, buffer, onData) {
173
+ const combined = `${buffer}${chunk}`;
174
+ const lines = combined.split(/\r?\n/);
175
+ const nextBuffer = lines.pop() ?? "";
176
+ for (const line of lines) {
177
+ const trimmed = line.trim();
178
+ if (!trimmed) {
179
+ continue;
180
+ }
181
+ if (trimmed.startsWith("data:")) {
182
+ onData(trimmed.slice(5).trim());
183
+ continue;
184
+ }
185
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
186
+ onData(trimmed);
187
+ }
188
+ }
189
+ return nextBuffer;
190
+ }
191
+ function appendToken(current, token) {
192
+ return `${current}${token}`;
193
+ }
194
+
195
+ // src/providers/gemini.ts
196
+ var DEFAULT_GEMINI_MODEL = "gemini-2.5-flash";
197
+ var GEMINI_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
198
+ function mapGeminiMessages(messages) {
199
+ return messages.map((message) => ({
200
+ role: message.role === "assistant" ? "model" : "user",
201
+ parts: [{ text: message.content }]
202
+ }));
203
+ }
204
+ async function requestGemini(options, request, handlers) {
205
+ const logger = getLogger(options.debug);
206
+ const controller = createRequestController(
207
+ request.signal,
208
+ request.timeoutMs ?? options.timeoutMs
209
+ );
210
+ const model = options.model ?? DEFAULT_GEMINI_MODEL;
211
+ const url = `${options.baseUrl ?? GEMINI_ENDPOINT}/${model}:${request.stream ? "streamGenerateContent" : "generateContent"}?key=${encodeURIComponent(options.apiKey)}`;
212
+ const contents = mapGeminiMessages(request.messages);
213
+ const body = {
214
+ ...options.systemPrompt ? { systemInstruction: { parts: [{ text: options.systemPrompt }] } } : null,
215
+ contents,
216
+ generationConfig: {
217
+ temperature: 0.7
218
+ }
219
+ };
220
+ try {
221
+ const response = await fetch(url, {
222
+ method: "POST",
223
+ signal: controller.signal,
224
+ headers: {
225
+ "Content-Type": "application/json"
226
+ },
227
+ body: JSON.stringify(body)
228
+ });
229
+ if (!response.ok) {
230
+ const errorText = await response.text().catch(() => "");
231
+ throw new Error(
232
+ errorText || `Gemini request failed with status ${response.status}`
233
+ );
234
+ }
235
+ if (!request.stream) {
236
+ const payload = await response.json();
237
+ const content = readGeminiContent(payload);
238
+ return buildTextResult(content, payload);
239
+ }
240
+ let collected = "";
241
+ let buffer = "";
242
+ await readStreamingBody(
243
+ response,
244
+ (chunk) => {
245
+ buffer = consumeSseLines(chunk, buffer, (data) => {
246
+ if (data === "[DONE]") {
247
+ return;
248
+ }
249
+ const payload = typeof data === "string" ? JSON.parse(data) : data;
250
+ const token = readGeminiContent(payload);
251
+ if (token) {
252
+ collected = appendToken(collected, token);
253
+ handlers?.onToken?.(token);
254
+ }
255
+ });
256
+ },
257
+ controller.signal
258
+ );
259
+ return buildTextResult(collected);
260
+ } catch (error) {
261
+ logger.error("Gemini request failed", error);
262
+ throw error;
263
+ } finally {
264
+ controller.cleanup();
265
+ }
266
+ }
267
+ function createGeminiProvider(options) {
268
+ return createProvider({
269
+ name: "gemini",
270
+ sendMessage(request) {
271
+ return requestGemini(options, { ...request, stream: false });
272
+ },
273
+ streamMessage(request) {
274
+ return requestGemini(
275
+ options,
276
+ { ...request, stream: true },
277
+ { onToken: request.onToken }
278
+ );
279
+ }
280
+ });
281
+ }
282
+
283
+ // src/providers/openai.ts
284
+ var DEFAULT_OPENAI_MODEL = "gpt-4o-mini";
285
+ var OPENAI_ENDPOINT = "https://api.openai.com/v1/chat/completions";
286
+ async function requestOpenAI(options, request, handlers) {
287
+ const logger = getLogger(options.debug);
288
+ const controller = createRequestController(
289
+ request.signal,
290
+ request.timeoutMs ?? options.timeoutMs
291
+ );
292
+ const messages = options.systemPrompt ? [{ role: "system", content: options.systemPrompt }, ...request.messages] : request.messages;
293
+ const body = {
294
+ model: options.model ?? DEFAULT_OPENAI_MODEL,
295
+ messages,
296
+ stream: Boolean(request.stream)
297
+ };
298
+ try {
299
+ const response = await fetch(options.baseUrl ?? OPENAI_ENDPOINT, {
300
+ method: "POST",
301
+ signal: controller.signal,
302
+ headers: {
303
+ Authorization: `Bearer ${options.apiKey}`,
304
+ "Content-Type": "application/json"
305
+ },
306
+ body: JSON.stringify(body)
307
+ });
308
+ if (!response.ok) {
309
+ const errorText = await response.text().catch(() => "");
310
+ throw new Error(
311
+ errorText || `OpenAI request failed with status ${response.status}`
312
+ );
313
+ }
314
+ if (!request.stream) {
315
+ const payload = await response.json();
316
+ const content = readOpenAIContent(payload);
317
+ return buildTextResult(content, payload);
318
+ }
319
+ let collected = "";
320
+ let buffer = "";
321
+ await readStreamingBody(
322
+ response,
323
+ (chunk) => {
324
+ buffer = consumeSseLines(chunk, buffer, (data) => {
325
+ if (data === "[DONE]") {
326
+ return;
327
+ }
328
+ const payload = typeof data === "string" ? JSON.parse(data) : data;
329
+ const token = readOpenAIContent(payload);
330
+ if (token) {
331
+ collected = `${collected}${token}`;
332
+ handlers?.onToken?.(token);
333
+ }
334
+ });
335
+ },
336
+ controller.signal
337
+ );
338
+ return buildTextResult(collected);
339
+ } catch (error) {
340
+ logger.error("OpenAI request failed", error);
341
+ throw error;
342
+ } finally {
343
+ controller.cleanup();
344
+ }
345
+ }
346
+ function createOpenAIProvider(options) {
347
+ return createProvider({
348
+ name: "openai",
349
+ sendMessage(request) {
350
+ return requestOpenAI(options, { ...request, stream: false });
351
+ },
352
+ streamMessage(request) {
353
+ return requestOpenAI(
354
+ options,
355
+ { ...request, stream: true },
356
+ { onToken: request.onToken }
357
+ );
358
+ }
359
+ });
360
+ }
361
+
362
+ // src/utils/cache.ts
363
+ var memoryStore = /* @__PURE__ */ new Map();
364
+ var asyncStoragePromise = null;
365
+ async function resolveStorage() {
366
+ if (!asyncStoragePromise) {
367
+ asyncStoragePromise = import('@react-native-async-storage/async-storage').then((module) => module.default ?? module).catch(() => null);
368
+ }
369
+ return asyncStoragePromise;
370
+ }
371
+ async function readRaw(key) {
372
+ const storage = await resolveStorage();
373
+ if (storage) {
374
+ return storage.getItem(key);
375
+ }
376
+ return memoryStore.get(key) ?? null;
377
+ }
378
+ async function writeRaw(key, value) {
379
+ const storage = await resolveStorage();
380
+ if (storage) {
381
+ await storage.setItem(key, value);
382
+ return;
383
+ }
384
+ memoryStore.set(key, value);
385
+ }
386
+ async function deleteRaw(key) {
387
+ const storage = await resolveStorage();
388
+ if (storage) {
389
+ await storage.removeItem(key);
390
+ return;
391
+ }
392
+ memoryStore.delete(key);
393
+ }
394
+ async function loadCache(key) {
395
+ const raw = await readRaw(key);
396
+ if (!raw) {
397
+ return null;
398
+ }
399
+ return safeJsonParse(raw, null);
400
+ }
401
+ async function saveCache(key, snapshot) {
402
+ await writeRaw(key, JSON.stringify(snapshot));
403
+ }
404
+ async function clearCache(key) {
405
+ await deleteRaw(key);
406
+ }
407
+
408
+ // src/hooks/useAIChat.ts
409
+ function createChatProvider(options) {
410
+ if (options.provider === "gemini") {
411
+ return createGeminiProvider({
412
+ apiKey: options.apiKey,
413
+ model: options.model,
414
+ systemPrompt: options.systemPrompt,
415
+ timeoutMs: options.timeoutMs,
416
+ debug: options.debug
417
+ });
418
+ }
419
+ return createOpenAIProvider({
420
+ apiKey: options.apiKey,
421
+ model: options.model,
422
+ systemPrompt: options.systemPrompt,
423
+ timeoutMs: options.timeoutMs,
424
+ debug: options.debug
425
+ });
426
+ }
427
+ function useAIChat(options) {
428
+ const {
429
+ provider,
430
+ apiKey,
431
+ model,
432
+ systemPrompt,
433
+ cacheKey = buildDefaultCacheKey(provider, model),
434
+ initialMessages = [],
435
+ timeoutMs = 3e4,
436
+ enableCache = true,
437
+ debug
438
+ } = options;
439
+ const providerInstance = react.useMemo(
440
+ () => createChatProvider({
441
+ provider,
442
+ apiKey,
443
+ model,
444
+ systemPrompt,
445
+ timeoutMs,
446
+ debug
447
+ }),
448
+ [provider, apiKey, model, systemPrompt, timeoutMs, debug]
449
+ );
450
+ const [messages, setMessages] = react.useState(initialMessages);
451
+ const [input, setInput] = react.useState("");
452
+ const [isLoading, setIsLoading] = react.useState(false);
453
+ const [error, setError] = react.useState(null);
454
+ const messagesRef = react.useRef(messages);
455
+ const inputRef = react.useRef(input);
456
+ const abortRef = react.useRef(null);
457
+ const assistantIdRef = react.useRef(null);
458
+ const partialRef = react.useRef("");
459
+ const loadingRef = react.useRef(false);
460
+ const streamFlushScheduledRef = react.useRef(false);
461
+ react.useEffect(() => {
462
+ messagesRef.current = messages;
463
+ }, [messages]);
464
+ react.useEffect(() => {
465
+ inputRef.current = input;
466
+ }, [input]);
467
+ react.useEffect(() => {
468
+ let mounted = true;
469
+ async function restore() {
470
+ if (!enableCache) {
471
+ return;
472
+ }
473
+ const snapshot = await loadCache(cacheKey);
474
+ if (!mounted || !snapshot) {
475
+ return;
476
+ }
477
+ if (snapshot.messages.length > 0 && messagesRef.current.length === 0) {
478
+ setMessages(snapshot.messages);
479
+ }
480
+ if (typeof snapshot.input === "string" && !inputRef.current) {
481
+ setInput(snapshot.input);
482
+ }
483
+ }
484
+ void restore();
485
+ return () => {
486
+ mounted = false;
487
+ };
488
+ }, [cacheKey, enableCache]);
489
+ react.useEffect(() => {
490
+ if (!enableCache) {
491
+ return;
492
+ }
493
+ void saveCache(cacheKey, {
494
+ messages,
495
+ input,
496
+ updatedAt: Date.now()
497
+ });
498
+ }, [cacheKey, enableCache, messages, input]);
499
+ react.useEffect(() => {
500
+ return () => {
501
+ abortRef.current?.abort();
502
+ };
503
+ }, []);
504
+ const flushPartial = react.useCallback((value) => {
505
+ partialRef.current = value;
506
+ if (streamFlushScheduledRef.current) {
507
+ return;
508
+ }
509
+ streamFlushScheduledRef.current = true;
510
+ requestAnimationFrame(() => {
511
+ streamFlushScheduledRef.current = false;
512
+ const assistantId = assistantIdRef.current;
513
+ if (!assistantId) {
514
+ return;
515
+ }
516
+ setMessages(
517
+ (current) => current.map(
518
+ (message) => message.id === assistantId ? { ...message, content: partialRef.current, status: "streaming" } : message
519
+ )
520
+ );
521
+ });
522
+ }, []);
523
+ const stopGenerating = react.useCallback(() => {
524
+ abortRef.current?.abort();
525
+ abortRef.current = null;
526
+ loadingRef.current = false;
527
+ setIsLoading(false);
528
+ }, []);
529
+ const sendMessage = react.useCallback(
530
+ async (messageText) => {
531
+ const text = trimContent(messageText ?? inputRef.current);
532
+ if (!text || loadingRef.current) {
533
+ return null;
534
+ }
535
+ const userMessage = {
536
+ id: createId("user"),
537
+ role: "user",
538
+ content: text,
539
+ createdAt: Date.now(),
540
+ status: "sent"
541
+ };
542
+ const assistantMessage = {
543
+ id: createId("assistant"),
544
+ role: "assistant",
545
+ content: "",
546
+ createdAt: Date.now(),
547
+ status: "streaming"
548
+ };
549
+ assistantIdRef.current = assistantMessage.id;
550
+ partialRef.current = "";
551
+ loadingRef.current = true;
552
+ setError(null);
553
+ setInput("");
554
+ setIsLoading(true);
555
+ setMessages((current) => [
556
+ ...current,
557
+ userMessage,
558
+ assistantMessage
559
+ ]);
560
+ const controller = new AbortController();
561
+ abortRef.current = controller;
562
+ try {
563
+ const conversation = [...messagesRef.current, userMessage];
564
+ const result = await providerInstance.streamMessage({
565
+ messages: conversation.map((message) => ({
566
+ role: message.role,
567
+ content: message.content
568
+ })),
569
+ signal: controller.signal,
570
+ timeoutMs,
571
+ onToken: (token) => {
572
+ flushPartial(`${partialRef.current}${token}`);
573
+ }
574
+ });
575
+ const content = result.content || partialRef.current;
576
+ setMessages((current) => {
577
+ const assistantId = assistantIdRef.current;
578
+ if (!assistantId) {
579
+ return current;
580
+ }
581
+ return current.map(
582
+ (message) => message.id === assistantId ? { ...message, content, status: "sent" } : message
583
+ );
584
+ });
585
+ const completedAssistantMessage = {
586
+ ...assistantMessage,
587
+ content,
588
+ status: "sent"
589
+ };
590
+ return completedAssistantMessage;
591
+ } catch (caughtError) {
592
+ const message = caughtError instanceof Error ? caughtError.message : "Failed to send message";
593
+ setError(message);
594
+ setMessages((current) => {
595
+ const assistantId = assistantIdRef.current;
596
+ if (!assistantId) {
597
+ return current;
598
+ }
599
+ return current.map(
600
+ (item) => item.id === assistantId ? {
601
+ ...item,
602
+ content: partialRef.current,
603
+ status: "error",
604
+ error: message
605
+ } : item
606
+ );
607
+ });
608
+ return null;
609
+ } finally {
610
+ loadingRef.current = false;
611
+ setIsLoading(false);
612
+ abortRef.current = null;
613
+ assistantIdRef.current = null;
614
+ }
615
+ },
616
+ [flushPartial, providerInstance, timeoutMs]
617
+ );
618
+ const clearMessages = react.useCallback(() => {
619
+ setMessages([]);
620
+ setError(null);
621
+ if (enableCache) {
622
+ void clearCache(cacheKey);
623
+ }
624
+ }, [cacheKey, enableCache]);
625
+ const retryLastMessage = react.useCallback(async () => {
626
+ const lastUserEntry = [...messagesRef.current].map((message, index) => ({ message, index })).reverse().find((entry) => entry.message.role === "user");
627
+ if (!lastUserEntry) {
628
+ return null;
629
+ }
630
+ const lastUserIndex = lastUserEntry.index;
631
+ const nextMessages = messagesRef.current.slice(0, lastUserIndex);
632
+ const content = lastUserEntry.message.content;
633
+ setMessages(nextMessages);
634
+ messagesRef.current = nextMessages;
635
+ return sendMessage(content);
636
+ }, [sendMessage]);
637
+ return {
638
+ messages,
639
+ input,
640
+ setInput,
641
+ sendMessage,
642
+ isLoading,
643
+ error,
644
+ stopGenerating,
645
+ clearMessages,
646
+ retryLastMessage
647
+ };
648
+ }
649
+ function useAIVoice(options = {}) {
650
+ const [transcript, setTranscript] = react.useState("");
651
+ const [isListening, setIsListening] = react.useState(false);
652
+ const [recordingUri, setRecordingUri] = react.useState(null);
653
+ const [error, setError] = react.useState(null);
654
+ const recognitionRef = react.useRef(null);
655
+ const recordingRef = react.useRef(null);
656
+ const cleanupRef = react.useRef(null);
657
+ const clearTranscript = react.useCallback(() => {
658
+ setTranscript("");
659
+ setError(null);
660
+ }, []);
661
+ const speak = react.useCallback(
662
+ async (text) => {
663
+ if (!text.trim()) {
664
+ return;
665
+ }
666
+ try {
667
+ const speechModule = await import('expo-speech').catch(() => null);
668
+ const speech = speechModule?.default ?? speechModule;
669
+ if (speech?.speak) {
670
+ speech.speak(text, {
671
+ rate: options.speechRate ?? 1,
672
+ pitch: options.speechPitch ?? 1
673
+ });
674
+ return;
675
+ }
676
+ if (typeof globalThis !== "undefined" && "speechSynthesis" in globalThis) {
677
+ const utterance = new SpeechSynthesisUtterance(text);
678
+ utterance.rate = options.speechRate ?? 1;
679
+ utterance.pitch = options.speechPitch ?? 1;
680
+ globalThis.speechSynthesis.speak(utterance);
681
+ }
682
+ } catch (caughtError) {
683
+ setError(
684
+ caughtError instanceof Error ? caughtError.message : "Failed to speak text"
685
+ );
686
+ }
687
+ },
688
+ [options.speechPitch, options.speechRate]
689
+ );
690
+ const stopListening = react.useCallback(async () => {
691
+ const recognition = recognitionRef.current;
692
+ if (recognition) {
693
+ recognition.stop?.();
694
+ recognitionRef.current = null;
695
+ cleanupRef.current?.();
696
+ cleanupRef.current = null;
697
+ setIsListening(false);
698
+ return;
699
+ }
700
+ const recording = recordingRef.current;
701
+ if (recording) {
702
+ try {
703
+ await recording.stopAndUnloadAsync();
704
+ setRecordingUri(recording.getURI?.() ?? null);
705
+ } catch (caughtError) {
706
+ setError(
707
+ caughtError instanceof Error ? caughtError.message : "Failed to stop recording"
708
+ );
709
+ } finally {
710
+ recordingRef.current = null;
711
+ setIsListening(false);
712
+ }
713
+ }
714
+ }, []);
715
+ const startListening = react.useCallback(async () => {
716
+ setError(null);
717
+ clearTranscript();
718
+ const SpeechRecognition = globalThis.SpeechRecognition ?? globalThis.webkitSpeechRecognition ?? null;
719
+ if (SpeechRecognition) {
720
+ const recognition = new SpeechRecognition();
721
+ recognition.lang = options.language ?? "en-US";
722
+ recognition.continuous = options.continuous ?? false;
723
+ recognition.interimResults = options.interimResults ?? true;
724
+ recognition.maxAlternatives = 1;
725
+ recognition.onresult = (event) => {
726
+ let nextTranscript = "";
727
+ for (let index = 0; index < event.results.length; index += 1) {
728
+ const result = event.results[index];
729
+ nextTranscript += result[0]?.transcript ?? "";
730
+ }
731
+ setTranscript(nextTranscript.trim());
732
+ };
733
+ recognition.onerror = (event) => {
734
+ setError(
735
+ event?.error ? String(event.error) : "Speech recognition failed"
736
+ );
737
+ setIsListening(false);
738
+ };
739
+ recognition.onend = () => {
740
+ setIsListening(false);
741
+ recognitionRef.current = null;
742
+ };
743
+ recognitionRef.current = recognition;
744
+ recognition.start();
745
+ setIsListening(true);
746
+ return;
747
+ }
748
+ try {
749
+ const expoAv = await import('expo-av').catch(() => null);
750
+ const Audio = expoAv?.Audio ?? null;
751
+ if (!Audio) {
752
+ throw new Error("Speech recognition is unavailable on this platform");
753
+ }
754
+ const permission = await Audio.requestPermissionsAsync();
755
+ if (!permission.granted) {
756
+ throw new Error("Microphone permission is required");
757
+ }
758
+ await Audio.setAudioModeAsync({
759
+ allowsRecordingIOS: true,
760
+ playsInSilentModeIOS: true,
761
+ shouldDuckAndroid: true,
762
+ staysActiveInBackground: false
763
+ });
764
+ const recording = new Audio.Recording();
765
+ await recording.prepareToRecordAsync(
766
+ Audio.RecordingOptionsPresets.HIGH_QUALITY
767
+ );
768
+ await recording.startAsync();
769
+ recordingRef.current = recording;
770
+ setIsListening(true);
771
+ setError(
772
+ "Speech recognition is not available in Expo Go. Audio recording started instead."
773
+ );
774
+ } catch (caughtError) {
775
+ setError(
776
+ caughtError instanceof Error ? caughtError.message : "Failed to start listening"
777
+ );
778
+ setIsListening(false);
779
+ }
780
+ }, [
781
+ clearTranscript,
782
+ options.continuous,
783
+ options.interimResults,
784
+ options.language
785
+ ]);
786
+ react.useEffect(() => {
787
+ return () => {
788
+ recognitionRef.current?.stop?.();
789
+ cleanupRef.current?.();
790
+ const recording = recordingRef.current;
791
+ if (recording) {
792
+ void recording.stopAndUnloadAsync().catch(() => void 0);
793
+ }
794
+ };
795
+ }, []);
796
+ return {
797
+ startListening,
798
+ stopListening,
799
+ transcript,
800
+ isListening,
801
+ recordingUri,
802
+ error,
803
+ speak,
804
+ clearTranscript
805
+ };
806
+ }
807
+ var defaultTheme = {
808
+ surfaceColor: "#111827",
809
+ textColor: "#f9fafb",
810
+ textMutedColor: "#94a3b8",
811
+ borderColor: "#334155",
812
+ primaryColor: "#38bdf8"
813
+ };
814
+ function AIInput({
815
+ value,
816
+ onChangeText,
817
+ onSend,
818
+ placeholder = "Type a message",
819
+ disabled,
820
+ loading,
821
+ className,
822
+ style,
823
+ inputStyle,
824
+ buttonStyle,
825
+ buttonTextStyle,
826
+ multiline = true,
827
+ showSendIcon = true,
828
+ sendLabel = "Send"
829
+ }) {
830
+ const isDisabled = disabled || loading || value.trim().length === 0;
831
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...{ className }, style: [styles.container, style, { borderColor: defaultTheme.borderColor, backgroundColor: defaultTheme.surfaceColor }], children: [
832
+ /* @__PURE__ */ jsxRuntime.jsx(
833
+ reactNative.TextInput,
834
+ {
835
+ value,
836
+ onChangeText,
837
+ placeholder,
838
+ placeholderTextColor: defaultTheme.textMutedColor,
839
+ editable: !disabled,
840
+ multiline,
841
+ style: [styles.input, { color: defaultTheme.textColor }, inputStyle]
842
+ }
843
+ ),
844
+ /* @__PURE__ */ jsxRuntime.jsx(
845
+ reactNative.Pressable,
846
+ {
847
+ accessibilityRole: "button",
848
+ onPress: () => {
849
+ if (!isDisabled) {
850
+ void onSend();
851
+ }
852
+ },
853
+ style: ({ pressed }) => [
854
+ styles.button,
855
+ {
856
+ opacity: isDisabled ? 0.5 : pressed ? 0.85 : 1,
857
+ backgroundColor: defaultTheme.primaryColor
858
+ },
859
+ buttonStyle
860
+ ],
861
+ children: loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: "#ffffff" }) : /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles.buttonText, buttonTextStyle], children: [
862
+ showSendIcon ? "\u27A4 " : "",
863
+ sendLabel
864
+ ] })
865
+ }
866
+ )
867
+ ] });
868
+ }
869
+ var styles = reactNative.StyleSheet.create({
870
+ container: {
871
+ borderRadius: 20,
872
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
873
+ flexDirection: "row",
874
+ gap: 10,
875
+ padding: 12,
876
+ alignItems: "flex-end"
877
+ },
878
+ input: {
879
+ flex: 1,
880
+ minHeight: 44,
881
+ maxHeight: 140,
882
+ fontSize: 15,
883
+ paddingVertical: 8
884
+ },
885
+ button: {
886
+ alignItems: "center",
887
+ borderRadius: 14,
888
+ minHeight: 42,
889
+ justifyContent: "center",
890
+ paddingHorizontal: 16,
891
+ paddingVertical: 10
892
+ },
893
+ buttonText: {
894
+ color: "#ffffff",
895
+ fontSize: 14,
896
+ fontWeight: "700"
897
+ }
898
+ });
899
+ function parseInline(content) {
900
+ const parts = [];
901
+ let cursor = 0;
902
+ const pattern = /\[([^\]]+)\]\(([^)]+)\)|`([^`]+)`|\*\*([^*]+)\*\*/g;
903
+ for (; ; ) {
904
+ const match = pattern.exec(content);
905
+ if (!match) {
906
+ break;
907
+ }
908
+ if (match.index > cursor) {
909
+ parts.push({ kind: "text", value: content.slice(cursor, match.index) });
910
+ }
911
+ if (match[1] && match[2]) {
912
+ parts.push({ kind: "link", value: match[1], url: match[2] });
913
+ } else if (match[3]) {
914
+ parts.push({ kind: "code", value: match[3] });
915
+ } else if (match[4]) {
916
+ parts.push({ kind: "bold", value: match[4] });
917
+ }
918
+ cursor = pattern.lastIndex;
919
+ }
920
+ if (cursor < content.length) {
921
+ parts.push({ kind: "text", value: content.slice(cursor) });
922
+ }
923
+ return parts;
924
+ }
925
+ function renderInlineParts(parts, onLinkPress) {
926
+ return parts.map((part, index) => {
927
+ if (part.kind === "text") {
928
+ return part.value;
929
+ }
930
+ if (part.kind === "code") {
931
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.inlineCode, children: part.value }, `code-${index}`);
932
+ }
933
+ if (part.kind === "bold") {
934
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.bold, children: part.value }, `bold-${index}`);
935
+ }
936
+ return /* @__PURE__ */ jsxRuntime.jsx(
937
+ reactNative.Text,
938
+ {
939
+ style: styles2.link,
940
+ onPress: () => {
941
+ if (onLinkPress) {
942
+ onLinkPress(part.url);
943
+ return;
944
+ }
945
+ reactNative.Linking.openURL(part.url).catch(() => void 0);
946
+ },
947
+ children: part.value
948
+ },
949
+ `link-${index}`
950
+ );
951
+ });
952
+ }
953
+ function splitMarkdownBlocks(content) {
954
+ const blocks = [];
955
+ const regex = /```([a-zA-Z0-9_-]+)?\n([\s\S]*?)```/g;
956
+ let cursor = 0;
957
+ for (; ; ) {
958
+ const match = regex.exec(content);
959
+ if (!match) {
960
+ break;
961
+ }
962
+ if (match.index > cursor) {
963
+ blocks.push({ type: "text", value: content.slice(cursor, match.index) });
964
+ }
965
+ blocks.push({ type: "code", value: match[2], language: match[1] });
966
+ cursor = regex.lastIndex;
967
+ }
968
+ if (cursor < content.length) {
969
+ blocks.push({ type: "text", value: content.slice(cursor) });
970
+ }
971
+ return blocks;
972
+ }
973
+ function MarkdownText({ content, textStyle, codeStyle, onLinkPress, selectable = true }) {
974
+ const blocks = react.useMemo(() => splitMarkdownBlocks(content), [content]);
975
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { children: blocks.map((block, blockIndex) => {
976
+ if (block.type === "code") {
977
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.codeBlock, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { selectable, style: [styles2.codeText, codeStyle], children: block.value.trimEnd() }) }, `codeblock-${blockIndex}`);
978
+ }
979
+ const paragraphs = block.value.split(/\n{2,}/).filter(Boolean);
980
+ return paragraphs.map((paragraph, paragraphIndex) => /* @__PURE__ */ jsxRuntime.jsxs(
981
+ reactNative.Text,
982
+ {
983
+ selectable,
984
+ style: textStyle,
985
+ children: [
986
+ renderInlineParts(parseInline(paragraph), onLinkPress),
987
+ paragraphIndex < paragraphs.length - 1 ? "\n\n" : ""
988
+ ]
989
+ },
990
+ `text-${blockIndex}-${paragraphIndex}`
991
+ ));
992
+ }) });
993
+ }
994
+ var styles2 = reactNative.StyleSheet.create({
995
+ codeBlock: {
996
+ borderRadius: 14,
997
+ marginTop: 10,
998
+ marginBottom: 10,
999
+ overflow: "hidden"
1000
+ },
1001
+ codeText: {
1002
+ backgroundColor: "#111827",
1003
+ color: "#f9fafb",
1004
+ fontFamily: "Courier",
1005
+ fontSize: 13,
1006
+ lineHeight: 18,
1007
+ padding: 12
1008
+ },
1009
+ inlineCode: {
1010
+ backgroundColor: "#374151",
1011
+ borderRadius: 6,
1012
+ color: "#f9fafb",
1013
+ fontFamily: "Courier",
1014
+ fontSize: 13,
1015
+ paddingHorizontal: 4,
1016
+ paddingVertical: 1
1017
+ },
1018
+ bold: {
1019
+ fontWeight: "700"
1020
+ },
1021
+ link: {
1022
+ color: "#2563eb",
1023
+ textDecorationLine: "underline"
1024
+ }
1025
+ });
1026
+ var defaultTheme2 = {
1027
+ backgroundColor: "#0b1020",
1028
+ surfaceColor: "#111827",
1029
+ surfaceMutedColor: "#1f2937",
1030
+ textColor: "#f9fafb",
1031
+ textMutedColor: "#cbd5e1",
1032
+ borderColor: "#334155",
1033
+ primaryColor: "#38bdf8",
1034
+ userBubbleColor: "#1d4ed8",
1035
+ assistantBubbleColor: "#111827",
1036
+ codeBackgroundColor: "#0f172a",
1037
+ codeTextColor: "#e2e8f0",
1038
+ errorColor: "#ef4444"
1039
+ };
1040
+ function AIMessageBubble({
1041
+ message,
1042
+ theme,
1043
+ className,
1044
+ style,
1045
+ contentStyle,
1046
+ codeStyle,
1047
+ onLinkPress,
1048
+ showTimestamp
1049
+ }) {
1050
+ const colors = { ...defaultTheme2, ...theme };
1051
+ const isUser = message.role === "user";
1052
+ const bubbleColor = isUser ? colors.userBubbleColor : colors.assistantBubbleColor;
1053
+ const alignSelf = isUser ? "flex-end" : "flex-start";
1054
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...{ className }, style: [styles3.wrapper, { alignSelf }, style], children: /* @__PURE__ */ jsxRuntime.jsxs(
1055
+ reactNative.View,
1056
+ {
1057
+ style: [
1058
+ styles3.bubble,
1059
+ {
1060
+ backgroundColor: bubbleColor,
1061
+ borderColor: colors.borderColor
1062
+ }
1063
+ ],
1064
+ children: [
1065
+ /* @__PURE__ */ jsxRuntime.jsx(
1066
+ MarkdownText,
1067
+ {
1068
+ content: message.content || (message.status === "streaming" ? " " : ""),
1069
+ textStyle: [styles3.content, { color: colors.textColor }, contentStyle],
1070
+ codeStyle: [{ color: colors.codeTextColor }, codeStyle],
1071
+ onLinkPress
1072
+ }
1073
+ ),
1074
+ message.status === "streaming" || message.status === "sending" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.status, { color: colors.textMutedColor }], children: "Generating..." }) : null,
1075
+ message.status === "error" && message.error ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.status, { color: colors.errorColor }], children: message.error }) : null,
1076
+ showTimestamp ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.timestamp, { color: colors.textMutedColor }], children: new Date(message.createdAt).toLocaleTimeString() }) : null
1077
+ ]
1078
+ }
1079
+ ) });
1080
+ }
1081
+ var styles3 = reactNative.StyleSheet.create({
1082
+ wrapper: {
1083
+ marginVertical: 6,
1084
+ maxWidth: "90%"
1085
+ },
1086
+ bubble: {
1087
+ borderRadius: 18,
1088
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
1089
+ paddingHorizontal: 14,
1090
+ paddingVertical: 12,
1091
+ shadowColor: "#000",
1092
+ shadowOpacity: 0.08,
1093
+ shadowRadius: 8,
1094
+ shadowOffset: {
1095
+ width: 0,
1096
+ height: 2
1097
+ },
1098
+ elevation: 1
1099
+ },
1100
+ content: {
1101
+ fontSize: 15,
1102
+ lineHeight: 21
1103
+ },
1104
+ status: {
1105
+ fontSize: 12,
1106
+ marginTop: 8,
1107
+ opacity: 0.85
1108
+ },
1109
+ timestamp: {
1110
+ fontSize: 11,
1111
+ marginTop: 6,
1112
+ opacity: 0.65,
1113
+ textAlign: "right"
1114
+ }
1115
+ });
1116
+ function AITypingIndicator({ color = "#38bdf8", style }) {
1117
+ const dotOne = react.useRef(new reactNative.Animated.Value(0)).current;
1118
+ const dotTwo = react.useRef(new reactNative.Animated.Value(0)).current;
1119
+ const dotThree = react.useRef(new reactNative.Animated.Value(0)).current;
1120
+ const animValues = react.useMemo(() => [dotOne, dotTwo, dotThree], [dotOne, dotTwo, dotThree]);
1121
+ react.useEffect(() => {
1122
+ const animations = animValues.map(
1123
+ (value, index) => reactNative.Animated.loop(
1124
+ reactNative.Animated.sequence([
1125
+ reactNative.Animated.delay(index * 140),
1126
+ reactNative.Animated.timing(value, { toValue: 1, duration: 420, useNativeDriver: true }),
1127
+ reactNative.Animated.timing(value, { toValue: 0, duration: 420, useNativeDriver: true })
1128
+ ])
1129
+ )
1130
+ );
1131
+ animations.forEach((animation) => animation.start());
1132
+ return () => {
1133
+ animations.forEach((animation) => animation.stop());
1134
+ };
1135
+ }, [animValues]);
1136
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles4.container, style], children: animValues.map((value, index) => /* @__PURE__ */ jsxRuntime.jsx(
1137
+ reactNative.Animated.View,
1138
+ {
1139
+ style: [
1140
+ styles4.dot,
1141
+ {
1142
+ backgroundColor: color,
1143
+ opacity: value.interpolate({ inputRange: [0, 1], outputRange: [0.35, 1] }),
1144
+ transform: [
1145
+ {
1146
+ translateY: value.interpolate({ inputRange: [0, 1], outputRange: [0, -4] })
1147
+ }
1148
+ ]
1149
+ }
1150
+ ]
1151
+ },
1152
+ index
1153
+ )) });
1154
+ }
1155
+ var styles4 = reactNative.StyleSheet.create({
1156
+ container: {
1157
+ flexDirection: "row",
1158
+ gap: 6,
1159
+ paddingVertical: 6,
1160
+ alignItems: "center"
1161
+ },
1162
+ dot: {
1163
+ borderRadius: 999,
1164
+ height: 8,
1165
+ width: 8
1166
+ }
1167
+ });
1168
+ var defaultTheme3 = {
1169
+ backgroundColor: "#020617",
1170
+ surfaceColor: "#0f172a",
1171
+ surfaceMutedColor: "#1e293b",
1172
+ textColor: "#f8fafc",
1173
+ textMutedColor: "#94a3b8",
1174
+ borderColor: "#334155",
1175
+ primaryColor: "#38bdf8",
1176
+ userBubbleColor: "#1d4ed8",
1177
+ assistantBubbleColor: "#111827",
1178
+ codeBackgroundColor: "#0f172a",
1179
+ codeTextColor: "#e2e8f0",
1180
+ errorColor: "#f87171"
1181
+ };
1182
+ function AIChatView({
1183
+ messages,
1184
+ input,
1185
+ setInput,
1186
+ sendMessage,
1187
+ isLoading,
1188
+ error,
1189
+ title = "AI Chat",
1190
+ subtitle,
1191
+ emptyStateTitle = "Start a conversation",
1192
+ emptyStateDescription = "Send a message to begin chatting with the provider.",
1193
+ className,
1194
+ style,
1195
+ contentContainerStyle,
1196
+ headerStyle,
1197
+ footerStyle,
1198
+ theme,
1199
+ renderMessage,
1200
+ renderFooter,
1201
+ renderHeader,
1202
+ onPressRetry
1203
+ }) {
1204
+ const colors = react.useMemo(() => ({ ...defaultTheme3, ...theme }), [theme]);
1205
+ const listRef = react.useRef(null);
1206
+ react.useEffect(() => {
1207
+ requestAnimationFrame(() => {
1208
+ listRef.current?.scrollToEnd({ animated: true });
1209
+ });
1210
+ }, [messages.length, isLoading]);
1211
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1212
+ reactNative.KeyboardAvoidingView,
1213
+ {
1214
+ ...{ className },
1215
+ behavior: reactNative.Platform.OS === "ios" ? "padding" : void 0,
1216
+ style: [styles5.root, { backgroundColor: colors.backgroundColor }, style],
1217
+ children: [
1218
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles5.header, headerStyle], children: [
1219
+ renderHeader ? renderHeader() : /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { children: [
1220
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.title, { color: colors.textColor }], children: title }),
1221
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.subtitle, { color: colors.textMutedColor }], children: subtitle }) : null
1222
+ ] }),
1223
+ onPressRetry ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onPressRetry, style: [styles5.retryButton, { borderColor: colors.borderColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.retryText, { color: colors.primaryColor }], children: "Retry" }) }) : null
1224
+ ] }),
1225
+ error ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.error, { color: colors.errorColor }], children: error }) : null,
1226
+ /* @__PURE__ */ jsxRuntime.jsx(
1227
+ reactNative.FlatList,
1228
+ {
1229
+ ref: listRef,
1230
+ data: messages,
1231
+ keyExtractor: (item) => item.id,
1232
+ contentContainerStyle: [styles5.listContent, contentContainerStyle, messages.length === 0 ? styles5.emptyList : null],
1233
+ renderItem: ({ item, index }) => /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderMessage ? renderMessage(item, index) : /* @__PURE__ */ jsxRuntime.jsx(AIMessageBubble, { message: item, theme }) }),
1234
+ ListEmptyComponent: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles5.emptyState, children: [
1235
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.emptyTitle, { color: colors.textColor }], children: emptyStateTitle }),
1236
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.emptyDescription, { color: colors.textMutedColor }], children: emptyStateDescription })
1237
+ ] }),
1238
+ ListFooterComponent: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: footerStyle, children: [
1239
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx(AITypingIndicator, { color: colors.primaryColor }) : null,
1240
+ renderFooter ? renderFooter() : null
1241
+ ] }),
1242
+ keyboardShouldPersistTaps: "handled"
1243
+ }
1244
+ ),
1245
+ /* @__PURE__ */ jsxRuntime.jsx(
1246
+ AIInput,
1247
+ {
1248
+ value: input,
1249
+ onChangeText: setInput,
1250
+ onSend: () => {
1251
+ void sendMessage();
1252
+ },
1253
+ loading: isLoading,
1254
+ style: styles5.input
1255
+ }
1256
+ )
1257
+ ]
1258
+ }
1259
+ );
1260
+ }
1261
+ var styles5 = reactNative.StyleSheet.create({
1262
+ root: {
1263
+ flex: 1,
1264
+ padding: 16
1265
+ },
1266
+ header: {
1267
+ flexDirection: "row",
1268
+ alignItems: "flex-start",
1269
+ justifyContent: "space-between",
1270
+ marginBottom: 12
1271
+ },
1272
+ title: {
1273
+ fontSize: 26,
1274
+ fontWeight: "800"
1275
+ },
1276
+ subtitle: {
1277
+ fontSize: 14,
1278
+ marginTop: 4
1279
+ },
1280
+ retryButton: {
1281
+ borderRadius: 999,
1282
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
1283
+ paddingHorizontal: 14,
1284
+ paddingVertical: 8
1285
+ },
1286
+ retryText: {
1287
+ fontSize: 13,
1288
+ fontWeight: "700"
1289
+ },
1290
+ error: {
1291
+ fontSize: 13,
1292
+ marginBottom: 8
1293
+ },
1294
+ listContent: {
1295
+ flexGrow: 1,
1296
+ paddingBottom: 16
1297
+ },
1298
+ emptyList: {
1299
+ justifyContent: "center"
1300
+ },
1301
+ emptyState: {
1302
+ alignItems: "center",
1303
+ flex: 1,
1304
+ justifyContent: "center",
1305
+ paddingVertical: 48
1306
+ },
1307
+ emptyTitle: {
1308
+ fontSize: 20,
1309
+ fontWeight: "700"
1310
+ },
1311
+ emptyDescription: {
1312
+ fontSize: 14,
1313
+ marginTop: 8,
1314
+ textAlign: "center"
1315
+ },
1316
+ input: {
1317
+ marginTop: 12
1318
+ }
1319
+ });
1320
+
1321
+ exports.AIChatView = AIChatView;
1322
+ exports.AIInput = AIInput;
1323
+ exports.AIMessageBubble = AIMessageBubble;
1324
+ exports.AITypingIndicator = AITypingIndicator;
1325
+ exports.MarkdownText = MarkdownText;
1326
+ exports.buildDefaultCacheKey = buildDefaultCacheKey;
1327
+ exports.clamp = clamp;
1328
+ exports.clearCache = clearCache;
1329
+ exports.createGeminiProvider = createGeminiProvider;
1330
+ exports.createId = createId;
1331
+ exports.createLogger = createLogger;
1332
+ exports.createOpenAIProvider = createOpenAIProvider;
1333
+ exports.createProvider = createProvider;
1334
+ exports.isNonEmptyString = isNonEmptyString;
1335
+ exports.loadCache = loadCache;
1336
+ exports.mergeAssistantText = mergeAssistantText;
1337
+ exports.safeJsonParse = safeJsonParse;
1338
+ exports.saveCache = saveCache;
1339
+ exports.toConversationMessages = toConversationMessages;
1340
+ exports.toPlainText = toPlainText;
1341
+ exports.trimContent = trimContent;
1342
+ exports.useAIChat = useAIChat;
1343
+ exports.useAIVoice = useAIVoice;
1344
+ //# sourceMappingURL=index.cjs.map
1345
+ //# sourceMappingURL=index.cjs.map