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