@assistant-ui/mcp-docs-server 0.1.22 → 0.1.23
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/.docs/organized/code-examples/waterfall.md +801 -0
- package/.docs/organized/code-examples/with-ag-ui.md +38 -26
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +38 -28
- package/.docs/organized/code-examples/with-artifacts.md +467 -0
- package/.docs/organized/code-examples/with-assistant-transport.md +31 -24
- package/.docs/organized/code-examples/with-chain-of-thought.md +41 -32
- package/.docs/organized/code-examples/with-cloud-standalone.md +675 -0
- package/.docs/organized/code-examples/with-cloud.md +34 -27
- package/.docs/organized/code-examples/with-custom-thread-list.md +34 -27
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +41 -30
- package/.docs/organized/code-examples/with-expo.md +2031 -0
- package/.docs/organized/code-examples/with-external-store.md +32 -25
- package/.docs/organized/code-examples/with-ffmpeg.md +31 -27
- package/.docs/organized/code-examples/with-langgraph.md +96 -38
- package/.docs/organized/code-examples/with-parent-id-grouping.md +32 -25
- package/.docs/organized/code-examples/with-react-hook-form.md +63 -58
- package/.docs/organized/code-examples/with-react-router.md +38 -30
- package/.docs/organized/code-examples/with-store.md +16 -24
- package/.docs/organized/code-examples/with-tanstack.md +36 -26
- package/.docs/organized/code-examples/with-tap-runtime.md +10 -24
- package/.docs/raw/docs/(docs)/cli.mdx +13 -6
- package/.docs/raw/docs/(docs)/guides/attachments.mdx +26 -3
- package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +5 -5
- package/.docs/raw/docs/(docs)/guides/context-api.mdx +53 -52
- package/.docs/raw/docs/(docs)/guides/dictation.mdx +0 -2
- package/.docs/raw/docs/(docs)/guides/message-timing.mdx +169 -0
- package/.docs/raw/docs/(docs)/guides/quoting.mdx +327 -0
- package/.docs/raw/docs/(docs)/guides/speech.mdx +0 -1
- package/.docs/raw/docs/(docs)/index.mdx +12 -2
- package/.docs/raw/docs/(docs)/installation.mdx +8 -2
- package/.docs/raw/docs/(docs)/llm.mdx +9 -7
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar-more.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +2 -2
- package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +27 -27
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +60 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +78 -4
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +32 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +61 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +1 -1
- package/.docs/raw/docs/(reference)/legacy/styled/assistant-modal.mdx +1 -6
- package/.docs/raw/docs/(reference)/legacy/styled/decomposition.mdx +2 -2
- package/.docs/raw/docs/(reference)/legacy/styled/markdown.mdx +1 -6
- package/.docs/raw/docs/(reference)/legacy/styled/thread.mdx +1 -5
- package/.docs/raw/docs/(reference)/migrations/v0-12.mdx +17 -17
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +205 -0
- package/.docs/raw/docs/cloud/ai-sdk.mdx +292 -0
- package/.docs/raw/docs/cloud/authorization.mdx +178 -79
- package/.docs/raw/docs/cloud/{persistence/langgraph.mdx → langgraph.mdx} +2 -2
- package/.docs/raw/docs/cloud/overview.mdx +29 -39
- package/.docs/raw/docs/react-native/adapters.mdx +118 -0
- package/.docs/raw/docs/react-native/custom-backend.mdx +210 -0
- package/.docs/raw/docs/react-native/hooks.mdx +364 -0
- package/.docs/raw/docs/react-native/index.mdx +332 -0
- package/.docs/raw/docs/react-native/primitives.mdx +653 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +7 -15
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +103 -0
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +25 -2
- package/.docs/raw/docs/runtimes/data-stream.mdx +1 -3
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +113 -9
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +1 -4
- package/.docs/raw/docs/ui/attachment.mdx +4 -2
- package/.docs/raw/docs/ui/message-timing.mdx +92 -0
- package/.docs/raw/docs/ui/part-grouping.mdx +1 -1
- package/.docs/raw/docs/ui/reasoning.mdx +4 -4
- package/.docs/raw/docs/ui/scrollbar.mdx +2 -2
- package/.docs/raw/docs/ui/syntax-highlighting.mdx +55 -50
- package/.docs/raw/docs/ui/thread.mdx +16 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/tools/tests/integration.test.ts +2 -2
- package/src/tools/tests/json-parsing.test.ts +1 -1
- package/src/tools/tests/mcp-protocol.test.ts +1 -3
- package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +0 -108
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hooks
|
|
3
|
+
description: Reactive hooks for accessing runtime state in React Native.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
All hooks support an optional **selector** function for fine-grained re-renders. Without a selector, the component re-renders on every state change. With a selector, it only re-renders when the selected value changes (shallow equality).
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
// Re-renders on every thread state change
|
|
10
|
+
const thread = useThread();
|
|
11
|
+
|
|
12
|
+
// Re-renders only when isRunning changes
|
|
13
|
+
const isRunning = useThread((s) => s.isRunning);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## State Hooks
|
|
17
|
+
|
|
18
|
+
### useThread
|
|
19
|
+
|
|
20
|
+
Access thread state.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { useThread } from "@assistant-ui/react-native";
|
|
24
|
+
|
|
25
|
+
const messages = useThread((s) => s.messages);
|
|
26
|
+
const isRunning = useThread((s) => s.isRunning);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**ThreadState fields:**
|
|
30
|
+
|
|
31
|
+
| Field | Type | Description |
|
|
32
|
+
|-------|------|-------------|
|
|
33
|
+
| `messages` | `ThreadMessage[]` | All messages in the thread |
|
|
34
|
+
| `isRunning` | `boolean` | Whether the model is generating |
|
|
35
|
+
| `isDisabled` | `boolean` | Whether the thread is disabled |
|
|
36
|
+
| `capabilities` | `RuntimeCapabilities` | What actions are supported |
|
|
37
|
+
|
|
38
|
+
### useComposer
|
|
39
|
+
|
|
40
|
+
Access composer state.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { useComposer } from "@assistant-ui/react-native";
|
|
44
|
+
|
|
45
|
+
const text = useComposer((s) => s.text);
|
|
46
|
+
const isEmpty = useComposer((s) => s.isEmpty);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**ComposerState fields:**
|
|
50
|
+
|
|
51
|
+
| Field | Type | Description |
|
|
52
|
+
|-------|------|-------------|
|
|
53
|
+
| `text` | `string` | Current input text |
|
|
54
|
+
| `isEmpty` | `boolean` | Whether the input is empty |
|
|
55
|
+
| `attachments` | `Attachment[]` | Current attachments |
|
|
56
|
+
| `canCancel` | `boolean` | Whether a run can be cancelled |
|
|
57
|
+
|
|
58
|
+
### useMessage
|
|
59
|
+
|
|
60
|
+
Access the current message state. Must be used inside a `MessageProvider` or a `renderMessage` / `renderItem` callback.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { useMessage } from "@assistant-ui/react-native";
|
|
64
|
+
|
|
65
|
+
const role = useMessage((s) => s.role);
|
|
66
|
+
const isLast = useMessage((s) => s.isLast);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**MessageState fields:**
|
|
70
|
+
|
|
71
|
+
| Field | Type | Description |
|
|
72
|
+
|-------|------|-------------|
|
|
73
|
+
| `message` | `ThreadMessage` | The message object |
|
|
74
|
+
| `role` | `MessageRole` | `"user"`, `"assistant"`, or `"system"` |
|
|
75
|
+
| `isLast` | `boolean` | Whether this is the last message |
|
|
76
|
+
| `branchNumber` | `number` | Current branch index |
|
|
77
|
+
| `branchCount` | `number` | Total number of branches |
|
|
78
|
+
|
|
79
|
+
### useContentPart
|
|
80
|
+
|
|
81
|
+
Access a specific content part by index within a message.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useContentPart } from "@assistant-ui/react-native";
|
|
85
|
+
|
|
86
|
+
const part = useContentPart(0); // first content part
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### useThreadList
|
|
90
|
+
|
|
91
|
+
Access the thread list state.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { useThreadList } from "@assistant-ui/react-native";
|
|
95
|
+
|
|
96
|
+
const { threadIds, mainThreadId, threadItems } = useThreadList();
|
|
97
|
+
|
|
98
|
+
// With selector
|
|
99
|
+
const mainThreadId = useThreadList((s) => s.mainThreadId);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**ThreadListState fields:**
|
|
103
|
+
|
|
104
|
+
| Field | Type | Description |
|
|
105
|
+
|-------|------|-------------|
|
|
106
|
+
| `threadIds` | `string[]` | All thread IDs |
|
|
107
|
+
| `mainThreadId` | `string` | Currently active thread ID |
|
|
108
|
+
| `threadItems` | `Record<string, ThreadListItemState>` | Thread metadata (title, etc.) |
|
|
109
|
+
|
|
110
|
+
## Runtime Hooks
|
|
111
|
+
|
|
112
|
+
### useAssistantRuntime
|
|
113
|
+
|
|
114
|
+
Get the `AssistantRuntime` from context. This is the top-level runtime with access to thread management.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { useAssistantRuntime } from "@assistant-ui/react-native";
|
|
118
|
+
|
|
119
|
+
const runtime = useAssistantRuntime();
|
|
120
|
+
|
|
121
|
+
// Switch threads
|
|
122
|
+
runtime.threads.switchToThread(threadId);
|
|
123
|
+
runtime.threads.switchToNewThread();
|
|
124
|
+
|
|
125
|
+
// Thread management
|
|
126
|
+
runtime.threads.rename(threadId, "New Title");
|
|
127
|
+
runtime.threads.delete(threadId);
|
|
128
|
+
|
|
129
|
+
// Access the current thread runtime
|
|
130
|
+
runtime.thread; // ThreadRuntime
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### useThreadRuntime
|
|
134
|
+
|
|
135
|
+
Get the `ThreadRuntime` for the current thread.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import { useThreadRuntime } from "@assistant-ui/react-native";
|
|
139
|
+
|
|
140
|
+
const threadRuntime = useThreadRuntime();
|
|
141
|
+
|
|
142
|
+
threadRuntime.cancelRun();
|
|
143
|
+
threadRuntime.appendMessage(message);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### useComposerRuntime
|
|
147
|
+
|
|
148
|
+
Get the `ComposerRuntime` for the current composer.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { useComposerRuntime } from "@assistant-ui/react-native";
|
|
152
|
+
|
|
153
|
+
const composerRuntime = useComposerRuntime();
|
|
154
|
+
|
|
155
|
+
composerRuntime.setText("Hello");
|
|
156
|
+
composerRuntime.send();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### useMessageRuntime
|
|
160
|
+
|
|
161
|
+
Get the `MessageRuntime` for the current message.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { useMessageRuntime } from "@assistant-ui/react-native";
|
|
165
|
+
|
|
166
|
+
const messageRuntime = useMessageRuntime();
|
|
167
|
+
|
|
168
|
+
messageRuntime.reload();
|
|
169
|
+
messageRuntime.switchToBranch({ position: "next" });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### useLocalRuntime
|
|
173
|
+
|
|
174
|
+
Create an `AssistantRuntime` with a `ChatModelAdapter`. Optionally pass `storage` for persistence.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { useLocalRuntime } from "@assistant-ui/react-native";
|
|
178
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
179
|
+
|
|
180
|
+
const runtime = useLocalRuntime(chatModel, {
|
|
181
|
+
storage: AsyncStorage,
|
|
182
|
+
titleGenerator: createSimpleTitleAdapter(),
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Option | Type | Description |
|
|
187
|
+
|--------|------|-------------|
|
|
188
|
+
| `initialMessages` | `ThreadMessageLike[]` | Messages to pre-populate |
|
|
189
|
+
| `storage` | `AsyncStorageLike` | Thread and message persistence |
|
|
190
|
+
| `storagePrefix` | `string` | Key prefix for storage (default `"@assistant-ui:"`) |
|
|
191
|
+
| `titleGenerator` | `TitleGenerationAdapter` | Auto-generate thread titles |
|
|
192
|
+
|
|
193
|
+
## Primitive Hooks
|
|
194
|
+
|
|
195
|
+
Low-level hooks for building custom components.
|
|
196
|
+
|
|
197
|
+
### useThreadMessages
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { useThreadMessages } from "@assistant-ui/react-native";
|
|
201
|
+
|
|
202
|
+
const messages = useThreadMessages(); // ThreadMessage[]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### useThreadIsRunning
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { useThreadIsRunning } from "@assistant-ui/react-native";
|
|
209
|
+
|
|
210
|
+
const isRunning = useThreadIsRunning(); // boolean
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### useThreadIsEmpty
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
import { useThreadIsEmpty } from "@assistant-ui/react-native";
|
|
217
|
+
|
|
218
|
+
const isEmpty = useThreadIsEmpty(); // boolean
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### useComposerSend
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { useComposerSend } from "@assistant-ui/react-native";
|
|
225
|
+
|
|
226
|
+
const { send, canSend } = useComposerSend();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### useComposerCancel
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { useComposerCancel } from "@assistant-ui/react-native";
|
|
233
|
+
|
|
234
|
+
const { cancel, canCancel } = useComposerCancel();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### useComposerAddAttachment
|
|
238
|
+
|
|
239
|
+
Add an attachment to the composer. Returns `{ addAttachment }` — call it with a `File` or `CreateAttachment` object. Pair with your own file picker (e.g. `expo-image-picker`).
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import { useComposerAddAttachment } from "@assistant-ui/react-native";
|
|
243
|
+
|
|
244
|
+
const { addAttachment } = useComposerAddAttachment();
|
|
245
|
+
|
|
246
|
+
// With expo-image-picker
|
|
247
|
+
const pickImage = async () => {
|
|
248
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
249
|
+
base64: true,
|
|
250
|
+
quality: 0.8,
|
|
251
|
+
});
|
|
252
|
+
if (result.canceled) return;
|
|
253
|
+
|
|
254
|
+
for (const asset of result.assets) {
|
|
255
|
+
await addAttachment({
|
|
256
|
+
name: asset.fileName ?? "image.jpg",
|
|
257
|
+
type: "image",
|
|
258
|
+
content: [
|
|
259
|
+
{ type: "image", image: `data:image/jpeg;base64,${asset.base64}` },
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
<Callout type="info">
|
|
267
|
+
You must configure an `AttachmentAdapter` (e.g. `SimpleImageAttachmentAdapter`) in your runtime options for attachments to work.
|
|
268
|
+
</Callout>
|
|
269
|
+
|
|
270
|
+
### useMessageReload
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import { useMessageReload } from "@assistant-ui/react-native";
|
|
274
|
+
|
|
275
|
+
const { reload, canReload } = useMessageReload();
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### useMessageBranching
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
import { useMessageBranching } from "@assistant-ui/react-native";
|
|
282
|
+
|
|
283
|
+
const { branchNumber, branchCount, goToPrev, goToNext } =
|
|
284
|
+
useMessageBranching();
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Model Context Hooks
|
|
288
|
+
|
|
289
|
+
### useAssistantTool
|
|
290
|
+
|
|
291
|
+
Register a tool with an optional UI renderer. The tool definition is forwarded to the model, and when the model calls it, the `execute` function runs and the `render` component displays the result.
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import { useAssistantTool } from "@assistant-ui/react-native";
|
|
295
|
+
|
|
296
|
+
useAssistantTool({
|
|
297
|
+
toolName: "get_weather",
|
|
298
|
+
description: "Get the current weather for a city",
|
|
299
|
+
parameters: {
|
|
300
|
+
type: "object",
|
|
301
|
+
properties: {
|
|
302
|
+
city: { type: "string" },
|
|
303
|
+
},
|
|
304
|
+
required: ["city"],
|
|
305
|
+
},
|
|
306
|
+
execute: async ({ city }) => {
|
|
307
|
+
const res = await fetch(`https://api.weather.example/${city}`);
|
|
308
|
+
return res.json();
|
|
309
|
+
},
|
|
310
|
+
render: ({ args, result }) => (
|
|
311
|
+
<View>
|
|
312
|
+
<Text>{args.city}: {result?.temperature}°F</Text>
|
|
313
|
+
</View>
|
|
314
|
+
),
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### useAssistantToolUI
|
|
319
|
+
|
|
320
|
+
Register only a UI renderer for a tool (without tool definition or execute function).
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { useAssistantToolUI } from "@assistant-ui/react-native";
|
|
324
|
+
|
|
325
|
+
useAssistantToolUI({
|
|
326
|
+
toolName: "get_weather",
|
|
327
|
+
render: ({ args, result, status }) => (
|
|
328
|
+
<View>
|
|
329
|
+
{status?.type === "running"
|
|
330
|
+
? <Text>Loading weather for {args.city}...</Text>
|
|
331
|
+
: <Text>{args.city}: {result?.temperature}°F</Text>}
|
|
332
|
+
</View>
|
|
333
|
+
),
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### useAssistantInstructions
|
|
338
|
+
|
|
339
|
+
Register system instructions in the model context.
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { useAssistantInstructions } from "@assistant-ui/react-native";
|
|
343
|
+
|
|
344
|
+
useAssistantInstructions("You are a helpful weather assistant.");
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### makeAssistantTool
|
|
348
|
+
|
|
349
|
+
Create a component that registers a tool when mounted. Useful for declarative tool registration.
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
import { makeAssistantTool } from "@assistant-ui/react-native";
|
|
353
|
+
|
|
354
|
+
const WeatherTool = makeAssistantTool({
|
|
355
|
+
toolName: "get_weather",
|
|
356
|
+
description: "Get weather",
|
|
357
|
+
parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
|
|
358
|
+
execute: async ({ city }) => ({ temperature: 72 }),
|
|
359
|
+
render: ({ args, result }) => <Text>{args.city}: {result?.temperature}°F</Text>,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Mount inside AssistantProvider to register
|
|
363
|
+
<WeatherTool />
|
|
364
|
+
```
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Getting Started
|
|
3
|
+
description: Build AI chat interfaces for iOS and Android with @assistant-ui/react-native.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`@assistant-ui/react-native` brings assistant-ui to React Native. It provides composable primitives, reactive hooks, and a local runtime — the same layered architecture as the web package, built on native components (`View`, `TextInput`, `FlatList`, `Pressable`).
|
|
9
|
+
|
|
10
|
+
**Key features:**
|
|
11
|
+
|
|
12
|
+
- **Primitives** — `Thread`, `Composer`, `Message`, `ThreadList`, `ThreadListItem`, `ChainOfThought`, `Suggestion`, `ActionBar`, `BranchPicker`, `Attachment` components that compose with standard React Native props
|
|
13
|
+
- **Reactive state** — `useAuiState` with selector support for fine-grained re-renders
|
|
14
|
+
- **Local runtime** — `useLocalRuntime` with pluggable `ChatModelAdapter` for any LLM API
|
|
15
|
+
- **Thread management** — Multi-thread support with create, switch, rename, delete
|
|
16
|
+
- **Tool system** — `useAssistantTool`, `makeAssistantTool` for registering tools with custom UI renderers
|
|
17
|
+
- **Attachments** — Image and file attachments with `useComposerAddAttachment` and attachment primitives
|
|
18
|
+
|
|
19
|
+
<Callout type="info">
|
|
20
|
+
`@assistant-ui/react-native` shares its runtime core with `@assistant-ui/react` via `@assistant-ui/core`. The type system, state management, and runtime logic are identical — only the UI layer differs.
|
|
21
|
+
</Callout>
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
|
|
25
|
+
This guide uses [Expo](https://expo.dev) with the OpenAI API. You can substitute any LLM provider.
|
|
26
|
+
|
|
27
|
+
<Steps>
|
|
28
|
+
<Step>
|
|
29
|
+
|
|
30
|
+
### Create an Expo project
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npx create-expo-app@latest my-chat-app
|
|
34
|
+
cd my-chat-app
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
</Step>
|
|
38
|
+
<Step>
|
|
39
|
+
|
|
40
|
+
### Install dependencies
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npx expo install @assistant-ui/react-native
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
</Step>
|
|
47
|
+
<Step>
|
|
48
|
+
|
|
49
|
+
### Create a ChatModelAdapter
|
|
50
|
+
|
|
51
|
+
The adapter connects your LLM API to the runtime. Here's an example for the OpenAI chat completions API with streaming:
|
|
52
|
+
|
|
53
|
+
```tsx title="adapters/openai-chat-adapter.ts"
|
|
54
|
+
import type {
|
|
55
|
+
ChatModelAdapter,
|
|
56
|
+
ChatModelRunResult,
|
|
57
|
+
} from "@assistant-ui/react-native";
|
|
58
|
+
|
|
59
|
+
type OpenAIModelConfig = {
|
|
60
|
+
apiKey: string;
|
|
61
|
+
model?: string;
|
|
62
|
+
baseURL?: string;
|
|
63
|
+
fetch?: typeof globalThis.fetch;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function createOpenAIChatModelAdapter(
|
|
67
|
+
config: OpenAIModelConfig,
|
|
68
|
+
): ChatModelAdapter {
|
|
69
|
+
const {
|
|
70
|
+
apiKey,
|
|
71
|
+
model = "gpt-4o-mini",
|
|
72
|
+
baseURL = "https://api.openai.com/v1",
|
|
73
|
+
fetch: customFetch = globalThis.fetch,
|
|
74
|
+
} = config;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
async *run({ messages, abortSignal }) {
|
|
78
|
+
const openAIMessages = messages
|
|
79
|
+
.filter((m) => m.role !== "system")
|
|
80
|
+
.map((m) => ({
|
|
81
|
+
role: m.role as "user" | "assistant",
|
|
82
|
+
content: m.content
|
|
83
|
+
.filter((p) => p.type === "text")
|
|
84
|
+
.map((p) => ("text" in p ? p.text : ""))
|
|
85
|
+
.join("\n"),
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const response = await customFetch(
|
|
89
|
+
`${baseURL}/chat/completions`,
|
|
90
|
+
{
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
Authorization: `Bearer ${apiKey}`,
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
model,
|
|
98
|
+
messages: openAIMessages,
|
|
99
|
+
stream: true,
|
|
100
|
+
}),
|
|
101
|
+
signal: abortSignal,
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const body = await response.text().catch(() => "");
|
|
107
|
+
throw new Error(
|
|
108
|
+
`OpenAI API error: ${response.status} ${body}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const reader = response.body?.getReader();
|
|
113
|
+
if (!reader) {
|
|
114
|
+
const json = await response.json();
|
|
115
|
+
const text = json.choices?.[0]?.message?.content ?? "";
|
|
116
|
+
yield {
|
|
117
|
+
content: [{ type: "text" as const, text }],
|
|
118
|
+
} satisfies ChatModelRunResult;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const decoder = new TextDecoder();
|
|
123
|
+
let fullText = "";
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
while (true) {
|
|
127
|
+
const { done, value } = await reader.read();
|
|
128
|
+
if (done) break;
|
|
129
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
130
|
+
for (const line of chunk.split("\n")) {
|
|
131
|
+
if (!line.startsWith("data: ")) continue;
|
|
132
|
+
const data = line.slice(6);
|
|
133
|
+
if (data === "[DONE]") continue;
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(data);
|
|
136
|
+
const content =
|
|
137
|
+
parsed.choices?.[0]?.delta?.content ?? "";
|
|
138
|
+
fullText += content;
|
|
139
|
+
yield {
|
|
140
|
+
content: [
|
|
141
|
+
{ type: "text" as const, text: fullText },
|
|
142
|
+
],
|
|
143
|
+
} satisfies ChatModelRunResult;
|
|
144
|
+
} catch {
|
|
145
|
+
// skip invalid JSON
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} finally {
|
|
150
|
+
reader.releaseLock();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
<Callout type="tip">
|
|
158
|
+
On Expo, import `fetch` from `expo/fetch` for streaming support and pass it as the `fetch` option.
|
|
159
|
+
</Callout>
|
|
160
|
+
|
|
161
|
+
</Step>
|
|
162
|
+
<Step>
|
|
163
|
+
|
|
164
|
+
### Set up the runtime
|
|
165
|
+
|
|
166
|
+
```tsx title="hooks/use-app-runtime.ts"
|
|
167
|
+
import { useMemo } from "react";
|
|
168
|
+
import { fetch } from "expo/fetch";
|
|
169
|
+
import { useLocalRuntime } from "@assistant-ui/react-native";
|
|
170
|
+
import { createOpenAIChatModelAdapter } from "@/adapters/openai-chat-adapter";
|
|
171
|
+
|
|
172
|
+
export function useAppRuntime() {
|
|
173
|
+
const chatModel = useMemo(
|
|
174
|
+
() =>
|
|
175
|
+
createOpenAIChatModelAdapter({
|
|
176
|
+
apiKey: process.env.EXPO_PUBLIC_OPENAI_API_KEY ?? "",
|
|
177
|
+
model: "gpt-4o-mini",
|
|
178
|
+
fetch,
|
|
179
|
+
}),
|
|
180
|
+
[],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return useLocalRuntime(chatModel);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
</Step>
|
|
188
|
+
<Step>
|
|
189
|
+
|
|
190
|
+
### Build the UI
|
|
191
|
+
|
|
192
|
+
Wrap your app with `AssistantProvider` — thread and composer contexts are set up automatically:
|
|
193
|
+
|
|
194
|
+
```tsx title="app/index.tsx"
|
|
195
|
+
import {
|
|
196
|
+
AssistantProvider,
|
|
197
|
+
useAuiState,
|
|
198
|
+
useAui,
|
|
199
|
+
} from "@assistant-ui/react-native";
|
|
200
|
+
import type { ThreadMessage } from "@assistant-ui/react-native";
|
|
201
|
+
import {
|
|
202
|
+
View,
|
|
203
|
+
Text,
|
|
204
|
+
TextInput,
|
|
205
|
+
FlatList,
|
|
206
|
+
Pressable,
|
|
207
|
+
KeyboardAvoidingView,
|
|
208
|
+
Platform,
|
|
209
|
+
} from "react-native";
|
|
210
|
+
import { useAppRuntime } from "@/hooks/use-app-runtime";
|
|
211
|
+
|
|
212
|
+
function MessageBubble({ message }: { message: ThreadMessage }) {
|
|
213
|
+
const isUser = message.role === "user";
|
|
214
|
+
const text = message.content
|
|
215
|
+
.filter((p) => p.type === "text")
|
|
216
|
+
.map((p) => ("text" in p ? p.text : ""))
|
|
217
|
+
.join("\n");
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<View
|
|
221
|
+
style={{
|
|
222
|
+
alignSelf: isUser ? "flex-end" : "flex-start",
|
|
223
|
+
backgroundColor: isUser ? "#007aff" : "#f0f0f0",
|
|
224
|
+
borderRadius: 16,
|
|
225
|
+
padding: 12,
|
|
226
|
+
marginVertical: 4,
|
|
227
|
+
marginHorizontal: 16,
|
|
228
|
+
maxWidth: "80%",
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
<Text style={{ color: isUser ? "#fff" : "#000" }}>{text}</Text>
|
|
232
|
+
</View>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function Composer() {
|
|
237
|
+
const aui = useAui();
|
|
238
|
+
const text = useAuiState((s) => s.composer.text);
|
|
239
|
+
const isEmpty = useAuiState((s) => s.composer.isEmpty);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<View
|
|
243
|
+
style={{
|
|
244
|
+
flexDirection: "row",
|
|
245
|
+
padding: 12,
|
|
246
|
+
alignItems: "flex-end",
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
<TextInput
|
|
250
|
+
value={text}
|
|
251
|
+
onChangeText={(t) => aui.composer().setText(t)}
|
|
252
|
+
placeholder="Message..."
|
|
253
|
+
multiline
|
|
254
|
+
style={{
|
|
255
|
+
flex: 1,
|
|
256
|
+
borderWidth: 1,
|
|
257
|
+
borderColor: "#ddd",
|
|
258
|
+
borderRadius: 20,
|
|
259
|
+
paddingHorizontal: 16,
|
|
260
|
+
paddingVertical: 10,
|
|
261
|
+
maxHeight: 120,
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
<Pressable
|
|
265
|
+
onPress={() => aui.composer().send()}
|
|
266
|
+
disabled={isEmpty}
|
|
267
|
+
style={{
|
|
268
|
+
marginLeft: 8,
|
|
269
|
+
backgroundColor: !isEmpty ? "#007aff" : "#ccc",
|
|
270
|
+
borderRadius: 20,
|
|
271
|
+
width: 36,
|
|
272
|
+
height: 36,
|
|
273
|
+
justifyContent: "center",
|
|
274
|
+
alignItems: "center",
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
<Text style={{ color: "#fff", fontWeight: "bold" }}>↑</Text>
|
|
278
|
+
</Pressable>
|
|
279
|
+
</View>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function ChatScreen() {
|
|
284
|
+
const messages = useAuiState(
|
|
285
|
+
(s) => s.thread.messages,
|
|
286
|
+
) as ThreadMessage[];
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<KeyboardAvoidingView
|
|
290
|
+
style={{ flex: 1 }}
|
|
291
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
292
|
+
>
|
|
293
|
+
<FlatList
|
|
294
|
+
data={messages}
|
|
295
|
+
keyExtractor={(m) => m.id}
|
|
296
|
+
renderItem={({ item }) => <MessageBubble message={item} />}
|
|
297
|
+
/>
|
|
298
|
+
<Composer />
|
|
299
|
+
</KeyboardAvoidingView>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default function App() {
|
|
304
|
+
const runtime = useAppRuntime();
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<AssistantProvider runtime={runtime}>
|
|
308
|
+
<ChatScreen />
|
|
309
|
+
</AssistantProvider>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
</Step>
|
|
315
|
+
</Steps>
|
|
316
|
+
|
|
317
|
+
## Architecture
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
useLocalRuntime(chatModel, options?)
|
|
321
|
+
└─ AssistantProvider
|
|
322
|
+
├─ useAuiState((s) => s.thread) → thread state (messages, isRunning, …)
|
|
323
|
+
├─ useAuiState((s) => s.composer) → composer state (text, isEmpty, …)
|
|
324
|
+
├─ useAuiState((s) => s.message) → single message state (inside renderItem)
|
|
325
|
+
└─ Primitives → ThreadRoot, ComposerInput, MessageContent, …
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The runtime core is shared with `@assistant-ui/react` — only the UI primitives are React Native-specific.
|
|
329
|
+
|
|
330
|
+
## Example
|
|
331
|
+
|
|
332
|
+
For a complete Expo example with drawer navigation, thread list, and styled chat UI, see the [`with-expo` example](https://github.com/assistant-ui/assistant-ui/tree/main/examples/with-expo).
|