@stewmore/expo-ai-react 0.1.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/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +13 -0
- package/build/index.js.map +1 -0
- package/build/internal.d.ts +10 -0
- package/build/internal.d.ts.map +1 -0
- package/build/internal.js +26 -0
- package/build/internal.js.map +1 -0
- package/build/useCapabilities.d.ts +21 -0
- package/build/useCapabilities.d.ts.map +1 -0
- package/build/useCapabilities.js +42 -0
- package/build/useCapabilities.js.map +1 -0
- package/build/useChat.d.ts +27 -0
- package/build/useChat.d.ts.map +1 -0
- package/build/useChat.js +108 -0
- package/build/useChat.js.map +1 -0
- package/build/useGenerate.d.ts +23 -0
- package/build/useGenerate.d.ts.map +1 -0
- package/build/useGenerate.js +100 -0
- package/build/useGenerate.js.map +1 -0
- package/build/useObject.d.ts +20 -0
- package/build/useObject.d.ts.map +1 -0
- package/build/useObject.js +61 -0
- package/build/useObject.js.map +1 -0
- package/package.json +59 -0
- package/src/__tests__/useCapabilities.test.tsx +47 -0
- package/src/__tests__/useChat.test.tsx +103 -0
- package/src/__tests__/useGenerate.test.tsx +100 -0
- package/src/__tests__/useObject.test.tsx +67 -0
- package/src/index.ts +18 -0
- package/src/internal.ts +29 -0
- package/src/useCapabilities.ts +67 -0
- package/src/useChat.ts +150 -0
- package/src/useGenerate.ts +132 -0
- package/src/useObject.ts +83 -0
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @stewmore/expo-ai-react
|
|
3
|
+
*
|
|
4
|
+
* React hooks for the Expo AI Runtime — `ai/react`-style ergonomics over the
|
|
5
|
+
* provider-agnostic {@link @stewmore/expo-ai-core} package. All hooks own an
|
|
6
|
+
* AbortController so `stop()` and unmount cancel cleanly, guard state updates
|
|
7
|
+
* after unmount, and treat cancellation as intentional (never an `error`).
|
|
8
|
+
*/
|
|
9
|
+
export { useCapabilities, type UseCapabilitiesResult } from './useCapabilities.js';
|
|
10
|
+
export { useGenerate, type UseGenerateResult } from './useGenerate.js';
|
|
11
|
+
export { useObject, type UseObjectResult } from './useObject.js';
|
|
12
|
+
export { useChat, type UseChatResult, type ChatMessage, type ChatRole, } from './useChat.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EACL,OAAO,EACP,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,QAAQ,GACd,MAAM,cAAc,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @stewmore/expo-ai-react
|
|
3
|
+
*
|
|
4
|
+
* React hooks for the Expo AI Runtime — `ai/react`-style ergonomics over the
|
|
5
|
+
* provider-agnostic {@link @stewmore/expo-ai-core} package. All hooks own an
|
|
6
|
+
* AbortController so `stop()` and unmount cancel cleanly, guard state updates
|
|
7
|
+
* after unmount, and treat cancellation as intentional (never an `error`).
|
|
8
|
+
*/
|
|
9
|
+
export { useCapabilities } from './useCapabilities.js';
|
|
10
|
+
export { useGenerate } from './useGenerate.js';
|
|
11
|
+
export { useObject } from './useObject.js';
|
|
12
|
+
export { useChat, } from './useChat.js';
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAA8B,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,WAAW,EAA0B,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,SAAS,EAAwB,MAAM,gBAAgB,CAAC;AACjE,OAAO,EACL,OAAO,GAIR,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ExpoAIError } from '@stewmore/expo-ai-core';
|
|
2
|
+
/** A ref that is `true` while the component is mounted, for post-await guards. */
|
|
3
|
+
export declare function useIsMounted(): {
|
|
4
|
+
readonly current: boolean;
|
|
5
|
+
};
|
|
6
|
+
/** Normalize any thrown value to an ExpoAIError (provider unknown at this layer). */
|
|
7
|
+
export declare function toError(value: unknown): ExpoAIError;
|
|
8
|
+
/** Stopping a request is intentional, not an error to surface to the UI. */
|
|
9
|
+
export declare function isCancelled(error: ExpoAIError): boolean;
|
|
10
|
+
//# sourceMappingURL=internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD,kFAAkF;AAClF,wBAAgB,YAAY,IAAI;IAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAS5D;AAED,qFAAqF;AACrF,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAEnD;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAEvD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small internals shared by the hooks. Kept dependency-free (react only) so the
|
|
3
|
+
* package stays testable under jsdom and usable on web.
|
|
4
|
+
*/
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import { ExpoAIError } from '@stewmore/expo-ai-core';
|
|
7
|
+
/** A ref that is `true` while the component is mounted, for post-await guards. */
|
|
8
|
+
export function useIsMounted() {
|
|
9
|
+
const mounted = useRef(true);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
mounted.current = true;
|
|
12
|
+
return () => {
|
|
13
|
+
mounted.current = false;
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
return mounted;
|
|
17
|
+
}
|
|
18
|
+
/** Normalize any thrown value to an ExpoAIError (provider unknown at this layer). */
|
|
19
|
+
export function toError(value) {
|
|
20
|
+
return ExpoAIError.from(value, 'none');
|
|
21
|
+
}
|
|
22
|
+
/** Stopping a request is intentional, not an error to surface to the UI. */
|
|
23
|
+
export function isCancelled(error) {
|
|
24
|
+
return error.code === 'CANCELLED';
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=internal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.js","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD,kFAAkF;AAClF,MAAM,UAAU,YAAY;IAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;QACvB,OAAO,GAAG,EAAE;YACV,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC;QAC1B,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,OAAO,CAAC,KAAc;IACpC,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,KAAkB;IAC5C,OAAO,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ExpoAIAvailability, type ExpoAICapabilities, type ExpoAIError, type ExpoAIProviderInfo } from '@stewmore/expo-ai-core';
|
|
2
|
+
export type UseCapabilitiesResult = {
|
|
3
|
+
/** Capabilities of the best currently-available provider, or null while loading. */
|
|
4
|
+
capabilities: ExpoAICapabilities | null;
|
|
5
|
+
/** Availability of the best currently-available provider. */
|
|
6
|
+
availability: ExpoAIAvailability | null;
|
|
7
|
+
/** Every registered provider and its capabilities. */
|
|
8
|
+
providers: ExpoAIProviderInfo[] | null;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
error: ExpoAIError | null;
|
|
11
|
+
/** Re-query availability/capabilities (e.g. after the user enables a model). */
|
|
12
|
+
refresh: () => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Query what the runtime can do on this device. Resolves capabilities,
|
|
16
|
+
* availability, and the full provider list on mount; call `refresh()` to re-query
|
|
17
|
+
* (capabilities can change when the user toggles Apple Intelligence, finishes a
|
|
18
|
+
* model download, etc.).
|
|
19
|
+
*/
|
|
20
|
+
export declare function useCapabilities(): UseCapabilitiesResult;
|
|
21
|
+
//# sourceMappingURL=useCapabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCapabilities.d.ts","sourceRoot":"","sources":["../src/useCapabilities.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACxB,MAAM,wBAAwB,CAAC;AAIhC,MAAM,MAAM,qBAAqB,GAAG;IAClC,oFAAoF;IACpF,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACxC,6DAA6D;IAC7D,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACxC,sDAAsD;IACtD,SAAS,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,gFAAgF;IAChF,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAYF;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,qBAAqB,CAyBvD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ExpoAI, } from '@stewmore/expo-ai-core';
|
|
3
|
+
import { toError, useIsMounted } from './internal.js';
|
|
4
|
+
const INITIAL = {
|
|
5
|
+
capabilities: null,
|
|
6
|
+
availability: null,
|
|
7
|
+
providers: null,
|
|
8
|
+
loading: true,
|
|
9
|
+
error: null,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Query what the runtime can do on this device. Resolves capabilities,
|
|
13
|
+
* availability, and the full provider list on mount; call `refresh()` to re-query
|
|
14
|
+
* (capabilities can change when the user toggles Apple Intelligence, finishes a
|
|
15
|
+
* model download, etc.).
|
|
16
|
+
*/
|
|
17
|
+
export function useCapabilities() {
|
|
18
|
+
const [state, setState] = useState(INITIAL);
|
|
19
|
+
const mounted = useIsMounted();
|
|
20
|
+
const requestId = useRef(0);
|
|
21
|
+
const refresh = useCallback(() => {
|
|
22
|
+
const id = ++requestId.current;
|
|
23
|
+
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
24
|
+
Promise.all([ExpoAI.getCapabilities(), ExpoAI.getAvailability(), ExpoAI.listProviders()])
|
|
25
|
+
.then(([capabilities, availability, providers]) => {
|
|
26
|
+
// Ignore a resolution superseded by a newer refresh, or after unmount.
|
|
27
|
+
if (!mounted.current || id !== requestId.current)
|
|
28
|
+
return;
|
|
29
|
+
setState({ capabilities, availability, providers, loading: false, error: null });
|
|
30
|
+
})
|
|
31
|
+
.catch((caught) => {
|
|
32
|
+
if (!mounted.current || id !== requestId.current)
|
|
33
|
+
return;
|
|
34
|
+
setState((prev) => ({ ...prev, loading: false, error: toError(caught) }));
|
|
35
|
+
});
|
|
36
|
+
}, [mounted]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
refresh();
|
|
39
|
+
}, [refresh]);
|
|
40
|
+
return { ...state, refresh };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=useCapabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCapabilities.js","sourceRoot":"","sources":["../src/useCapabilities.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjE,OAAO,EACL,MAAM,GAKP,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAiBtD,MAAM,OAAO,GAAU;IACrB,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,IAAI;IAClB,SAAS,EAAE,IAAI;IACf,OAAO,EAAE,IAAI;IACb,KAAK,EAAE,IAAI;CACZ,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAQ,OAAO,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAE5B,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE;QAC/B,MAAM,EAAE,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC;QAC/B,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,eAAe,EAAE,EAAE,MAAM,CAAC,eAAe,EAAE,EAAE,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;aACtF,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,EAAE;YAChD,uEAAuE;YACvE,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,KAAK,SAAS,CAAC,OAAO;gBAAE,OAAO;YACzD,QAAQ,CAAC,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACnF,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE;YAChB,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,KAAK,SAAS,CAAC,OAAO;gBAAE,OAAO;YACzD,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QAC5E,CAAC,CAAC,CAAC;IACP,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,EAAE,CAAC;IACZ,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type CreateSessionOptions, type ExpoAIError } from '@stewmore/expo-ai-core';
|
|
2
|
+
export type ChatRole = 'user' | 'assistant';
|
|
3
|
+
export type ChatMessage = {
|
|
4
|
+
id: string;
|
|
5
|
+
role: ChatRole;
|
|
6
|
+
content: string;
|
|
7
|
+
};
|
|
8
|
+
export type UseChatResult = {
|
|
9
|
+
messages: ChatMessage[];
|
|
10
|
+
input: string;
|
|
11
|
+
setInput: (value: string) => void;
|
|
12
|
+
/** Send a turn (defaults to the current `input`) and stream the assistant reply. */
|
|
13
|
+
append: (content?: string) => Promise<void>;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
error: ExpoAIError | null;
|
|
16
|
+
/** Abort the in-flight reply. Cancellation is not surfaced as an error. */
|
|
17
|
+
stop: () => void;
|
|
18
|
+
/** Dispose the session and clear the transcript. */
|
|
19
|
+
reset: () => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A streaming chat transcript over a cross-platform {@link ExpoAISession}. The
|
|
23
|
+
* session is created lazily on the first `append` and disposed on unmount.
|
|
24
|
+
* `options` is captured on first use; later changes are ignored.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useChat(options?: CreateSessionOptions): UseChatResult;
|
|
27
|
+
//# sourceMappingURL=useChat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useChat.d.ts","sourceRoot":"","sources":["../src/useChat.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,WAAW,EAEjB,MAAM,wBAAwB,CAAC;AAIhC,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAE5C,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,oFAAoF;IACpF,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,oDAAoD;IACpD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,CA+GrE"}
|
package/build/useChat.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ExpoAI, } from '@stewmore/expo-ai-core';
|
|
3
|
+
import { isCancelled, toError, useIsMounted } from './internal.js';
|
|
4
|
+
/**
|
|
5
|
+
* A streaming chat transcript over a cross-platform {@link ExpoAISession}. The
|
|
6
|
+
* session is created lazily on the first `append` and disposed on unmount.
|
|
7
|
+
* `options` is captured on first use; later changes are ignored.
|
|
8
|
+
*/
|
|
9
|
+
export function useChat(options) {
|
|
10
|
+
const [messages, setMessages] = useState([]);
|
|
11
|
+
const [input, setInput] = useState('');
|
|
12
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const sessionRef = useRef(null);
|
|
15
|
+
const controllerRef = useRef(null);
|
|
16
|
+
const idRef = useRef(0);
|
|
17
|
+
const optionsRef = useRef(options);
|
|
18
|
+
const mounted = useIsMounted();
|
|
19
|
+
const nextId = useCallback((role) => `${role}-${++idRef.current}`, []);
|
|
20
|
+
const disposeSession = useCallback(() => {
|
|
21
|
+
const session = sessionRef.current;
|
|
22
|
+
sessionRef.current = null;
|
|
23
|
+
void session?.dispose().catch(() => { });
|
|
24
|
+
}, []);
|
|
25
|
+
// Abort the in-flight reply and dispose the session on unmount.
|
|
26
|
+
useEffect(() => () => {
|
|
27
|
+
controllerRef.current?.abort();
|
|
28
|
+
disposeSession();
|
|
29
|
+
}, [disposeSession]);
|
|
30
|
+
const stop = useCallback(() => {
|
|
31
|
+
controllerRef.current?.abort();
|
|
32
|
+
}, []);
|
|
33
|
+
const append = useCallback(async (content) => {
|
|
34
|
+
const text = (content ?? input).trim();
|
|
35
|
+
if (text.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
controllerRef.current?.abort();
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
controllerRef.current = controller;
|
|
40
|
+
const isCurrent = () => controllerRef.current === controller;
|
|
41
|
+
const assistantId = nextId('assistant');
|
|
42
|
+
setMessages((prev) => [
|
|
43
|
+
...prev,
|
|
44
|
+
{ id: nextId('user'), role: 'user', content: text },
|
|
45
|
+
{ id: assistantId, role: 'assistant', content: '' },
|
|
46
|
+
]);
|
|
47
|
+
if (content === undefined)
|
|
48
|
+
setInput('');
|
|
49
|
+
setError(null);
|
|
50
|
+
setIsLoading(true);
|
|
51
|
+
try {
|
|
52
|
+
if (!sessionRef.current) {
|
|
53
|
+
// createSession is not abortable; if we unmounted (or another append
|
|
54
|
+
// already created one) while it was pending, dispose this one.
|
|
55
|
+
const created = await ExpoAI.createSession(optionsRef.current);
|
|
56
|
+
if (!mounted.current || sessionRef.current) {
|
|
57
|
+
void created.dispose().catch(() => { });
|
|
58
|
+
if (!mounted.current)
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
sessionRef.current = created;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const session = sessionRef.current;
|
|
66
|
+
if (!session)
|
|
67
|
+
return;
|
|
68
|
+
for await (const chunk of session.stream({ prompt: text, signal: controller.signal })) {
|
|
69
|
+
if (!isCurrent())
|
|
70
|
+
break;
|
|
71
|
+
if (chunk.type === 'delta' && mounted.current) {
|
|
72
|
+
setMessages((prev) => prev.map((message) => message.id === assistantId
|
|
73
|
+
? { ...message, content: message.content + chunk.text }
|
|
74
|
+
: message));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (caught) {
|
|
79
|
+
const normalized = toError(caught);
|
|
80
|
+
if (mounted.current && isCurrent()) {
|
|
81
|
+
// Drop the still-empty assistant placeholder; surface the error instead.
|
|
82
|
+
setMessages((prev) => prev.filter((message) => !(message.id === assistantId && message.content === '')));
|
|
83
|
+
if (!isCancelled(normalized))
|
|
84
|
+
setError(normalized);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (isCurrent()) {
|
|
89
|
+
controllerRef.current = null;
|
|
90
|
+
if (mounted.current)
|
|
91
|
+
setIsLoading(false);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, [input, mounted, nextId]);
|
|
95
|
+
const reset = useCallback(async () => {
|
|
96
|
+
controllerRef.current?.abort();
|
|
97
|
+
const session = sessionRef.current;
|
|
98
|
+
sessionRef.current = null;
|
|
99
|
+
if (mounted.current) {
|
|
100
|
+
setMessages([]);
|
|
101
|
+
setError(null);
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
await session?.dispose().catch(() => { });
|
|
105
|
+
}, [mounted]);
|
|
106
|
+
return { messages, input, setInput, append, isLoading, error, stop, reset };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=useChat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useChat.js","sourceRoot":"","sources":["../src/useChat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ExpoAIError, type GenerateOptions, type GenerateResult } from '@stewmore/expo-ai-core';
|
|
2
|
+
export type UseGenerateResult = {
|
|
3
|
+
/** One-shot generation. Resolves the result, or undefined on error/cancel. */
|
|
4
|
+
generate: (options: GenerateOptions) => Promise<GenerateResult | undefined>;
|
|
5
|
+
/** Streamed generation. `text` accumulates as tokens arrive. */
|
|
6
|
+
stream: (options: GenerateOptions) => Promise<GenerateResult | undefined>;
|
|
7
|
+
/** Generated text (accumulates during `stream`, set whole by `generate`). */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Final result with provider + privacy metadata, once complete. */
|
|
10
|
+
result: GenerateResult | null;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
error: ExpoAIError | null;
|
|
13
|
+
/** Abort the in-flight request. Cancellation is not surfaced as an error. */
|
|
14
|
+
stop: () => void;
|
|
15
|
+
/** Clear text/result/error back to the initial state. */
|
|
16
|
+
reset: () => void;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Imperatively generate text — one-shot via `generate` or token-streamed via
|
|
20
|
+
* `stream`. Owns an AbortController so `stop()` (and unmount) cancel cleanly.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useGenerate(): UseGenerateResult;
|
|
23
|
+
//# sourceMappingURL=useGenerate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGenerate.d.ts","sourceRoot":"","sources":["../src/useGenerate.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAIhC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,8EAA8E;IAC9E,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAC5E,gEAAgE;IAChE,MAAM,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAC1E,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,yDAAyD;IACzD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAmG/C"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ExpoAI, } from '@stewmore/expo-ai-core';
|
|
3
|
+
import { isCancelled, toError, useIsMounted } from './internal.js';
|
|
4
|
+
/**
|
|
5
|
+
* Imperatively generate text — one-shot via `generate` or token-streamed via
|
|
6
|
+
* `stream`. Owns an AbortController so `stop()` (and unmount) cancel cleanly.
|
|
7
|
+
*/
|
|
8
|
+
export function useGenerate() {
|
|
9
|
+
const [text, setText] = useState('');
|
|
10
|
+
const [result, setResult] = useState(null);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const controllerRef = useRef(null);
|
|
14
|
+
const mounted = useIsMounted();
|
|
15
|
+
// Abort any in-flight request when the component unmounts.
|
|
16
|
+
useEffect(() => () => controllerRef.current?.abort(), []);
|
|
17
|
+
const stop = useCallback(() => {
|
|
18
|
+
controllerRef.current?.abort();
|
|
19
|
+
}, []);
|
|
20
|
+
const reset = useCallback(() => {
|
|
21
|
+
setText('');
|
|
22
|
+
setResult(null);
|
|
23
|
+
setError(null);
|
|
24
|
+
}, []);
|
|
25
|
+
const begin = useCallback(() => {
|
|
26
|
+
controllerRef.current?.abort();
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
controllerRef.current = controller;
|
|
29
|
+
setText('');
|
|
30
|
+
setResult(null);
|
|
31
|
+
setError(null);
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
return controller;
|
|
34
|
+
}, []);
|
|
35
|
+
// Only the controller still owning the ref may write state — a superseded
|
|
36
|
+
// call (aborted by a newer begin()) must not clobber the live request.
|
|
37
|
+
const isCurrent = useCallback((controller) => {
|
|
38
|
+
return controllerRef.current === controller;
|
|
39
|
+
}, []);
|
|
40
|
+
const finish = useCallback((controller) => {
|
|
41
|
+
if (!isCurrent(controller))
|
|
42
|
+
return;
|
|
43
|
+
controllerRef.current = null;
|
|
44
|
+
if (mounted.current)
|
|
45
|
+
setIsLoading(false);
|
|
46
|
+
}, [isCurrent, mounted]);
|
|
47
|
+
const generate = useCallback(async (options) => {
|
|
48
|
+
const controller = begin();
|
|
49
|
+
try {
|
|
50
|
+
const generated = await ExpoAI.generate({ ...options, signal: controller.signal });
|
|
51
|
+
if (mounted.current && isCurrent(controller)) {
|
|
52
|
+
setResult(generated);
|
|
53
|
+
setText(generated.text);
|
|
54
|
+
}
|
|
55
|
+
return generated;
|
|
56
|
+
}
|
|
57
|
+
catch (caught) {
|
|
58
|
+
const normalized = toError(caught);
|
|
59
|
+
if (mounted.current && isCurrent(controller) && !isCancelled(normalized)) {
|
|
60
|
+
setError(normalized);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
finish(controller);
|
|
66
|
+
}
|
|
67
|
+
}, [begin, finish, isCurrent, mounted]);
|
|
68
|
+
const stream = useCallback(async (options) => {
|
|
69
|
+
const controller = begin();
|
|
70
|
+
let final;
|
|
71
|
+
try {
|
|
72
|
+
for await (const chunk of ExpoAI.stream({ ...options, signal: controller.signal })) {
|
|
73
|
+
if (!isCurrent(controller))
|
|
74
|
+
break;
|
|
75
|
+
if (chunk.type === 'delta') {
|
|
76
|
+
if (mounted.current)
|
|
77
|
+
setText((current) => current + chunk.text);
|
|
78
|
+
}
|
|
79
|
+
else if (chunk.type === 'done') {
|
|
80
|
+
final = chunk.result;
|
|
81
|
+
if (mounted.current)
|
|
82
|
+
setResult(chunk.result);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return final;
|
|
86
|
+
}
|
|
87
|
+
catch (caught) {
|
|
88
|
+
const normalized = toError(caught);
|
|
89
|
+
if (mounted.current && isCurrent(controller) && !isCancelled(normalized)) {
|
|
90
|
+
setError(normalized);
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
finish(controller);
|
|
96
|
+
}
|
|
97
|
+
}, [begin, finish, isCurrent, mounted]);
|
|
98
|
+
return { generate, stream, text, result, isLoading, error, stop, reset };
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=useGenerate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGenerate.js","sourceRoot":"","sources":["../src/useGenerate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DeepPartial, type ExpoAIError, type StreamObjectOptions } from '@stewmore/expo-ai-core';
|
|
2
|
+
export type UseObjectResult<T> = {
|
|
3
|
+
/** Start streaming a structured object. Resolves the validated value, or undefined. */
|
|
4
|
+
submit: (options: StreamObjectOptions) => Promise<T | undefined>;
|
|
5
|
+
/** Best-effort partial snapshot, growing as tokens arrive; the final validated value last. */
|
|
6
|
+
object: DeepPartial<T> | null;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: ExpoAIError | null;
|
|
9
|
+
/** Abort the in-flight stream. Cancellation is not surfaced as an error. */
|
|
10
|
+
stop: () => void;
|
|
11
|
+
/** Clear object/error back to the initial state. */
|
|
12
|
+
reset: () => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Stream a structured object into React state. `object` updates with each partial
|
|
16
|
+
* snapshot as tokens arrive, then becomes the validated (repaired) final value.
|
|
17
|
+
* Built on {@link ExpoAI.streamObject}.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useObject<T = unknown>(): UseObjectResult<T>;
|
|
20
|
+
//# sourceMappingURL=useObject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useObject.d.ts","sourceRoot":"","sources":["../src/useObject.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAC;AAIhC,MAAM,MAAM,eAAe,CAAC,CAAC,IAAI;IAC/B,uFAAuF;IACvF,MAAM,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACjE,8FAA8F;IAC9F,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,oDAAoD;IACpD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,GAAG,OAAO,KAAK,eAAe,CAAC,CAAC,CAAC,CAqD3D"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ExpoAI, } from '@stewmore/expo-ai-core';
|
|
3
|
+
import { isCancelled, toError, useIsMounted } from './internal.js';
|
|
4
|
+
/**
|
|
5
|
+
* Stream a structured object into React state. `object` updates with each partial
|
|
6
|
+
* snapshot as tokens arrive, then becomes the validated (repaired) final value.
|
|
7
|
+
* Built on {@link ExpoAI.streamObject}.
|
|
8
|
+
*/
|
|
9
|
+
export function useObject() {
|
|
10
|
+
const [object, setObject] = useState(null);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const controllerRef = useRef(null);
|
|
14
|
+
const mounted = useIsMounted();
|
|
15
|
+
useEffect(() => () => controllerRef.current?.abort(), []);
|
|
16
|
+
const stop = useCallback(() => {
|
|
17
|
+
controllerRef.current?.abort();
|
|
18
|
+
}, []);
|
|
19
|
+
const reset = useCallback(() => {
|
|
20
|
+
setObject(null);
|
|
21
|
+
setError(null);
|
|
22
|
+
}, []);
|
|
23
|
+
const submit = useCallback(async (options) => {
|
|
24
|
+
controllerRef.current?.abort();
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
controllerRef.current = controller;
|
|
27
|
+
setObject(null);
|
|
28
|
+
setError(null);
|
|
29
|
+
setIsLoading(true);
|
|
30
|
+
const isCurrent = () => controllerRef.current === controller;
|
|
31
|
+
const handle = ExpoAI.streamObject({ ...options, signal: controller.signal });
|
|
32
|
+
try {
|
|
33
|
+
for await (const partial of handle.partialObjectStream) {
|
|
34
|
+
if (!isCurrent())
|
|
35
|
+
break;
|
|
36
|
+
if (mounted.current)
|
|
37
|
+
setObject(partial);
|
|
38
|
+
}
|
|
39
|
+
const final = await handle.object;
|
|
40
|
+
if (mounted.current && isCurrent())
|
|
41
|
+
setObject(final);
|
|
42
|
+
return final;
|
|
43
|
+
}
|
|
44
|
+
catch (caught) {
|
|
45
|
+
const normalized = toError(caught);
|
|
46
|
+
// Only the live submit may write state — a superseded one must not clobber it.
|
|
47
|
+
if (mounted.current && isCurrent() && !isCancelled(normalized))
|
|
48
|
+
setError(normalized);
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (isCurrent()) {
|
|
53
|
+
controllerRef.current = null;
|
|
54
|
+
if (mounted.current)
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, [mounted]);
|
|
59
|
+
return { submit, object, isLoading, error, stop, reset };
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=useObject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useObject.js","sourceRoot":"","sources":["../src/useObject.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stewmore/expo-ai-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React hooks for the Expo AI Runtime: useGenerate, useChat, useObject, useCapabilities — ai/react-style ergonomics over the on-device, privacy-first core.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Stewart Moreland <stewart.moreland@gmail.com>",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./build/index.js",
|
|
12
|
+
"module": "./build/index.js",
|
|
13
|
+
"types": "./build/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./build/index.d.ts",
|
|
17
|
+
"default": "./build/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"build",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"keywords": [
|
|
26
|
+
"expo",
|
|
27
|
+
"react-native",
|
|
28
|
+
"react",
|
|
29
|
+
"hooks",
|
|
30
|
+
"ai",
|
|
31
|
+
"on-device",
|
|
32
|
+
"streaming"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"prepack": "npm run build",
|
|
37
|
+
"clean": "rm -rf build",
|
|
38
|
+
"typecheck": "tsc --noEmit -p tsconfig.build.json",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:coverage": "vitest run --coverage"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@stewmore/expo-ai-core": "^0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@testing-library/dom": "^10.4.0",
|
|
50
|
+
"@testing-library/react": "^16.1.0",
|
|
51
|
+
"@types/react": "~19.2.0",
|
|
52
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
53
|
+
"jsdom": "^26.0.0",
|
|
54
|
+
"react": "19.2.3",
|
|
55
|
+
"react-dom": "19.2.3",
|
|
56
|
+
"typescript": "~6.0.3",
|
|
57
|
+
"vitest": "^4.1.9"
|
|
58
|
+
}
|
|
59
|
+
}
|