expo-ai-kit 0.3.6 → 0.4.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/src/hooks.ts DELETED
@@ -1,502 +0,0 @@
1
- import { useState, useEffect, useRef, useCallback } from 'react';
2
- import {
3
- isAvailable,
4
- streamMessage,
5
- getBuiltInModels as getBuiltInModelsApi,
6
- getDownloadableModels as getDownloadableModelsApi,
7
- downloadModel as downloadModelApi,
8
- deleteModel as deleteModelApi,
9
- } from './index';
10
- import { ChatMemoryManager } from './memory';
11
- import ExpoAiKitModule from './ExpoAiKitModule';
12
- import type {
13
- LLMMessage,
14
- UseChatOptions,
15
- UseChatReturn,
16
- UseCompletionOptions,
17
- UseCompletionReturn,
18
- UseOnDeviceAIReturn,
19
- BuiltInModel,
20
- DownloadableModel,
21
- DownloadableModelStatus,
22
- ModelError,
23
- } from './types';
24
-
25
- // Cache availability result across all hook instances
26
- let availabilityCache: boolean | null = null;
27
- let availabilityPromise: Promise<boolean> | null = null;
28
-
29
- function checkAvailability(): Promise<boolean> {
30
- if (availabilityCache !== null) {
31
- return Promise.resolve(availabilityCache);
32
- }
33
- if (!availabilityPromise) {
34
- availabilityPromise = isAvailable().then((result) => {
35
- availabilityCache = result;
36
- return result;
37
- });
38
- }
39
- return availabilityPromise;
40
- }
41
-
42
- /**
43
- * React hook to check if on-device AI is available.
44
- *
45
- * Caches the result so multiple components don't re-check.
46
- *
47
- * @returns Object with isAvailable and isChecking states
48
- *
49
- * @example
50
- * ```tsx
51
- * function MyComponent() {
52
- * const { isAvailable, isChecking } = useOnDeviceAI();
53
- *
54
- * if (isChecking) return <Text>Checking AI availability...</Text>;
55
- * if (!isAvailable) return <Text>On-device AI not available</Text>;
56
- *
57
- * return <ChatComponent />;
58
- * }
59
- * ```
60
- */
61
- export function useOnDeviceAI(): UseOnDeviceAIReturn {
62
- const [available, setAvailable] = useState(availabilityCache ?? false);
63
- const [checking, setChecking] = useState(availabilityCache === null);
64
-
65
- useEffect(() => {
66
- if (availabilityCache !== null) {
67
- setAvailable(availabilityCache);
68
- setChecking(false);
69
- return;
70
- }
71
-
72
- let mounted = true;
73
- checkAvailability().then((result) => {
74
- if (mounted) {
75
- setAvailable(result);
76
- setChecking(false);
77
- }
78
- });
79
- return () => {
80
- mounted = false;
81
- };
82
- }, []);
83
-
84
- return { isAvailable: available, isChecking: checking };
85
- }
86
-
87
- /**
88
- * React hook for building chat interfaces with on-device AI.
89
- *
90
- * Manages messages, input state, streaming, and conversation memory automatically.
91
- * Built on top of ChatMemoryManager and streamMessage.
92
- *
93
- * @param options - Configuration options for the chat
94
- * @returns Chat state and control functions
95
- *
96
- * @example
97
- * ```tsx
98
- * function ChatScreen() {
99
- * const { messages, input, setInput, sendMessage, isStreaming, stop } = useChat({
100
- * systemPrompt: 'You are a helpful assistant.',
101
- * });
102
- *
103
- * return (
104
- * <View>
105
- * <FlatList
106
- * data={messages}
107
- * renderItem={({ item }) => (
108
- * <Text>{item.role}: {item.content}</Text>
109
- * )}
110
- * />
111
- * <TextInput value={input} onChangeText={setInput} />
112
- * {isStreaming ? (
113
- * <Button title="Stop" onPress={stop} />
114
- * ) : (
115
- * <Button title="Send" onPress={() => sendMessage()} />
116
- * )}
117
- * </View>
118
- * );
119
- * }
120
- * ```
121
- */
122
- export function useChat(options: UseChatOptions = {}): UseChatReturn {
123
- const { systemPrompt, maxTurns, initialMessages, onFinish, onError } = options;
124
-
125
- const memoryRef = useRef(new ChatMemoryManager({ maxTurns, systemPrompt }));
126
-
127
- // Initialize with initial messages
128
- const initializedRef = useRef(false);
129
- if (!initializedRef.current && initialMessages) {
130
- for (const msg of initialMessages) {
131
- memoryRef.current.addMessage(msg);
132
- }
133
- initializedRef.current = true;
134
- }
135
-
136
- const [messages, setMessages] = useState<LLMMessage[]>(memoryRef.current.getMessages());
137
- const [input, setInput] = useState('');
138
- const [isStreaming, setIsStreaming] = useState(false);
139
- const [error, setError] = useState<Error | null>(null);
140
- const stopRef = useRef<(() => void) | null>(null);
141
- const mountedRef = useRef(true);
142
- const inputRef = useRef(input);
143
- inputRef.current = input;
144
- const streamingRef = useRef(isStreaming);
145
- streamingRef.current = isStreaming;
146
- const onFinishRef = useRef(onFinish);
147
- onFinishRef.current = onFinish;
148
- const onErrorRef = useRef(onError);
149
- onErrorRef.current = onError;
150
-
151
- useEffect(() => {
152
- return () => {
153
- mountedRef.current = false;
154
- stopRef.current?.();
155
- };
156
- }, []);
157
-
158
- const send = useCallback(
159
- async (text?: string) => {
160
- const content = (text ?? inputRef.current).trim();
161
- if (!content || streamingRef.current) return;
162
-
163
- setError(null);
164
-
165
- // Add user message
166
- memoryRef.current.addUserMessage(content);
167
- setMessages([...memoryRef.current.getMessages()]);
168
- if (!text) setInput('');
169
-
170
- setIsStreaming(true);
171
-
172
- try {
173
- const allMessages = memoryRef.current.getAllMessages();
174
-
175
- // Use a temporary message for streaming display
176
- let accumulatedText = '';
177
- const { promise, stop } = streamMessage(allMessages, (event) => {
178
- if (!mountedRef.current) return;
179
- accumulatedText = event.accumulatedText;
180
- // Update messages with streaming assistant response
181
- const currentMessages = memoryRef.current.getMessages();
182
- setMessages([
183
- ...currentMessages,
184
- { role: 'assistant' as const, content: accumulatedText },
185
- ]);
186
- });
187
-
188
- stopRef.current = stop;
189
- const response = await promise;
190
-
191
- if (!mountedRef.current) return;
192
-
193
- // Add the final assistant message to memory
194
- memoryRef.current.addAssistantMessage(response.text);
195
- setMessages([...memoryRef.current.getMessages()]);
196
- onFinishRef.current?.(response);
197
- } catch (err) {
198
- if (!mountedRef.current) return;
199
- const e = err instanceof Error ? err : new Error(String(err));
200
- setError(e);
201
- onErrorRef.current?.(e);
202
- } finally {
203
- stopRef.current = null;
204
- if (mountedRef.current) {
205
- setIsStreaming(false);
206
- }
207
- }
208
- },
209
- []
210
- );
211
-
212
- const stop = useCallback(() => {
213
- stopRef.current?.();
214
- }, []);
215
-
216
- const clear = useCallback(() => {
217
- memoryRef.current.clear();
218
- setMessages([]);
219
- setError(null);
220
- }, []);
221
-
222
- return {
223
- messages,
224
- input,
225
- setInput,
226
- sendMessage: send,
227
- isStreaming,
228
- stop,
229
- clear,
230
- error,
231
- };
232
- }
233
-
234
- /**
235
- * React hook for single-shot AI completions.
236
- *
237
- * Unlike useChat (which manages a conversation), useCompletion is for
238
- * one-off tasks like summarization, translation, or content generation.
239
- *
240
- * @param options - Configuration options
241
- * @returns Completion state and control functions
242
- *
243
- * @example
244
- * ```tsx
245
- * function SummarizerScreen() {
246
- * const { completion, isLoading, complete, stop } = useCompletion({
247
- * systemPrompt: 'You are a summarization assistant. Summarize the given text concisely.',
248
- * });
249
- *
250
- * return (
251
- * <View>
252
- * <Button
253
- * title="Summarize"
254
- * onPress={() => complete('Long article text here...')}
255
- * />
256
- * {isLoading && <Button title="Stop" onPress={stop} />}
257
- * <Text>{completion}</Text>
258
- * </View>
259
- * );
260
- * }
261
- * ```
262
- */
263
- export function useCompletion(options: UseCompletionOptions = {}): UseCompletionReturn {
264
- const { systemPrompt, onFinish, onError } = options;
265
-
266
- const [completion, setCompletion] = useState('');
267
- const [isLoading, setIsLoading] = useState(false);
268
- const [error, setError] = useState<Error | null>(null);
269
- const stopRef = useRef<(() => void) | null>(null);
270
- const mountedRef = useRef(true);
271
- const loadingRef = useRef(isLoading);
272
- loadingRef.current = isLoading;
273
- const completionRef = useRef(completion);
274
- completionRef.current = completion;
275
- const systemPromptRef = useRef(systemPrompt);
276
- systemPromptRef.current = systemPrompt;
277
- const onFinishRef = useRef(onFinish);
278
- onFinishRef.current = onFinish;
279
- const onErrorRef = useRef(onError);
280
- onErrorRef.current = onError;
281
-
282
- useEffect(() => {
283
- return () => {
284
- mountedRef.current = false;
285
- stopRef.current?.();
286
- };
287
- }, []);
288
-
289
- const complete = useCallback(
290
- async (prompt: string) => {
291
- if (loadingRef.current) return completionRef.current;
292
-
293
- setError(null);
294
- setCompletion('');
295
- setIsLoading(true);
296
-
297
- try {
298
- const messages: LLMMessage[] = [{ role: 'user', content: prompt }];
299
- const { promise, stop } = streamMessage(
300
- messages,
301
- (event) => {
302
- if (!mountedRef.current) return;
303
- setCompletion(event.accumulatedText);
304
- },
305
- systemPromptRef.current ? { systemPrompt: systemPromptRef.current } : undefined
306
- );
307
-
308
- stopRef.current = stop;
309
- const response = await promise;
310
-
311
- if (!mountedRef.current) return '';
312
-
313
- setCompletion(response.text);
314
- onFinishRef.current?.(response);
315
- return response.text;
316
- } catch (err) {
317
- if (!mountedRef.current) return '';
318
- const e = err instanceof Error ? err : new Error(String(err));
319
- setError(e);
320
- onErrorRef.current?.(e);
321
- return '';
322
- } finally {
323
- stopRef.current = null;
324
- if (mountedRef.current) {
325
- setIsLoading(false);
326
- }
327
- }
328
- },
329
- []
330
- );
331
-
332
- const stop = useCallback(() => {
333
- stopRef.current?.();
334
- }, []);
335
-
336
- return {
337
- completion,
338
- isLoading,
339
- complete,
340
- stop,
341
- error,
342
- };
343
- }
344
-
345
- /**
346
- * React hook for managing a downloadable model's lifecycle.
347
- *
348
- * Tracks download status, progress, and provides download/delete controls.
349
- *
350
- * @param modelId - The downloadable model ID (e.g. 'gemma-e2b')
351
- * @returns Model status, progress, and control functions
352
- *
353
- * @example
354
- * ```tsx
355
- * function ModelManager() {
356
- * const { status, progress, download, delete: remove, error } = useModel('gemma-e2b');
357
- *
358
- * if (status === 'not-downloaded') {
359
- * return <Button title="Download" onPress={download} />;
360
- * }
361
- * if (status === 'downloading') {
362
- * return <Text>Downloading: {Math.round(progress * 100)}%</Text>;
363
- * }
364
- * if (status === 'loading') {
365
- * return <Text>Loading model...</Text>;
366
- * }
367
- * return <Text>Model ready!</Text>;
368
- * }
369
- * ```
370
- */
371
- export function useModel(modelId: string): {
372
- status: DownloadableModelStatus;
373
- progress: number;
374
- download: () => Promise<void>;
375
- delete: () => Promise<void>;
376
- error: ModelError | null;
377
- } {
378
- const [status, setStatus] = useState<DownloadableModelStatus>('not-downloaded');
379
- const [progress, setProgress] = useState(0);
380
- const [error, setError] = useState<ModelError | null>(null);
381
- const mountedRef = useRef(true);
382
-
383
- useEffect(() => {
384
- // Query initial status
385
- try {
386
- const initialStatus = ExpoAiKitModule.getDownloadableModelStatus(modelId);
387
- setStatus(initialStatus);
388
- } catch {
389
- // Model not found or native module not ready
390
- }
391
-
392
- // Listen for state changes
393
- const stateSubscription = ExpoAiKitModule.addListener(
394
- 'onModelStateChange',
395
- (event) => {
396
- if (event.modelId === modelId && mountedRef.current) {
397
- setStatus(event.status);
398
- }
399
- }
400
- );
401
-
402
- // Listen for download progress
403
- const progressSubscription = ExpoAiKitModule.addListener(
404
- 'onDownloadProgress',
405
- (event) => {
406
- if (event.modelId === modelId && mountedRef.current) {
407
- setProgress(event.progress);
408
- }
409
- }
410
- );
411
-
412
- return () => {
413
- mountedRef.current = false;
414
- stateSubscription.remove();
415
- progressSubscription.remove();
416
- };
417
- }, [modelId]);
418
-
419
- const download = useCallback(async () => {
420
- setError(null);
421
- setProgress(0);
422
- try {
423
- await downloadModelApi(modelId, {
424
- onProgress: (p) => {
425
- if (mountedRef.current) setProgress(p);
426
- },
427
- });
428
- } catch (err) {
429
- if (mountedRef.current) {
430
- setError(err as ModelError);
431
- }
432
- }
433
- }, [modelId]);
434
-
435
- const del = useCallback(async () => {
436
- setError(null);
437
- try {
438
- await deleteModelApi(modelId);
439
- if (mountedRef.current) {
440
- setStatus('not-downloaded');
441
- setProgress(0);
442
- }
443
- } catch (err) {
444
- if (mountedRef.current) {
445
- setError(err as ModelError);
446
- }
447
- }
448
- }, [modelId]);
449
-
450
- return { status, progress, download, delete: del, error };
451
- }
452
-
453
- /**
454
- * React hook to discover all available models (built-in and downloadable).
455
- *
456
- * @returns Built-in models, downloadable models, and loading state
457
- *
458
- * @example
459
- * ```tsx
460
- * function ModelPicker() {
461
- * const { builtIn, downloadable, isLoading } = useAvailableModels();
462
- *
463
- * if (isLoading) return <Text>Loading models...</Text>;
464
- *
465
- * return (
466
- * <View>
467
- * <Text>Built-in: {builtIn.map(m => m.name).join(', ')}</Text>
468
- * <Text>Downloadable: {downloadable.map(m => m.name).join(', ')}</Text>
469
- * </View>
470
- * );
471
- * }
472
- * ```
473
- */
474
- export function useAvailableModels(): {
475
- builtIn: BuiltInModel[];
476
- downloadable: DownloadableModel[];
477
- isLoading: boolean;
478
- } {
479
- const [builtIn, setBuiltIn] = useState<BuiltInModel[]>([]);
480
- const [downloadable, setDownloadable] = useState<DownloadableModel[]>([]);
481
- const [isLoading, setIsLoading] = useState(true);
482
-
483
- useEffect(() => {
484
- let mounted = true;
485
-
486
- Promise.all([getBuiltInModelsApi(), getDownloadableModelsApi()]).then(
487
- ([bi, dl]) => {
488
- if (mounted) {
489
- setBuiltIn(bi);
490
- setDownloadable(dl);
491
- setIsLoading(false);
492
- }
493
- }
494
- );
495
-
496
- return () => {
497
- mounted = false;
498
- };
499
- }, []);
500
-
501
- return { builtIn, downloadable, isLoading };
502
- }