@assistant-ui/mcp-docs-server 0.1.23 → 0.1.25
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 +5 -3
- package/.docs/organized/code-examples/with-a2a.md +676 -0
- package/.docs/organized/code-examples/with-ag-ui.md +7 -8
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +28 -16
- package/.docs/organized/code-examples/with-artifacts.md +5 -5
- package/.docs/organized/code-examples/with-assistant-transport.md +3 -3
- package/.docs/organized/code-examples/with-chain-of-thought.md +34 -26
- package/.docs/organized/code-examples/with-cloud-standalone.md +10 -8
- package/.docs/organized/code-examples/with-cloud.md +5 -5
- package/.docs/organized/code-examples/with-custom-thread-list.md +7 -7
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +8 -8
- package/.docs/organized/code-examples/with-expo.md +571 -539
- package/.docs/organized/code-examples/with-external-store.md +3 -4
- package/.docs/organized/code-examples/with-ffmpeg.md +5 -5
- package/.docs/organized/code-examples/with-google-adk.md +353 -0
- package/.docs/organized/code-examples/with-heat-graph.md +304 -0
- package/.docs/organized/code-examples/with-langgraph.md +25 -23
- package/.docs/organized/code-examples/with-parent-id-grouping.md +4 -4
- package/.docs/organized/code-examples/with-react-hook-form.md +6 -9
- package/.docs/organized/code-examples/with-react-ink.md +265 -0
- package/.docs/organized/code-examples/with-react-router.md +10 -11
- package/.docs/organized/code-examples/with-store.md +29 -18
- package/.docs/organized/code-examples/with-tanstack.md +7 -7
- package/.docs/organized/code-examples/with-tap-runtime.md +6 -4
- package/.docs/raw/blog/2025-01-31-changelog/index.mdx +1 -1
- package/.docs/raw/blog/2026-03-launch-week/index.mdx +227 -0
- package/.docs/raw/docs/(docs)/architecture.mdx +1 -1
- package/.docs/raw/docs/(docs)/cli.mdx +14 -9
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +8 -3
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +5 -1
- package/.docs/raw/docs/(docs)/copilots/{make-assistant-readable.mdx → make-assistant-visible.mdx} +14 -5
- package/.docs/raw/docs/(docs)/copilots/model-context.mdx +11 -11
- package/.docs/raw/docs/(docs)/copilots/motivation.mdx +2 -2
- package/.docs/raw/docs/(docs)/devtools.mdx +3 -2
- package/.docs/raw/docs/(docs)/guides/attachments.mdx +9 -11
- package/.docs/raw/docs/(docs)/guides/branching.mdx +11 -6
- package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +18 -16
- package/.docs/raw/docs/(docs)/guides/context-api.mdx +81 -43
- package/.docs/raw/docs/(docs)/guides/dictation.mdx +5 -5
- package/.docs/raw/docs/(docs)/guides/editing.mdx +16 -7
- package/.docs/raw/docs/(docs)/guides/latex.mdx +3 -0
- package/.docs/raw/docs/(docs)/guides/message-timing.mdx +2 -1
- package/.docs/raw/docs/(docs)/guides/multi-agent.mdx +173 -0
- package/.docs/raw/docs/(docs)/guides/quoting.mdx +55 -206
- package/.docs/raw/docs/(docs)/guides/speech.mdx +1 -4
- package/.docs/raw/docs/(docs)/guides/suggestions.mdx +9 -15
- package/.docs/raw/docs/(docs)/guides/tool-ui.mdx +17 -7
- package/.docs/raw/docs/(docs)/guides/tools.mdx +24 -9
- package/.docs/raw/docs/(docs)/index.mdx +3 -3
- package/.docs/raw/docs/(docs)/installation.mdx +69 -46
- package/.docs/raw/docs/(reference)/api-reference/context-providers/text-message-part-provider.mdx +20 -6
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-data-stream.mdx +24 -4
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-hook-form.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/integrations/vercel-ai-sdk.mdx +20 -19
- package/.docs/raw/docs/(reference)/api-reference/overview.mdx +28 -53
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +4 -4
- package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-modal.mdx +7 -1
- package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +20 -14
- package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +99 -45
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +52 -40
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +343 -23
- package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +4 -6
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list-item.mdx +4 -2
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list.mdx +3 -5
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +169 -22
- package/.docs/raw/docs/(reference)/api-reference/runtimes/assistant-runtime.mdx +14 -4
- package/.docs/raw/docs/(reference)/api-reference/runtimes/attachment-runtime.mdx +15 -26
- package/.docs/raw/docs/(reference)/api-reference/runtimes/composer-runtime.mdx +39 -21
- package/.docs/raw/docs/(reference)/api-reference/runtimes/message-part-runtime.mdx +33 -9
- package/.docs/raw/docs/(reference)/api-reference/runtimes/message-runtime.mdx +48 -21
- package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-list-item-runtime.mdx +36 -7
- package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-list-runtime.mdx +30 -10
- package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-runtime.mdx +12 -10
- package/.docs/raw/docs/(reference)/migrations/deprecation-policy.mdx +1 -1
- package/.docs/raw/docs/(reference)/migrations/react-langgraph-v0-7.mdx +9 -4
- package/.docs/raw/docs/(reference)/migrations/v0-11.mdx +7 -5
- package/.docs/raw/docs/(reference)/migrations/v0-12.mdx +9 -7
- package/.docs/raw/docs/(reference)/migrations/v0-14.mdx +159 -0
- package/.docs/raw/docs/(reference)/react-compatibility.mdx +5 -134
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +89 -7
- package/.docs/raw/docs/cloud/ai-sdk.mdx +19 -5
- package/.docs/raw/docs/cloud/langgraph.mdx +13 -3
- package/.docs/raw/docs/ink/adapters.mdx +41 -0
- package/.docs/raw/docs/ink/custom-backend.mdx +203 -0
- package/.docs/raw/docs/ink/hooks.mdx +448 -0
- package/.docs/raw/docs/ink/index.mdx +239 -0
- package/.docs/raw/docs/ink/migration.mdx +140 -0
- package/.docs/raw/docs/ink/primitives.mdx +699 -0
- package/.docs/raw/docs/react-native/adapters.mdx +63 -87
- package/.docs/raw/docs/react-native/custom-backend.mdx +11 -14
- package/.docs/raw/docs/react-native/hooks.mdx +214 -232
- package/.docs/raw/docs/react-native/index.mdx +118 -159
- package/.docs/raw/docs/react-native/migration.mdx +144 -0
- package/.docs/raw/docs/react-native/primitives.mdx +431 -302
- package/.docs/raw/docs/runtimes/a2a/index.mdx +294 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +9 -9
- package/.docs/raw/docs/runtimes/ai-sdk/v5-legacy.mdx +14 -3
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +53 -0
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +59 -25
- package/.docs/raw/docs/runtimes/custom/custom-thread-list.mdx +13 -6
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +138 -38
- package/.docs/raw/docs/runtimes/custom/local.mdx +184 -42
- package/.docs/raw/docs/runtimes/data-stream.mdx +92 -19
- package/.docs/raw/docs/runtimes/google-adk/index.mdx +624 -0
- package/.docs/raw/docs/runtimes/helicone.mdx +6 -6
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +38 -27
- package/.docs/raw/docs/runtimes/langgraph/tutorial/introduction.mdx +1 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +15 -20
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +7 -11
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +8 -11
- package/.docs/raw/docs/runtimes/langserve.mdx +6 -7
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +18 -3
- package/.docs/raw/docs/ui/context-display.mdx +147 -0
- package/.docs/raw/docs/ui/file.mdx +5 -4
- package/.docs/raw/docs/ui/image.mdx +5 -4
- package/.docs/raw/docs/ui/markdown.mdx +3 -1
- package/.docs/raw/docs/ui/model-selector.mdx +8 -8
- package/.docs/raw/docs/ui/part-grouping.mdx +7 -10
- package/.docs/raw/docs/ui/quote.mdx +210 -0
- package/.docs/raw/docs/ui/reasoning.mdx +12 -11
- package/.docs/raw/docs/ui/sources.mdx +88 -17
- package/.docs/raw/docs/ui/streamdown.mdx +16 -7
- package/.docs/raw/docs/ui/thread-list.mdx +11 -13
- package/.docs/raw/docs/ui/thread.mdx +28 -33
- package/.docs/raw/docs/ui/tool-fallback.mdx +5 -6
- package/.docs/raw/docs/ui/tool-group.mdx +9 -8
- package/.docs/raw/docs/utilities/heat-graph.mdx +236 -0
- package/.docs/raw/docs/utilities/tw-shimmer.mdx +211 -0
- package/package.json +4 -4
- package/.docs/raw/docs/(reference)/legacy/styled/assistant-modal.mdx +0 -77
- package/.docs/raw/docs/(reference)/legacy/styled/decomposition.mdx +0 -635
- package/.docs/raw/docs/(reference)/legacy/styled/markdown.mdx +0 -77
- package/.docs/raw/docs/(reference)/legacy/styled/scrollbar.mdx +0 -72
- package/.docs/raw/docs/(reference)/legacy/styled/thread-width.mdx +0 -22
- package/.docs/raw/docs/(reference)/legacy/styled/thread.mdx +0 -77
- /package/.docs/raw/docs/cloud/{overview.mdx → index.mdx} +0 -0
|
@@ -20,336 +20,6 @@
|
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
## adapters/openai-chat-adapter.ts
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import type { ChatModelAdapter } from "@assistant-ui/react-native";
|
|
27
|
-
|
|
28
|
-
export type OpenAIModelConfig = {
|
|
29
|
-
apiKey: string;
|
|
30
|
-
model?: string;
|
|
31
|
-
baseURL?: string;
|
|
32
|
-
/** Custom fetch implementation — pass `fetch` from `expo/fetch` for streaming support */
|
|
33
|
-
fetch?: typeof globalThis.fetch;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type OpenAIMessage = {
|
|
37
|
-
role: string;
|
|
38
|
-
content: string | any[] | null;
|
|
39
|
-
tool_calls?: {
|
|
40
|
-
id: string;
|
|
41
|
-
type: string;
|
|
42
|
-
function: { name: string; arguments: string };
|
|
43
|
-
}[];
|
|
44
|
-
tool_call_id?: string;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
type ToolCallAccumulator = Record<
|
|
48
|
-
number,
|
|
49
|
-
{ id: string; name: string; arguments: string }
|
|
50
|
-
>;
|
|
51
|
-
|
|
52
|
-
export function createOpenAIChatModelAdapter(
|
|
53
|
-
config: OpenAIModelConfig,
|
|
54
|
-
): ChatModelAdapter {
|
|
55
|
-
const {
|
|
56
|
-
apiKey,
|
|
57
|
-
model = "gpt-4o-mini",
|
|
58
|
-
baseURL = "https://api.openai.com/v1",
|
|
59
|
-
fetch: customFetch = globalThis.fetch,
|
|
60
|
-
} = config;
|
|
61
|
-
|
|
62
|
-
const callOpenAI = async (
|
|
63
|
-
messages: OpenAIMessage[],
|
|
64
|
-
openAITools: any[] | undefined,
|
|
65
|
-
abortSignal: AbortSignal,
|
|
66
|
-
) => {
|
|
67
|
-
const response = await customFetch(`${baseURL}/chat/completions`, {
|
|
68
|
-
method: "POST",
|
|
69
|
-
headers: {
|
|
70
|
-
"Content-Type": "application/json",
|
|
71
|
-
Authorization: `Bearer ${apiKey}`,
|
|
72
|
-
},
|
|
73
|
-
body: JSON.stringify({
|
|
74
|
-
model,
|
|
75
|
-
messages,
|
|
76
|
-
stream: true,
|
|
77
|
-
...(openAITools ? { tools: openAITools } : {}),
|
|
78
|
-
}),
|
|
79
|
-
signal: abortSignal,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (!response.ok) {
|
|
83
|
-
const body = await response.text().catch(() => "");
|
|
84
|
-
throw new Error(`OpenAI API error: ${response.status} ${body}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return response;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const streamResponse = async function* (
|
|
91
|
-
response: Response,
|
|
92
|
-
onUpdate: (text: string, toolCalls: ToolCallAccumulator) => any,
|
|
93
|
-
) {
|
|
94
|
-
const reader = response.body?.getReader();
|
|
95
|
-
if (!reader) {
|
|
96
|
-
const json = await response.json();
|
|
97
|
-
const choice = json.choices?.[0]?.message;
|
|
98
|
-
return {
|
|
99
|
-
text: (choice?.content as string) ?? "",
|
|
100
|
-
toolCalls: {} as ToolCallAccumulator,
|
|
101
|
-
rawToolCalls: choice?.tool_calls,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const decoder = new TextDecoder();
|
|
106
|
-
let fullText = "";
|
|
107
|
-
const toolCalls: ToolCallAccumulator = {};
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
while (true) {
|
|
111
|
-
const { done, value } = await reader.read();
|
|
112
|
-
if (done) break;
|
|
113
|
-
|
|
114
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
115
|
-
for (const line of chunk.split("\n")) {
|
|
116
|
-
if (!line.startsWith("data: ")) continue;
|
|
117
|
-
const data = line.slice(6);
|
|
118
|
-
if (data === "[DONE]") continue;
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const delta = JSON.parse(data).choices?.[0]?.delta;
|
|
122
|
-
if (!delta) continue;
|
|
123
|
-
|
|
124
|
-
if (delta.content) fullText += delta.content;
|
|
125
|
-
if (delta.tool_calls) {
|
|
126
|
-
for (const tc of delta.tool_calls) {
|
|
127
|
-
if (!toolCalls[tc.index]) {
|
|
128
|
-
toolCalls[tc.index] = {
|
|
129
|
-
id: tc.id ?? "",
|
|
130
|
-
name: tc.function?.name ?? "",
|
|
131
|
-
arguments: "",
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (tc.id) toolCalls[tc.index].id = tc.id;
|
|
135
|
-
if (tc.function?.name)
|
|
136
|
-
toolCalls[tc.index].name = tc.function.name;
|
|
137
|
-
if (tc.function?.arguments)
|
|
138
|
-
toolCalls[tc.index].arguments += tc.function.arguments;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
yield* onUpdate(fullText, toolCalls);
|
|
143
|
-
} catch {
|
|
144
|
-
// skip invalid JSON
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
} finally {
|
|
149
|
-
reader.releaseLock();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return { text: fullText, toolCalls };
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
async *run({ messages, context, abortSignal }) {
|
|
157
|
-
const tools = context.tools;
|
|
158
|
-
|
|
159
|
-
// Convert messages to OpenAI format
|
|
160
|
-
const openAIMessages: OpenAIMessage[] = messages
|
|
161
|
-
.filter((m) => m.role !== "system")
|
|
162
|
-
.flatMap((m) => {
|
|
163
|
-
if (m.role === "user") {
|
|
164
|
-
const textParts = m.content.filter((p) => p.type === "text");
|
|
165
|
-
const text = textParts
|
|
166
|
-
.map((p) => ("text" in p ? p.text : ""))
|
|
167
|
-
.join("\n");
|
|
168
|
-
|
|
169
|
-
// Check for image attachments
|
|
170
|
-
const imageAttachments = (m.attachments ?? []).flatMap((a) =>
|
|
171
|
-
(a.content ?? []).filter((c: any) => c.type === "image"),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
if (imageAttachments.length > 0) {
|
|
175
|
-
const content: any[] = [];
|
|
176
|
-
if (text) content.push({ type: "text", text });
|
|
177
|
-
for (const img of imageAttachments) {
|
|
178
|
-
content.push({
|
|
179
|
-
type: "image_url",
|
|
180
|
-
image_url: { url: (img as any).image },
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
return [{ role: "user", content }];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return [{ role: "user", content: text }];
|
|
187
|
-
}
|
|
188
|
-
if (m.role === "assistant") {
|
|
189
|
-
const result: OpenAIMessage[] = [];
|
|
190
|
-
const textParts = m.content.filter((p) => p.type === "text");
|
|
191
|
-
const toolCallParts = m.content.filter(
|
|
192
|
-
(p) => p.type === "tool-call",
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
if (toolCallParts.length > 0) {
|
|
196
|
-
result.push({
|
|
197
|
-
role: "assistant",
|
|
198
|
-
content:
|
|
199
|
-
textParts.length > 0
|
|
200
|
-
? textParts
|
|
201
|
-
.map((p) => ("text" in p ? p.text : ""))
|
|
202
|
-
.join("\n")
|
|
203
|
-
: null,
|
|
204
|
-
tool_calls: toolCallParts.map((p: any) => ({
|
|
205
|
-
id: p.toolCallId,
|
|
206
|
-
type: "function",
|
|
207
|
-
function: {
|
|
208
|
-
name: p.toolName,
|
|
209
|
-
arguments: JSON.stringify(p.args),
|
|
210
|
-
},
|
|
211
|
-
})),
|
|
212
|
-
});
|
|
213
|
-
for (const tc of toolCallParts) {
|
|
214
|
-
if ((tc as any).result !== undefined) {
|
|
215
|
-
result.push({
|
|
216
|
-
role: "tool",
|
|
217
|
-
content: JSON.stringify((tc as any).result),
|
|
218
|
-
tool_call_id: (tc as any).toolCallId,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
} else if (textParts.length > 0) {
|
|
223
|
-
result.push({
|
|
224
|
-
role: "assistant",
|
|
225
|
-
content: textParts
|
|
226
|
-
.map((p) => ("text" in p ? p.text : ""))
|
|
227
|
-
.join("\n"),
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
233
|
-
return [];
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const openAITools =
|
|
237
|
-
tools && Object.keys(tools).length > 0
|
|
238
|
-
? Object.entries(tools).map(([name, t]) => ({
|
|
239
|
-
type: "function" as const,
|
|
240
|
-
function: {
|
|
241
|
-
name,
|
|
242
|
-
description: (t as any).description ?? "",
|
|
243
|
-
parameters: (t as any).parameters ?? {},
|
|
244
|
-
},
|
|
245
|
-
}))
|
|
246
|
-
: undefined;
|
|
247
|
-
|
|
248
|
-
// Tool execution loop — keep calling OpenAI until we get a text response
|
|
249
|
-
const maxToolRounds = 5;
|
|
250
|
-
const priorParts: any[] = []; // accumulate tool-call parts across rounds
|
|
251
|
-
|
|
252
|
-
for (let round = 0; round <= maxToolRounds; round++) {
|
|
253
|
-
const response = await callOpenAI(
|
|
254
|
-
openAIMessages,
|
|
255
|
-
openAITools,
|
|
256
|
-
abortSignal,
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
let lastText = "";
|
|
260
|
-
const gen = streamResponse(response, function* (text, toolCalls) {
|
|
261
|
-
lastText = text;
|
|
262
|
-
const content: any[] = [...priorParts];
|
|
263
|
-
if (text) content.push({ type: "text" as const, text });
|
|
264
|
-
for (const tc of Object.values(toolCalls)) {
|
|
265
|
-
let args = {};
|
|
266
|
-
try {
|
|
267
|
-
args = JSON.parse(tc.arguments);
|
|
268
|
-
} catch {
|
|
269
|
-
// still streaming
|
|
270
|
-
}
|
|
271
|
-
content.push({
|
|
272
|
-
type: "tool-call" as const,
|
|
273
|
-
toolCallId: tc.id,
|
|
274
|
-
toolName: tc.name,
|
|
275
|
-
args,
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
if (content.length > 0) yield { content };
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// Consume the stream
|
|
282
|
-
let streamResult: any;
|
|
283
|
-
while (true) {
|
|
284
|
-
const { value, done } = await gen.next();
|
|
285
|
-
if (done) {
|
|
286
|
-
streamResult = value;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
yield value;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const { toolCalls } = (streamResult as {
|
|
293
|
-
toolCalls: ToolCallAccumulator;
|
|
294
|
-
}) ?? { toolCalls: {} };
|
|
295
|
-
const pendingToolCalls = Object.values(toolCalls) as {
|
|
296
|
-
id: string;
|
|
297
|
-
name: string;
|
|
298
|
-
arguments: string;
|
|
299
|
-
}[];
|
|
300
|
-
|
|
301
|
-
// No tool calls — done
|
|
302
|
-
if (pendingToolCalls.length === 0) break;
|
|
303
|
-
|
|
304
|
-
// Execute tools and add results to messages for next round
|
|
305
|
-
openAIMessages.push({
|
|
306
|
-
role: "assistant",
|
|
307
|
-
content: lastText || null,
|
|
308
|
-
tool_calls: pendingToolCalls.map((tc) => ({
|
|
309
|
-
id: tc.id,
|
|
310
|
-
type: "function",
|
|
311
|
-
function: { name: tc.name, arguments: tc.arguments },
|
|
312
|
-
})),
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
const executedToolCalls: any[] = [];
|
|
316
|
-
for (const tc of pendingToolCalls) {
|
|
317
|
-
const args = JSON.parse(tc.arguments);
|
|
318
|
-
const toolDef = tools?.[tc.name];
|
|
319
|
-
let result: any;
|
|
320
|
-
if (toolDef?.execute) {
|
|
321
|
-
result = await (toolDef as any).execute(args);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
executedToolCalls.push({
|
|
325
|
-
type: "tool-call" as const,
|
|
326
|
-
toolCallId: tc.id,
|
|
327
|
-
toolName: tc.name,
|
|
328
|
-
args,
|
|
329
|
-
result,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// Yield with all prior parts + executed tool calls so far
|
|
333
|
-
yield { content: [...priorParts, ...executedToolCalls] };
|
|
334
|
-
|
|
335
|
-
openAIMessages.push({
|
|
336
|
-
role: "tool",
|
|
337
|
-
content: JSON.stringify(result),
|
|
338
|
-
tool_call_id: tc.id,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Add executed tool calls to prior parts for next round
|
|
343
|
-
priorParts.push(...executedToolCalls);
|
|
344
|
-
|
|
345
|
-
// Next iteration will call OpenAI with tool results
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
```
|
|
352
|
-
|
|
353
23
|
## app.json
|
|
354
24
|
|
|
355
25
|
```json
|
|
@@ -418,25 +88,27 @@ import { StatusBar } from "expo-status-bar";
|
|
|
418
88
|
import "react-native-reanimated";
|
|
419
89
|
import { Pressable, useColorScheme } from "react-native";
|
|
420
90
|
import { Ionicons } from "@expo/vector-icons";
|
|
91
|
+
import { useFonts } from "expo-font";
|
|
421
92
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
422
93
|
|
|
423
94
|
import {
|
|
424
|
-
|
|
425
|
-
|
|
95
|
+
AssistantRuntimeProvider,
|
|
96
|
+
useAui,
|
|
97
|
+
Tools,
|
|
426
98
|
} from "@assistant-ui/react-native";
|
|
427
99
|
import { useAppRuntime } from "@/hooks/use-app-runtime";
|
|
428
100
|
import { ThreadListDrawer } from "@/components/thread-list/ThreadListDrawer";
|
|
429
|
-
import {
|
|
101
|
+
import { expoToolkit } from "@/components/assistant-ui/tools";
|
|
430
102
|
|
|
431
103
|
function NewChatButton() {
|
|
432
|
-
const
|
|
104
|
+
const aui = useAui();
|
|
433
105
|
const colorScheme = useColorScheme();
|
|
434
106
|
const isDark = colorScheme === "dark";
|
|
435
107
|
|
|
436
108
|
return (
|
|
437
109
|
<Pressable
|
|
438
110
|
onPress={() => {
|
|
439
|
-
|
|
111
|
+
aui.threads().switchToNewThread();
|
|
440
112
|
}}
|
|
441
113
|
style={{ marginRight: 16 }}
|
|
442
114
|
>
|
|
@@ -471,20 +143,61 @@ function DrawerLayout() {
|
|
|
471
143
|
}
|
|
472
144
|
|
|
473
145
|
export default function RootLayout() {
|
|
146
|
+
const [fontsLoaded] = useFonts(Ionicons.font);
|
|
474
147
|
const runtime = useAppRuntime();
|
|
148
|
+
const aui = useAui({
|
|
149
|
+
tools: Tools({ toolkit: expoToolkit }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!fontsLoaded) return null;
|
|
475
153
|
|
|
476
154
|
return (
|
|
477
155
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
478
|
-
<
|
|
479
|
-
<WeatherTool />
|
|
156
|
+
<AssistantRuntimeProvider runtime={runtime} aui={aui}>
|
|
480
157
|
<DrawerLayout />
|
|
481
|
-
</
|
|
158
|
+
</AssistantRuntimeProvider>
|
|
482
159
|
</GestureHandlerRootView>
|
|
483
160
|
);
|
|
484
161
|
}
|
|
485
162
|
|
|
486
163
|
```
|
|
487
164
|
|
|
165
|
+
## app/api/chat+api.ts
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { frontendTools } from "@assistant-ui/react-ai-sdk";
|
|
169
|
+
import { openai } from "@ai-sdk/openai";
|
|
170
|
+
import {
|
|
171
|
+
convertToModelMessages,
|
|
172
|
+
pruneMessages,
|
|
173
|
+
stepCountIs,
|
|
174
|
+
streamText,
|
|
175
|
+
} from "ai";
|
|
176
|
+
|
|
177
|
+
export async function POST(req: Request) {
|
|
178
|
+
const body = await req.json();
|
|
179
|
+
const { messages, tools } = body;
|
|
180
|
+
|
|
181
|
+
const model = openai("gpt-4o-mini");
|
|
182
|
+
|
|
183
|
+
const prunedMessages = pruneMessages({
|
|
184
|
+
messages: await convertToModelMessages(messages),
|
|
185
|
+
reasoning: "none",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = streamText({
|
|
189
|
+
model,
|
|
190
|
+
messages: prunedMessages,
|
|
191
|
+
maxOutputTokens: 15000,
|
|
192
|
+
stopWhen: stepCountIs(10),
|
|
193
|
+
tools: frontendTools(tools),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return result.toUIMessageStreamResponse();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
|
|
488
201
|
## app/index.tsx
|
|
489
202
|
|
|
490
203
|
```tsx
|
|
@@ -501,9 +214,9 @@ export default function ChatPage() {
|
|
|
501
214
|
```tsx
|
|
502
215
|
import {
|
|
503
216
|
View,
|
|
504
|
-
TextInput,
|
|
505
217
|
Pressable,
|
|
506
218
|
Image,
|
|
219
|
+
Platform,
|
|
507
220
|
StyleSheet,
|
|
508
221
|
useColorScheme,
|
|
509
222
|
} from "react-native";
|
|
@@ -512,12 +225,8 @@ import * as ImagePicker from "expo-image-picker";
|
|
|
512
225
|
import {
|
|
513
226
|
useAui,
|
|
514
227
|
useAuiState,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
useComposerAddAttachment,
|
|
518
|
-
ComposerAttachments,
|
|
519
|
-
AttachmentRoot,
|
|
520
|
-
AttachmentRemove,
|
|
228
|
+
ComposerPrimitive,
|
|
229
|
+
AttachmentPrimitive,
|
|
521
230
|
} from "@assistant-ui/react-native";
|
|
522
231
|
|
|
523
232
|
function AttachmentPreview() {
|
|
@@ -529,27 +238,25 @@ function AttachmentPreview() {
|
|
|
529
238
|
const uri = (imageContent as any)?.image;
|
|
530
239
|
|
|
531
240
|
return (
|
|
532
|
-
<
|
|
241
|
+
<AttachmentPrimitive.Root style={styles.attachmentItem}>
|
|
533
242
|
{uri ? <Image source={{ uri }} style={styles.attachmentImage} /> : null}
|
|
534
|
-
<
|
|
243
|
+
<AttachmentPrimitive.Remove style={styles.attachmentRemoveButton}>
|
|
535
244
|
<Ionicons name="close-circle" size={20} color="#ff453a" />
|
|
536
|
-
</
|
|
537
|
-
</
|
|
245
|
+
</AttachmentPrimitive.Remove>
|
|
246
|
+
</AttachmentPrimitive.Root>
|
|
538
247
|
);
|
|
539
248
|
}
|
|
540
249
|
|
|
541
|
-
const attachmentComponents = { Attachment: AttachmentPreview };
|
|
542
|
-
|
|
543
250
|
export function Composer() {
|
|
544
251
|
const colorScheme = useColorScheme();
|
|
545
252
|
const isDark = colorScheme === "dark";
|
|
546
253
|
|
|
547
254
|
const aui = useAui();
|
|
548
|
-
const text = useAuiState((s) => s.composer.text);
|
|
549
255
|
const attachmentsCount = useAuiState((s) => s.composer.attachments.length);
|
|
550
|
-
const
|
|
551
|
-
const
|
|
552
|
-
|
|
256
|
+
const canCancel = useAuiState((s) => s.composer.canCancel);
|
|
257
|
+
const canSend = useAuiState(
|
|
258
|
+
(s) => !s.thread.isRunning && s.composer.isEditing && !s.composer.isEmpty,
|
|
259
|
+
);
|
|
553
260
|
|
|
554
261
|
const pickImage = async () => {
|
|
555
262
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
@@ -565,7 +272,7 @@ export function Composer() {
|
|
|
565
272
|
// Force JPEG mime type — iOS may report HEIC which OpenAI doesn't support
|
|
566
273
|
const dataUrl = `data:image/jpeg;base64,${asset.base64}`;
|
|
567
274
|
|
|
568
|
-
await addAttachment({
|
|
275
|
+
await aui.composer().addAttachment({
|
|
569
276
|
name: asset.fileName ?? "image.jpg",
|
|
570
277
|
contentType: "image/jpeg",
|
|
571
278
|
type: "image",
|
|
@@ -587,7 +294,9 @@ export function Composer() {
|
|
|
587
294
|
>
|
|
588
295
|
{attachmentsCount > 0 && (
|
|
589
296
|
<View style={styles.attachmentsList}>
|
|
590
|
-
<
|
|
297
|
+
<ComposerPrimitive.Attachments>
|
|
298
|
+
{() => <AttachmentPreview />}
|
|
299
|
+
</ComposerPrimitive.Attachments>
|
|
591
300
|
</View>
|
|
592
301
|
)}
|
|
593
302
|
<View
|
|
@@ -610,25 +319,20 @@ export function Composer() {
|
|
|
610
319
|
color={isDark ? "#8e8e93" : "#6e6e73"}
|
|
611
320
|
/>
|
|
612
321
|
</Pressable>
|
|
613
|
-
<
|
|
322
|
+
<ComposerPrimitive.Input
|
|
614
323
|
style={[styles.input, { color: isDark ? "#ffffff" : "#000000" }]}
|
|
615
324
|
placeholder="Message..."
|
|
616
325
|
placeholderTextColor="#8e8e93"
|
|
617
|
-
value={text}
|
|
618
|
-
onChangeText={(newText) => aui.composer().setText(newText)}
|
|
619
326
|
multiline
|
|
620
327
|
maxLength={4000}
|
|
621
328
|
editable={!canCancel}
|
|
622
329
|
/>
|
|
623
330
|
{canCancel ? (
|
|
624
|
-
<
|
|
625
|
-
style={[styles.button, styles.stopButton]}
|
|
626
|
-
onPress={cancel}
|
|
627
|
-
>
|
|
331
|
+
<ComposerPrimitive.Cancel style={[styles.button, styles.stopButton]}>
|
|
628
332
|
<View style={styles.stopIcon} />
|
|
629
|
-
</
|
|
333
|
+
</ComposerPrimitive.Cancel>
|
|
630
334
|
) : (
|
|
631
|
-
<
|
|
335
|
+
<ComposerPrimitive.Send
|
|
632
336
|
style={[
|
|
633
337
|
styles.button,
|
|
634
338
|
styles.sendButton,
|
|
@@ -642,15 +346,13 @@ export function Composer() {
|
|
|
642
346
|
: "#e5e5ea",
|
|
643
347
|
},
|
|
644
348
|
]}
|
|
645
|
-
onPress={send}
|
|
646
|
-
disabled={!canSend}
|
|
647
349
|
>
|
|
648
350
|
<Ionicons
|
|
649
351
|
name="arrow-up"
|
|
650
352
|
size={20}
|
|
651
353
|
color={canSend ? "#ffffff" : "#8e8e93"}
|
|
652
354
|
/>
|
|
653
|
-
</
|
|
355
|
+
</ComposerPrimitive.Send>
|
|
654
356
|
)}
|
|
655
357
|
</View>
|
|
656
358
|
</View>
|
|
@@ -660,8 +362,7 @@ export function Composer() {
|
|
|
660
362
|
const styles = StyleSheet.create({
|
|
661
363
|
container: {
|
|
662
364
|
paddingHorizontal: 16,
|
|
663
|
-
|
|
664
|
-
paddingBottom: 8,
|
|
365
|
+
paddingVertical: 8,
|
|
665
366
|
},
|
|
666
367
|
attachmentsList: {
|
|
667
368
|
flexDirection: "row",
|
|
@@ -685,36 +386,35 @@ const styles = StyleSheet.create({
|
|
|
685
386
|
inputWrapper: {
|
|
686
387
|
flexDirection: "row",
|
|
687
388
|
alignItems: "flex-end",
|
|
688
|
-
borderRadius:
|
|
389
|
+
borderRadius: 20,
|
|
689
390
|
borderWidth: 1,
|
|
690
|
-
|
|
691
|
-
paddingRight: 6,
|
|
692
|
-
paddingVertical: 6,
|
|
693
|
-
minHeight: 48,
|
|
391
|
+
padding: 6,
|
|
694
392
|
},
|
|
695
393
|
attachButton: {
|
|
696
|
-
width:
|
|
697
|
-
height:
|
|
394
|
+
width: 30,
|
|
395
|
+
height: 30,
|
|
698
396
|
justifyContent: "center",
|
|
699
397
|
alignItems: "center",
|
|
700
398
|
},
|
|
701
399
|
input: {
|
|
702
400
|
flex: 1,
|
|
703
401
|
fontSize: 16,
|
|
704
|
-
lineHeight: 22,
|
|
705
402
|
maxHeight: 120,
|
|
706
|
-
|
|
707
|
-
|
|
403
|
+
alignSelf: "center",
|
|
404
|
+
paddingVertical: 0,
|
|
405
|
+
...Platform.select({
|
|
406
|
+
web: { paddingHorizontal: 4, outlineStyle: "none" },
|
|
407
|
+
default: {},
|
|
408
|
+
}),
|
|
708
409
|
},
|
|
709
410
|
button: {
|
|
710
|
-
width:
|
|
711
|
-
height:
|
|
712
|
-
borderRadius:
|
|
411
|
+
width: 30,
|
|
412
|
+
height: 30,
|
|
413
|
+
borderRadius: 15,
|
|
713
414
|
justifyContent: "center",
|
|
714
415
|
alignItems: "center",
|
|
715
|
-
marginLeft:
|
|
416
|
+
marginLeft: 6,
|
|
716
417
|
},
|
|
717
|
-
sendButton: {},
|
|
718
418
|
stopButton: {
|
|
719
419
|
backgroundColor: "#ff453a",
|
|
720
420
|
},
|
|
@@ -731,33 +431,29 @@ const styles = StyleSheet.create({
|
|
|
731
431
|
## components/assistant-ui/message-action-bar.tsx
|
|
732
432
|
|
|
733
433
|
```tsx
|
|
734
|
-
import {
|
|
434
|
+
import { View, StyleSheet, useColorScheme } from "react-native";
|
|
735
435
|
import { Ionicons } from "@expo/vector-icons";
|
|
736
|
-
import {
|
|
737
|
-
useActionBarCopy,
|
|
738
|
-
useActionBarReload,
|
|
739
|
-
} from "@assistant-ui/react-native";
|
|
436
|
+
import { ActionBarPrimitive } from "@assistant-ui/react-native";
|
|
740
437
|
|
|
741
438
|
export function MessageActionBar() {
|
|
742
439
|
const colorScheme = useColorScheme();
|
|
743
440
|
const isDark = colorScheme === "dark";
|
|
744
441
|
const iconColor = isDark ? "#8e8e93" : "#6e6e73";
|
|
745
442
|
|
|
746
|
-
const { copy, isCopied } = useActionBarCopy();
|
|
747
|
-
const { reload } = useActionBarReload();
|
|
748
|
-
|
|
749
443
|
return (
|
|
750
444
|
<View style={styles.container}>
|
|
751
|
-
<
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
445
|
+
<ActionBarPrimitive.Copy style={styles.button}>
|
|
446
|
+
{({ isCopied }) => (
|
|
447
|
+
<Ionicons
|
|
448
|
+
name={isCopied ? "checkmark" : "copy-outline"}
|
|
449
|
+
size={16}
|
|
450
|
+
color={isCopied ? "#34c759" : iconColor}
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
453
|
+
</ActionBarPrimitive.Copy>
|
|
454
|
+
<ActionBarPrimitive.Reload style={styles.button}>
|
|
759
455
|
<Ionicons name="refresh-outline" size={16} color={iconColor} />
|
|
760
|
-
</
|
|
456
|
+
</ActionBarPrimitive.Reload>
|
|
761
457
|
</View>
|
|
762
458
|
);
|
|
763
459
|
}
|
|
@@ -779,14 +475,14 @@ const styles = StyleSheet.create({
|
|
|
779
475
|
## components/assistant-ui/message-branch-picker.tsx
|
|
780
476
|
|
|
781
477
|
```tsx
|
|
782
|
-
import {
|
|
478
|
+
import { View, StyleSheet, useColorScheme } from "react-native";
|
|
783
479
|
import { Ionicons } from "@expo/vector-icons";
|
|
784
480
|
import { ThemedText } from "@/components/themed-text";
|
|
785
|
-
import {
|
|
481
|
+
import { BranchPickerPrimitive, useAuiState } from "@assistant-ui/react-native";
|
|
786
482
|
|
|
787
483
|
export function MessageBranchPicker() {
|
|
788
|
-
const
|
|
789
|
-
|
|
484
|
+
const branchNumber = useAuiState((s) => s.message.branchNumber);
|
|
485
|
+
const branchCount = useAuiState((s) => s.message.branchCount);
|
|
790
486
|
|
|
791
487
|
const colorScheme = useColorScheme();
|
|
792
488
|
const isDark = colorScheme === "dark";
|
|
@@ -796,11 +492,7 @@ export function MessageBranchPicker() {
|
|
|
796
492
|
|
|
797
493
|
return (
|
|
798
494
|
<View style={styles.container}>
|
|
799
|
-
<
|
|
800
|
-
style={styles.button}
|
|
801
|
-
onPress={goToPrev}
|
|
802
|
-
disabled={branchNumber <= 1}
|
|
803
|
-
>
|
|
495
|
+
<BranchPickerPrimitive.Previous style={styles.button}>
|
|
804
496
|
<Ionicons
|
|
805
497
|
name="chevron-back"
|
|
806
498
|
size={14}
|
|
@@ -808,15 +500,11 @@ export function MessageBranchPicker() {
|
|
|
808
500
|
branchNumber <= 1 ? (isDark ? "#3a3a3c" : "#d1d1d6") : iconColor
|
|
809
501
|
}
|
|
810
502
|
/>
|
|
811
|
-
</
|
|
503
|
+
</BranchPickerPrimitive.Previous>
|
|
812
504
|
<ThemedText style={styles.label} lightColor="#6e6e73" darkColor="#8e8e93">
|
|
813
505
|
{branchNumber} / {branchCount}
|
|
814
506
|
</ThemedText>
|
|
815
|
-
<
|
|
816
|
-
style={styles.button}
|
|
817
|
-
onPress={goToNext}
|
|
818
|
-
disabled={branchNumber >= branchCount}
|
|
819
|
-
>
|
|
507
|
+
<BranchPickerPrimitive.Next style={styles.button}>
|
|
820
508
|
<Ionicons
|
|
821
509
|
name="chevron-forward"
|
|
822
510
|
size={14}
|
|
@@ -828,7 +516,7 @@ export function MessageBranchPicker() {
|
|
|
828
516
|
: iconColor
|
|
829
517
|
}
|
|
830
518
|
/>
|
|
831
|
-
</
|
|
519
|
+
</BranchPickerPrimitive.Next>
|
|
832
520
|
</View>
|
|
833
521
|
);
|
|
834
522
|
}
|
|
@@ -856,11 +544,7 @@ const styles = StyleSheet.create({
|
|
|
856
544
|
```tsx
|
|
857
545
|
import { View, Image, StyleSheet, useColorScheme } from "react-native";
|
|
858
546
|
import { ThemedText } from "@/components/themed-text";
|
|
859
|
-
import {
|
|
860
|
-
useAuiState,
|
|
861
|
-
MessageContent,
|
|
862
|
-
MessageAttachments,
|
|
863
|
-
} from "@assistant-ui/react-native";
|
|
547
|
+
import { useAuiState, MessagePrimitive } from "@assistant-ui/react-native";
|
|
864
548
|
import { MessageActionBar } from "./message-action-bar";
|
|
865
549
|
import { MessageBranchPicker } from "./message-branch-picker";
|
|
866
550
|
|
|
@@ -915,8 +599,6 @@ function MessageImageAttachment() {
|
|
|
915
599
|
return <Image source={{ uri }} style={styles.messageImage} />;
|
|
916
600
|
}
|
|
917
601
|
|
|
918
|
-
const messageAttachmentComponents = { Attachment: MessageImageAttachment };
|
|
919
|
-
|
|
920
602
|
export function MessageBubble() {
|
|
921
603
|
const colorScheme = useColorScheme();
|
|
922
604
|
const isDark = colorScheme === "dark";
|
|
@@ -927,7 +609,9 @@ export function MessageBubble() {
|
|
|
927
609
|
if (isUser) {
|
|
928
610
|
return (
|
|
929
611
|
<View style={[styles.container, styles.userContainer]}>
|
|
930
|
-
<
|
|
612
|
+
<MessagePrimitive.Attachments>
|
|
613
|
+
{() => <MessageImageAttachment />}
|
|
614
|
+
</MessagePrimitive.Attachments>
|
|
931
615
|
<View
|
|
932
616
|
style={[
|
|
933
617
|
styles.bubble,
|
|
@@ -935,7 +619,9 @@ export function MessageBubble() {
|
|
|
935
619
|
{ backgroundColor: isDark ? "#0a84ff" : "#007aff" },
|
|
936
620
|
]}
|
|
937
621
|
>
|
|
938
|
-
<
|
|
622
|
+
<MessagePrimitive.Content
|
|
623
|
+
renderText={({ part }) => <TextPart part={part} />}
|
|
624
|
+
/>
|
|
939
625
|
</View>
|
|
940
626
|
<MessageBranchPicker />
|
|
941
627
|
</View>
|
|
@@ -955,7 +641,9 @@ export function MessageBubble() {
|
|
|
955
641
|
},
|
|
956
642
|
]}
|
|
957
643
|
>
|
|
958
|
-
<
|
|
644
|
+
<MessagePrimitive.Content
|
|
645
|
+
renderText={({ part }) => <TextPart part={part} />}
|
|
646
|
+
/>
|
|
959
647
|
<MessageError />
|
|
960
648
|
</View>
|
|
961
649
|
{!isRunning && (
|
|
@@ -1040,11 +728,7 @@ import {
|
|
|
1040
728
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
1041
729
|
import { MessageBubble } from "./message";
|
|
1042
730
|
import { Composer } from "./composer";
|
|
1043
|
-
import {
|
|
1044
|
-
ThreadMessages,
|
|
1045
|
-
useThreadIsEmpty,
|
|
1046
|
-
useAui,
|
|
1047
|
-
} from "@assistant-ui/react-native";
|
|
731
|
+
import { ThreadPrimitive, useAui } from "@assistant-ui/react-native";
|
|
1048
732
|
|
|
1049
733
|
function SuggestionChip({ title, prompt }: { title: string; prompt: string }) {
|
|
1050
734
|
const colorScheme = useColorScheme();
|
|
@@ -1126,21 +810,21 @@ function EmptyState() {
|
|
|
1126
810
|
);
|
|
1127
811
|
}
|
|
1128
812
|
|
|
1129
|
-
const renderMessage = () => <MessageBubble />;
|
|
1130
|
-
|
|
1131
813
|
function ChatMessages() {
|
|
1132
|
-
const isEmpty = useThreadIsEmpty();
|
|
1133
|
-
|
|
1134
|
-
if (isEmpty) {
|
|
1135
|
-
return <EmptyState />;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
814
|
return (
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
815
|
+
<>
|
|
816
|
+
<ThreadPrimitive.Empty>
|
|
817
|
+
<EmptyState />
|
|
818
|
+
</ThreadPrimitive.Empty>
|
|
819
|
+
<ThreadPrimitive.If empty={false}>
|
|
820
|
+
<ThreadPrimitive.Messages
|
|
821
|
+
contentContainerStyle={styles.messageList}
|
|
822
|
+
showsVerticalScrollIndicator={false}
|
|
823
|
+
>
|
|
824
|
+
{() => <MessageBubble />}
|
|
825
|
+
</ThreadPrimitive.Messages>
|
|
826
|
+
</ThreadPrimitive.If>
|
|
827
|
+
</>
|
|
1144
828
|
);
|
|
1145
829
|
}
|
|
1146
830
|
|
|
@@ -1238,14 +922,123 @@ const styles = StyleSheet.create({
|
|
|
1238
922
|
|
|
1239
923
|
```tsx
|
|
1240
924
|
import { View, Text, StyleSheet, useColorScheme } from "react-native";
|
|
1241
|
-
import {
|
|
1242
|
-
|
|
1243
|
-
|
|
925
|
+
import type {
|
|
926
|
+
Toolkit,
|
|
927
|
+
ToolCallMessagePartProps,
|
|
1244
928
|
} from "@assistant-ui/react-native";
|
|
929
|
+
import { z } from "zod";
|
|
1245
930
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
) => {
|
|
931
|
+
// Open-Meteo API adapters (free, no API key needed)
|
|
932
|
+
|
|
933
|
+
const geocodeLocationWithOpenMeteo = async (query: string) => {
|
|
934
|
+
try {
|
|
935
|
+
const response = await fetch(
|
|
936
|
+
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1`,
|
|
937
|
+
);
|
|
938
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
939
|
+
const data = await response.json();
|
|
940
|
+
if (!data.results || data.results.length === 0)
|
|
941
|
+
throw new Error("No results found");
|
|
942
|
+
return { success: true as const, result: data.results[0] };
|
|
943
|
+
} catch (error) {
|
|
944
|
+
return {
|
|
945
|
+
success: false as const,
|
|
946
|
+
error:
|
|
947
|
+
error instanceof Error ? error.message : "Failed to geocode location",
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
const mapWeatherCode = (code: number): string => {
|
|
953
|
+
if (code === 0) return "Clear";
|
|
954
|
+
if (code <= 3) return "Partly Cloudy";
|
|
955
|
+
if (code <= 48) return "Foggy";
|
|
956
|
+
if (code <= 57) return "Drizzle";
|
|
957
|
+
if (code <= 67) return "Rain";
|
|
958
|
+
if (code <= 77) return "Snow";
|
|
959
|
+
if (code <= 82) return "Showers";
|
|
960
|
+
if (code <= 86) return "Snow Showers";
|
|
961
|
+
if (code === 95) return "Thunderstorm";
|
|
962
|
+
return "Stormy";
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const mapWeatherEmoji = (code: number): string => {
|
|
966
|
+
if (code === 0) return "\u2600\uFE0F";
|
|
967
|
+
if (code <= 3) return "\u26C5";
|
|
968
|
+
if (code <= 48) return "\uD83C\uDF2B\uFE0F";
|
|
969
|
+
if (code <= 57) return "\uD83C\uDF26\uFE0F";
|
|
970
|
+
if (code <= 67) return "\uD83C\uDF27\uFE0F";
|
|
971
|
+
if (code <= 77) return "\u2744\uFE0F";
|
|
972
|
+
if (code <= 82) return "\uD83C\uDF26\uFE0F";
|
|
973
|
+
if (code <= 86) return "\uD83C\uDF28\uFE0F";
|
|
974
|
+
if (code === 95) return "\u26C8\uFE0F";
|
|
975
|
+
return "\uD83C\uDF29\uFE0F";
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const fetchWeatherFromOpenMeteo = async ({
|
|
979
|
+
query,
|
|
980
|
+
longitude,
|
|
981
|
+
latitude,
|
|
982
|
+
}: {
|
|
983
|
+
query: string;
|
|
984
|
+
longitude: number;
|
|
985
|
+
latitude: number;
|
|
986
|
+
}) => {
|
|
987
|
+
try {
|
|
988
|
+
const response = await fetch(
|
|
989
|
+
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&timezone=auto&temperature_unit=fahrenheit¤t=temperature_2m,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&forecast_days=5`,
|
|
990
|
+
);
|
|
991
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
992
|
+
const data = await response.json();
|
|
993
|
+
const current = data.current;
|
|
994
|
+
const daily = data.daily;
|
|
995
|
+
if (!current || !daily?.time) throw new Error("Invalid API response");
|
|
996
|
+
|
|
997
|
+
const forecast = daily.time.slice(0, 5).map((date: string, i: number) => {
|
|
998
|
+
const d = new Date(`${date}T12:00:00Z`);
|
|
999
|
+
const label =
|
|
1000
|
+
i === 0
|
|
1001
|
+
? "Today"
|
|
1002
|
+
: new Intl.DateTimeFormat("en-US", {
|
|
1003
|
+
weekday: "short",
|
|
1004
|
+
timeZone: "UTC",
|
|
1005
|
+
}).format(d);
|
|
1006
|
+
return {
|
|
1007
|
+
label,
|
|
1008
|
+
code: daily.weather_code[i],
|
|
1009
|
+
min: Math.round(daily.temperature_2m_min[i]),
|
|
1010
|
+
max: Math.round(daily.temperature_2m_max[i]),
|
|
1011
|
+
};
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
return {
|
|
1015
|
+
success: true as const,
|
|
1016
|
+
location: query,
|
|
1017
|
+
temperature: Math.round(current.temperature_2m),
|
|
1018
|
+
weatherCode: current.weather_code,
|
|
1019
|
+
windSpeed: Math.round(current.wind_speed_10m),
|
|
1020
|
+
forecast,
|
|
1021
|
+
};
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
return {
|
|
1024
|
+
success: false as const,
|
|
1025
|
+
error: error instanceof Error ? error.message : "Failed to fetch weather",
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// Tool UI Components
|
|
1031
|
+
|
|
1032
|
+
function GeocodeToolUI(
|
|
1033
|
+
props: ToolCallMessagePartProps<
|
|
1034
|
+
{ query: string },
|
|
1035
|
+
{
|
|
1036
|
+
success: boolean;
|
|
1037
|
+
result?: { name: string; latitude: number; longitude: number };
|
|
1038
|
+
error?: string;
|
|
1039
|
+
}
|
|
1040
|
+
>,
|
|
1041
|
+
) {
|
|
1249
1042
|
const colorScheme = useColorScheme();
|
|
1250
1043
|
const isDark = colorScheme === "dark";
|
|
1251
1044
|
|
|
@@ -1258,65 +1051,315 @@ const WeatherToolUI = (
|
|
|
1258
1051
|
]}
|
|
1259
1052
|
>
|
|
1260
1053
|
<Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
|
|
1261
|
-
|
|
1054
|
+
Finding location...
|
|
1055
|
+
</Text>
|
|
1056
|
+
</View>
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (props.result?.error) {
|
|
1061
|
+
return (
|
|
1062
|
+
<View
|
|
1063
|
+
style={[
|
|
1064
|
+
styles.card,
|
|
1065
|
+
{ backgroundColor: isDark ? "#3a1c1c" : "#fff0f0" },
|
|
1066
|
+
]}
|
|
1067
|
+
>
|
|
1068
|
+
<Text style={[styles.label, { color: "#ff453a" }]}>
|
|
1069
|
+
Geocoding failed: {props.result.error}
|
|
1262
1070
|
</Text>
|
|
1263
1071
|
</View>
|
|
1264
1072
|
);
|
|
1265
1073
|
}
|
|
1266
1074
|
|
|
1075
|
+
const result = props.result?.result;
|
|
1076
|
+
if (!result) return null;
|
|
1077
|
+
|
|
1267
1078
|
return (
|
|
1268
1079
|
<View
|
|
1269
1080
|
style={[styles.card, { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" }]}
|
|
1270
1081
|
>
|
|
1271
|
-
<
|
|
1272
|
-
{
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1082
|
+
<View style={styles.row}>
|
|
1083
|
+
<Text style={styles.pin}>{"\uD83D\uDCCD"}</Text>
|
|
1084
|
+
<View>
|
|
1085
|
+
<Text
|
|
1086
|
+
style={[
|
|
1087
|
+
styles.locationName,
|
|
1088
|
+
{ color: isDark ? "#ffffff" : "#000000" },
|
|
1089
|
+
]}
|
|
1090
|
+
>
|
|
1091
|
+
{result.name}
|
|
1092
|
+
</Text>
|
|
1093
|
+
<Text
|
|
1094
|
+
style={[styles.coords, { color: isDark ? "#8e8e93" : "#6e6e73" }]}
|
|
1095
|
+
>
|
|
1096
|
+
{Math.abs(result.latitude).toFixed(2)}
|
|
1097
|
+
{"\u00B0"}
|
|
1098
|
+
{result.latitude >= 0 ? "N" : "S"},{" "}
|
|
1099
|
+
{Math.abs(result.longitude).toFixed(2)}
|
|
1100
|
+
{"\u00B0"}
|
|
1101
|
+
{result.longitude >= 0 ? "E" : "W"}
|
|
1102
|
+
</Text>
|
|
1103
|
+
</View>
|
|
1104
|
+
</View>
|
|
1105
|
+
</View>
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function WeatherToolUI(
|
|
1110
|
+
props: ToolCallMessagePartProps<
|
|
1111
|
+
{ query: string; longitude: number; latitude: number },
|
|
1112
|
+
{
|
|
1113
|
+
success: boolean;
|
|
1114
|
+
location?: string;
|
|
1115
|
+
temperature?: number;
|
|
1116
|
+
weatherCode?: number;
|
|
1117
|
+
windSpeed?: number;
|
|
1118
|
+
forecast?: Array<{
|
|
1119
|
+
label: string;
|
|
1120
|
+
code: number;
|
|
1121
|
+
min: number;
|
|
1122
|
+
max: number;
|
|
1123
|
+
}>;
|
|
1124
|
+
error?: string;
|
|
1125
|
+
}
|
|
1126
|
+
>,
|
|
1127
|
+
) {
|
|
1128
|
+
const colorScheme = useColorScheme();
|
|
1129
|
+
const isDark = colorScheme === "dark";
|
|
1130
|
+
|
|
1131
|
+
if (props.status?.type === "running") {
|
|
1132
|
+
return (
|
|
1133
|
+
<View
|
|
1134
|
+
style={[
|
|
1135
|
+
styles.card,
|
|
1136
|
+
{ backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
|
|
1137
|
+
]}
|
|
1138
|
+
>
|
|
1139
|
+
<Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
|
|
1140
|
+
Fetching weather for {props.args.query}...
|
|
1141
|
+
</Text>
|
|
1142
|
+
</View>
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (!props.result?.success) {
|
|
1147
|
+
return (
|
|
1148
|
+
<View
|
|
1149
|
+
style={[
|
|
1150
|
+
styles.card,
|
|
1151
|
+
{ backgroundColor: isDark ? "#3a1c1c" : "#fff0f0" },
|
|
1152
|
+
]}
|
|
1153
|
+
>
|
|
1154
|
+
<Text style={[styles.label, { color: "#ff453a" }]}>
|
|
1155
|
+
Weather unavailable: {props.result?.error ?? "Unknown error"}
|
|
1156
|
+
</Text>
|
|
1157
|
+
</View>
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const { location, temperature, weatherCode, windSpeed, forecast } =
|
|
1162
|
+
props.result;
|
|
1163
|
+
|
|
1164
|
+
return (
|
|
1165
|
+
<View
|
|
1166
|
+
style={[
|
|
1167
|
+
styles.weatherCard,
|
|
1168
|
+
{ backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
|
|
1169
|
+
]}
|
|
1170
|
+
>
|
|
1171
|
+
<View style={styles.weatherHeader}>
|
|
1172
|
+
<Text style={styles.weatherEmoji}>
|
|
1173
|
+
{mapWeatherEmoji(weatherCode ?? 0)}
|
|
1174
|
+
</Text>
|
|
1175
|
+
<View>
|
|
1176
|
+
<Text
|
|
1177
|
+
style={[
|
|
1178
|
+
styles.locationName,
|
|
1179
|
+
{ color: isDark ? "#ffffff" : "#000000" },
|
|
1180
|
+
]}
|
|
1181
|
+
>
|
|
1182
|
+
{location}
|
|
1183
|
+
</Text>
|
|
1184
|
+
<Text
|
|
1185
|
+
style={[
|
|
1186
|
+
styles.condition,
|
|
1187
|
+
{ color: isDark ? "#8e8e93" : "#6e6e73" },
|
|
1188
|
+
]}
|
|
1189
|
+
>
|
|
1190
|
+
{mapWeatherCode(weatherCode ?? 0)}
|
|
1191
|
+
</Text>
|
|
1192
|
+
</View>
|
|
1193
|
+
</View>
|
|
1194
|
+
|
|
1195
|
+
<Text style={styles.tempLarge}>
|
|
1196
|
+
{temperature ?? "--"}
|
|
1197
|
+
{"\u00B0"}F
|
|
1277
1198
|
</Text>
|
|
1199
|
+
|
|
1200
|
+
{windSpeed != null && (
|
|
1201
|
+
<Text style={[styles.wind, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
|
|
1202
|
+
Wind: {windSpeed} mph
|
|
1203
|
+
</Text>
|
|
1204
|
+
)}
|
|
1205
|
+
|
|
1206
|
+
{forecast && forecast.length > 0 && (
|
|
1207
|
+
<View
|
|
1208
|
+
style={[
|
|
1209
|
+
styles.forecastRow,
|
|
1210
|
+
{
|
|
1211
|
+
borderTopColor: isDark
|
|
1212
|
+
? "rgba(255,255,255,0.1)"
|
|
1213
|
+
: "rgba(0,0,0,0.1)",
|
|
1214
|
+
},
|
|
1215
|
+
]}
|
|
1216
|
+
>
|
|
1217
|
+
{forecast.map((day, i) => (
|
|
1218
|
+
<View key={i} style={styles.forecastDay}>
|
|
1219
|
+
<Text
|
|
1220
|
+
style={[
|
|
1221
|
+
styles.forecastLabel,
|
|
1222
|
+
{ color: isDark ? "#8e8e93" : "#6e6e73" },
|
|
1223
|
+
]}
|
|
1224
|
+
>
|
|
1225
|
+
{day.label}
|
|
1226
|
+
</Text>
|
|
1227
|
+
<Text style={styles.forecastEmoji}>
|
|
1228
|
+
{mapWeatherEmoji(day.code)}
|
|
1229
|
+
</Text>
|
|
1230
|
+
<Text
|
|
1231
|
+
style={[
|
|
1232
|
+
styles.forecastTemp,
|
|
1233
|
+
{ color: isDark ? "#ffffff" : "#000000" },
|
|
1234
|
+
]}
|
|
1235
|
+
>
|
|
1236
|
+
{day.max}
|
|
1237
|
+
{"\u00B0"}
|
|
1238
|
+
</Text>
|
|
1239
|
+
<Text
|
|
1240
|
+
style={[
|
|
1241
|
+
styles.forecastTempLow,
|
|
1242
|
+
{ color: isDark ? "#8e8e93" : "#6e6e73" },
|
|
1243
|
+
]}
|
|
1244
|
+
>
|
|
1245
|
+
{day.min}
|
|
1246
|
+
{"\u00B0"}
|
|
1247
|
+
</Text>
|
|
1248
|
+
</View>
|
|
1249
|
+
))}
|
|
1250
|
+
</View>
|
|
1251
|
+
)}
|
|
1278
1252
|
</View>
|
|
1279
1253
|
);
|
|
1280
|
-
}
|
|
1254
|
+
}
|
|
1281
1255
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
},
|
|
1290
|
-
|
|
1256
|
+
// Toolkit definition
|
|
1257
|
+
|
|
1258
|
+
export const expoToolkit: Toolkit = {
|
|
1259
|
+
geocode_location: {
|
|
1260
|
+
description: "Geocode a location using Open-Meteo's geocoding API",
|
|
1261
|
+
parameters: z.object({
|
|
1262
|
+
query: z.string(),
|
|
1263
|
+
}),
|
|
1264
|
+
execute: async (args: { query: string }) =>
|
|
1265
|
+
geocodeLocationWithOpenMeteo(args.query),
|
|
1266
|
+
render: GeocodeToolUI,
|
|
1267
|
+
},
|
|
1268
|
+
weather_search: {
|
|
1269
|
+
description:
|
|
1270
|
+
"Find the weather in a location given a longitude and latitude",
|
|
1271
|
+
parameters: z.object({
|
|
1272
|
+
query: z.string(),
|
|
1273
|
+
longitude: z.number(),
|
|
1274
|
+
latitude: z.number(),
|
|
1275
|
+
}),
|
|
1276
|
+
execute: async (args: {
|
|
1277
|
+
query: string;
|
|
1278
|
+
longitude: number;
|
|
1279
|
+
latitude: number;
|
|
1280
|
+
}) => fetchWeatherFromOpenMeteo(args),
|
|
1281
|
+
render: WeatherToolUI,
|
|
1291
1282
|
},
|
|
1292
|
-
|
|
1293
|
-
// Simulated weather API — use city to vary seed
|
|
1294
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1295
|
-
const seed = city.length;
|
|
1296
|
-
const temperature = Math.round(50 + ((seed * 17) % 40));
|
|
1297
|
-
return { temperature };
|
|
1298
|
-
},
|
|
1299
|
-
render: WeatherToolUI,
|
|
1300
|
-
});
|
|
1283
|
+
};
|
|
1301
1284
|
|
|
1302
1285
|
const styles = StyleSheet.create({
|
|
1303
1286
|
card: {
|
|
1287
|
+
padding: 12,
|
|
1288
|
+
borderRadius: 12,
|
|
1289
|
+
marginVertical: 4,
|
|
1290
|
+
},
|
|
1291
|
+
weatherCard: {
|
|
1304
1292
|
padding: 16,
|
|
1305
1293
|
borderRadius: 12,
|
|
1306
1294
|
marginVertical: 4,
|
|
1307
1295
|
gap: 4,
|
|
1308
1296
|
},
|
|
1309
|
-
|
|
1297
|
+
row: {
|
|
1298
|
+
flexDirection: "row",
|
|
1299
|
+
alignItems: "center",
|
|
1300
|
+
gap: 10,
|
|
1301
|
+
},
|
|
1302
|
+
pin: {
|
|
1303
|
+
fontSize: 20,
|
|
1304
|
+
},
|
|
1305
|
+
locationName: {
|
|
1310
1306
|
fontSize: 15,
|
|
1311
1307
|
fontWeight: "600",
|
|
1312
1308
|
},
|
|
1313
|
-
|
|
1309
|
+
coords: {
|
|
1310
|
+
fontSize: 13,
|
|
1311
|
+
marginTop: 2,
|
|
1312
|
+
},
|
|
1313
|
+
label: {
|
|
1314
|
+
fontSize: 13,
|
|
1315
|
+
},
|
|
1316
|
+
weatherHeader: {
|
|
1317
|
+
flexDirection: "row",
|
|
1318
|
+
alignItems: "center",
|
|
1319
|
+
gap: 10,
|
|
1320
|
+
marginBottom: 4,
|
|
1321
|
+
},
|
|
1322
|
+
weatherEmoji: {
|
|
1314
1323
|
fontSize: 32,
|
|
1324
|
+
},
|
|
1325
|
+
condition: {
|
|
1326
|
+
fontSize: 13,
|
|
1327
|
+
marginTop: 2,
|
|
1328
|
+
},
|
|
1329
|
+
tempLarge: {
|
|
1330
|
+
fontSize: 40,
|
|
1315
1331
|
fontWeight: "700",
|
|
1316
1332
|
color: "#007aff",
|
|
1317
1333
|
},
|
|
1318
|
-
|
|
1334
|
+
wind: {
|
|
1319
1335
|
fontSize: 13,
|
|
1336
|
+
marginTop: 2,
|
|
1337
|
+
},
|
|
1338
|
+
forecastRow: {
|
|
1339
|
+
flexDirection: "row",
|
|
1340
|
+
justifyContent: "space-between",
|
|
1341
|
+
marginTop: 12,
|
|
1342
|
+
paddingTop: 12,
|
|
1343
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
1344
|
+
},
|
|
1345
|
+
forecastDay: {
|
|
1346
|
+
alignItems: "center",
|
|
1347
|
+
flex: 1,
|
|
1348
|
+
gap: 4,
|
|
1349
|
+
},
|
|
1350
|
+
forecastLabel: {
|
|
1351
|
+
fontSize: 11,
|
|
1352
|
+
fontWeight: "500",
|
|
1353
|
+
},
|
|
1354
|
+
forecastEmoji: {
|
|
1355
|
+
fontSize: 18,
|
|
1356
|
+
},
|
|
1357
|
+
forecastTemp: {
|
|
1358
|
+
fontSize: 13,
|
|
1359
|
+
fontWeight: "600",
|
|
1360
|
+
},
|
|
1361
|
+
forecastTempLow: {
|
|
1362
|
+
fontSize: 12,
|
|
1320
1363
|
},
|
|
1321
1364
|
});
|
|
1322
1365
|
|
|
@@ -1421,12 +1464,12 @@ export function ThemedView({
|
|
|
1421
1464
|
```tsx
|
|
1422
1465
|
import { FlatList, View, StyleSheet, useColorScheme } from "react-native";
|
|
1423
1466
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
1424
|
-
import {
|
|
1467
|
+
import { useAui, useAuiState } from "@assistant-ui/react-native";
|
|
1425
1468
|
import { ThreadListItem } from "./ThreadListItem";
|
|
1426
1469
|
import type { DrawerContentComponentProps } from "@react-navigation/drawer";
|
|
1427
1470
|
|
|
1428
1471
|
export function ThreadListDrawer({ navigation }: DrawerContentComponentProps) {
|
|
1429
|
-
const
|
|
1472
|
+
const aui = useAui();
|
|
1430
1473
|
const threadIds = useAuiState((s) => s.threads.threadIds);
|
|
1431
1474
|
const mainThreadId = useAuiState((s) => s.threads.mainThreadId);
|
|
1432
1475
|
const threadItems = useAuiState((s) => s.threads.threadItems);
|
|
@@ -1455,7 +1498,7 @@ export function ThreadListDrawer({ navigation }: DrawerContentComponentProps) {
|
|
|
1455
1498
|
title={threadItem?.title ?? "New Chat"}
|
|
1456
1499
|
isActive={threadId === mainThreadId}
|
|
1457
1500
|
onPress={() => {
|
|
1458
|
-
|
|
1501
|
+
aui.threads().switchToThread(threadId);
|
|
1459
1502
|
navigation.closeDrawer();
|
|
1460
1503
|
}}
|
|
1461
1504
|
/>
|
|
@@ -1748,52 +1791,26 @@ export const Fonts = Platform.select({
|
|
|
1748
1791
|
|
|
1749
1792
|
```
|
|
1750
1793
|
|
|
1751
|
-
## eslint.config.js
|
|
1752
|
-
|
|
1753
|
-
```javascript
|
|
1754
|
-
// https://docs.expo.dev/guides/using-eslint/
|
|
1755
|
-
const { defineConfig } = require("eslint/config");
|
|
1756
|
-
const expoConfig = require("eslint-config-expo/flat");
|
|
1757
|
-
|
|
1758
|
-
module.exports = defineConfig([
|
|
1759
|
-
expoConfig,
|
|
1760
|
-
{
|
|
1761
|
-
ignores: ["dist/*"],
|
|
1762
|
-
},
|
|
1763
|
-
]);
|
|
1764
|
-
|
|
1765
|
-
```
|
|
1766
|
-
|
|
1767
1794
|
## hooks/use-app-runtime.ts
|
|
1768
1795
|
|
|
1769
1796
|
```typescript
|
|
1770
1797
|
import { useMemo } from "react";
|
|
1771
|
-
import { fetch } from "expo/fetch";
|
|
1772
1798
|
import {
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
} from "
|
|
1777
|
-
|
|
1799
|
+
useChatRuntime,
|
|
1800
|
+
AssistantChatTransport,
|
|
1801
|
+
} from "@assistant-ui/react-ai-sdk";
|
|
1802
|
+
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
1803
|
+
|
|
1804
|
+
const CHAT_API = process.env.EXPO_PUBLIC_CHAT_ENDPOINT_URL ?? "/api/chat";
|
|
1778
1805
|
|
|
1779
1806
|
export function useAppRuntime() {
|
|
1780
|
-
const
|
|
1781
|
-
() =>
|
|
1782
|
-
createOpenAIChatModelAdapter({
|
|
1783
|
-
apiKey: process.env.EXPO_PUBLIC_OPENAI_API_KEY ?? "",
|
|
1784
|
-
model: "gpt-4o-mini",
|
|
1785
|
-
fetch,
|
|
1786
|
-
}),
|
|
1807
|
+
const transport = useMemo(
|
|
1808
|
+
() => new AssistantChatTransport({ api: CHAT_API }),
|
|
1787
1809
|
[],
|
|
1788
1810
|
);
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
return useLocalRuntime(chatModel, {
|
|
1793
|
-
titleGenerator,
|
|
1794
|
-
adapters: {
|
|
1795
|
-
attachments: new SimpleImageAttachmentAdapter(),
|
|
1796
|
-
},
|
|
1811
|
+
return useChatRuntime({
|
|
1812
|
+
transport,
|
|
1813
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
1797
1814
|
});
|
|
1798
1815
|
}
|
|
1799
1816
|
|
|
@@ -1920,36 +1937,40 @@ module.exports = config;
|
|
|
1920
1937
|
"android": "expo run:android",
|
|
1921
1938
|
"ios": "expo run:ios",
|
|
1922
1939
|
"web": "expo start --web",
|
|
1923
|
-
"
|
|
1940
|
+
"export:web": "expo export --platform web && node scripts/flatten-assets.mjs"
|
|
1924
1941
|
},
|
|
1925
1942
|
"dependencies": {
|
|
1943
|
+
"@ai-sdk/openai": "^3.0.41",
|
|
1944
|
+
"@ai-sdk/react": "^3.0.118",
|
|
1945
|
+
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
1926
1946
|
"@assistant-ui/react-native": "workspace:*",
|
|
1927
1947
|
"@expo/vector-icons": "^15.1.1",
|
|
1928
|
-
"@react-navigation/drawer": "^7.
|
|
1948
|
+
"@react-navigation/drawer": "^7.7.2",
|
|
1929
1949
|
"@react-navigation/native": "^7.1.28",
|
|
1930
|
-
"
|
|
1931
|
-
"expo
|
|
1932
|
-
"expo-
|
|
1933
|
-
"expo-
|
|
1934
|
-
"expo-
|
|
1935
|
-
"expo-
|
|
1936
|
-
"expo-
|
|
1937
|
-
"expo-
|
|
1938
|
-
"expo-
|
|
1939
|
-
"
|
|
1940
|
-
"
|
|
1941
|
-
"react
|
|
1950
|
+
"ai": "^6.0.116",
|
|
1951
|
+
"expo": "~55.0.6",
|
|
1952
|
+
"expo-constants": "~55.0.7",
|
|
1953
|
+
"expo-font": "~55.0.4",
|
|
1954
|
+
"expo-image-picker": "~55.0.12",
|
|
1955
|
+
"expo-linking": "~55.0.7",
|
|
1956
|
+
"expo-router": "~55.0.5",
|
|
1957
|
+
"expo-server": "~55.0.6",
|
|
1958
|
+
"expo-splash-screen": "~55.0.10",
|
|
1959
|
+
"expo-status-bar": "~55.0.4",
|
|
1960
|
+
"expo-system-ui": "~55.0.9",
|
|
1961
|
+
"react": "19.2.0",
|
|
1962
|
+
"react-dom": "19.2.0",
|
|
1963
|
+
"react-native": "0.83.2",
|
|
1942
1964
|
"react-native-gesture-handler": "~2.30.0",
|
|
1943
1965
|
"react-native-reanimated": "~4.2.2",
|
|
1944
1966
|
"react-native-safe-area-context": "~5.7.0",
|
|
1945
1967
|
"react-native-screens": "~4.24.0",
|
|
1946
1968
|
"react-native-web": "~0.21.2",
|
|
1947
|
-
"react-native-worklets": "0.7.
|
|
1969
|
+
"react-native-worklets": "0.7.2",
|
|
1970
|
+
"zod": "^4.3.6"
|
|
1948
1971
|
},
|
|
1949
1972
|
"devDependencies": {
|
|
1950
1973
|
"@types/react": "~19.2.14",
|
|
1951
|
-
"eslint": "^10.0.2",
|
|
1952
|
-
"eslint-config-expo": "~10.0.0",
|
|
1953
1974
|
"typescript": "~5.9.3"
|
|
1954
1975
|
},
|
|
1955
1976
|
"private": true
|
|
@@ -2029,3 +2050,14 @@ Join our community of developers creating universal apps.
|
|
|
2029
2050
|
|
|
2030
2051
|
```
|
|
2031
2052
|
|
|
2053
|
+
## vercel.json
|
|
2054
|
+
|
|
2055
|
+
```json
|
|
2056
|
+
{
|
|
2057
|
+
"buildCommand": "cd ../.. && pnpm turbo build --filter=@assistant-ui/react-native --filter=@assistant-ui/react-ai-sdk && cd examples/with-expo && pnpm run export:web",
|
|
2058
|
+
"outputDirectory": "dist",
|
|
2059
|
+
"framework": null
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
```
|
|
2063
|
+
|