@guideai/react-native 0.0.0-stage → 0.2.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 +121 -2
- package/dist/index.cjs +86 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1091 -0
- package/dist/index.mjs +87 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ui.cjs +1525 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.ts +212 -0
- package/dist/ui.mjs +1525 -0
- package/dist/ui.mjs.map +1 -0
- package/dist/useOnboarding-DAEGoYSs.js +3592 -0
- package/dist/useOnboarding-DAEGoYSs.js.map +1 -0
- package/dist/useOnboarding-DWkCW5v0.cjs +3591 -0
- package/dist/useOnboarding-DWkCW5v0.cjs.map +1 -0
- package/package.json +60 -3
package/README.md
CHANGED
|
@@ -1,3 +1,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @guideai/react-native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Your GuideAI agent inside a React Native app, in assistant mode. You design the chat; we run the conversation, call your API as the signed-in user, and hand you everything a reply carries: text, pictures, follow-up suggestions, and changes waiting for the user's approval.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @guideai/react-native
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
React ≥ 18 and React Native ≥ 0.73. No native module of its own, so it works in Expo Go and over OTA updates.
|
|
10
|
+
|
|
11
|
+
## Set up once, at startup
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
15
|
+
import { GuideAI } from '@guideai/react-native';
|
|
16
|
+
|
|
17
|
+
GuideAI.configure({
|
|
18
|
+
apiKey: 'pk_live_…', // Settings → Install
|
|
19
|
+
origin: 'https://app.example.com', // a domain registered on the site
|
|
20
|
+
agent: 'tutor', // an Agent Blueprint key; omit for the main agent
|
|
21
|
+
getAuthToken: () => AsyncStorage.getItem('accessToken'),
|
|
22
|
+
onAuthExpired: () => refreshSession(), // return the new token
|
|
23
|
+
userProfile: { age: 12, class: 'JSS1', country: 'Nigeria', language: 'en' },
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### What else your API needs
|
|
28
|
+
|
|
29
|
+
Declare it once in the dashboard (**Copilot Tools → your API → Supplied by your app**), then supply the values:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
GuideAI.configure({
|
|
33
|
+
…,
|
|
34
|
+
getRequestHeaders: async () => ({ 'device-id': await getDeviceId() }), // declared headers
|
|
35
|
+
toolContext: { course: openCourseId }, // declared values; or GuideAI.setToolContext(…) later
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **Headers:** sent on every call the assistant makes to your API, never to GuideAI. Only names declared in Copilot Tools are sent, and the app's value wins over anything the model sets. `Authorization` comes from `getAuthToken`, never from here.
|
|
40
|
+
- **Values:** each one fills the API inputs it's declared to fill (app key `course` fills `content_id`), so the assistant never asks the user for them.
|
|
41
|
+
|
|
42
|
+
The dashboard shows the exact snippet for your declarations.
|
|
43
|
+
|
|
44
|
+
### Local or staging GuideAI
|
|
45
|
+
|
|
46
|
+
Production is the default. Point a development build somewhere else with `baseUrl`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
GuideAI.configure({ …, baseUrl: __DEV__ ? 'http://localhost:8000' : undefined });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `http://10.0.2.2:8000` from an Android emulator.
|
|
53
|
+
|
|
54
|
+
`userProfile` is a few facts the agent pitches answers to (level, curriculum, language). Update it after login with `GuideAI.setUserProfile(profile)`.
|
|
55
|
+
|
|
56
|
+
`getAuthToken` is read before every turn and may be async. The assistant calls your endpoints (the site's Copilot toolset) from the device with that token, so your API enforces the user's access exactly as it does for your own screens. A `401` asks `onAuthExpired` once and repeats the call. The token is sent only to your API, never to GuideAI, unless the toolset has `forward_user_token` switched on.
|
|
57
|
+
|
|
58
|
+
## Draw your own chat
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { useGuideAI } from '@guideai/react-native';
|
|
62
|
+
|
|
63
|
+
function Tutor() {
|
|
64
|
+
const chat = useGuideAI();
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
{chat.messages.map((m) => (
|
|
68
|
+
<View key={m.id}>
|
|
69
|
+
<YourMarkdown text={m.content} /> {/* pictures are Markdown images here */}
|
|
70
|
+
{m.suggestions?.map((s) => (
|
|
71
|
+
<Chip key={s} label={s} onPress={() => chat.send(s)} />
|
|
72
|
+
))}
|
|
73
|
+
{m.pendingAction && (
|
|
74
|
+
<ApproveBar
|
|
75
|
+
label={m.pendingAction.label} // "Create the course “Algebra”"
|
|
76
|
+
onApprove={() => chat.confirm(m.id)}
|
|
77
|
+
onCancel={() => chat.cancel(m.id)}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
))}
|
|
82
|
+
<TextInput value={chat.input} onChangeText={chat.setInput} />
|
|
83
|
+
<Button title="Send" disabled={!chat.canSend} onPress={() => chat.send()} />
|
|
84
|
+
{chat.status && <Text>{chat.status}</Text>}
|
|
85
|
+
</>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| On `useGuideAI()` | |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `messages` | `{ id, role, content, streaming, error, suggestions, images, pendingAction }` |
|
|
93
|
+
| `send(text?)`, `input`, `setInput`, `canSend`, `busy`, `status` | The conversation. |
|
|
94
|
+
| `confirm(id)`, `cancel(id)` | Answer a `pendingAction`. Nothing is written until `confirm`. |
|
|
95
|
+
| `sendVoice({ uri, mimeType?, languageCode? })` | Push-to-talk: you record (e.g. `expo-audio`), we transcribe and send. Returns `{ text, error? }`. |
|
|
96
|
+
| `retry()`, `clear()`, `transcribing` | |
|
|
97
|
+
|
|
98
|
+
`suggestions` appear when the agent's Blueprint has the **Suggest follow-ups** node, and `images` when it has **Draw images**.
|
|
99
|
+
|
|
100
|
+
## Or use ours
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { GuideAILauncher } from '@guideai/react-native/ui';
|
|
104
|
+
|
|
105
|
+
<GuideAILauncher />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Telling the agent what is on screen (optional)
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { GuideAIScreen, useGuideAIElement } from '@guideai/react-native';
|
|
112
|
+
|
|
113
|
+
<GuideAIScreen name="Courses" path="/courses" title="My courses">…</GuideAIScreen>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Build
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
pnpm build # dist/: index + ui, ESM and CommonJS, bundled types
|
|
120
|
+
pnpm test
|
|
121
|
+
pnpm typecheck
|
|
122
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const useOnboarding = require("./useOnboarding-DWkCW5v0.cjs");
|
|
4
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
5
|
+
const React = require("react");
|
|
6
|
+
function GuideAIScreen({
|
|
7
|
+
name,
|
|
8
|
+
path,
|
|
9
|
+
title,
|
|
10
|
+
active = true,
|
|
11
|
+
children
|
|
12
|
+
}) {
|
|
13
|
+
const runtime = useOnboarding.useGuideAIRuntime();
|
|
14
|
+
const registry = React.useMemo(() => new useOnboarding.ScreenRegistry(), [name, path]);
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (!active) return;
|
|
17
|
+
runtime.setActiveScreen({ path, title: title != null ? title : name, registry });
|
|
18
|
+
return () => {
|
|
19
|
+
var _a;
|
|
20
|
+
if (((_a = runtime.getActiveScreen()) == null ? void 0 : _a.registry) === registry) {
|
|
21
|
+
runtime.setActiveScreen(void 0);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}, [runtime, registry, path, title, name, active]);
|
|
25
|
+
return /* @__PURE__ */ jsxRuntime.jsx(useOnboarding.RegistryContext.Provider, { value: registry, children });
|
|
26
|
+
}
|
|
27
|
+
function useGuideAIElement(id, options) {
|
|
28
|
+
const registry = useOnboarding.useScreenRegistry();
|
|
29
|
+
const handlers = React.useRef({});
|
|
30
|
+
handlers.current = {
|
|
31
|
+
press: options.press,
|
|
32
|
+
setValue: options.setValue,
|
|
33
|
+
scrollTo: options.scrollTo
|
|
34
|
+
};
|
|
35
|
+
const {
|
|
36
|
+
label,
|
|
37
|
+
kind,
|
|
38
|
+
value,
|
|
39
|
+
accessibilityLabel,
|
|
40
|
+
placeholder,
|
|
41
|
+
hidden,
|
|
42
|
+
sectionTitle
|
|
43
|
+
} = options;
|
|
44
|
+
const canPress = Boolean(options.press);
|
|
45
|
+
const canSetValue = Boolean(options.setValue);
|
|
46
|
+
const canScrollTo = Boolean(options.scrollTo);
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
if (!registry) return;
|
|
49
|
+
const bound = {};
|
|
50
|
+
if (canPress) bound.press = () => {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
return (_b = (_a = handlers.current).press) == null ? void 0 : _b.call(_a);
|
|
53
|
+
};
|
|
54
|
+
if (canSetValue) bound.setValue = (next) => {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
return (_b = (_a = handlers.current).setValue) == null ? void 0 : _b.call(_a, next);
|
|
57
|
+
};
|
|
58
|
+
if (canScrollTo) bound.scrollTo = () => {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
return (_b = (_a = handlers.current).scrollTo) == null ? void 0 : _b.call(_a);
|
|
61
|
+
};
|
|
62
|
+
return registry.register(
|
|
63
|
+
{ id, label, kind, value, accessibilityLabel, placeholder, hidden, sectionTitle },
|
|
64
|
+
bound
|
|
65
|
+
);
|
|
66
|
+
}, [
|
|
67
|
+
registry,
|
|
68
|
+
id,
|
|
69
|
+
label,
|
|
70
|
+
kind,
|
|
71
|
+
value,
|
|
72
|
+
accessibilityLabel,
|
|
73
|
+
placeholder,
|
|
74
|
+
hidden,
|
|
75
|
+
sectionTitle,
|
|
76
|
+
canPress,
|
|
77
|
+
canSetValue,
|
|
78
|
+
canScrollTo
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
exports.GuideAI = useOnboarding.GuideAI;
|
|
82
|
+
exports.useGuideAI = useOnboarding.useGuideAI;
|
|
83
|
+
exports.useOnboarding = useOnboarding.useOnboarding;
|
|
84
|
+
exports.GuideAIScreen = GuideAIScreen;
|
|
85
|
+
exports.useGuideAIElement = useGuideAIElement;
|
|
86
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/react/GuideAIScreen.tsx","../src/react/useGuideAIElement.ts"],"sourcesContent":["// ============================================================\n// @guideai/react-native — screen declaration\n// ============================================================\n//\n// Wraps a screen once. Everything inside can then register itself with\n// useGuideAIElement() from any depth.\n//\n// Two identities per screen, and they are not interchangeable:\n//\n// path — \"/share\" what the mobile scanner emitted. Grounding on\n// the backend is EXACT STRING EQUALITY against\n// this, so a mismatch silently loses all\n// route-level knowledge for the screen.\n// name — \"ShareForm\" the React Navigation screen name, which is what\n// navigate() takes.\n//\n// Mixing them up produces no error — just an assistant that stops\n// knowing anything about the screen. Hence both are required.\n// ============================================================\n\nimport React, { useEffect, useMemo } from 'react';\nimport { RegistryContext, useGuideAIRuntime } from './context';\nimport { ScreenRegistry } from '../screen/screen-registry';\n\nexport interface GuideAIScreenProps {\n /** React Navigation screen name, e.g. \"ShareForm\". Used by navigate(). */\n name: string;\n /** Scanner route path, e.g. \"/share\". Used for grounding. */\n path: string;\n /** Human title shown to the model. Defaults to `name`. */\n title?: string;\n /**\n * Whether this screen is the one currently on top.\n *\n * Defaults to true. React Navigation keeps previous screens MOUNTED in\n * the stack, so without this a backgrounded screen would keep claiming\n * to be active and the model would be told about controls the user\n * cannot see. Hosts using React Navigation should pass\n * `useIsFocused()`.\n */\n active?: boolean;\n children: React.ReactNode;\n}\n\nexport function GuideAIScreen({\n name, path, title, active = true, children,\n}: GuideAIScreenProps): React.ReactElement {\n const runtime = useGuideAIRuntime();\n\n // One registry per mounted screen. Recreated only if the identity\n // changes, so elements registered by children survive re-renders.\n const registry = useMemo(() => new ScreenRegistry(), [name, path]);\n\n useEffect(() => {\n if (!active) return;\n\n runtime.setActiveScreen({ path, title: title ?? name, registry });\n\n return () => {\n // Only stand down if still the active screen. During a transition\n // the next screen's effect may run before this one's cleanup, and\n // clearing then would blank a screen that just became active.\n if (runtime.getActiveScreen()?.registry === registry) {\n runtime.setActiveScreen(undefined);\n }\n };\n }, [runtime, registry, path, title, name, active]);\n\n return (\n <RegistryContext.Provider value={registry}>\n {children}\n </RegistryContext.Provider>\n );\n}\n","// ============================================================\n// @guideai/react-native — element declaration hook\n// ============================================================\n//\n// The manual path: a component declares itself so the assistant can see\n// and act on it.\n//\n// KNOWN LIMIT, stated rather than hidden: this is per-element work. On a\n// screen with 200 controls that is 200 calls, and an element nobody\n// declares is INVISIBLE to the assistant — it does not degrade, it is\n// simply not there, with no warning and no failing test. Automatic\n// harvesting (instrumented components, accessibility props, static\n// extraction) is the intended answer and is being explored separately.\n// Until then this is the escape hatch, and it is honest about being one.\n// ============================================================\n\nimport { useEffect, useRef } from 'react';\nimport { useScreenRegistry } from './context';\nimport type { DeclaredElement, ElementHandlers } from '../screen/screen-registry';\n\nexport type UseGuideAIElementOptions =\n Omit<DeclaredElement, 'id'> & ElementHandlers;\n\n/**\n * Declare one element to the assistant.\n *\n * Registration is an effect, so it runs after mount and unwinds on\n * unmount. Handlers are kept in a ref and read through a stable wrapper:\n * without that, an inline `press={() => …}` would be a new function on\n * every render and re-register the element on every render with it.\n */\nexport function useGuideAIElement(\n id: string,\n options: UseGuideAIElementOptions,\n): void {\n const registry = useScreenRegistry();\n\n // Latest handlers, without making them a dependency of registration.\n const handlers = useRef<ElementHandlers>({});\n handlers.current = {\n press: options.press,\n setValue: options.setValue,\n scrollTo: options.scrollTo,\n };\n\n const {\n label, kind, value, accessibilityLabel, placeholder, hidden, sectionTitle,\n } = options;\n\n // WHICH capabilities exist must be part of registration, not just their\n // implementations. Always installing a wrapper would make every element\n // report itself as pressable, so `element_not_pressable` could never\n // fire and the model would be told a static label is a button.\n const canPress = Boolean(options.press);\n const canSetValue = Boolean(options.setValue);\n const canScrollTo = Boolean(options.scrollTo);\n\n useEffect(() => {\n if (!registry) return;\n\n // Stable indirection: the registry holds these for the element's\n // lifetime, and they read whatever the latest render supplied — so an\n // inline arrow function does not re-register on every render.\n const bound: ElementHandlers = {};\n if (canPress) bound.press = () => handlers.current.press?.();\n if (canSetValue) bound.setValue = (next: string) => handlers.current.setValue?.(next);\n if (canScrollTo) bound.scrollTo = () => handlers.current.scrollTo?.();\n\n return registry.register(\n { id, label, kind, value, accessibilityLabel, placeholder, hidden, sectionTitle },\n bound,\n );\n }, [\n registry, id, label, kind, value,\n accessibilityLabel, placeholder, hidden, sectionTitle,\n canPress, canSetValue, canScrollTo,\n ]);\n}\n"],"names":["useGuideAIRuntime","useMemo","ScreenRegistry","useEffect","RegistryContext","useScreenRegistry","useRef"],"mappings":";;;;;AA4CO,SAAS,cAAc;AAAA,EAC5B;AAAA,EAAM;AAAA,EAAM;AAAA,EAAO,SAAS;AAAA,EAAM;AACpC,GAA2C;AACzC,QAAM,UAAUA,cAAAA,kBAAA;AAIhB,QAAM,WAAWC,MAAAA,QAAQ,MAAM,IAAIC,cAAAA,kBAAkB,CAAC,MAAM,IAAI,CAAC;AAEjEC,QAAAA,UAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AAEb,YAAQ,gBAAgB,EAAE,MAAM,OAAO,wBAAS,MAAM,UAAU;AAEhE,WAAO,MAAM;;AAIX,YAAI,aAAQ,sBAAR,mBAA2B,cAAa,UAAU;AACpD,gBAAQ,gBAAgB,MAAS;AAAA,MACnC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,UAAU,MAAM,OAAO,MAAM,MAAM,CAAC;AAEjD,wCACGC,cAAAA,gBAAgB,UAAhB,EAAyB,OAAO,UAC9B,UACH;AAEJ;AC1CO,SAAS,kBACd,IACA,SACM;AACN,QAAM,WAAWC,cAAAA,kBAAA;AAGjB,QAAM,WAAWC,MAAAA,OAAwB,EAAE;AAC3C,WAAS,UAAU;AAAA,IACjB,OAAO,QAAQ;AAAA,IACf,UAAU,QAAQ;AAAA,IAClB,UAAU,QAAQ;AAAA,EAAA;AAGpB,QAAM;AAAA,IACJ;AAAA,IAAO;AAAA,IAAM;AAAA,IAAO;AAAA,IAAoB;AAAA,IAAa;AAAA,IAAQ;AAAA,EAAA,IAC3D;AAMJ,QAAM,WAAW,QAAQ,QAAQ,KAAK;AACtC,QAAM,cAAc,QAAQ,QAAQ,QAAQ;AAC5C,QAAM,cAAc,QAAQ,QAAQ,QAAQ;AAE5CH,QAAAA,UAAU,MAAM;AACd,QAAI,CAAC,SAAU;AAKf,UAAM,QAAyB,CAAA;AAC/B,QAAI,SAAU,OAAM,QAAQ,MAAA;;AAAM,kCAAS,SAAQ,UAAjB;AAAA;AAClC,QAAI,mBAAmB,WAAW,CAAC;;AAAiB,kCAAS,SAAQ,aAAjB,4BAA4B;AAAA;AAChF,QAAI,YAAa,OAAM,WAAW,MAAA;;AAAM,kCAAS,SAAQ,aAAjB;AAAA;AAExC,WAAO,SAAS;AAAA,MACd,EAAE,IAAI,OAAO,MAAM,OAAO,oBAAoB,aAAa,QAAQ,aAAA;AAAA,MACnE;AAAA,IAAA;AAAA,EAEJ,GAAG;AAAA,IACD;AAAA,IAAU;AAAA,IAAI;AAAA,IAAO;AAAA,IAAM;AAAA,IAC3B;AAAA,IAAoB;AAAA,IAAa;AAAA,IAAQ;AAAA,IACzC;AAAA,IAAU;AAAA,IAAa;AAAA,EAAA,CACxB;AACH;;;;;;"}
|