@assistant-ui/mcp-docs-server 0.1.22 → 0.1.24
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 +39 -27
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +39 -29
- package/.docs/organized/code-examples/with-artifacts.md +467 -0
- package/.docs/organized/code-examples/with-assistant-transport.md +32 -25
- package/.docs/organized/code-examples/with-chain-of-thought.md +42 -33
- package/.docs/organized/code-examples/with-cloud-standalone.md +674 -0
- package/.docs/organized/code-examples/with-cloud.md +35 -28
- package/.docs/organized/code-examples/with-custom-thread-list.md +35 -28
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +42 -31
- package/.docs/organized/code-examples/with-expo.md +2012 -0
- package/.docs/organized/code-examples/with-external-store.md +32 -26
- package/.docs/organized/code-examples/with-ffmpeg.md +32 -28
- package/.docs/organized/code-examples/with-langgraph.md +97 -39
- package/.docs/organized/code-examples/with-parent-id-grouping.md +33 -26
- package/.docs/organized/code-examples/with-react-hook-form.md +63 -61
- package/.docs/organized/code-examples/with-react-router.md +38 -31
- package/.docs/organized/code-examples/with-store.md +17 -25
- package/.docs/organized/code-examples/with-tanstack.md +36 -26
- package/.docs/organized/code-examples/with-tap-runtime.md +11 -25
- 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 +209 -0
- package/.docs/raw/docs/cloud/ai-sdk.mdx +296 -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 +60 -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/context-display.mdx +147 -0
- 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,2012 @@
|
|
|
1
|
+
# Example: with-expo
|
|
2
|
+
|
|
3
|
+
## .vscode/extensions.json
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{ "recommendations": ["expo.vscode-expo-tools"] }
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## .vscode/settings.json
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"editor.codeActionsOnSave": {
|
|
15
|
+
"source.fixAll": "explicit",
|
|
16
|
+
"source.organizeImports": "explicit",
|
|
17
|
+
"source.sortMembers": "explicit"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
```
|
|
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
|
+
## app.json
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"expo": {
|
|
358
|
+
"name": "with-expo",
|
|
359
|
+
"slug": "with-expo",
|
|
360
|
+
"version": "0.0.0",
|
|
361
|
+
"orientation": "portrait",
|
|
362
|
+
"icon": "./assets/images/icon.png",
|
|
363
|
+
"scheme": "withexpo",
|
|
364
|
+
"userInterfaceStyle": "automatic",
|
|
365
|
+
"newArchEnabled": true,
|
|
366
|
+
"ios": {
|
|
367
|
+
"supportsTablet": true,
|
|
368
|
+
"bundleIdentifier": "com.assistant-ui.with-expo"
|
|
369
|
+
},
|
|
370
|
+
"android": {
|
|
371
|
+
"adaptiveIcon": {
|
|
372
|
+
"backgroundColor": "#E6F4FE",
|
|
373
|
+
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
|
374
|
+
"backgroundImage": "./assets/images/android-icon-background.png",
|
|
375
|
+
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
|
376
|
+
},
|
|
377
|
+
"edgeToEdgeEnabled": true,
|
|
378
|
+
"predictiveBackGestureEnabled": false
|
|
379
|
+
},
|
|
380
|
+
"web": {
|
|
381
|
+
"output": "static",
|
|
382
|
+
"favicon": "./assets/images/favicon.png"
|
|
383
|
+
},
|
|
384
|
+
"plugins": [
|
|
385
|
+
"expo-router",
|
|
386
|
+
[
|
|
387
|
+
"expo-splash-screen",
|
|
388
|
+
{
|
|
389
|
+
"image": "./assets/images/splash-icon.png",
|
|
390
|
+
"imageWidth": 200,
|
|
391
|
+
"resizeMode": "contain",
|
|
392
|
+
"backgroundColor": "#ffffff",
|
|
393
|
+
"dark": {
|
|
394
|
+
"backgroundColor": "#000000"
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
],
|
|
399
|
+
"experiments": {
|
|
400
|
+
"typedRoutes": true,
|
|
401
|
+
"reactCompiler": true
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## app/_layout.tsx
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
import {
|
|
412
|
+
DarkTheme,
|
|
413
|
+
DefaultTheme,
|
|
414
|
+
ThemeProvider,
|
|
415
|
+
} from "@react-navigation/native";
|
|
416
|
+
import { Drawer } from "expo-router/drawer";
|
|
417
|
+
import { StatusBar } from "expo-status-bar";
|
|
418
|
+
import "react-native-reanimated";
|
|
419
|
+
import { Pressable, useColorScheme } from "react-native";
|
|
420
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
421
|
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
422
|
+
|
|
423
|
+
import {
|
|
424
|
+
AssistantProvider,
|
|
425
|
+
useAssistantRuntime,
|
|
426
|
+
} from "@assistant-ui/react-native";
|
|
427
|
+
import { useAppRuntime } from "@/hooks/use-app-runtime";
|
|
428
|
+
import { ThreadListDrawer } from "@/components/thread-list/ThreadListDrawer";
|
|
429
|
+
import { WeatherTool } from "@/components/assistant-ui/tools";
|
|
430
|
+
|
|
431
|
+
function NewChatButton() {
|
|
432
|
+
const runtime = useAssistantRuntime();
|
|
433
|
+
const colorScheme = useColorScheme();
|
|
434
|
+
const isDark = colorScheme === "dark";
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<Pressable
|
|
438
|
+
onPress={() => {
|
|
439
|
+
runtime.threads.switchToNewThread();
|
|
440
|
+
}}
|
|
441
|
+
style={{ marginRight: 16 }}
|
|
442
|
+
>
|
|
443
|
+
<Ionicons
|
|
444
|
+
name="create-outline"
|
|
445
|
+
size={24}
|
|
446
|
+
color={isDark ? "#ffffff" : "#000000"}
|
|
447
|
+
/>
|
|
448
|
+
</Pressable>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function DrawerLayout() {
|
|
453
|
+
const colorScheme = useColorScheme();
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
|
457
|
+
<Drawer
|
|
458
|
+
drawerContent={(props) => <ThreadListDrawer {...props} />}
|
|
459
|
+
screenOptions={{
|
|
460
|
+
headerRight: () => <NewChatButton />,
|
|
461
|
+
drawerType: "front",
|
|
462
|
+
swipeEnabled: true,
|
|
463
|
+
drawerStyle: { backgroundColor: "transparent" },
|
|
464
|
+
}}
|
|
465
|
+
>
|
|
466
|
+
<Drawer.Screen name="index" options={{ title: "Chat" }} />
|
|
467
|
+
</Drawer>
|
|
468
|
+
<StatusBar style="auto" />
|
|
469
|
+
</ThemeProvider>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export default function RootLayout() {
|
|
474
|
+
const runtime = useAppRuntime();
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
478
|
+
<AssistantProvider runtime={runtime}>
|
|
479
|
+
<WeatherTool />
|
|
480
|
+
<DrawerLayout />
|
|
481
|
+
</AssistantProvider>
|
|
482
|
+
</GestureHandlerRootView>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## app/index.tsx
|
|
489
|
+
|
|
490
|
+
```tsx
|
|
491
|
+
import { Thread } from "@/components/assistant-ui/thread";
|
|
492
|
+
|
|
493
|
+
export default function ChatPage() {
|
|
494
|
+
return <Thread />;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## components/assistant-ui/composer.tsx
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
import {
|
|
503
|
+
View,
|
|
504
|
+
TextInput,
|
|
505
|
+
Pressable,
|
|
506
|
+
Image,
|
|
507
|
+
StyleSheet,
|
|
508
|
+
useColorScheme,
|
|
509
|
+
} from "react-native";
|
|
510
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
511
|
+
import * as ImagePicker from "expo-image-picker";
|
|
512
|
+
import {
|
|
513
|
+
useAui,
|
|
514
|
+
useAuiState,
|
|
515
|
+
useComposerSend,
|
|
516
|
+
useComposerCancel,
|
|
517
|
+
useComposerAddAttachment,
|
|
518
|
+
ComposerAttachments,
|
|
519
|
+
AttachmentRoot,
|
|
520
|
+
AttachmentRemove,
|
|
521
|
+
} from "@assistant-ui/react-native";
|
|
522
|
+
|
|
523
|
+
function AttachmentPreview() {
|
|
524
|
+
const attachment = useAuiState((s) => s.attachment);
|
|
525
|
+
if (!attachment) return null;
|
|
526
|
+
|
|
527
|
+
// Find image content for preview URI
|
|
528
|
+
const imageContent = attachment.content?.find((c: any) => c.type === "image");
|
|
529
|
+
const uri = (imageContent as any)?.image;
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<AttachmentRoot style={styles.attachmentItem}>
|
|
533
|
+
{uri ? <Image source={{ uri }} style={styles.attachmentImage} /> : null}
|
|
534
|
+
<AttachmentRemove style={styles.attachmentRemoveButton}>
|
|
535
|
+
<Ionicons name="close-circle" size={20} color="#ff453a" />
|
|
536
|
+
</AttachmentRemove>
|
|
537
|
+
</AttachmentRoot>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const attachmentComponents = { Attachment: AttachmentPreview };
|
|
542
|
+
|
|
543
|
+
export function Composer() {
|
|
544
|
+
const colorScheme = useColorScheme();
|
|
545
|
+
const isDark = colorScheme === "dark";
|
|
546
|
+
|
|
547
|
+
const aui = useAui();
|
|
548
|
+
const text = useAuiState((s) => s.composer.text);
|
|
549
|
+
const attachmentsCount = useAuiState((s) => s.composer.attachments.length);
|
|
550
|
+
const { send, canSend } = useComposerSend();
|
|
551
|
+
const { cancel, canCancel } = useComposerCancel();
|
|
552
|
+
const { addAttachment } = useComposerAddAttachment();
|
|
553
|
+
|
|
554
|
+
const pickImage = async () => {
|
|
555
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
556
|
+
mediaTypes: ["images"],
|
|
557
|
+
allowsMultipleSelection: true,
|
|
558
|
+
quality: 0.8,
|
|
559
|
+
base64: true,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
if (result.canceled) return;
|
|
563
|
+
|
|
564
|
+
for (const asset of result.assets) {
|
|
565
|
+
// Force JPEG mime type — iOS may report HEIC which OpenAI doesn't support
|
|
566
|
+
const dataUrl = `data:image/jpeg;base64,${asset.base64}`;
|
|
567
|
+
|
|
568
|
+
await addAttachment({
|
|
569
|
+
name: asset.fileName ?? "image.jpg",
|
|
570
|
+
contentType: "image/jpeg",
|
|
571
|
+
type: "image",
|
|
572
|
+
content: [{ type: "image", image: dataUrl }],
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
return (
|
|
578
|
+
<View
|
|
579
|
+
style={[
|
|
580
|
+
styles.container,
|
|
581
|
+
{
|
|
582
|
+
backgroundColor: isDark
|
|
583
|
+
? "rgba(28, 28, 30, 0.8)"
|
|
584
|
+
: "rgba(242, 242, 247, 0.8)",
|
|
585
|
+
},
|
|
586
|
+
]}
|
|
587
|
+
>
|
|
588
|
+
{attachmentsCount > 0 && (
|
|
589
|
+
<View style={styles.attachmentsList}>
|
|
590
|
+
<ComposerAttachments components={attachmentComponents} />
|
|
591
|
+
</View>
|
|
592
|
+
)}
|
|
593
|
+
<View
|
|
594
|
+
style={[
|
|
595
|
+
styles.inputWrapper,
|
|
596
|
+
{
|
|
597
|
+
backgroundColor: isDark ? "#1c1c1e" : "#ffffff",
|
|
598
|
+
borderColor: isDark ? "#3a3a3c" : "#e5e5ea",
|
|
599
|
+
},
|
|
600
|
+
]}
|
|
601
|
+
>
|
|
602
|
+
<Pressable
|
|
603
|
+
style={styles.attachButton}
|
|
604
|
+
onPress={pickImage}
|
|
605
|
+
disabled={canCancel}
|
|
606
|
+
>
|
|
607
|
+
<Ionicons
|
|
608
|
+
name="add-circle-outline"
|
|
609
|
+
size={24}
|
|
610
|
+
color={isDark ? "#8e8e93" : "#6e6e73"}
|
|
611
|
+
/>
|
|
612
|
+
</Pressable>
|
|
613
|
+
<TextInput
|
|
614
|
+
style={[styles.input, { color: isDark ? "#ffffff" : "#000000" }]}
|
|
615
|
+
placeholder="Message..."
|
|
616
|
+
placeholderTextColor="#8e8e93"
|
|
617
|
+
value={text}
|
|
618
|
+
onChangeText={(newText) => aui.composer().setText(newText)}
|
|
619
|
+
multiline
|
|
620
|
+
maxLength={4000}
|
|
621
|
+
editable={!canCancel}
|
|
622
|
+
/>
|
|
623
|
+
{canCancel ? (
|
|
624
|
+
<Pressable
|
|
625
|
+
style={[styles.button, styles.stopButton]}
|
|
626
|
+
onPress={cancel}
|
|
627
|
+
>
|
|
628
|
+
<View style={styles.stopIcon} />
|
|
629
|
+
</Pressable>
|
|
630
|
+
) : (
|
|
631
|
+
<Pressable
|
|
632
|
+
style={[
|
|
633
|
+
styles.button,
|
|
634
|
+
styles.sendButton,
|
|
635
|
+
{
|
|
636
|
+
backgroundColor: canSend
|
|
637
|
+
? isDark
|
|
638
|
+
? "#0a84ff"
|
|
639
|
+
: "#007aff"
|
|
640
|
+
: isDark
|
|
641
|
+
? "#3a3a3c"
|
|
642
|
+
: "#e5e5ea",
|
|
643
|
+
},
|
|
644
|
+
]}
|
|
645
|
+
onPress={send}
|
|
646
|
+
disabled={!canSend}
|
|
647
|
+
>
|
|
648
|
+
<Ionicons
|
|
649
|
+
name="arrow-up"
|
|
650
|
+
size={20}
|
|
651
|
+
color={canSend ? "#ffffff" : "#8e8e93"}
|
|
652
|
+
/>
|
|
653
|
+
</Pressable>
|
|
654
|
+
)}
|
|
655
|
+
</View>
|
|
656
|
+
</View>
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const styles = StyleSheet.create({
|
|
661
|
+
container: {
|
|
662
|
+
paddingHorizontal: 16,
|
|
663
|
+
paddingTop: 12,
|
|
664
|
+
paddingBottom: 8,
|
|
665
|
+
},
|
|
666
|
+
attachmentsList: {
|
|
667
|
+
flexDirection: "row",
|
|
668
|
+
flexWrap: "wrap",
|
|
669
|
+
gap: 8,
|
|
670
|
+
paddingBottom: 8,
|
|
671
|
+
},
|
|
672
|
+
attachmentItem: {
|
|
673
|
+
position: "relative",
|
|
674
|
+
},
|
|
675
|
+
attachmentImage: {
|
|
676
|
+
width: 60,
|
|
677
|
+
height: 60,
|
|
678
|
+
borderRadius: 8,
|
|
679
|
+
},
|
|
680
|
+
attachmentRemoveButton: {
|
|
681
|
+
position: "absolute",
|
|
682
|
+
top: -6,
|
|
683
|
+
right: -6,
|
|
684
|
+
},
|
|
685
|
+
inputWrapper: {
|
|
686
|
+
flexDirection: "row",
|
|
687
|
+
alignItems: "flex-end",
|
|
688
|
+
borderRadius: 24,
|
|
689
|
+
borderWidth: 1,
|
|
690
|
+
paddingLeft: 6,
|
|
691
|
+
paddingRight: 6,
|
|
692
|
+
paddingVertical: 6,
|
|
693
|
+
minHeight: 48,
|
|
694
|
+
},
|
|
695
|
+
attachButton: {
|
|
696
|
+
width: 34,
|
|
697
|
+
height: 34,
|
|
698
|
+
justifyContent: "center",
|
|
699
|
+
alignItems: "center",
|
|
700
|
+
},
|
|
701
|
+
input: {
|
|
702
|
+
flex: 1,
|
|
703
|
+
fontSize: 16,
|
|
704
|
+
lineHeight: 22,
|
|
705
|
+
maxHeight: 120,
|
|
706
|
+
paddingVertical: 6,
|
|
707
|
+
letterSpacing: -0.2,
|
|
708
|
+
},
|
|
709
|
+
button: {
|
|
710
|
+
width: 34,
|
|
711
|
+
height: 34,
|
|
712
|
+
borderRadius: 17,
|
|
713
|
+
justifyContent: "center",
|
|
714
|
+
alignItems: "center",
|
|
715
|
+
marginLeft: 8,
|
|
716
|
+
},
|
|
717
|
+
sendButton: {},
|
|
718
|
+
stopButton: {
|
|
719
|
+
backgroundColor: "#ff453a",
|
|
720
|
+
},
|
|
721
|
+
stopIcon: {
|
|
722
|
+
width: 12,
|
|
723
|
+
height: 12,
|
|
724
|
+
borderRadius: 2,
|
|
725
|
+
backgroundColor: "#ffffff",
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
## components/assistant-ui/message-action-bar.tsx
|
|
732
|
+
|
|
733
|
+
```tsx
|
|
734
|
+
import { Pressable, View, StyleSheet, useColorScheme } from "react-native";
|
|
735
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
736
|
+
import {
|
|
737
|
+
useActionBarCopy,
|
|
738
|
+
useActionBarReload,
|
|
739
|
+
} from "@assistant-ui/react-native";
|
|
740
|
+
|
|
741
|
+
export function MessageActionBar() {
|
|
742
|
+
const colorScheme = useColorScheme();
|
|
743
|
+
const isDark = colorScheme === "dark";
|
|
744
|
+
const iconColor = isDark ? "#8e8e93" : "#6e6e73";
|
|
745
|
+
|
|
746
|
+
const { copy, isCopied } = useActionBarCopy();
|
|
747
|
+
const { reload } = useActionBarReload();
|
|
748
|
+
|
|
749
|
+
return (
|
|
750
|
+
<View style={styles.container}>
|
|
751
|
+
<Pressable style={styles.button} onPress={copy}>
|
|
752
|
+
<Ionicons
|
|
753
|
+
name={isCopied ? "checkmark" : "copy-outline"}
|
|
754
|
+
size={16}
|
|
755
|
+
color={isCopied ? "#34c759" : iconColor}
|
|
756
|
+
/>
|
|
757
|
+
</Pressable>
|
|
758
|
+
<Pressable style={styles.button} onPress={reload}>
|
|
759
|
+
<Ionicons name="refresh-outline" size={16} color={iconColor} />
|
|
760
|
+
</Pressable>
|
|
761
|
+
</View>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const styles = StyleSheet.create({
|
|
766
|
+
container: {
|
|
767
|
+
flexDirection: "row",
|
|
768
|
+
gap: 4,
|
|
769
|
+
marginTop: 4,
|
|
770
|
+
},
|
|
771
|
+
button: {
|
|
772
|
+
padding: 6,
|
|
773
|
+
borderRadius: 8,
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
## components/assistant-ui/message-branch-picker.tsx
|
|
780
|
+
|
|
781
|
+
```tsx
|
|
782
|
+
import { Pressable, View, StyleSheet, useColorScheme } from "react-native";
|
|
783
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
784
|
+
import { ThemedText } from "@/components/themed-text";
|
|
785
|
+
import { useMessageBranching } from "@assistant-ui/react-native";
|
|
786
|
+
|
|
787
|
+
export function MessageBranchPicker() {
|
|
788
|
+
const { branchNumber, branchCount, goToPrev, goToNext } =
|
|
789
|
+
useMessageBranching();
|
|
790
|
+
|
|
791
|
+
const colorScheme = useColorScheme();
|
|
792
|
+
const isDark = colorScheme === "dark";
|
|
793
|
+
const iconColor = isDark ? "#8e8e93" : "#6e6e73";
|
|
794
|
+
|
|
795
|
+
if (branchCount <= 1) return null;
|
|
796
|
+
|
|
797
|
+
return (
|
|
798
|
+
<View style={styles.container}>
|
|
799
|
+
<Pressable
|
|
800
|
+
style={styles.button}
|
|
801
|
+
onPress={goToPrev}
|
|
802
|
+
disabled={branchNumber <= 1}
|
|
803
|
+
>
|
|
804
|
+
<Ionicons
|
|
805
|
+
name="chevron-back"
|
|
806
|
+
size={14}
|
|
807
|
+
color={
|
|
808
|
+
branchNumber <= 1 ? (isDark ? "#3a3a3c" : "#d1d1d6") : iconColor
|
|
809
|
+
}
|
|
810
|
+
/>
|
|
811
|
+
</Pressable>
|
|
812
|
+
<ThemedText style={styles.label} lightColor="#6e6e73" darkColor="#8e8e93">
|
|
813
|
+
{branchNumber} / {branchCount}
|
|
814
|
+
</ThemedText>
|
|
815
|
+
<Pressable
|
|
816
|
+
style={styles.button}
|
|
817
|
+
onPress={goToNext}
|
|
818
|
+
disabled={branchNumber >= branchCount}
|
|
819
|
+
>
|
|
820
|
+
<Ionicons
|
|
821
|
+
name="chevron-forward"
|
|
822
|
+
size={14}
|
|
823
|
+
color={
|
|
824
|
+
branchNumber >= branchCount
|
|
825
|
+
? isDark
|
|
826
|
+
? "#3a3a3c"
|
|
827
|
+
: "#d1d1d6"
|
|
828
|
+
: iconColor
|
|
829
|
+
}
|
|
830
|
+
/>
|
|
831
|
+
</Pressable>
|
|
832
|
+
</View>
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const styles = StyleSheet.create({
|
|
837
|
+
container: {
|
|
838
|
+
flexDirection: "row",
|
|
839
|
+
alignItems: "center",
|
|
840
|
+
gap: 2,
|
|
841
|
+
},
|
|
842
|
+
button: {
|
|
843
|
+
padding: 4,
|
|
844
|
+
borderRadius: 6,
|
|
845
|
+
},
|
|
846
|
+
label: {
|
|
847
|
+
fontSize: 12,
|
|
848
|
+
fontVariant: ["tabular-nums"],
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
## components/assistant-ui/message.tsx
|
|
855
|
+
|
|
856
|
+
```tsx
|
|
857
|
+
import { View, Image, StyleSheet, useColorScheme } from "react-native";
|
|
858
|
+
import { ThemedText } from "@/components/themed-text";
|
|
859
|
+
import {
|
|
860
|
+
useAuiState,
|
|
861
|
+
MessageContent,
|
|
862
|
+
MessageAttachments,
|
|
863
|
+
} from "@assistant-ui/react-native";
|
|
864
|
+
import { MessageActionBar } from "./message-action-bar";
|
|
865
|
+
import { MessageBranchPicker } from "./message-branch-picker";
|
|
866
|
+
|
|
867
|
+
function MessageError() {
|
|
868
|
+
const error = useAuiState((s) => {
|
|
869
|
+
const status = s.message.status;
|
|
870
|
+
if (status?.type === "incomplete" && status.reason === "error") {
|
|
871
|
+
return status.error ?? "An error occurred";
|
|
872
|
+
}
|
|
873
|
+
return null;
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
if (!error) return null;
|
|
877
|
+
|
|
878
|
+
return (
|
|
879
|
+
<View style={styles.errorContainer}>
|
|
880
|
+
<ThemedText
|
|
881
|
+
style={styles.errorText}
|
|
882
|
+
lightColor="#ff453a"
|
|
883
|
+
darkColor="#ff6961"
|
|
884
|
+
>
|
|
885
|
+
{typeof error === "string" ? error : "An error occurred"}
|
|
886
|
+
</ThemedText>
|
|
887
|
+
</View>
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function TextPart({ part }: { part: { type: "text"; text: string } }) {
|
|
892
|
+
const role = useAuiState((s) => s.message.role);
|
|
893
|
+
if (role === "user") {
|
|
894
|
+
return <ThemedText style={styles.userText}>{part.text}</ThemedText>;
|
|
895
|
+
}
|
|
896
|
+
return (
|
|
897
|
+
<ThemedText
|
|
898
|
+
style={styles.assistantText}
|
|
899
|
+
lightColor="#000000"
|
|
900
|
+
darkColor="#ffffff"
|
|
901
|
+
>
|
|
902
|
+
{part.text}
|
|
903
|
+
</ThemedText>
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function MessageImageAttachment() {
|
|
908
|
+
const attachment = useAuiState((s) => s.attachment);
|
|
909
|
+
if (!attachment) return null;
|
|
910
|
+
|
|
911
|
+
const imageContent = attachment.content?.find((c: any) => c.type === "image");
|
|
912
|
+
const uri = (imageContent as any)?.image;
|
|
913
|
+
if (!uri) return null;
|
|
914
|
+
|
|
915
|
+
return <Image source={{ uri }} style={styles.messageImage} />;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const messageAttachmentComponents = { Attachment: MessageImageAttachment };
|
|
919
|
+
|
|
920
|
+
export function MessageBubble() {
|
|
921
|
+
const colorScheme = useColorScheme();
|
|
922
|
+
const isDark = colorScheme === "dark";
|
|
923
|
+
const role = useAuiState((s) => s.message.role);
|
|
924
|
+
const isRunning = useAuiState((s) => s.message.status?.type === "running");
|
|
925
|
+
const isUser = role === "user";
|
|
926
|
+
|
|
927
|
+
if (isUser) {
|
|
928
|
+
return (
|
|
929
|
+
<View style={[styles.container, styles.userContainer]}>
|
|
930
|
+
<MessageAttachments components={messageAttachmentComponents} />
|
|
931
|
+
<View
|
|
932
|
+
style={[
|
|
933
|
+
styles.bubble,
|
|
934
|
+
styles.userBubble,
|
|
935
|
+
{ backgroundColor: isDark ? "#0a84ff" : "#007aff" },
|
|
936
|
+
]}
|
|
937
|
+
>
|
|
938
|
+
<MessageContent renderText={({ part }) => <TextPart part={part} />} />
|
|
939
|
+
</View>
|
|
940
|
+
<MessageBranchPicker />
|
|
941
|
+
</View>
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return (
|
|
946
|
+
<View style={[styles.container, styles.assistantContainer]}>
|
|
947
|
+
<View
|
|
948
|
+
style={[
|
|
949
|
+
styles.bubble,
|
|
950
|
+
styles.assistantBubble,
|
|
951
|
+
{
|
|
952
|
+
backgroundColor: isDark
|
|
953
|
+
? "rgba(44, 44, 46, 0.8)"
|
|
954
|
+
: "rgba(229, 229, 234, 0.8)",
|
|
955
|
+
},
|
|
956
|
+
]}
|
|
957
|
+
>
|
|
958
|
+
<MessageContent renderText={({ part }) => <TextPart part={part} />} />
|
|
959
|
+
<MessageError />
|
|
960
|
+
</View>
|
|
961
|
+
{!isRunning && (
|
|
962
|
+
<View style={styles.actionsRow}>
|
|
963
|
+
<MessageBranchPicker />
|
|
964
|
+
<MessageActionBar />
|
|
965
|
+
</View>
|
|
966
|
+
)}
|
|
967
|
+
</View>
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const styles = StyleSheet.create({
|
|
972
|
+
container: {
|
|
973
|
+
paddingHorizontal: 16,
|
|
974
|
+
paddingVertical: 6,
|
|
975
|
+
},
|
|
976
|
+
userContainer: {
|
|
977
|
+
alignItems: "flex-end",
|
|
978
|
+
},
|
|
979
|
+
assistantContainer: {
|
|
980
|
+
alignItems: "flex-start",
|
|
981
|
+
},
|
|
982
|
+
bubble: {
|
|
983
|
+
maxWidth: "85%",
|
|
984
|
+
paddingHorizontal: 16,
|
|
985
|
+
paddingVertical: 12,
|
|
986
|
+
borderRadius: 20,
|
|
987
|
+
},
|
|
988
|
+
userBubble: {
|
|
989
|
+
borderBottomRightRadius: 6,
|
|
990
|
+
},
|
|
991
|
+
actionsRow: {
|
|
992
|
+
flexDirection: "row",
|
|
993
|
+
alignItems: "center",
|
|
994
|
+
gap: 4,
|
|
995
|
+
marginTop: 4,
|
|
996
|
+
},
|
|
997
|
+
assistantBubble: {
|
|
998
|
+
borderBottomLeftRadius: 6,
|
|
999
|
+
},
|
|
1000
|
+
errorContainer: {
|
|
1001
|
+
paddingTop: 4,
|
|
1002
|
+
},
|
|
1003
|
+
errorText: {
|
|
1004
|
+
fontSize: 14,
|
|
1005
|
+
lineHeight: 20,
|
|
1006
|
+
},
|
|
1007
|
+
messageImage: {
|
|
1008
|
+
width: 200,
|
|
1009
|
+
height: 200,
|
|
1010
|
+
borderRadius: 12,
|
|
1011
|
+
marginBottom: 4,
|
|
1012
|
+
},
|
|
1013
|
+
userText: {
|
|
1014
|
+
fontSize: 16,
|
|
1015
|
+
lineHeight: 22,
|
|
1016
|
+
color: "#ffffff",
|
|
1017
|
+
letterSpacing: -0.2,
|
|
1018
|
+
},
|
|
1019
|
+
assistantText: {
|
|
1020
|
+
fontSize: 16,
|
|
1021
|
+
lineHeight: 24,
|
|
1022
|
+
letterSpacing: -0.2,
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
## components/assistant-ui/thread.tsx
|
|
1029
|
+
|
|
1030
|
+
```tsx
|
|
1031
|
+
import {
|
|
1032
|
+
View,
|
|
1033
|
+
Text,
|
|
1034
|
+
Pressable,
|
|
1035
|
+
StyleSheet,
|
|
1036
|
+
KeyboardAvoidingView,
|
|
1037
|
+
Platform,
|
|
1038
|
+
useColorScheme,
|
|
1039
|
+
} from "react-native";
|
|
1040
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
1041
|
+
import { MessageBubble } from "./message";
|
|
1042
|
+
import { Composer } from "./composer";
|
|
1043
|
+
import {
|
|
1044
|
+
ThreadMessages,
|
|
1045
|
+
useThreadIsEmpty,
|
|
1046
|
+
useAui,
|
|
1047
|
+
} from "@assistant-ui/react-native";
|
|
1048
|
+
|
|
1049
|
+
function SuggestionChip({ title, prompt }: { title: string; prompt: string }) {
|
|
1050
|
+
const colorScheme = useColorScheme();
|
|
1051
|
+
const isDark = colorScheme === "dark";
|
|
1052
|
+
const aui = useAui();
|
|
1053
|
+
|
|
1054
|
+
return (
|
|
1055
|
+
<Pressable
|
|
1056
|
+
onPress={() => aui.thread().append(prompt)}
|
|
1057
|
+
style={[
|
|
1058
|
+
styles.suggestionChip,
|
|
1059
|
+
{
|
|
1060
|
+
backgroundColor: isDark
|
|
1061
|
+
? "rgba(44, 44, 46, 0.8)"
|
|
1062
|
+
: "rgba(229, 229, 234, 0.8)",
|
|
1063
|
+
},
|
|
1064
|
+
]}
|
|
1065
|
+
>
|
|
1066
|
+
<Text
|
|
1067
|
+
style={[
|
|
1068
|
+
styles.suggestionText,
|
|
1069
|
+
{ color: isDark ? "#ffffff" : "#000000" },
|
|
1070
|
+
]}
|
|
1071
|
+
>
|
|
1072
|
+
{title}
|
|
1073
|
+
</Text>
|
|
1074
|
+
</Pressable>
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const defaultSuggestions = [
|
|
1079
|
+
{
|
|
1080
|
+
title: "What's the weather in Tokyo?",
|
|
1081
|
+
prompt: "What's the weather in Tokyo?",
|
|
1082
|
+
},
|
|
1083
|
+
{ title: "Tell me a joke", prompt: "Tell me a joke" },
|
|
1084
|
+
{ title: "Help me write an email", prompt: "Help me write an email" },
|
|
1085
|
+
];
|
|
1086
|
+
|
|
1087
|
+
function Suggestions() {
|
|
1088
|
+
return (
|
|
1089
|
+
<View style={styles.suggestionsContainer}>
|
|
1090
|
+
{defaultSuggestions.map((s, i) => (
|
|
1091
|
+
<SuggestionChip key={i} title={s.title} prompt={s.prompt} />
|
|
1092
|
+
))}
|
|
1093
|
+
</View>
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function EmptyState() {
|
|
1098
|
+
const colorScheme = useColorScheme();
|
|
1099
|
+
const isDark = colorScheme === "dark";
|
|
1100
|
+
|
|
1101
|
+
return (
|
|
1102
|
+
<View
|
|
1103
|
+
style={[
|
|
1104
|
+
styles.emptyContainer,
|
|
1105
|
+
{ backgroundColor: isDark ? "#000000" : "#ffffff" },
|
|
1106
|
+
]}
|
|
1107
|
+
>
|
|
1108
|
+
<View style={styles.emptyIconContainer}>
|
|
1109
|
+
<Text style={styles.emptyIcon}>💭</Text>
|
|
1110
|
+
</View>
|
|
1111
|
+
<Text
|
|
1112
|
+
style={[styles.emptyTitle, { color: isDark ? "#ffffff" : "#000000" }]}
|
|
1113
|
+
>
|
|
1114
|
+
How can I help?
|
|
1115
|
+
</Text>
|
|
1116
|
+
<Text
|
|
1117
|
+
style={[
|
|
1118
|
+
styles.emptySubtitle,
|
|
1119
|
+
{ color: isDark ? "#8e8e93" : "#6e6e73" },
|
|
1120
|
+
]}
|
|
1121
|
+
>
|
|
1122
|
+
Send a message to start chatting
|
|
1123
|
+
</Text>
|
|
1124
|
+
<Suggestions />
|
|
1125
|
+
</View>
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const renderMessage = () => <MessageBubble />;
|
|
1130
|
+
|
|
1131
|
+
function ChatMessages() {
|
|
1132
|
+
const isEmpty = useThreadIsEmpty();
|
|
1133
|
+
|
|
1134
|
+
if (isEmpty) {
|
|
1135
|
+
return <EmptyState />;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return (
|
|
1139
|
+
<ThreadMessages
|
|
1140
|
+
renderMessage={renderMessage}
|
|
1141
|
+
contentContainerStyle={styles.messageList}
|
|
1142
|
+
showsVerticalScrollIndicator={false}
|
|
1143
|
+
/>
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export function Thread() {
|
|
1148
|
+
const insets = useSafeAreaInsets();
|
|
1149
|
+
const colorScheme = useColorScheme();
|
|
1150
|
+
const isDark = colorScheme === "dark";
|
|
1151
|
+
return (
|
|
1152
|
+
<View
|
|
1153
|
+
style={[
|
|
1154
|
+
styles.container,
|
|
1155
|
+
{ backgroundColor: isDark ? "#000000" : "#ffffff" },
|
|
1156
|
+
]}
|
|
1157
|
+
>
|
|
1158
|
+
<KeyboardAvoidingView
|
|
1159
|
+
style={styles.keyboardAvoid}
|
|
1160
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
1161
|
+
>
|
|
1162
|
+
<View style={styles.messagesContainer}>
|
|
1163
|
+
<ChatMessages />
|
|
1164
|
+
</View>
|
|
1165
|
+
<View style={{ paddingBottom: insets.bottom }}>
|
|
1166
|
+
<Composer />
|
|
1167
|
+
</View>
|
|
1168
|
+
</KeyboardAvoidingView>
|
|
1169
|
+
</View>
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const styles = StyleSheet.create({
|
|
1174
|
+
container: {
|
|
1175
|
+
flex: 1,
|
|
1176
|
+
},
|
|
1177
|
+
keyboardAvoid: {
|
|
1178
|
+
flex: 1,
|
|
1179
|
+
},
|
|
1180
|
+
messagesContainer: {
|
|
1181
|
+
flex: 1,
|
|
1182
|
+
},
|
|
1183
|
+
messageList: {
|
|
1184
|
+
paddingVertical: 20,
|
|
1185
|
+
paddingHorizontal: 4,
|
|
1186
|
+
},
|
|
1187
|
+
emptyContainer: {
|
|
1188
|
+
flex: 1,
|
|
1189
|
+
justifyContent: "center",
|
|
1190
|
+
alignItems: "center",
|
|
1191
|
+
padding: 40,
|
|
1192
|
+
},
|
|
1193
|
+
emptyIconContainer: {
|
|
1194
|
+
width: 72,
|
|
1195
|
+
height: 72,
|
|
1196
|
+
borderRadius: 36,
|
|
1197
|
+
backgroundColor: "rgba(0, 122, 255, 0.1)",
|
|
1198
|
+
justifyContent: "center",
|
|
1199
|
+
alignItems: "center",
|
|
1200
|
+
marginBottom: 20,
|
|
1201
|
+
},
|
|
1202
|
+
emptyIcon: {
|
|
1203
|
+
fontSize: 32,
|
|
1204
|
+
},
|
|
1205
|
+
emptyTitle: {
|
|
1206
|
+
fontSize: 22,
|
|
1207
|
+
fontWeight: "600",
|
|
1208
|
+
marginBottom: 8,
|
|
1209
|
+
letterSpacing: -0.4,
|
|
1210
|
+
},
|
|
1211
|
+
emptySubtitle: {
|
|
1212
|
+
fontSize: 15,
|
|
1213
|
+
textAlign: "center",
|
|
1214
|
+
letterSpacing: -0.2,
|
|
1215
|
+
},
|
|
1216
|
+
suggestionsContainer: {
|
|
1217
|
+
flexDirection: "row",
|
|
1218
|
+
flexWrap: "wrap",
|
|
1219
|
+
justifyContent: "center",
|
|
1220
|
+
gap: 8,
|
|
1221
|
+
marginTop: 20,
|
|
1222
|
+
paddingHorizontal: 16,
|
|
1223
|
+
},
|
|
1224
|
+
suggestionChip: {
|
|
1225
|
+
paddingHorizontal: 16,
|
|
1226
|
+
paddingVertical: 10,
|
|
1227
|
+
borderRadius: 18,
|
|
1228
|
+
},
|
|
1229
|
+
suggestionText: {
|
|
1230
|
+
fontSize: 14,
|
|
1231
|
+
letterSpacing: -0.2,
|
|
1232
|
+
},
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
## components/assistant-ui/tools.tsx
|
|
1238
|
+
|
|
1239
|
+
```tsx
|
|
1240
|
+
import { View, Text, StyleSheet, useColorScheme } from "react-native";
|
|
1241
|
+
import {
|
|
1242
|
+
makeAssistantTool,
|
|
1243
|
+
type ToolCallMessagePartProps,
|
|
1244
|
+
} from "@assistant-ui/react-native";
|
|
1245
|
+
|
|
1246
|
+
const WeatherToolUI = (
|
|
1247
|
+
props: ToolCallMessagePartProps<{ city: string }, { temperature: number }>,
|
|
1248
|
+
) => {
|
|
1249
|
+
const colorScheme = useColorScheme();
|
|
1250
|
+
const isDark = colorScheme === "dark";
|
|
1251
|
+
|
|
1252
|
+
if (props.status?.type === "running") {
|
|
1253
|
+
return (
|
|
1254
|
+
<View
|
|
1255
|
+
style={[
|
|
1256
|
+
styles.card,
|
|
1257
|
+
{ backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
|
|
1258
|
+
]}
|
|
1259
|
+
>
|
|
1260
|
+
<Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
|
|
1261
|
+
Looking up weather for {props.args.city}...
|
|
1262
|
+
</Text>
|
|
1263
|
+
</View>
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
return (
|
|
1268
|
+
<View
|
|
1269
|
+
style={[styles.card, { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" }]}
|
|
1270
|
+
>
|
|
1271
|
+
<Text style={[styles.city, { color: isDark ? "#ffffff" : "#000000" }]}>
|
|
1272
|
+
{props.args.city}
|
|
1273
|
+
</Text>
|
|
1274
|
+
<Text style={styles.temp}>{props.result?.temperature ?? "—"}°F</Text>
|
|
1275
|
+
<Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
|
|
1276
|
+
Current Weather
|
|
1277
|
+
</Text>
|
|
1278
|
+
</View>
|
|
1279
|
+
);
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
export const WeatherTool = makeAssistantTool({
|
|
1283
|
+
toolName: "get_weather",
|
|
1284
|
+
description: "Get the current weather for a city",
|
|
1285
|
+
parameters: {
|
|
1286
|
+
type: "object",
|
|
1287
|
+
properties: {
|
|
1288
|
+
city: { type: "string", description: "The city name" },
|
|
1289
|
+
},
|
|
1290
|
+
required: ["city"],
|
|
1291
|
+
},
|
|
1292
|
+
execute: async ({ city }) => {
|
|
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
|
+
});
|
|
1301
|
+
|
|
1302
|
+
const styles = StyleSheet.create({
|
|
1303
|
+
card: {
|
|
1304
|
+
padding: 16,
|
|
1305
|
+
borderRadius: 12,
|
|
1306
|
+
marginVertical: 4,
|
|
1307
|
+
gap: 4,
|
|
1308
|
+
},
|
|
1309
|
+
city: {
|
|
1310
|
+
fontSize: 15,
|
|
1311
|
+
fontWeight: "600",
|
|
1312
|
+
},
|
|
1313
|
+
temp: {
|
|
1314
|
+
fontSize: 32,
|
|
1315
|
+
fontWeight: "700",
|
|
1316
|
+
color: "#007aff",
|
|
1317
|
+
},
|
|
1318
|
+
label: {
|
|
1319
|
+
fontSize: 13,
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
## components/themed-text.tsx
|
|
1326
|
+
|
|
1327
|
+
```tsx
|
|
1328
|
+
import { StyleSheet, Text, type TextProps } from "react-native";
|
|
1329
|
+
|
|
1330
|
+
import { useThemeColor } from "@/hooks/use-theme-color";
|
|
1331
|
+
|
|
1332
|
+
export type ThemedTextProps = TextProps & {
|
|
1333
|
+
lightColor?: string;
|
|
1334
|
+
darkColor?: string;
|
|
1335
|
+
type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link";
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
export function ThemedText({
|
|
1339
|
+
style,
|
|
1340
|
+
lightColor,
|
|
1341
|
+
darkColor,
|
|
1342
|
+
type = "default",
|
|
1343
|
+
...rest
|
|
1344
|
+
}: ThemedTextProps) {
|
|
1345
|
+
const color = useThemeColor({ light: lightColor, dark: darkColor }, "text");
|
|
1346
|
+
|
|
1347
|
+
return (
|
|
1348
|
+
<Text
|
|
1349
|
+
style={[
|
|
1350
|
+
{ color },
|
|
1351
|
+
type === "default" ? styles.default : undefined,
|
|
1352
|
+
type === "title" ? styles.title : undefined,
|
|
1353
|
+
type === "defaultSemiBold" ? styles.defaultSemiBold : undefined,
|
|
1354
|
+
type === "subtitle" ? styles.subtitle : undefined,
|
|
1355
|
+
type === "link" ? styles.link : undefined,
|
|
1356
|
+
style,
|
|
1357
|
+
]}
|
|
1358
|
+
{...rest}
|
|
1359
|
+
/>
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const styles = StyleSheet.create({
|
|
1364
|
+
default: {
|
|
1365
|
+
fontSize: 16,
|
|
1366
|
+
lineHeight: 24,
|
|
1367
|
+
},
|
|
1368
|
+
defaultSemiBold: {
|
|
1369
|
+
fontSize: 16,
|
|
1370
|
+
lineHeight: 24,
|
|
1371
|
+
fontWeight: "600",
|
|
1372
|
+
},
|
|
1373
|
+
title: {
|
|
1374
|
+
fontSize: 32,
|
|
1375
|
+
fontWeight: "bold",
|
|
1376
|
+
lineHeight: 32,
|
|
1377
|
+
},
|
|
1378
|
+
subtitle: {
|
|
1379
|
+
fontSize: 20,
|
|
1380
|
+
fontWeight: "bold",
|
|
1381
|
+
},
|
|
1382
|
+
link: {
|
|
1383
|
+
lineHeight: 30,
|
|
1384
|
+
fontSize: 16,
|
|
1385
|
+
color: "#0a7ea4",
|
|
1386
|
+
},
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
## components/themed-view.tsx
|
|
1392
|
+
|
|
1393
|
+
```tsx
|
|
1394
|
+
import { View, type ViewProps } from "react-native";
|
|
1395
|
+
|
|
1396
|
+
import { useThemeColor } from "@/hooks/use-theme-color";
|
|
1397
|
+
|
|
1398
|
+
export type ThemedViewProps = ViewProps & {
|
|
1399
|
+
lightColor?: string;
|
|
1400
|
+
darkColor?: string;
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
export function ThemedView({
|
|
1404
|
+
style,
|
|
1405
|
+
lightColor,
|
|
1406
|
+
darkColor,
|
|
1407
|
+
...otherProps
|
|
1408
|
+
}: ThemedViewProps) {
|
|
1409
|
+
const backgroundColor = useThemeColor(
|
|
1410
|
+
{ light: lightColor, dark: darkColor },
|
|
1411
|
+
"background",
|
|
1412
|
+
);
|
|
1413
|
+
|
|
1414
|
+
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
```
|
|
1418
|
+
|
|
1419
|
+
## components/thread-list/ThreadListDrawer.tsx
|
|
1420
|
+
|
|
1421
|
+
```tsx
|
|
1422
|
+
import { FlatList, View, StyleSheet, useColorScheme } from "react-native";
|
|
1423
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
1424
|
+
import { useAssistantRuntime, useAuiState } from "@assistant-ui/react-native";
|
|
1425
|
+
import { ThreadListItem } from "./ThreadListItem";
|
|
1426
|
+
import type { DrawerContentComponentProps } from "@react-navigation/drawer";
|
|
1427
|
+
|
|
1428
|
+
export function ThreadListDrawer({ navigation }: DrawerContentComponentProps) {
|
|
1429
|
+
const runtime = useAssistantRuntime();
|
|
1430
|
+
const threadIds = useAuiState((s) => s.threads.threadIds);
|
|
1431
|
+
const mainThreadId = useAuiState((s) => s.threads.mainThreadId);
|
|
1432
|
+
const threadItems = useAuiState((s) => s.threads.threadItems);
|
|
1433
|
+
const insets = useSafeAreaInsets();
|
|
1434
|
+
const isDark = useColorScheme() === "dark";
|
|
1435
|
+
|
|
1436
|
+
return (
|
|
1437
|
+
<View
|
|
1438
|
+
style={[
|
|
1439
|
+
styles.container,
|
|
1440
|
+
{
|
|
1441
|
+
backgroundColor: isDark
|
|
1442
|
+
? "rgba(28, 28, 30, 0.85)"
|
|
1443
|
+
: "rgba(242, 242, 247, 0.85)",
|
|
1444
|
+
paddingTop: insets.top,
|
|
1445
|
+
},
|
|
1446
|
+
]}
|
|
1447
|
+
>
|
|
1448
|
+
<FlatList
|
|
1449
|
+
data={threadIds}
|
|
1450
|
+
keyExtractor={(item) => item}
|
|
1451
|
+
renderItem={({ item: threadId }) => {
|
|
1452
|
+
const threadItem = threadItems.find((t) => t.id === threadId);
|
|
1453
|
+
return (
|
|
1454
|
+
<ThreadListItem
|
|
1455
|
+
title={threadItem?.title ?? "New Chat"}
|
|
1456
|
+
isActive={threadId === mainThreadId}
|
|
1457
|
+
onPress={() => {
|
|
1458
|
+
runtime.threads.switchToThread(threadId);
|
|
1459
|
+
navigation.closeDrawer();
|
|
1460
|
+
}}
|
|
1461
|
+
/>
|
|
1462
|
+
);
|
|
1463
|
+
}}
|
|
1464
|
+
contentContainerStyle={styles.list}
|
|
1465
|
+
showsVerticalScrollIndicator={false}
|
|
1466
|
+
/>
|
|
1467
|
+
</View>
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const styles = StyleSheet.create({
|
|
1472
|
+
container: {
|
|
1473
|
+
flex: 1,
|
|
1474
|
+
},
|
|
1475
|
+
list: {
|
|
1476
|
+
paddingVertical: 8,
|
|
1477
|
+
},
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
## components/thread-list/ThreadListItem.tsx
|
|
1483
|
+
|
|
1484
|
+
```tsx
|
|
1485
|
+
import {
|
|
1486
|
+
Pressable,
|
|
1487
|
+
Text,
|
|
1488
|
+
View,
|
|
1489
|
+
StyleSheet,
|
|
1490
|
+
useColorScheme,
|
|
1491
|
+
} from "react-native";
|
|
1492
|
+
|
|
1493
|
+
type ThreadListItemProps = {
|
|
1494
|
+
title: string;
|
|
1495
|
+
isActive: boolean;
|
|
1496
|
+
onPress: () => void;
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
export function ThreadListItem({
|
|
1500
|
+
title,
|
|
1501
|
+
isActive,
|
|
1502
|
+
onPress,
|
|
1503
|
+
}: ThreadListItemProps) {
|
|
1504
|
+
const isDark = useColorScheme() === "dark";
|
|
1505
|
+
|
|
1506
|
+
const content = (
|
|
1507
|
+
<View style={styles.row}>
|
|
1508
|
+
{isActive && (
|
|
1509
|
+
<View style={[styles.indicator, { backgroundColor: "#007AFF" }]} />
|
|
1510
|
+
)}
|
|
1511
|
+
<Text
|
|
1512
|
+
numberOfLines={1}
|
|
1513
|
+
style={[
|
|
1514
|
+
styles.title,
|
|
1515
|
+
{ color: isDark ? "#ffffff" : "#000000" },
|
|
1516
|
+
isActive && styles.titleActive,
|
|
1517
|
+
]}
|
|
1518
|
+
>
|
|
1519
|
+
{title}
|
|
1520
|
+
</Text>
|
|
1521
|
+
</View>
|
|
1522
|
+
);
|
|
1523
|
+
|
|
1524
|
+
if (isActive) {
|
|
1525
|
+
return (
|
|
1526
|
+
<Pressable onPress={onPress} style={styles.itemOuter}>
|
|
1527
|
+
<View
|
|
1528
|
+
style={[
|
|
1529
|
+
styles.glassItem,
|
|
1530
|
+
{
|
|
1531
|
+
backgroundColor: isDark
|
|
1532
|
+
? "rgba(44, 44, 46, 0.6)"
|
|
1533
|
+
: "rgba(209, 209, 214, 0.6)",
|
|
1534
|
+
},
|
|
1535
|
+
]}
|
|
1536
|
+
>
|
|
1537
|
+
{content}
|
|
1538
|
+
</View>
|
|
1539
|
+
</Pressable>
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
return (
|
|
1544
|
+
<Pressable
|
|
1545
|
+
onPress={onPress}
|
|
1546
|
+
style={({ pressed }) => [
|
|
1547
|
+
styles.itemOuter,
|
|
1548
|
+
styles.itemPadding,
|
|
1549
|
+
pressed && {
|
|
1550
|
+
backgroundColor: isDark ? "#3a3a3c" : "#d1d1d6",
|
|
1551
|
+
borderRadius: 10,
|
|
1552
|
+
},
|
|
1553
|
+
]}
|
|
1554
|
+
>
|
|
1555
|
+
{content}
|
|
1556
|
+
</Pressable>
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const styles = StyleSheet.create({
|
|
1561
|
+
itemOuter: {
|
|
1562
|
+
marginHorizontal: 8,
|
|
1563
|
+
marginVertical: 2,
|
|
1564
|
+
},
|
|
1565
|
+
itemPadding: {
|
|
1566
|
+
paddingVertical: 12,
|
|
1567
|
+
paddingHorizontal: 16,
|
|
1568
|
+
},
|
|
1569
|
+
glassItem: {
|
|
1570
|
+
paddingVertical: 12,
|
|
1571
|
+
paddingHorizontal: 16,
|
|
1572
|
+
borderRadius: 10,
|
|
1573
|
+
overflow: "hidden",
|
|
1574
|
+
},
|
|
1575
|
+
row: {
|
|
1576
|
+
flexDirection: "row",
|
|
1577
|
+
alignItems: "center",
|
|
1578
|
+
},
|
|
1579
|
+
indicator: {
|
|
1580
|
+
width: 4,
|
|
1581
|
+
height: 20,
|
|
1582
|
+
borderRadius: 2,
|
|
1583
|
+
marginRight: 10,
|
|
1584
|
+
},
|
|
1585
|
+
title: {
|
|
1586
|
+
fontSize: 15,
|
|
1587
|
+
flex: 1,
|
|
1588
|
+
},
|
|
1589
|
+
titleActive: {
|
|
1590
|
+
fontWeight: "600",
|
|
1591
|
+
},
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
## components/ui/icon-symbol.ios.tsx
|
|
1597
|
+
|
|
1598
|
+
```tsx
|
|
1599
|
+
import { SymbolView, SymbolViewProps, SymbolWeight } from "expo-symbols";
|
|
1600
|
+
import { StyleProp, ViewStyle } from "react-native";
|
|
1601
|
+
|
|
1602
|
+
export function IconSymbol({
|
|
1603
|
+
name,
|
|
1604
|
+
size = 24,
|
|
1605
|
+
color,
|
|
1606
|
+
style,
|
|
1607
|
+
weight = "regular",
|
|
1608
|
+
}: {
|
|
1609
|
+
name: SymbolViewProps["name"];
|
|
1610
|
+
size?: number;
|
|
1611
|
+
color: string;
|
|
1612
|
+
style?: StyleProp<ViewStyle>;
|
|
1613
|
+
weight?: SymbolWeight;
|
|
1614
|
+
}) {
|
|
1615
|
+
return (
|
|
1616
|
+
<SymbolView
|
|
1617
|
+
weight={weight}
|
|
1618
|
+
tintColor={color}
|
|
1619
|
+
resizeMode="scaleAspectFit"
|
|
1620
|
+
name={name}
|
|
1621
|
+
style={[
|
|
1622
|
+
{
|
|
1623
|
+
width: size,
|
|
1624
|
+
height: size,
|
|
1625
|
+
},
|
|
1626
|
+
style,
|
|
1627
|
+
]}
|
|
1628
|
+
/>
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
## components/ui/icon-symbol.tsx
|
|
1635
|
+
|
|
1636
|
+
```tsx
|
|
1637
|
+
// Fallback for using MaterialIcons on Android and web.
|
|
1638
|
+
|
|
1639
|
+
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
1640
|
+
import { SymbolWeight, SymbolViewProps } from "expo-symbols";
|
|
1641
|
+
import { ComponentProps } from "react";
|
|
1642
|
+
import { OpaqueColorValue, type StyleProp, type TextStyle } from "react-native";
|
|
1643
|
+
|
|
1644
|
+
type IconMapping = Record<
|
|
1645
|
+
SymbolViewProps["name"],
|
|
1646
|
+
ComponentProps<typeof MaterialIcons>["name"]
|
|
1647
|
+
>;
|
|
1648
|
+
type IconSymbolName = keyof typeof MAPPING;
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Add your SF Symbols to Material Icons mappings here.
|
|
1652
|
+
* - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
|
|
1653
|
+
* - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
|
|
1654
|
+
*/
|
|
1655
|
+
const MAPPING = {
|
|
1656
|
+
"house.fill": "home",
|
|
1657
|
+
"paperplane.fill": "send",
|
|
1658
|
+
"chevron.left.forwardslash.chevron.right": "code",
|
|
1659
|
+
"chevron.right": "chevron-right",
|
|
1660
|
+
} as IconMapping;
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
|
|
1664
|
+
* This ensures a consistent look across platforms, and optimal resource usage.
|
|
1665
|
+
* Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
|
|
1666
|
+
*/
|
|
1667
|
+
export function IconSymbol({
|
|
1668
|
+
name,
|
|
1669
|
+
size = 24,
|
|
1670
|
+
color,
|
|
1671
|
+
style,
|
|
1672
|
+
}: {
|
|
1673
|
+
name: IconSymbolName;
|
|
1674
|
+
size?: number;
|
|
1675
|
+
color: string | OpaqueColorValue;
|
|
1676
|
+
style?: StyleProp<TextStyle>;
|
|
1677
|
+
weight?: SymbolWeight;
|
|
1678
|
+
}) {
|
|
1679
|
+
return (
|
|
1680
|
+
<MaterialIcons
|
|
1681
|
+
color={color}
|
|
1682
|
+
size={size}
|
|
1683
|
+
name={MAPPING[name]}
|
|
1684
|
+
style={style}
|
|
1685
|
+
/>
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
```
|
|
1690
|
+
|
|
1691
|
+
## constants/theme.ts
|
|
1692
|
+
|
|
1693
|
+
```typescript
|
|
1694
|
+
/**
|
|
1695
|
+
* Below are the colors that are used in the app. The colors are defined in the light and dark mode.
|
|
1696
|
+
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
|
1697
|
+
*/
|
|
1698
|
+
|
|
1699
|
+
import { Platform } from "react-native";
|
|
1700
|
+
|
|
1701
|
+
const tintColorLight = "#0a7ea4";
|
|
1702
|
+
const tintColorDark = "#fff";
|
|
1703
|
+
|
|
1704
|
+
export const Colors = {
|
|
1705
|
+
light: {
|
|
1706
|
+
text: "#11181C",
|
|
1707
|
+
background: "#fff",
|
|
1708
|
+
tint: tintColorLight,
|
|
1709
|
+
icon: "#687076",
|
|
1710
|
+
tabIconDefault: "#687076",
|
|
1711
|
+
tabIconSelected: tintColorLight,
|
|
1712
|
+
},
|
|
1713
|
+
dark: {
|
|
1714
|
+
text: "#ECEDEE",
|
|
1715
|
+
background: "#151718",
|
|
1716
|
+
tint: tintColorDark,
|
|
1717
|
+
icon: "#9BA1A6",
|
|
1718
|
+
tabIconDefault: "#9BA1A6",
|
|
1719
|
+
tabIconSelected: tintColorDark,
|
|
1720
|
+
},
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1723
|
+
export const Fonts = Platform.select({
|
|
1724
|
+
ios: {
|
|
1725
|
+
/** iOS `UIFontDescriptorSystemDesignDefault` */
|
|
1726
|
+
sans: "system-ui",
|
|
1727
|
+
/** iOS `UIFontDescriptorSystemDesignSerif` */
|
|
1728
|
+
serif: "ui-serif",
|
|
1729
|
+
/** iOS `UIFontDescriptorSystemDesignRounded` */
|
|
1730
|
+
rounded: "ui-rounded",
|
|
1731
|
+
/** iOS `UIFontDescriptorSystemDesignMonospaced` */
|
|
1732
|
+
mono: "ui-monospace",
|
|
1733
|
+
},
|
|
1734
|
+
default: {
|
|
1735
|
+
sans: "normal",
|
|
1736
|
+
serif: "serif",
|
|
1737
|
+
rounded: "normal",
|
|
1738
|
+
mono: "monospace",
|
|
1739
|
+
},
|
|
1740
|
+
web: {
|
|
1741
|
+
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
1742
|
+
serif: "Georgia, 'Times New Roman', serif",
|
|
1743
|
+
rounded:
|
|
1744
|
+
"'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
|
|
1745
|
+
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
1746
|
+
},
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
```
|
|
1750
|
+
|
|
1751
|
+
## hooks/use-app-runtime.ts
|
|
1752
|
+
|
|
1753
|
+
```typescript
|
|
1754
|
+
import { useMemo } from "react";
|
|
1755
|
+
import { fetch } from "expo/fetch";
|
|
1756
|
+
import {
|
|
1757
|
+
useLocalRuntime,
|
|
1758
|
+
createSimpleTitleAdapter,
|
|
1759
|
+
SimpleImageAttachmentAdapter,
|
|
1760
|
+
} from "@assistant-ui/react-native";
|
|
1761
|
+
import { createOpenAIChatModelAdapter } from "@/adapters/openai-chat-adapter";
|
|
1762
|
+
|
|
1763
|
+
export function useAppRuntime() {
|
|
1764
|
+
const chatModel = useMemo(
|
|
1765
|
+
() =>
|
|
1766
|
+
createOpenAIChatModelAdapter({
|
|
1767
|
+
apiKey: process.env.EXPO_PUBLIC_OPENAI_API_KEY ?? "",
|
|
1768
|
+
model: "gpt-4o-mini",
|
|
1769
|
+
fetch,
|
|
1770
|
+
}),
|
|
1771
|
+
[],
|
|
1772
|
+
);
|
|
1773
|
+
|
|
1774
|
+
const titleGenerator = useMemo(() => createSimpleTitleAdapter(), []);
|
|
1775
|
+
|
|
1776
|
+
return useLocalRuntime(chatModel, {
|
|
1777
|
+
titleGenerator,
|
|
1778
|
+
adapters: {
|
|
1779
|
+
attachments: new SimpleImageAttachmentAdapter(),
|
|
1780
|
+
},
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
## hooks/use-color-scheme.ts
|
|
1787
|
+
|
|
1788
|
+
```typescript
|
|
1789
|
+
export { useColorScheme } from "react-native";
|
|
1790
|
+
|
|
1791
|
+
```
|
|
1792
|
+
|
|
1793
|
+
## hooks/use-color-scheme.web.ts
|
|
1794
|
+
|
|
1795
|
+
```typescript
|
|
1796
|
+
import { useEffect, useState } from "react";
|
|
1797
|
+
import { useColorScheme as useRNColorScheme } from "react-native";
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* To support static rendering, this value needs to be re-calculated on the client side for web
|
|
1801
|
+
*/
|
|
1802
|
+
export function useColorScheme() {
|
|
1803
|
+
const [hasHydrated, setHasHydrated] = useState(false);
|
|
1804
|
+
|
|
1805
|
+
useEffect(() => {
|
|
1806
|
+
setHasHydrated(true);
|
|
1807
|
+
}, []);
|
|
1808
|
+
|
|
1809
|
+
const colorScheme = useRNColorScheme();
|
|
1810
|
+
|
|
1811
|
+
if (hasHydrated) {
|
|
1812
|
+
return colorScheme;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
return "light";
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
## hooks/use-theme-color.ts
|
|
1821
|
+
|
|
1822
|
+
```typescript
|
|
1823
|
+
/**
|
|
1824
|
+
* Learn more about light and dark modes:
|
|
1825
|
+
* https://docs.expo.dev/guides/color-schemes/
|
|
1826
|
+
*/
|
|
1827
|
+
|
|
1828
|
+
import { Colors } from "@/constants/theme";
|
|
1829
|
+
import { useColorScheme } from "@/hooks/use-color-scheme";
|
|
1830
|
+
|
|
1831
|
+
export function useThemeColor(
|
|
1832
|
+
props: { light?: string; dark?: string },
|
|
1833
|
+
colorName: keyof typeof Colors.light & keyof typeof Colors.dark,
|
|
1834
|
+
) {
|
|
1835
|
+
const theme = useColorScheme() ?? "light";
|
|
1836
|
+
const colorFromProps = props[theme];
|
|
1837
|
+
|
|
1838
|
+
if (colorFromProps) {
|
|
1839
|
+
return colorFromProps;
|
|
1840
|
+
} else {
|
|
1841
|
+
return Colors[theme][colorName];
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1847
|
+
## metro.config.js
|
|
1848
|
+
|
|
1849
|
+
```javascript
|
|
1850
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
1851
|
+
const path = require("node:path");
|
|
1852
|
+
|
|
1853
|
+
const projectRoot = __dirname;
|
|
1854
|
+
const monorepoRoot = path.resolve(projectRoot, "../..");
|
|
1855
|
+
|
|
1856
|
+
const config = getDefaultConfig(projectRoot);
|
|
1857
|
+
|
|
1858
|
+
// Watch all files within the monorepo
|
|
1859
|
+
config.watchFolders = [monorepoRoot];
|
|
1860
|
+
|
|
1861
|
+
// Enable symlinks support for pnpm
|
|
1862
|
+
config.resolver.unstable_enableSymlinks = true;
|
|
1863
|
+
|
|
1864
|
+
// Let Metro know where to resolve packages
|
|
1865
|
+
config.resolver.nodeModulesPaths = [
|
|
1866
|
+
path.resolve(projectRoot, "node_modules"),
|
|
1867
|
+
path.resolve(monorepoRoot, "node_modules"),
|
|
1868
|
+
];
|
|
1869
|
+
|
|
1870
|
+
// Force resolving shared dependencies from the app's node_modules
|
|
1871
|
+
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
1872
|
+
if (
|
|
1873
|
+
moduleName === "react" ||
|
|
1874
|
+
moduleName === "react-native" ||
|
|
1875
|
+
moduleName.startsWith("react/") ||
|
|
1876
|
+
moduleName.startsWith("react-native/")
|
|
1877
|
+
) {
|
|
1878
|
+
return context.resolveRequest(
|
|
1879
|
+
{
|
|
1880
|
+
...context,
|
|
1881
|
+
originModulePath: path.resolve(projectRoot, "package.json"),
|
|
1882
|
+
},
|
|
1883
|
+
moduleName,
|
|
1884
|
+
platform,
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
return context.resolveRequest(context, moduleName, platform);
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
module.exports = config;
|
|
1892
|
+
|
|
1893
|
+
```
|
|
1894
|
+
|
|
1895
|
+
## package.json
|
|
1896
|
+
|
|
1897
|
+
```json
|
|
1898
|
+
{
|
|
1899
|
+
"name": "with-expo",
|
|
1900
|
+
"main": "expo-router/entry",
|
|
1901
|
+
"version": "0.0.0",
|
|
1902
|
+
"scripts": {
|
|
1903
|
+
"start": "expo start",
|
|
1904
|
+
"android": "expo run:android",
|
|
1905
|
+
"ios": "expo run:ios",
|
|
1906
|
+
"web": "expo start --web"
|
|
1907
|
+
},
|
|
1908
|
+
"dependencies": {
|
|
1909
|
+
"@assistant-ui/react-native": "workspace:*",
|
|
1910
|
+
"@expo/vector-icons": "^15.1.1",
|
|
1911
|
+
"@react-navigation/drawer": "^7.7.2",
|
|
1912
|
+
"@react-navigation/native": "^7.1.28",
|
|
1913
|
+
"expo": "~55.0.4",
|
|
1914
|
+
"expo-constants": "~55.0.7",
|
|
1915
|
+
"expo-font": "~55.0.4",
|
|
1916
|
+
"expo-image-picker": "~55.0.10",
|
|
1917
|
+
"expo-linking": "~55.0.7",
|
|
1918
|
+
"expo-router": "~55.0.3",
|
|
1919
|
+
"expo-splash-screen": "~55.0.10",
|
|
1920
|
+
"expo-status-bar": "~55.0.4",
|
|
1921
|
+
"expo-system-ui": "~55.0.9",
|
|
1922
|
+
"react": "19.2.0",
|
|
1923
|
+
"react-dom": "19.2.0",
|
|
1924
|
+
"react-native": "0.83.2",
|
|
1925
|
+
"react-native-gesture-handler": "~2.30.0",
|
|
1926
|
+
"react-native-reanimated": "~4.2.1",
|
|
1927
|
+
"react-native-safe-area-context": "~5.6.2",
|
|
1928
|
+
"react-native-screens": "~4.23.0",
|
|
1929
|
+
"react-native-web": "~0.21.2",
|
|
1930
|
+
"react-native-worklets": "0.7.2"
|
|
1931
|
+
},
|
|
1932
|
+
"devDependencies": {
|
|
1933
|
+
"@types/react": "~19.2.14",
|
|
1934
|
+
"typescript": "~5.9.3"
|
|
1935
|
+
},
|
|
1936
|
+
"private": true
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
```
|
|
1940
|
+
|
|
1941
|
+
## README.md
|
|
1942
|
+
|
|
1943
|
+
```markdown
|
|
1944
|
+
# Welcome to your Expo app 👋
|
|
1945
|
+
|
|
1946
|
+
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
|
1947
|
+
|
|
1948
|
+
## Get started
|
|
1949
|
+
|
|
1950
|
+
1. Install dependencies
|
|
1951
|
+
|
|
1952
|
+
```bash
|
|
1953
|
+
npm install
|
|
1954
|
+
```
|
|
1955
|
+
|
|
1956
|
+
2. Start the app
|
|
1957
|
+
|
|
1958
|
+
```bash
|
|
1959
|
+
npx expo start
|
|
1960
|
+
```
|
|
1961
|
+
|
|
1962
|
+
In the output, you'll find options to open the app in a
|
|
1963
|
+
|
|
1964
|
+
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
|
1965
|
+
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
|
1966
|
+
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
|
1967
|
+
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
|
1968
|
+
|
|
1969
|
+
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
|
1970
|
+
|
|
1971
|
+
## Get a fresh project
|
|
1972
|
+
|
|
1973
|
+
When you're ready, run:
|
|
1974
|
+
|
|
1975
|
+
```bash
|
|
1976
|
+
npm run reset-project
|
|
1977
|
+
```
|
|
1978
|
+
|
|
1979
|
+
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
|
1980
|
+
|
|
1981
|
+
## Learn more
|
|
1982
|
+
|
|
1983
|
+
To learn more about developing your project with Expo, look at the following resources:
|
|
1984
|
+
|
|
1985
|
+
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
|
1986
|
+
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
|
1987
|
+
|
|
1988
|
+
## Join the community
|
|
1989
|
+
|
|
1990
|
+
Join our community of developers creating universal apps.
|
|
1991
|
+
|
|
1992
|
+
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
|
1993
|
+
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
|
1994
|
+
|
|
1995
|
+
```
|
|
1996
|
+
|
|
1997
|
+
## tsconfig.json
|
|
1998
|
+
|
|
1999
|
+
```json
|
|
2000
|
+
{
|
|
2001
|
+
"extends": "expo/tsconfig.base",
|
|
2002
|
+
"compilerOptions": {
|
|
2003
|
+
"strict": true,
|
|
2004
|
+
"paths": {
|
|
2005
|
+
"@/*": ["./*"]
|
|
2006
|
+
}
|
|
2007
|
+
},
|
|
2008
|
+
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
```
|
|
2012
|
+
|