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/README.md +68 -1106
- package/build/ExpoAiKitModule.d.ts +1 -1
- package/build/ExpoAiKitModule.d.ts.map +1 -1
- package/build/ExpoAiKitModule.js.map +1 -1
- package/build/index.d.ts +1 -436
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -737
- package/build/index.js.map +1 -1
- package/build/types.d.ts +3 -244
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/package.json +1 -1
- package/src/ExpoAiKitModule.ts +2 -2
- package/src/index.ts +1 -943
- package/src/types.ts +3 -284
- package/build/hooks.d.ts +0 -147
- package/build/hooks.d.ts.map +0 -1
- package/build/hooks.js +0 -426
- package/build/hooks.js.map +0 -1
- package/build/memory.d.ts +0 -235
- package/build/memory.d.ts.map +0 -1
- package/build/memory.js +0 -353
- package/build/memory.js.map +0 -1
- package/src/__tests__/index.test.js +0 -630
- package/src/hooks.ts +0 -502
- package/src/memory.ts +0 -394
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
|
-
}
|