@assistant-ui/mcp-docs-server 0.1.35 → 0.1.36
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 +1 -1
- package/.docs/organized/code-examples/with-a2a.md +2 -2
- package/.docs/organized/code-examples/with-ag-ui.md +2 -2
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +4 -4
- package/.docs/organized/code-examples/with-artifacts.md +4 -4
- package/.docs/organized/code-examples/with-assistant-transport.md +70 -54
- package/.docs/organized/code-examples/with-browser-extension.md +4 -4
- package/.docs/organized/code-examples/with-chain-of-thought.md +40 -89
- package/.docs/organized/code-examples/with-cloud-standalone.md +4 -4
- package/.docs/organized/code-examples/with-cloud.md +4 -4
- package/.docs/organized/code-examples/with-custom-thread-list.md +4 -4
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +6 -6
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +6 -6
- package/.docs/organized/code-examples/with-eve.md +343 -0
- package/.docs/organized/code-examples/with-expo.md +909 -932
- package/.docs/organized/code-examples/with-external-store.md +2 -2
- package/.docs/organized/code-examples/with-ffmpeg.md +5 -8
- package/.docs/organized/code-examples/with-generative-ui.md +5 -5
- package/.docs/organized/code-examples/with-google-adk.md +2 -2
- package/.docs/organized/code-examples/with-heat-graph.md +1 -1
- package/.docs/organized/code-examples/with-image-generation.md +4 -4
- package/.docs/organized/code-examples/with-interactables.md +166 -338
- package/.docs/organized/code-examples/with-langchain.md +4 -4
- package/.docs/organized/code-examples/with-langgraph.md +20 -157
- package/.docs/organized/code-examples/with-livekit.md +6 -6
- package/.docs/organized/code-examples/with-mcp.md +4 -4
- package/.docs/organized/code-examples/with-opencode.md +3 -3
- package/.docs/organized/code-examples/with-pi.md +9 -7
- package/.docs/organized/code-examples/with-react-hook-form.md +13 -6
- package/.docs/organized/code-examples/with-react-ink-web.md +5 -5
- package/.docs/organized/code-examples/with-react-ink.md +1 -1
- package/.docs/organized/code-examples/with-react-router.md +10 -10
- package/.docs/organized/code-examples/with-resumable-stream.md +5 -5
- package/.docs/organized/code-examples/with-store.md +1 -1
- package/.docs/organized/code-examples/with-tanstack.md +5 -5
- package/.docs/organized/code-examples/with-tap-runtime.md +2 -2
- package/.docs/organized/code-examples/with-virtualized-thread.md +44 -24
- package/.docs/raw/docs/(docs)/cli.mdx +4 -2
- package/.docs/raw/docs/(docs)/devtools.mdx +25 -2
- package/.docs/raw/docs/(docs)/index.mdx +5 -2
- package/.docs/raw/docs/(docs)/installation.mdx +5 -2
- package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +16 -16
- package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +73 -42
- package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +3 -20
- package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +234 -166
- package/.docs/raw/docs/(reference)/api-reference/integrations/eve.mdx +51 -0
- package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +4 -1
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +70 -38
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +30 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +7 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +26 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/chain-of-thought.mdx +15 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +103 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +16 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +56 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/queue-item.mdx +12 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +12 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +14 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +57 -1
- package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +6 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/interactables-legacy.mdx +55 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/interactables.mdx +151 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +15 -15
- package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +20 -20
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +25 -9
- package/.docs/raw/docs/guides/headless-composer-input.mdx +113 -0
- package/.docs/raw/docs/guides/latex.mdx +28 -22
- package/.docs/raw/docs/guides/mentions.mdx +32 -7
- package/.docs/raw/docs/guides/slash-commands.mdx +3 -6
- package/.docs/raw/docs/guides/speech.mdx +5 -7
- package/.docs/raw/docs/guides/virtualization.mdx +77 -7
- package/.docs/raw/docs/guides/voice.mdx +3 -2
- package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +4 -12
- package/.docs/raw/docs/integrations/auth/better-auth.mdx +7 -10
- package/.docs/raw/docs/integrations/auth/clerk.mdx +3 -9
- package/.docs/raw/docs/integrations/auth/next-auth.mdx +3 -8
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +8 -5
- package/.docs/raw/docs/integrations/index.mdx +5 -12
- package/.docs/raw/docs/integrations/observability/helicone.mdx +3 -4
- package/.docs/raw/docs/integrations/observability/langfuse.mdx +3 -5
- package/.docs/raw/docs/integrations/observability/langsmith.mdx +3 -2
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +4 -3
- package/.docs/raw/docs/primitives/composer.mdx +8 -0
- package/.docs/raw/docs/primitives/thread.mdx +24 -0
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +21 -0
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +54 -0
- package/.docs/raw/docs/runtimes/eve/overview.mdx +100 -0
- package/.docs/raw/docs/runtimes/eve/quickstart.mdx +143 -0
- package/.docs/raw/docs/runtimes/langchain.mdx +386 -18
- package/.docs/raw/docs/runtimes/langgraph/quickstart.mdx +1 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +1 -1
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +6 -0
- package/.docs/raw/docs/tools/defining-tools.mdx +4 -0
- package/.docs/raw/docs/tools/interactables-legacy.mdx +410 -0
- package/.docs/raw/docs/tools/interactables.mdx +892 -223
- package/.docs/raw/docs/tools/mcp.mdx +4 -4
- package/.docs/raw/docs/tools/multi-agent.mdx +5 -0
- package/.docs/raw/docs/ui/composer-trigger-popover.mdx +2 -0
- package/.docs/raw/docs/ui/file.mdx +1 -1
- package/.docs/raw/docs/ui/part-grouping.mdx +38 -0
- package/.docs/raw/docs/utilities/react-o11y.mdx +92 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +14 -6
- package/src/tools/tests/mcp-protocol.test.ts +9 -0
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"resizeMode": "contain",
|
|
62
62
|
"backgroundColor": "#ffffff",
|
|
63
63
|
"dark": {
|
|
64
|
-
"backgroundColor": "#
|
|
64
|
+
"backgroundColor": "#18181b"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
],
|
|
@@ -83,12 +83,13 @@ import {
|
|
|
83
83
|
DarkTheme,
|
|
84
84
|
DefaultTheme,
|
|
85
85
|
ThemeProvider,
|
|
86
|
+
type Theme,
|
|
86
87
|
} from "@react-navigation/native";
|
|
87
88
|
import { Drawer } from "expo-router/drawer";
|
|
88
89
|
import { StatusBar } from "expo-status-bar";
|
|
89
90
|
import "react-native-reanimated";
|
|
90
|
-
import {
|
|
91
|
-
import
|
|
91
|
+
import { Platform, Pressable } from "react-native";
|
|
92
|
+
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
92
93
|
import { useFonts } from "expo-font";
|
|
93
94
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
94
95
|
|
|
@@ -98,42 +99,60 @@ import {
|
|
|
98
99
|
Tools,
|
|
99
100
|
} from "@assistant-ui/react-native";
|
|
100
101
|
import { useAppRuntime } from "@/hooks/use-app-runtime";
|
|
102
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
103
|
+
import { Icon } from "@/components/ui/icon";
|
|
101
104
|
import { ThreadListDrawer } from "@/components/thread-list/ThreadListDrawer";
|
|
105
|
+
import { haptics } from "@/lib/haptics";
|
|
102
106
|
import toolkit from "@/components/assistant-ui/tools";
|
|
103
107
|
|
|
104
108
|
function NewChatButton() {
|
|
105
109
|
const aui = useAui();
|
|
106
|
-
const
|
|
107
|
-
const isDark = colorScheme === "dark";
|
|
110
|
+
const { colors } = useTheme();
|
|
108
111
|
|
|
109
112
|
return (
|
|
110
113
|
<Pressable
|
|
114
|
+
accessibilityLabel="New chat"
|
|
115
|
+
hitSlop={8}
|
|
111
116
|
onPress={() => {
|
|
117
|
+
haptics.selection();
|
|
112
118
|
aui.threads().switchToNewThread();
|
|
113
119
|
}}
|
|
114
120
|
style={{ marginRight: 16 }}
|
|
115
121
|
>
|
|
116
|
-
<
|
|
117
|
-
name="create-outline"
|
|
118
|
-
size={24}
|
|
119
|
-
color={isDark ? "#ffffff" : "#000000"}
|
|
120
|
-
/>
|
|
122
|
+
<Icon name="compose" size={22} color={colors.foreground} />
|
|
121
123
|
</Pressable>
|
|
122
124
|
);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
function DrawerLayout() {
|
|
126
|
-
const
|
|
128
|
+
const { isDark, colors } = useTheme();
|
|
129
|
+
|
|
130
|
+
const base = isDark ? DarkTheme : DefaultTheme;
|
|
131
|
+
const navTheme: Theme = {
|
|
132
|
+
...base,
|
|
133
|
+
colors: {
|
|
134
|
+
...base.colors,
|
|
135
|
+
background: colors.background,
|
|
136
|
+
card: colors.background,
|
|
137
|
+
text: colors.foreground,
|
|
138
|
+
border: colors.border,
|
|
139
|
+
primary: colors.foreground,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
127
142
|
|
|
128
143
|
return (
|
|
129
|
-
<ThemeProvider value={
|
|
144
|
+
<ThemeProvider value={navTheme}>
|
|
130
145
|
<Drawer
|
|
131
146
|
drawerContent={(props) => <ThreadListDrawer {...props} />}
|
|
132
147
|
screenOptions={{
|
|
133
148
|
headerRight: () => <NewChatButton />,
|
|
149
|
+
headerShadowVisible: false,
|
|
150
|
+
headerTintColor: colors.foreground,
|
|
151
|
+
headerStyle: { backgroundColor: colors.background },
|
|
152
|
+
headerTitleStyle: { fontWeight: "600" },
|
|
134
153
|
drawerType: "front",
|
|
135
154
|
swipeEnabled: true,
|
|
136
|
-
drawerStyle: { backgroundColor:
|
|
155
|
+
drawerStyle: { backgroundColor: colors.background },
|
|
137
156
|
}}
|
|
138
157
|
>
|
|
139
158
|
<Drawer.Screen name="index" options={{ title: "Chat" }} />
|
|
@@ -144,7 +163,10 @@ function DrawerLayout() {
|
|
|
144
163
|
}
|
|
145
164
|
|
|
146
165
|
export default function RootLayout() {
|
|
147
|
-
|
|
166
|
+
// iOS renders SF Symbols, so it never needs the MaterialIcons glyph font.
|
|
167
|
+
const [fontsLoaded] = useFonts(
|
|
168
|
+
Platform.OS === "ios" ? {} : MaterialIcons.font,
|
|
169
|
+
);
|
|
148
170
|
const runtime = useAppRuntime();
|
|
149
171
|
const aui = useAui({
|
|
150
172
|
tools: Tools({ toolkit }),
|
|
@@ -239,66 +261,105 @@ module.exports = (api) => {
|
|
|
239
261
|
## components/assistant-ui/composer.tsx
|
|
240
262
|
|
|
241
263
|
```tsx
|
|
242
|
-
import {
|
|
243
|
-
|
|
244
|
-
Pressable,
|
|
245
|
-
Image,
|
|
246
|
-
Platform,
|
|
247
|
-
StyleSheet,
|
|
248
|
-
useColorScheme,
|
|
249
|
-
} from "react-native";
|
|
250
|
-
import { Ionicons } from "@expo/vector-icons";
|
|
264
|
+
import { View, Pressable, Platform, StyleSheet } from "react-native";
|
|
265
|
+
import { Image } from "expo-image";
|
|
251
266
|
import * as ImagePicker from "expo-image-picker";
|
|
252
267
|
import {
|
|
253
268
|
useAui,
|
|
254
269
|
useAuiState,
|
|
270
|
+
AuiIf,
|
|
255
271
|
ComposerPrimitive,
|
|
256
272
|
AttachmentPrimitive,
|
|
257
273
|
} from "@assistant-ui/react-native";
|
|
274
|
+
import { Icon } from "@/components/ui/icon";
|
|
275
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
276
|
+
import { Radius, Spacing } from "@/constants/theme";
|
|
277
|
+
import { haptics } from "@/lib/haptics";
|
|
258
278
|
|
|
259
279
|
function AttachmentPreview() {
|
|
280
|
+
const { colors } = useTheme();
|
|
260
281
|
const attachment = useAuiState((s) => s.attachment);
|
|
261
282
|
if (!attachment) return null;
|
|
262
283
|
|
|
263
|
-
// Find image content for preview URI
|
|
264
284
|
const imageContent = attachment.content?.find((c: any) => c.type === "image");
|
|
265
285
|
const uri = (imageContent as any)?.image;
|
|
266
286
|
|
|
267
287
|
return (
|
|
268
288
|
<AttachmentPrimitive.Root style={styles.attachmentItem}>
|
|
269
|
-
{uri ?
|
|
270
|
-
|
|
271
|
-
|
|
289
|
+
{uri ? (
|
|
290
|
+
<Image
|
|
291
|
+
source={{ uri }}
|
|
292
|
+
style={[styles.attachmentImage, { backgroundColor: colors.muted }]}
|
|
293
|
+
contentFit="cover"
|
|
294
|
+
transition={120}
|
|
295
|
+
/>
|
|
296
|
+
) : null}
|
|
297
|
+
<AttachmentPrimitive.Remove
|
|
298
|
+
onPressIn={haptics.light}
|
|
299
|
+
style={styles.attachmentRemove}
|
|
300
|
+
>
|
|
301
|
+
<View
|
|
302
|
+
style={[styles.removeBadge, { backgroundColor: colors.foreground }]}
|
|
303
|
+
>
|
|
304
|
+
<Icon name="remove" size={16} color={colors.background} />
|
|
305
|
+
</View>
|
|
272
306
|
</AttachmentPrimitive.Remove>
|
|
273
307
|
</AttachmentPrimitive.Root>
|
|
274
308
|
);
|
|
275
309
|
}
|
|
276
310
|
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
const
|
|
311
|
+
function SendButton() {
|
|
312
|
+
const { colors } = useTheme();
|
|
313
|
+
const canSend = useAuiState((s) => s.composer.canSend);
|
|
280
314
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
315
|
+
return (
|
|
316
|
+
<ComposerPrimitive.Send
|
|
317
|
+
accessibilityLabel="Send message"
|
|
318
|
+
onPressIn={() => canSend && haptics.success()}
|
|
319
|
+
style={[
|
|
320
|
+
styles.actionButton,
|
|
321
|
+
{ backgroundColor: canSend ? colors.primary : colors.muted },
|
|
322
|
+
]}
|
|
323
|
+
>
|
|
324
|
+
<Icon
|
|
325
|
+
name="send"
|
|
326
|
+
size={18}
|
|
327
|
+
color={canSend ? colors.primaryForeground : colors.mutedForeground}
|
|
328
|
+
/>
|
|
329
|
+
</ComposerPrimitive.Send>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function CancelButton() {
|
|
334
|
+
const { colors } = useTheme();
|
|
335
|
+
return (
|
|
336
|
+
<ComposerPrimitive.Cancel
|
|
337
|
+
accessibilityLabel="Stop generating"
|
|
338
|
+
onPressIn={haptics.light}
|
|
339
|
+
style={[styles.actionButton, { backgroundColor: colors.foreground }]}
|
|
340
|
+
>
|
|
341
|
+
<Icon name="stop" size={15} color={colors.background} />
|
|
342
|
+
</ComposerPrimitive.Cancel>
|
|
286
343
|
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function AttachButton() {
|
|
347
|
+
const { colors } = useTheme();
|
|
348
|
+
const aui = useAui();
|
|
287
349
|
|
|
288
350
|
const pickImage = async () => {
|
|
351
|
+
haptics.selection();
|
|
289
352
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
290
353
|
mediaTypes: ["images"],
|
|
291
354
|
allowsMultipleSelection: true,
|
|
292
355
|
quality: 0.8,
|
|
293
356
|
base64: true,
|
|
294
357
|
});
|
|
295
|
-
|
|
296
358
|
if (result.canceled) return;
|
|
297
359
|
|
|
298
360
|
for (const asset of result.assets) {
|
|
299
|
-
//
|
|
361
|
+
// iOS may report HEIC, which OpenAI rejects; normalize to JPEG.
|
|
300
362
|
const dataUrl = `data:image/jpeg;base64,${asset.base64}`;
|
|
301
|
-
|
|
302
363
|
await aui.composer().addAttachment({
|
|
303
364
|
name: asset.fileName ?? "image.jpg",
|
|
304
365
|
contentType: "image/jpeg",
|
|
@@ -309,78 +370,57 @@ export function Composer() {
|
|
|
309
370
|
};
|
|
310
371
|
|
|
311
372
|
return (
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
},
|
|
373
|
+
<Pressable
|
|
374
|
+
accessibilityLabel="Add image"
|
|
375
|
+
onPress={pickImage}
|
|
376
|
+
hitSlop={6}
|
|
377
|
+
style={({ pressed }) => [
|
|
378
|
+
styles.attachButton,
|
|
379
|
+
{ backgroundColor: pressed ? colors.muted : "transparent" },
|
|
320
380
|
]}
|
|
321
381
|
>
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
382
|
+
<Icon name="add" size={20} color={colors.mutedForeground} />
|
|
383
|
+
</Pressable>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function Composer() {
|
|
388
|
+
const { colors } = useTheme();
|
|
389
|
+
const hasAttachments = useAuiState((s) => s.composer.attachments.length > 0);
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<View style={styles.container}>
|
|
329
393
|
<View
|
|
330
394
|
style={[
|
|
331
|
-
styles.
|
|
332
|
-
{
|
|
333
|
-
backgroundColor: isDark ? "#1c1c1e" : "#ffffff",
|
|
334
|
-
borderColor: isDark ? "#3a3a3c" : "#e5e5ea",
|
|
335
|
-
},
|
|
395
|
+
styles.shell,
|
|
396
|
+
{ backgroundColor: colors.composer, borderColor: colors.border },
|
|
336
397
|
]}
|
|
337
398
|
>
|
|
338
|
-
|
|
339
|
-
style={styles.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
color={isDark ? "#8e8e93" : "#6e6e73"}
|
|
347
|
-
/>
|
|
348
|
-
</Pressable>
|
|
399
|
+
{hasAttachments && (
|
|
400
|
+
<View style={styles.attachmentsRow}>
|
|
401
|
+
<ComposerPrimitive.Attachments>
|
|
402
|
+
{() => <AttachmentPreview />}
|
|
403
|
+
</ComposerPrimitive.Attachments>
|
|
404
|
+
</View>
|
|
405
|
+
)}
|
|
406
|
+
|
|
349
407
|
<ComposerPrimitive.Input
|
|
350
|
-
style={[styles.input, { color:
|
|
351
|
-
placeholder="Message
|
|
352
|
-
placeholderTextColor=
|
|
408
|
+
style={[styles.input, { color: colors.foreground }]}
|
|
409
|
+
placeholder="Message…"
|
|
410
|
+
placeholderTextColor={colors.mutedForeground}
|
|
353
411
|
multiline
|
|
354
412
|
maxLength={4000}
|
|
355
|
-
editable={!canCancel}
|
|
356
413
|
/>
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
backgroundColor: canSend
|
|
368
|
-
? isDark
|
|
369
|
-
? "#0a84ff"
|
|
370
|
-
: "#007aff"
|
|
371
|
-
: isDark
|
|
372
|
-
? "#3a3a3c"
|
|
373
|
-
: "#e5e5ea",
|
|
374
|
-
},
|
|
375
|
-
]}
|
|
376
|
-
>
|
|
377
|
-
<Ionicons
|
|
378
|
-
name="arrow-up"
|
|
379
|
-
size={20}
|
|
380
|
-
color={canSend ? "#ffffff" : "#8e8e93"}
|
|
381
|
-
/>
|
|
382
|
-
</ComposerPrimitive.Send>
|
|
383
|
-
)}
|
|
414
|
+
|
|
415
|
+
<View style={styles.actionRow}>
|
|
416
|
+
<AttachButton />
|
|
417
|
+
<AuiIf condition={(s) => !s.thread.isRunning}>
|
|
418
|
+
<SendButton />
|
|
419
|
+
</AuiIf>
|
|
420
|
+
<AuiIf condition={(s) => s.thread.isRunning}>
|
|
421
|
+
<CancelButton />
|
|
422
|
+
</AuiIf>
|
|
423
|
+
</View>
|
|
384
424
|
</View>
|
|
385
425
|
</View>
|
|
386
426
|
);
|
|
@@ -388,69 +428,77 @@ export function Composer() {
|
|
|
388
428
|
|
|
389
429
|
const styles = StyleSheet.create({
|
|
390
430
|
container: {
|
|
391
|
-
|
|
392
|
-
|
|
431
|
+
width: "100%",
|
|
432
|
+
maxWidth: Spacing.threadMaxWidth + 24,
|
|
433
|
+
alignSelf: "center",
|
|
434
|
+
paddingHorizontal: 12,
|
|
435
|
+
paddingTop: 8,
|
|
436
|
+
},
|
|
437
|
+
shell: {
|
|
438
|
+
flexDirection: "column",
|
|
439
|
+
gap: 6,
|
|
440
|
+
borderRadius: Radius.composer,
|
|
441
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
442
|
+
padding: 8,
|
|
393
443
|
},
|
|
394
|
-
|
|
444
|
+
attachmentsRow: {
|
|
395
445
|
flexDirection: "row",
|
|
396
446
|
flexWrap: "wrap",
|
|
397
447
|
gap: 8,
|
|
398
|
-
|
|
448
|
+
paddingHorizontal: 4,
|
|
449
|
+
paddingTop: 4,
|
|
399
450
|
},
|
|
400
451
|
attachmentItem: {
|
|
401
452
|
position: "relative",
|
|
402
453
|
},
|
|
403
454
|
attachmentImage: {
|
|
404
|
-
width:
|
|
405
|
-
height:
|
|
406
|
-
borderRadius:
|
|
455
|
+
width: 56,
|
|
456
|
+
height: 56,
|
|
457
|
+
borderRadius: Radius.attachment,
|
|
407
458
|
},
|
|
408
|
-
|
|
459
|
+
attachmentRemove: {
|
|
409
460
|
position: "absolute",
|
|
410
461
|
top: -6,
|
|
411
462
|
right: -6,
|
|
412
463
|
},
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
borderRadius:
|
|
417
|
-
borderWidth: 1,
|
|
418
|
-
padding: 6,
|
|
419
|
-
},
|
|
420
|
-
attachButton: {
|
|
421
|
-
width: 30,
|
|
422
|
-
height: 30,
|
|
423
|
-
justifyContent: "center",
|
|
464
|
+
removeBadge: {
|
|
465
|
+
width: 20,
|
|
466
|
+
height: 20,
|
|
467
|
+
borderRadius: 10,
|
|
424
468
|
alignItems: "center",
|
|
469
|
+
justifyContent: "center",
|
|
425
470
|
},
|
|
426
471
|
input: {
|
|
427
|
-
flex: 1,
|
|
428
472
|
fontSize: 16,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
473
|
+
lineHeight: 22,
|
|
474
|
+
minHeight: 28,
|
|
475
|
+
maxHeight: 132,
|
|
476
|
+
paddingHorizontal: 10,
|
|
477
|
+
paddingTop: 6,
|
|
478
|
+
paddingBottom: 2,
|
|
432
479
|
...Platform.select({
|
|
433
|
-
web: {
|
|
480
|
+
web: { outlineStyle: "none" } as any,
|
|
434
481
|
default: {},
|
|
435
482
|
}),
|
|
436
483
|
},
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
height: 30,
|
|
440
|
-
borderRadius: 15,
|
|
441
|
-
justifyContent: "center",
|
|
484
|
+
actionRow: {
|
|
485
|
+
flexDirection: "row",
|
|
442
486
|
alignItems: "center",
|
|
443
|
-
|
|
487
|
+
justifyContent: "space-between",
|
|
444
488
|
},
|
|
445
|
-
|
|
446
|
-
|
|
489
|
+
attachButton: {
|
|
490
|
+
width: 32,
|
|
491
|
+
height: 32,
|
|
492
|
+
borderRadius: Radius.pill,
|
|
493
|
+
alignItems: "center",
|
|
494
|
+
justifyContent: "center",
|
|
447
495
|
},
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
496
|
+
actionButton: {
|
|
497
|
+
width: 32,
|
|
498
|
+
height: 32,
|
|
499
|
+
borderRadius: Radius.pill,
|
|
500
|
+
alignItems: "center",
|
|
501
|
+
justifyContent: "center",
|
|
454
502
|
},
|
|
455
503
|
});
|
|
456
504
|
|
|
@@ -459,28 +507,46 @@ const styles = StyleSheet.create({
|
|
|
459
507
|
## components/assistant-ui/message-action-bar.tsx
|
|
460
508
|
|
|
461
509
|
```tsx
|
|
462
|
-
import { View, StyleSheet
|
|
463
|
-
import
|
|
510
|
+
import { View, StyleSheet } from "react-native";
|
|
511
|
+
import * as Clipboard from "expo-clipboard";
|
|
512
|
+
import { Icon } from "@/components/ui/icon";
|
|
464
513
|
import { ActionBarPrimitive } from "@assistant-ui/react-native";
|
|
514
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
515
|
+
import { haptics } from "@/lib/haptics";
|
|
516
|
+
import { Radius } from "@/constants/theme";
|
|
517
|
+
|
|
518
|
+
const copyToClipboard = async (text: string) => {
|
|
519
|
+
await Clipboard.setStringAsync(text);
|
|
520
|
+
};
|
|
465
521
|
|
|
466
522
|
export function MessageActionBar() {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
const
|
|
523
|
+
const { colors } = useTheme();
|
|
524
|
+
|
|
525
|
+
const buttonStyle = ({ pressed }: { pressed: boolean }) => [
|
|
526
|
+
styles.button,
|
|
527
|
+
pressed && { backgroundColor: colors.muted },
|
|
528
|
+
];
|
|
470
529
|
|
|
471
530
|
return (
|
|
472
531
|
<View style={styles.container}>
|
|
473
|
-
<ActionBarPrimitive.Copy
|
|
532
|
+
<ActionBarPrimitive.Copy
|
|
533
|
+
copyToClipboard={copyToClipboard}
|
|
534
|
+
onPressIn={haptics.selection}
|
|
535
|
+
style={buttonStyle}
|
|
536
|
+
>
|
|
474
537
|
{({ isCopied }) => (
|
|
475
|
-
<
|
|
476
|
-
name={isCopied ? "
|
|
538
|
+
<Icon
|
|
539
|
+
name={isCopied ? "check" : "copy"}
|
|
477
540
|
size={16}
|
|
478
|
-
color={isCopied ?
|
|
541
|
+
color={isCopied ? colors.foreground : colors.mutedForeground}
|
|
479
542
|
/>
|
|
480
543
|
)}
|
|
481
544
|
</ActionBarPrimitive.Copy>
|
|
482
|
-
<ActionBarPrimitive.Reload
|
|
483
|
-
|
|
545
|
+
<ActionBarPrimitive.Reload
|
|
546
|
+
onPressIn={haptics.selection}
|
|
547
|
+
style={buttonStyle}
|
|
548
|
+
>
|
|
549
|
+
<Icon name="reload" size={16} color={colors.mutedForeground} />
|
|
484
550
|
</ActionBarPrimitive.Reload>
|
|
485
551
|
</View>
|
|
486
552
|
);
|
|
@@ -489,12 +555,12 @@ export function MessageActionBar() {
|
|
|
489
555
|
const styles = StyleSheet.create({
|
|
490
556
|
container: {
|
|
491
557
|
flexDirection: "row",
|
|
492
|
-
|
|
493
|
-
|
|
558
|
+
alignItems: "center",
|
|
559
|
+
gap: 2,
|
|
494
560
|
},
|
|
495
561
|
button: {
|
|
496
562
|
padding: 6,
|
|
497
|
-
borderRadius:
|
|
563
|
+
borderRadius: Radius.sm,
|
|
498
564
|
},
|
|
499
565
|
});
|
|
500
566
|
|
|
@@ -503,47 +569,41 @@ const styles = StyleSheet.create({
|
|
|
503
569
|
## components/assistant-ui/message-branch-picker.tsx
|
|
504
570
|
|
|
505
571
|
```tsx
|
|
506
|
-
import { View,
|
|
507
|
-
import {
|
|
508
|
-
import { ThemedText } from "@/components/themed-text";
|
|
572
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
573
|
+
import { Icon } from "@/components/ui/icon";
|
|
509
574
|
import { BranchPickerPrimitive, useAuiState } from "@assistant-ui/react-native";
|
|
575
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
510
576
|
|
|
511
|
-
export function MessageBranchPicker(
|
|
577
|
+
export function MessageBranchPicker({
|
|
578
|
+
align = "flex-start",
|
|
579
|
+
}: {
|
|
580
|
+
align?: "flex-start" | "flex-end";
|
|
581
|
+
}) {
|
|
582
|
+
const { colors } = useTheme();
|
|
512
583
|
const branchNumber = useAuiState((s) => s.message.branchNumber);
|
|
513
584
|
const branchCount = useAuiState((s) => s.message.branchCount);
|
|
514
585
|
|
|
515
|
-
const colorScheme = useColorScheme();
|
|
516
|
-
const isDark = colorScheme === "dark";
|
|
517
|
-
const iconColor = isDark ? "#8e8e93" : "#6e6e73";
|
|
518
|
-
|
|
519
586
|
if (branchCount <= 1) return null;
|
|
520
587
|
|
|
521
588
|
return (
|
|
522
|
-
<View style={styles.container}>
|
|
523
|
-
<BranchPickerPrimitive.Previous
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
branchNumber <= 1 ? (isDark ? "#3a3a3c" : "#d1d1d6") : iconColor
|
|
529
|
-
}
|
|
530
|
-
/>
|
|
589
|
+
<View style={[styles.container, { justifyContent: align }]}>
|
|
590
|
+
<BranchPickerPrimitive.Previous
|
|
591
|
+
style={[styles.button, { opacity: branchNumber <= 1 ? 0.35 : 1 }]}
|
|
592
|
+
hitSlop={4}
|
|
593
|
+
>
|
|
594
|
+
<Icon name="chevron-left" size={16} color={colors.mutedForeground} />
|
|
531
595
|
</BranchPickerPrimitive.Previous>
|
|
532
|
-
<
|
|
533
|
-
|
|
534
|
-
</
|
|
535
|
-
<BranchPickerPrimitive.Next
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
: "#d1d1d6"
|
|
544
|
-
: iconColor
|
|
545
|
-
}
|
|
546
|
-
/>
|
|
596
|
+
<Text style={[styles.label, { color: colors.mutedForeground }]}>
|
|
597
|
+
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
598
|
+
</Text>
|
|
599
|
+
<BranchPickerPrimitive.Next
|
|
600
|
+
style={[
|
|
601
|
+
styles.button,
|
|
602
|
+
{ opacity: branchNumber >= branchCount ? 0.35 : 1 },
|
|
603
|
+
]}
|
|
604
|
+
hitSlop={4}
|
|
605
|
+
>
|
|
606
|
+
<Icon name="chevron-right" size={16} color={colors.mutedForeground} />
|
|
547
607
|
</BranchPickerPrimitive.Next>
|
|
548
608
|
</View>
|
|
549
609
|
);
|
|
@@ -557,7 +617,6 @@ const styles = StyleSheet.create({
|
|
|
557
617
|
},
|
|
558
618
|
button: {
|
|
559
619
|
padding: 4,
|
|
560
|
-
borderRadius: 6,
|
|
561
620
|
},
|
|
562
621
|
label: {
|
|
563
622
|
fontSize: 12,
|
|
@@ -570,53 +629,82 @@ const styles = StyleSheet.create({
|
|
|
570
629
|
## components/assistant-ui/message.tsx
|
|
571
630
|
|
|
572
631
|
```tsx
|
|
573
|
-
import {
|
|
574
|
-
import {
|
|
575
|
-
import {
|
|
632
|
+
import { useEffect, useRef } from "react";
|
|
633
|
+
import { View, Text, Animated, Platform, StyleSheet } from "react-native";
|
|
634
|
+
import { Image } from "expo-image";
|
|
635
|
+
import {
|
|
636
|
+
useAuiState,
|
|
637
|
+
MessagePrimitive,
|
|
638
|
+
ErrorPrimitive,
|
|
639
|
+
type TextMessagePartComponent,
|
|
640
|
+
} from "@assistant-ui/react-native";
|
|
641
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
642
|
+
import { Radius } from "@/constants/theme";
|
|
576
643
|
import { MessageActionBar } from "./message-action-bar";
|
|
577
644
|
import { MessageBranchPicker } from "./message-branch-picker";
|
|
578
645
|
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
646
|
+
const UserText: TextMessagePartComponent = ({ text }) => {
|
|
647
|
+
const { colors } = useTheme();
|
|
648
|
+
return (
|
|
649
|
+
<Text style={[styles.userText, { color: colors.foreground }]}>{text}</Text>
|
|
650
|
+
);
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const AssistantText: TextMessagePartComponent = ({ text }) => {
|
|
654
|
+
const { colors } = useTheme();
|
|
655
|
+
return (
|
|
656
|
+
<Text style={[styles.assistantText, { color: colors.foreground }]}>
|
|
657
|
+
{text}
|
|
658
|
+
</Text>
|
|
659
|
+
);
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
function TypingDot({ delay }: { delay: number }) {
|
|
663
|
+
const { colors } = useTheme();
|
|
664
|
+
const opacity = useRef(new Animated.Value(0.3)).current;
|
|
587
665
|
|
|
588
|
-
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
const anim = Animated.loop(
|
|
668
|
+
Animated.sequence([
|
|
669
|
+
Animated.timing(opacity, {
|
|
670
|
+
toValue: 1,
|
|
671
|
+
duration: 400,
|
|
672
|
+
delay,
|
|
673
|
+
useNativeDriver: Platform.OS !== "web",
|
|
674
|
+
}),
|
|
675
|
+
Animated.timing(opacity, {
|
|
676
|
+
toValue: 0.3,
|
|
677
|
+
duration: 400,
|
|
678
|
+
useNativeDriver: Platform.OS !== "web",
|
|
679
|
+
}),
|
|
680
|
+
]),
|
|
681
|
+
);
|
|
682
|
+
anim.start();
|
|
683
|
+
return () => anim.stop();
|
|
684
|
+
}, [opacity, delay]);
|
|
589
685
|
|
|
590
686
|
return (
|
|
591
|
-
<View
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
lightColor="#ff453a"
|
|
595
|
-
darkColor="#ff6961"
|
|
596
|
-
>
|
|
597
|
-
{typeof error === "string" ? error : "An error occurred"}
|
|
598
|
-
</ThemedText>
|
|
599
|
-
</View>
|
|
687
|
+
<Animated.View
|
|
688
|
+
style={[styles.dot, { opacity, backgroundColor: colors.mutedForeground }]}
|
|
689
|
+
/>
|
|
600
690
|
);
|
|
601
691
|
}
|
|
602
692
|
|
|
603
|
-
function
|
|
604
|
-
const
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
}
|
|
693
|
+
function TypingIndicator() {
|
|
694
|
+
const isRunning = useAuiState((s) => s.message.status?.type === "running");
|
|
695
|
+
if (!isRunning) return null;
|
|
696
|
+
|
|
608
697
|
return (
|
|
609
|
-
<
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
>
|
|
614
|
-
{part.text}
|
|
615
|
-
</ThemedText>
|
|
698
|
+
<View style={styles.typing}>
|
|
699
|
+
<TypingDot delay={0} />
|
|
700
|
+
<TypingDot delay={160} />
|
|
701
|
+
<TypingDot delay={320} />
|
|
702
|
+
</View>
|
|
616
703
|
);
|
|
617
704
|
}
|
|
618
705
|
|
|
619
706
|
function MessageImageAttachment() {
|
|
707
|
+
const { colors } = useTheme();
|
|
620
708
|
const attachment = useAuiState((s) => s.attachment);
|
|
621
709
|
if (!attachment) return null;
|
|
622
710
|
|
|
@@ -624,119 +712,138 @@ function MessageImageAttachment() {
|
|
|
624
712
|
const uri = (imageContent as any)?.image;
|
|
625
713
|
if (!uri) return null;
|
|
626
714
|
|
|
627
|
-
return
|
|
715
|
+
return (
|
|
716
|
+
<Image
|
|
717
|
+
source={{ uri }}
|
|
718
|
+
style={[styles.messageImage, { backgroundColor: colors.muted }]}
|
|
719
|
+
contentFit="cover"
|
|
720
|
+
transition={120}
|
|
721
|
+
/>
|
|
722
|
+
);
|
|
628
723
|
}
|
|
629
724
|
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const isUser = role === "user";
|
|
636
|
-
|
|
637
|
-
if (isUser) {
|
|
638
|
-
return (
|
|
639
|
-
<View style={[styles.container, styles.userContainer]}>
|
|
725
|
+
function UserMessage() {
|
|
726
|
+
const { colors } = useTheme();
|
|
727
|
+
return (
|
|
728
|
+
<MessagePrimitive.Root style={styles.userContainer}>
|
|
729
|
+
<View style={styles.userAttachments}>
|
|
640
730
|
<MessagePrimitive.Attachments>
|
|
641
731
|
{() => <MessageImageAttachment />}
|
|
642
732
|
</MessagePrimitive.Attachments>
|
|
643
|
-
<View
|
|
644
|
-
style={[
|
|
645
|
-
styles.bubble,
|
|
646
|
-
styles.userBubble,
|
|
647
|
-
{ backgroundColor: isDark ? "#0a84ff" : "#007aff" },
|
|
648
|
-
]}
|
|
649
|
-
>
|
|
650
|
-
<MessagePrimitive.Content
|
|
651
|
-
renderText={({ part }) => <TextPart part={part} />}
|
|
652
|
-
/>
|
|
653
|
-
</View>
|
|
654
|
-
<MessageBranchPicker />
|
|
655
733
|
</View>
|
|
656
|
-
|
|
657
|
-
|
|
734
|
+
<View style={[styles.userBubble, { backgroundColor: colors.muted }]}>
|
|
735
|
+
<MessagePrimitive.Parts components={{ Text: UserText }} />
|
|
736
|
+
</View>
|
|
737
|
+
<MessageBranchPicker align="flex-end" />
|
|
738
|
+
</MessagePrimitive.Root>
|
|
739
|
+
);
|
|
740
|
+
}
|
|
658
741
|
|
|
742
|
+
function AssistantMessage() {
|
|
743
|
+
const { colors } = useTheme();
|
|
659
744
|
return (
|
|
660
|
-
<
|
|
661
|
-
<View
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
styles.assistantBubble,
|
|
665
|
-
{
|
|
666
|
-
backgroundColor: isDark
|
|
667
|
-
? "rgba(44, 44, 46, 0.8)"
|
|
668
|
-
: "rgba(229, 229, 234, 0.8)",
|
|
669
|
-
},
|
|
670
|
-
]}
|
|
671
|
-
>
|
|
672
|
-
<MessagePrimitive.Content
|
|
673
|
-
renderText={({ part }) => <TextPart part={part} />}
|
|
745
|
+
<MessagePrimitive.Root style={styles.assistantContainer}>
|
|
746
|
+
<View style={styles.assistantContent}>
|
|
747
|
+
<MessagePrimitive.Parts
|
|
748
|
+
components={{ Text: AssistantText, Empty: TypingIndicator }}
|
|
674
749
|
/>
|
|
675
|
-
<
|
|
750
|
+
<ErrorPrimitive.Root
|
|
751
|
+
style={[
|
|
752
|
+
styles.error,
|
|
753
|
+
{
|
|
754
|
+
backgroundColor: colors.destructiveSurface,
|
|
755
|
+
borderColor: colors.destructive,
|
|
756
|
+
},
|
|
757
|
+
]}
|
|
758
|
+
>
|
|
759
|
+
<ErrorPrimitive.Message
|
|
760
|
+
style={[styles.errorText, { color: colors.destructive }]}
|
|
761
|
+
/>
|
|
762
|
+
</ErrorPrimitive.Root>
|
|
676
763
|
</View>
|
|
677
|
-
{
|
|
764
|
+
<MessagePrimitive.If running={false}>
|
|
678
765
|
<View style={styles.actionsRow}>
|
|
679
|
-
<MessageBranchPicker />
|
|
766
|
+
<MessageBranchPicker align="flex-start" />
|
|
680
767
|
<MessageActionBar />
|
|
681
768
|
</View>
|
|
682
|
-
|
|
683
|
-
</
|
|
769
|
+
</MessagePrimitive.If>
|
|
770
|
+
</MessagePrimitive.Root>
|
|
684
771
|
);
|
|
685
772
|
}
|
|
686
773
|
|
|
774
|
+
export function MessageBubble() {
|
|
775
|
+
const role = useAuiState((s) => s.message.role);
|
|
776
|
+
if (role === "user") return <UserMessage />;
|
|
777
|
+
return <AssistantMessage />;
|
|
778
|
+
}
|
|
779
|
+
|
|
687
780
|
const styles = StyleSheet.create({
|
|
688
|
-
container: {
|
|
689
|
-
paddingHorizontal: 16,
|
|
690
|
-
paddingVertical: 6,
|
|
691
|
-
},
|
|
692
781
|
userContainer: {
|
|
693
782
|
alignItems: "flex-end",
|
|
694
783
|
},
|
|
695
|
-
|
|
696
|
-
|
|
784
|
+
userAttachments: {
|
|
785
|
+
flexDirection: "row",
|
|
786
|
+
flexWrap: "wrap",
|
|
787
|
+
justifyContent: "flex-end",
|
|
788
|
+
gap: 6,
|
|
697
789
|
},
|
|
698
|
-
|
|
790
|
+
userBubble: {
|
|
699
791
|
maxWidth: "85%",
|
|
700
792
|
paddingHorizontal: 16,
|
|
701
|
-
paddingVertical:
|
|
702
|
-
borderRadius:
|
|
703
|
-
},
|
|
704
|
-
userBubble: {
|
|
705
|
-
borderBottomRightRadius: 6,
|
|
706
|
-
},
|
|
707
|
-
actionsRow: {
|
|
708
|
-
flexDirection: "row",
|
|
709
|
-
alignItems: "center",
|
|
710
|
-
gap: 4,
|
|
711
|
-
marginTop: 4,
|
|
712
|
-
},
|
|
713
|
-
assistantBubble: {
|
|
714
|
-
borderBottomLeftRadius: 6,
|
|
715
|
-
},
|
|
716
|
-
errorContainer: {
|
|
717
|
-
paddingTop: 4,
|
|
793
|
+
paddingVertical: 10,
|
|
794
|
+
borderRadius: Radius.bubble,
|
|
718
795
|
},
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
lineHeight: 20,
|
|
796
|
+
assistantContainer: {
|
|
797
|
+
alignItems: "flex-start",
|
|
722
798
|
},
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
height: 200,
|
|
726
|
-
borderRadius: 12,
|
|
727
|
-
marginBottom: 4,
|
|
799
|
+
assistantContent: {
|
|
800
|
+
paddingHorizontal: 2,
|
|
728
801
|
},
|
|
729
802
|
userText: {
|
|
730
803
|
fontSize: 16,
|
|
731
804
|
lineHeight: 22,
|
|
732
|
-
color: "#ffffff",
|
|
733
805
|
letterSpacing: -0.2,
|
|
734
806
|
},
|
|
735
807
|
assistantText: {
|
|
736
808
|
fontSize: 16,
|
|
737
|
-
lineHeight:
|
|
809
|
+
lineHeight: 25,
|
|
738
810
|
letterSpacing: -0.2,
|
|
739
811
|
},
|
|
812
|
+
typing: {
|
|
813
|
+
flexDirection: "row",
|
|
814
|
+
alignItems: "center",
|
|
815
|
+
gap: 5,
|
|
816
|
+
paddingVertical: 8,
|
|
817
|
+
},
|
|
818
|
+
dot: {
|
|
819
|
+
width: 7,
|
|
820
|
+
height: 7,
|
|
821
|
+
borderRadius: 3.5,
|
|
822
|
+
},
|
|
823
|
+
messageImage: {
|
|
824
|
+
width: 200,
|
|
825
|
+
height: 200,
|
|
826
|
+
borderRadius: Radius.card,
|
|
827
|
+
marginBottom: 6,
|
|
828
|
+
},
|
|
829
|
+
actionsRow: {
|
|
830
|
+
flexDirection: "row",
|
|
831
|
+
alignItems: "center",
|
|
832
|
+
gap: 4,
|
|
833
|
+
marginTop: 6,
|
|
834
|
+
marginLeft: -4,
|
|
835
|
+
},
|
|
836
|
+
error: {
|
|
837
|
+
marginTop: 8,
|
|
838
|
+
borderRadius: Radius.md,
|
|
839
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
840
|
+
paddingHorizontal: 12,
|
|
841
|
+
paddingVertical: 10,
|
|
842
|
+
},
|
|
843
|
+
errorText: {
|
|
844
|
+
fontSize: 14,
|
|
845
|
+
lineHeight: 20,
|
|
846
|
+
},
|
|
740
847
|
});
|
|
741
848
|
|
|
742
849
|
```
|
|
@@ -744,101 +851,71 @@ const styles = StyleSheet.create({
|
|
|
744
851
|
## components/assistant-ui/thread.tsx
|
|
745
852
|
|
|
746
853
|
```tsx
|
|
854
|
+
import { useRef } from "react";
|
|
747
855
|
import {
|
|
748
856
|
View,
|
|
749
857
|
Text,
|
|
750
|
-
Pressable,
|
|
751
858
|
StyleSheet,
|
|
752
859
|
KeyboardAvoidingView,
|
|
753
860
|
Platform,
|
|
754
|
-
|
|
861
|
+
type FlatList,
|
|
755
862
|
} from "react-native";
|
|
756
863
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
757
864
|
import { MessageBubble } from "./message";
|
|
758
865
|
import { Composer } from "./composer";
|
|
759
|
-
import { ThreadPrimitive
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
866
|
+
import { ThreadPrimitive } from "@assistant-ui/react-native";
|
|
867
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
868
|
+
import { Radius, Spacing } from "@/constants/theme";
|
|
869
|
+
import { haptics } from "@/lib/haptics";
|
|
870
|
+
|
|
871
|
+
const suggestions = [
|
|
872
|
+
"What's the weather in Tokyo?",
|
|
873
|
+
"Tell me a joke",
|
|
874
|
+
"Help me write an email",
|
|
875
|
+
];
|
|
765
876
|
|
|
877
|
+
function SuggestionChip({ prompt }: { prompt: string }) {
|
|
878
|
+
const { colors } = useTheme();
|
|
766
879
|
return (
|
|
767
|
-
<
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
880
|
+
<ThreadPrimitive.Suggestion
|
|
881
|
+
prompt={prompt}
|
|
882
|
+
send
|
|
883
|
+
onPressIn={haptics.selection}
|
|
884
|
+
style={({ pressed }: { pressed: boolean }) => [
|
|
885
|
+
styles.chip,
|
|
771
886
|
{
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
: "rgba(229, 229, 234, 0.8)",
|
|
887
|
+
borderColor: colors.border,
|
|
888
|
+
backgroundColor: pressed ? colors.muted : colors.background,
|
|
775
889
|
},
|
|
776
890
|
]}
|
|
777
891
|
>
|
|
778
|
-
<Text
|
|
779
|
-
|
|
780
|
-
styles.suggestionText,
|
|
781
|
-
{ color: isDark ? "#ffffff" : "#000000" },
|
|
782
|
-
]}
|
|
783
|
-
>
|
|
784
|
-
{title}
|
|
892
|
+
<Text style={[styles.chipText, { color: colors.foreground }]}>
|
|
893
|
+
{prompt}
|
|
785
894
|
</Text>
|
|
786
|
-
</
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const defaultSuggestions = [
|
|
791
|
-
{
|
|
792
|
-
title: "What's the weather in Tokyo?",
|
|
793
|
-
prompt: "What's the weather in Tokyo?",
|
|
794
|
-
},
|
|
795
|
-
{ title: "Tell me a joke", prompt: "Tell me a joke" },
|
|
796
|
-
{ title: "Help me write an email", prompt: "Help me write an email" },
|
|
797
|
-
];
|
|
798
|
-
|
|
799
|
-
function Suggestions() {
|
|
800
|
-
return (
|
|
801
|
-
<View style={styles.suggestionsContainer}>
|
|
802
|
-
{defaultSuggestions.map((s, i) => (
|
|
803
|
-
<SuggestionChip key={i} title={s.title} prompt={s.prompt} />
|
|
804
|
-
))}
|
|
805
|
-
</View>
|
|
895
|
+
</ThreadPrimitive.Suggestion>
|
|
806
896
|
);
|
|
807
897
|
}
|
|
808
898
|
|
|
809
899
|
function EmptyState() {
|
|
810
|
-
const
|
|
811
|
-
const isDark = colorScheme === "dark";
|
|
812
|
-
|
|
900
|
+
const { colors } = useTheme();
|
|
813
901
|
return (
|
|
814
|
-
<View
|
|
815
|
-
style={[
|
|
816
|
-
|
|
817
|
-
{ backgroundColor: isDark ? "#000000" : "#ffffff" },
|
|
818
|
-
]}
|
|
819
|
-
>
|
|
820
|
-
<View style={styles.emptyIconContainer}>
|
|
821
|
-
<Text style={styles.emptyIcon}>💭</Text>
|
|
822
|
-
</View>
|
|
823
|
-
<Text
|
|
824
|
-
style={[styles.emptyTitle, { color: isDark ? "#ffffff" : "#000000" }]}
|
|
825
|
-
>
|
|
826
|
-
How can I help?
|
|
827
|
-
</Text>
|
|
828
|
-
<Text
|
|
829
|
-
style={[
|
|
830
|
-
styles.emptySubtitle,
|
|
831
|
-
{ color: isDark ? "#8e8e93" : "#6e6e73" },
|
|
832
|
-
]}
|
|
833
|
-
>
|
|
834
|
-
Send a message to start chatting
|
|
902
|
+
<View style={styles.empty}>
|
|
903
|
+
<Text style={[styles.welcome, { color: colors.foreground }]}>
|
|
904
|
+
How can I help you today?
|
|
835
905
|
</Text>
|
|
836
|
-
<
|
|
906
|
+
<View style={styles.chips}>
|
|
907
|
+
{suggestions.map((prompt) => (
|
|
908
|
+
<SuggestionChip key={prompt} prompt={prompt} />
|
|
909
|
+
))}
|
|
910
|
+
</View>
|
|
837
911
|
</View>
|
|
838
912
|
);
|
|
839
913
|
}
|
|
840
914
|
|
|
841
915
|
function ChatMessages() {
|
|
916
|
+
const listRef = useRef<FlatList>(null);
|
|
917
|
+
const nearBottom = useRef(true);
|
|
918
|
+
|
|
842
919
|
return (
|
|
843
920
|
<>
|
|
844
921
|
<ThreadPrimitive.Empty>
|
|
@@ -846,8 +923,24 @@ function ChatMessages() {
|
|
|
846
923
|
</ThreadPrimitive.Empty>
|
|
847
924
|
<ThreadPrimitive.If empty={false}>
|
|
848
925
|
<ThreadPrimitive.Messages
|
|
926
|
+
// Messages isn't a forwardRef component, but it spreads extra props
|
|
927
|
+
// onto its FlatList, so React 19 still forwards this ref to the list.
|
|
928
|
+
{...({ ref: listRef } as object)}
|
|
929
|
+
style={styles.flex}
|
|
849
930
|
contentContainerStyle={styles.messageList}
|
|
850
931
|
showsVerticalScrollIndicator={false}
|
|
932
|
+
scrollEventThrottle={32}
|
|
933
|
+
onScroll={(e) => {
|
|
934
|
+
const { contentOffset, contentSize, layoutMeasurement } =
|
|
935
|
+
e.nativeEvent;
|
|
936
|
+
nearBottom.current =
|
|
937
|
+
contentOffset.y + layoutMeasurement.height >=
|
|
938
|
+
contentSize.height - 48;
|
|
939
|
+
}}
|
|
940
|
+
onContentSizeChange={() => {
|
|
941
|
+
if (nearBottom.current)
|
|
942
|
+
listRef.current?.scrollToEnd({ animated: true });
|
|
943
|
+
}}
|
|
851
944
|
>
|
|
852
945
|
{() => <MessageBubble />}
|
|
853
946
|
</ThreadPrimitive.Messages>
|
|
@@ -858,23 +951,18 @@ function ChatMessages() {
|
|
|
858
951
|
|
|
859
952
|
export function Thread() {
|
|
860
953
|
const insets = useSafeAreaInsets();
|
|
861
|
-
const
|
|
862
|
-
|
|
954
|
+
const { colors } = useTheme();
|
|
955
|
+
|
|
863
956
|
return (
|
|
864
|
-
<View
|
|
865
|
-
style={[
|
|
866
|
-
styles.container,
|
|
867
|
-
{ backgroundColor: isDark ? "#000000" : "#ffffff" },
|
|
868
|
-
]}
|
|
869
|
-
>
|
|
957
|
+
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
|
870
958
|
<KeyboardAvoidingView
|
|
871
|
-
style={styles.
|
|
872
|
-
behavior={Platform.OS === "ios" ? "padding" :
|
|
959
|
+
style={styles.flex}
|
|
960
|
+
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
|
873
961
|
>
|
|
874
|
-
<View style={styles.
|
|
962
|
+
<View style={styles.flex}>
|
|
875
963
|
<ChatMessages />
|
|
876
964
|
</View>
|
|
877
|
-
<View style={{ paddingBottom: insets.bottom }}>
|
|
965
|
+
<View style={{ paddingBottom: insets.bottom + 8 }}>
|
|
878
966
|
<Composer />
|
|
879
967
|
</View>
|
|
880
968
|
</KeyboardAvoidingView>
|
|
@@ -886,59 +974,43 @@ const styles = StyleSheet.create({
|
|
|
886
974
|
container: {
|
|
887
975
|
flex: 1,
|
|
888
976
|
},
|
|
889
|
-
|
|
890
|
-
flex: 1,
|
|
891
|
-
},
|
|
892
|
-
messagesContainer: {
|
|
977
|
+
flex: {
|
|
893
978
|
flex: 1,
|
|
894
979
|
},
|
|
895
980
|
messageList: {
|
|
981
|
+
width: "100%",
|
|
982
|
+
maxWidth: Spacing.threadMaxWidth,
|
|
983
|
+
marginHorizontal: "auto",
|
|
896
984
|
paddingVertical: 20,
|
|
897
|
-
paddingHorizontal:
|
|
985
|
+
paddingHorizontal: 12,
|
|
986
|
+
gap: 18,
|
|
898
987
|
},
|
|
899
|
-
|
|
988
|
+
empty: {
|
|
900
989
|
flex: 1,
|
|
901
990
|
justifyContent: "center",
|
|
902
991
|
alignItems: "center",
|
|
903
|
-
|
|
992
|
+
paddingHorizontal: 24,
|
|
904
993
|
},
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
height: 72,
|
|
908
|
-
borderRadius: 36,
|
|
909
|
-
backgroundColor: "rgba(0, 122, 255, 0.1)",
|
|
910
|
-
justifyContent: "center",
|
|
911
|
-
alignItems: "center",
|
|
912
|
-
marginBottom: 20,
|
|
913
|
-
},
|
|
914
|
-
emptyIcon: {
|
|
915
|
-
fontSize: 32,
|
|
916
|
-
},
|
|
917
|
-
emptyTitle: {
|
|
918
|
-
fontSize: 22,
|
|
994
|
+
welcome: {
|
|
995
|
+
fontSize: 24,
|
|
919
996
|
fontWeight: "600",
|
|
920
|
-
marginBottom: 8,
|
|
921
997
|
letterSpacing: -0.4,
|
|
922
|
-
},
|
|
923
|
-
emptySubtitle: {
|
|
924
|
-
fontSize: 15,
|
|
925
998
|
textAlign: "center",
|
|
926
|
-
|
|
999
|
+
marginBottom: 24,
|
|
927
1000
|
},
|
|
928
|
-
|
|
1001
|
+
chips: {
|
|
929
1002
|
flexDirection: "row",
|
|
930
1003
|
flexWrap: "wrap",
|
|
931
1004
|
justifyContent: "center",
|
|
932
1005
|
gap: 8,
|
|
933
|
-
marginTop: 20,
|
|
934
|
-
paddingHorizontal: 16,
|
|
935
1006
|
},
|
|
936
|
-
|
|
937
|
-
paddingHorizontal:
|
|
938
|
-
paddingVertical:
|
|
939
|
-
borderRadius:
|
|
1007
|
+
chip: {
|
|
1008
|
+
paddingHorizontal: 14,
|
|
1009
|
+
paddingVertical: 8,
|
|
1010
|
+
borderRadius: Radius.pill,
|
|
1011
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
940
1012
|
},
|
|
941
|
-
|
|
1013
|
+
chipText: {
|
|
942
1014
|
fontSize: 14,
|
|
943
1015
|
letterSpacing: -0.2,
|
|
944
1016
|
},
|
|
@@ -951,12 +1023,15 @@ const styles = StyleSheet.create({
|
|
|
951
1023
|
```tsx
|
|
952
1024
|
"use generative";
|
|
953
1025
|
|
|
954
|
-
import {
|
|
1026
|
+
import type { ReactNode } from "react";
|
|
1027
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
955
1028
|
import {
|
|
956
1029
|
defineToolkit,
|
|
957
1030
|
type ToolCallMessagePartProps,
|
|
958
1031
|
} from "@assistant-ui/react-native";
|
|
959
1032
|
import { z } from "zod";
|
|
1033
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
1034
|
+
import { Radius } from "@/constants/theme";
|
|
960
1035
|
|
|
961
1036
|
// Open-Meteo API adapters (free, no API key needed)
|
|
962
1037
|
|
|
@@ -993,16 +1068,16 @@ const mapWeatherCode = (code: number): string => {
|
|
|
993
1068
|
};
|
|
994
1069
|
|
|
995
1070
|
const mapWeatherEmoji = (code: number): string => {
|
|
996
|
-
if (code === 0) return "
|
|
997
|
-
if (code <= 3) return "
|
|
998
|
-
if (code <= 48) return "
|
|
999
|
-
if (code <= 57) return "
|
|
1000
|
-
if (code <= 67) return "
|
|
1001
|
-
if (code <= 77) return "
|
|
1002
|
-
if (code <= 82) return "
|
|
1003
|
-
if (code <= 86) return "
|
|
1004
|
-
if (code === 95) return "
|
|
1005
|
-
return "
|
|
1071
|
+
if (code === 0) return "☀️";
|
|
1072
|
+
if (code <= 3) return "⛅";
|
|
1073
|
+
if (code <= 48) return "🌫️";
|
|
1074
|
+
if (code <= 57) return "🌦️";
|
|
1075
|
+
if (code <= 67) return "🌧️";
|
|
1076
|
+
if (code <= 77) return "❄️";
|
|
1077
|
+
if (code <= 82) return "🌦️";
|
|
1078
|
+
if (code <= 86) return "🌨️";
|
|
1079
|
+
if (code === 95) return "⛈️";
|
|
1080
|
+
return "🌩️";
|
|
1006
1081
|
};
|
|
1007
1082
|
|
|
1008
1083
|
const fetchWeatherFromOpenMeteo = async ({
|
|
@@ -1059,6 +1134,50 @@ const fetchWeatherFromOpenMeteo = async ({
|
|
|
1059
1134
|
|
|
1060
1135
|
// Tool UI Components
|
|
1061
1136
|
|
|
1137
|
+
function ToolCard({ children }: { children: ReactNode }) {
|
|
1138
|
+
const { colors } = useTheme();
|
|
1139
|
+
return (
|
|
1140
|
+
<View
|
|
1141
|
+
style={[
|
|
1142
|
+
styles.card,
|
|
1143
|
+
{ backgroundColor: colors.muted, borderColor: colors.border },
|
|
1144
|
+
]}
|
|
1145
|
+
>
|
|
1146
|
+
{children}
|
|
1147
|
+
</View>
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function ToolStatus({ label }: { label: string }) {
|
|
1152
|
+
const { colors } = useTheme();
|
|
1153
|
+
return (
|
|
1154
|
+
<ToolCard>
|
|
1155
|
+
<Text style={[styles.statusText, { color: colors.mutedForeground }]}>
|
|
1156
|
+
{label}
|
|
1157
|
+
</Text>
|
|
1158
|
+
</ToolCard>
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function ToolError({ label }: { label: string }) {
|
|
1163
|
+
const { colors } = useTheme();
|
|
1164
|
+
return (
|
|
1165
|
+
<View
|
|
1166
|
+
style={[
|
|
1167
|
+
styles.card,
|
|
1168
|
+
{
|
|
1169
|
+
backgroundColor: colors.destructiveSurface,
|
|
1170
|
+
borderColor: colors.destructive,
|
|
1171
|
+
},
|
|
1172
|
+
]}
|
|
1173
|
+
>
|
|
1174
|
+
<Text style={[styles.statusText, { color: colors.destructive }]}>
|
|
1175
|
+
{label}
|
|
1176
|
+
</Text>
|
|
1177
|
+
</View>
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1062
1181
|
function GeocodeToolUI(
|
|
1063
1182
|
props: ToolCallMessagePartProps<
|
|
1064
1183
|
{ query: string },
|
|
@@ -1069,70 +1188,38 @@ function GeocodeToolUI(
|
|
|
1069
1188
|
}
|
|
1070
1189
|
>,
|
|
1071
1190
|
) {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
{ backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
|
|
1081
|
-
]}
|
|
1082
|
-
>
|
|
1083
|
-
<Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
|
|
1084
|
-
Finding location...
|
|
1085
|
-
</Text>
|
|
1086
|
-
</View>
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
if (props.result?.error) {
|
|
1091
|
-
return (
|
|
1092
|
-
<View
|
|
1093
|
-
style={[
|
|
1094
|
-
styles.card,
|
|
1095
|
-
{ backgroundColor: isDark ? "#3a1c1c" : "#fff0f0" },
|
|
1096
|
-
]}
|
|
1097
|
-
>
|
|
1098
|
-
<Text style={[styles.label, { color: "#ff453a" }]}>
|
|
1099
|
-
Geocoding failed: {props.result.error}
|
|
1100
|
-
</Text>
|
|
1101
|
-
</View>
|
|
1102
|
-
);
|
|
1191
|
+
const { colors } = useTheme();
|
|
1192
|
+
|
|
1193
|
+
if (props.status?.type === "running") {
|
|
1194
|
+
return <ToolStatus label="Finding location…" />;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
if (props.result?.error) {
|
|
1198
|
+
return <ToolError label={`Geocoding failed: ${props.result.error}`} />;
|
|
1103
1199
|
}
|
|
1104
1200
|
|
|
1105
1201
|
const result = props.result?.result;
|
|
1106
1202
|
if (!result) return null;
|
|
1107
1203
|
|
|
1108
1204
|
return (
|
|
1109
|
-
<
|
|
1110
|
-
style={[styles.card, { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" }]}
|
|
1111
|
-
>
|
|
1205
|
+
<ToolCard>
|
|
1112
1206
|
<View style={styles.row}>
|
|
1113
|
-
<Text style={styles.pin}>{"
|
|
1207
|
+
<Text style={styles.pin}>{"📍"}</Text>
|
|
1114
1208
|
<View>
|
|
1115
|
-
<Text
|
|
1116
|
-
style={[
|
|
1117
|
-
styles.locationName,
|
|
1118
|
-
{ color: isDark ? "#ffffff" : "#000000" },
|
|
1119
|
-
]}
|
|
1120
|
-
>
|
|
1209
|
+
<Text style={[styles.locationName, { color: colors.foreground }]}>
|
|
1121
1210
|
{result.name}
|
|
1122
1211
|
</Text>
|
|
1123
|
-
<Text
|
|
1124
|
-
style={[styles.coords, { color: isDark ? "#8e8e93" : "#6e6e73" }]}
|
|
1125
|
-
>
|
|
1212
|
+
<Text style={[styles.coords, { color: colors.mutedForeground }]}>
|
|
1126
1213
|
{Math.abs(result.latitude).toFixed(2)}
|
|
1127
|
-
{"
|
|
1214
|
+
{"°"}
|
|
1128
1215
|
{result.latitude >= 0 ? "N" : "S"},{" "}
|
|
1129
1216
|
{Math.abs(result.longitude).toFixed(2)}
|
|
1130
|
-
{"
|
|
1217
|
+
{"°"}
|
|
1131
1218
|
{result.longitude >= 0 ? "E" : "W"}
|
|
1132
1219
|
</Text>
|
|
1133
1220
|
</View>
|
|
1134
1221
|
</View>
|
|
1135
|
-
</
|
|
1222
|
+
</ToolCard>
|
|
1136
1223
|
);
|
|
1137
1224
|
}
|
|
1138
1225
|
|
|
@@ -1155,36 +1242,17 @@ function WeatherToolUI(
|
|
|
1155
1242
|
}
|
|
1156
1243
|
>,
|
|
1157
1244
|
) {
|
|
1158
|
-
const
|
|
1159
|
-
const isDark = colorScheme === "dark";
|
|
1245
|
+
const { colors } = useTheme();
|
|
1160
1246
|
|
|
1161
1247
|
if (props.status?.type === "running") {
|
|
1162
|
-
return
|
|
1163
|
-
<View
|
|
1164
|
-
style={[
|
|
1165
|
-
styles.card,
|
|
1166
|
-
{ backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
|
|
1167
|
-
]}
|
|
1168
|
-
>
|
|
1169
|
-
<Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
|
|
1170
|
-
Fetching weather for {props.args.query}...
|
|
1171
|
-
</Text>
|
|
1172
|
-
</View>
|
|
1173
|
-
);
|
|
1248
|
+
return <ToolStatus label={`Fetching weather for ${props.args.query}…`} />;
|
|
1174
1249
|
}
|
|
1175
1250
|
|
|
1176
1251
|
if (!props.result?.success) {
|
|
1177
1252
|
return (
|
|
1178
|
-
<
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
{ backgroundColor: isDark ? "#3a1c1c" : "#fff0f0" },
|
|
1182
|
-
]}
|
|
1183
|
-
>
|
|
1184
|
-
<Text style={[styles.label, { color: "#ff453a" }]}>
|
|
1185
|
-
Weather unavailable: {props.result?.error ?? "Unknown error"}
|
|
1186
|
-
</Text>
|
|
1187
|
-
</View>
|
|
1253
|
+
<ToolError
|
|
1254
|
+
label={`Weather unavailable: ${props.result?.error ?? "Unknown error"}`}
|
|
1255
|
+
/>
|
|
1188
1256
|
);
|
|
1189
1257
|
}
|
|
1190
1258
|
|
|
@@ -1192,64 +1260,40 @@ function WeatherToolUI(
|
|
|
1192
1260
|
props.result;
|
|
1193
1261
|
|
|
1194
1262
|
return (
|
|
1195
|
-
<
|
|
1196
|
-
style={[
|
|
1197
|
-
styles.weatherCard,
|
|
1198
|
-
{ backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
|
|
1199
|
-
]}
|
|
1200
|
-
>
|
|
1263
|
+
<ToolCard>
|
|
1201
1264
|
<View style={styles.weatherHeader}>
|
|
1202
1265
|
<Text style={styles.weatherEmoji}>
|
|
1203
1266
|
{mapWeatherEmoji(weatherCode ?? 0)}
|
|
1204
1267
|
</Text>
|
|
1205
1268
|
<View>
|
|
1206
|
-
<Text
|
|
1207
|
-
style={[
|
|
1208
|
-
styles.locationName,
|
|
1209
|
-
{ color: isDark ? "#ffffff" : "#000000" },
|
|
1210
|
-
]}
|
|
1211
|
-
>
|
|
1269
|
+
<Text style={[styles.locationName, { color: colors.foreground }]}>
|
|
1212
1270
|
{location}
|
|
1213
1271
|
</Text>
|
|
1214
|
-
<Text
|
|
1215
|
-
style={[
|
|
1216
|
-
styles.condition,
|
|
1217
|
-
{ color: isDark ? "#8e8e93" : "#6e6e73" },
|
|
1218
|
-
]}
|
|
1219
|
-
>
|
|
1272
|
+
<Text style={[styles.condition, { color: colors.mutedForeground }]}>
|
|
1220
1273
|
{mapWeatherCode(weatherCode ?? 0)}
|
|
1221
1274
|
</Text>
|
|
1222
1275
|
</View>
|
|
1223
1276
|
</View>
|
|
1224
1277
|
|
|
1225
|
-
<Text style={styles.tempLarge}>
|
|
1278
|
+
<Text style={[styles.tempLarge, { color: colors.foreground }]}>
|
|
1226
1279
|
{temperature ?? "--"}
|
|
1227
|
-
{"
|
|
1280
|
+
{"°"}F
|
|
1228
1281
|
</Text>
|
|
1229
1282
|
|
|
1230
1283
|
{windSpeed != null && (
|
|
1231
|
-
<Text style={[styles.wind, { color:
|
|
1284
|
+
<Text style={[styles.wind, { color: colors.mutedForeground }]}>
|
|
1232
1285
|
Wind: {windSpeed} mph
|
|
1233
1286
|
</Text>
|
|
1234
1287
|
)}
|
|
1235
1288
|
|
|
1236
1289
|
{forecast && forecast.length > 0 && (
|
|
1237
|
-
<View
|
|
1238
|
-
style={[
|
|
1239
|
-
styles.forecastRow,
|
|
1240
|
-
{
|
|
1241
|
-
borderTopColor: isDark
|
|
1242
|
-
? "rgba(255,255,255,0.1)"
|
|
1243
|
-
: "rgba(0,0,0,0.1)",
|
|
1244
|
-
},
|
|
1245
|
-
]}
|
|
1246
|
-
>
|
|
1290
|
+
<View style={[styles.forecastRow, { borderTopColor: colors.border }]}>
|
|
1247
1291
|
{forecast.map((day, i) => (
|
|
1248
1292
|
<View key={i} style={styles.forecastDay}>
|
|
1249
1293
|
<Text
|
|
1250
1294
|
style={[
|
|
1251
1295
|
styles.forecastLabel,
|
|
1252
|
-
{ color:
|
|
1296
|
+
{ color: colors.mutedForeground },
|
|
1253
1297
|
]}
|
|
1254
1298
|
>
|
|
1255
1299
|
{day.label}
|
|
@@ -1257,29 +1301,24 @@ function WeatherToolUI(
|
|
|
1257
1301
|
<Text style={styles.forecastEmoji}>
|
|
1258
1302
|
{mapWeatherEmoji(day.code)}
|
|
1259
1303
|
</Text>
|
|
1260
|
-
<Text
|
|
1261
|
-
style={[
|
|
1262
|
-
styles.forecastTemp,
|
|
1263
|
-
{ color: isDark ? "#ffffff" : "#000000" },
|
|
1264
|
-
]}
|
|
1265
|
-
>
|
|
1304
|
+
<Text style={[styles.forecastTemp, { color: colors.foreground }]}>
|
|
1266
1305
|
{day.max}
|
|
1267
|
-
{"
|
|
1306
|
+
{"°"}
|
|
1268
1307
|
</Text>
|
|
1269
1308
|
<Text
|
|
1270
1309
|
style={[
|
|
1271
1310
|
styles.forecastTempLow,
|
|
1272
|
-
{ color:
|
|
1311
|
+
{ color: colors.mutedForeground },
|
|
1273
1312
|
]}
|
|
1274
1313
|
>
|
|
1275
1314
|
{day.min}
|
|
1276
|
-
{"
|
|
1315
|
+
{"°"}
|
|
1277
1316
|
</Text>
|
|
1278
1317
|
</View>
|
|
1279
1318
|
))}
|
|
1280
1319
|
</View>
|
|
1281
1320
|
)}
|
|
1282
|
-
</
|
|
1321
|
+
</ToolCard>
|
|
1283
1322
|
);
|
|
1284
1323
|
}
|
|
1285
1324
|
|
|
@@ -1319,16 +1358,15 @@ export default defineToolkit({
|
|
|
1319
1358
|
|
|
1320
1359
|
const styles = StyleSheet.create({
|
|
1321
1360
|
card: {
|
|
1322
|
-
padding:
|
|
1323
|
-
borderRadius:
|
|
1324
|
-
|
|
1325
|
-
},
|
|
1326
|
-
weatherCard: {
|
|
1327
|
-
padding: 16,
|
|
1328
|
-
borderRadius: 12,
|
|
1361
|
+
padding: 14,
|
|
1362
|
+
borderRadius: Radius.card,
|
|
1363
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
1329
1364
|
marginVertical: 4,
|
|
1330
1365
|
gap: 4,
|
|
1331
1366
|
},
|
|
1367
|
+
statusText: {
|
|
1368
|
+
fontSize: 13,
|
|
1369
|
+
},
|
|
1332
1370
|
row: {
|
|
1333
1371
|
flexDirection: "row",
|
|
1334
1372
|
alignItems: "center",
|
|
@@ -1345,9 +1383,6 @@ const styles = StyleSheet.create({
|
|
|
1345
1383
|
fontSize: 13,
|
|
1346
1384
|
marginTop: 2,
|
|
1347
1385
|
},
|
|
1348
|
-
label: {
|
|
1349
|
-
fontSize: 13,
|
|
1350
|
-
},
|
|
1351
1386
|
weatherHeader: {
|
|
1352
1387
|
flexDirection: "row",
|
|
1353
1388
|
alignItems: "center",
|
|
@@ -1364,7 +1399,7 @@ const styles = StyleSheet.create({
|
|
|
1364
1399
|
tempLarge: {
|
|
1365
1400
|
fontSize: 40,
|
|
1366
1401
|
fontWeight: "700",
|
|
1367
|
-
|
|
1402
|
+
letterSpacing: -1,
|
|
1368
1403
|
},
|
|
1369
1404
|
wind: {
|
|
1370
1405
|
fontSize: 13,
|
|
@@ -1400,148 +1435,68 @@ const styles = StyleSheet.create({
|
|
|
1400
1435
|
|
|
1401
1436
|
```
|
|
1402
1437
|
|
|
1403
|
-
## components/themed-text.tsx
|
|
1404
|
-
|
|
1405
|
-
```tsx
|
|
1406
|
-
import { StyleSheet, Text, type TextProps } from "react-native";
|
|
1407
|
-
|
|
1408
|
-
import { useThemeColor } from "@/hooks/use-theme-color";
|
|
1409
|
-
|
|
1410
|
-
export type ThemedTextProps = TextProps & {
|
|
1411
|
-
lightColor?: string;
|
|
1412
|
-
darkColor?: string;
|
|
1413
|
-
type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link";
|
|
1414
|
-
};
|
|
1415
|
-
|
|
1416
|
-
export function ThemedText({
|
|
1417
|
-
style,
|
|
1418
|
-
lightColor,
|
|
1419
|
-
darkColor,
|
|
1420
|
-
type = "default",
|
|
1421
|
-
...rest
|
|
1422
|
-
}: ThemedTextProps) {
|
|
1423
|
-
const color = useThemeColor({ light: lightColor, dark: darkColor }, "text");
|
|
1424
|
-
|
|
1425
|
-
return (
|
|
1426
|
-
<Text
|
|
1427
|
-
style={[
|
|
1428
|
-
{ color },
|
|
1429
|
-
type === "default" ? styles.default : undefined,
|
|
1430
|
-
type === "title" ? styles.title : undefined,
|
|
1431
|
-
type === "defaultSemiBold" ? styles.defaultSemiBold : undefined,
|
|
1432
|
-
type === "subtitle" ? styles.subtitle : undefined,
|
|
1433
|
-
type === "link" ? styles.link : undefined,
|
|
1434
|
-
style,
|
|
1435
|
-
]}
|
|
1436
|
-
{...rest}
|
|
1437
|
-
/>
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
const styles = StyleSheet.create({
|
|
1442
|
-
default: {
|
|
1443
|
-
fontSize: 16,
|
|
1444
|
-
lineHeight: 24,
|
|
1445
|
-
},
|
|
1446
|
-
defaultSemiBold: {
|
|
1447
|
-
fontSize: 16,
|
|
1448
|
-
lineHeight: 24,
|
|
1449
|
-
fontWeight: "600",
|
|
1450
|
-
},
|
|
1451
|
-
title: {
|
|
1452
|
-
fontSize: 32,
|
|
1453
|
-
fontWeight: "bold",
|
|
1454
|
-
lineHeight: 32,
|
|
1455
|
-
},
|
|
1456
|
-
subtitle: {
|
|
1457
|
-
fontSize: 20,
|
|
1458
|
-
fontWeight: "bold",
|
|
1459
|
-
},
|
|
1460
|
-
link: {
|
|
1461
|
-
lineHeight: 30,
|
|
1462
|
-
fontSize: 16,
|
|
1463
|
-
color: "#0a7ea4",
|
|
1464
|
-
},
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
```
|
|
1468
|
-
|
|
1469
|
-
## components/themed-view.tsx
|
|
1470
|
-
|
|
1471
|
-
```tsx
|
|
1472
|
-
import { View, type ViewProps } from "react-native";
|
|
1473
|
-
|
|
1474
|
-
import { useThemeColor } from "@/hooks/use-theme-color";
|
|
1475
|
-
|
|
1476
|
-
export type ThemedViewProps = ViewProps & {
|
|
1477
|
-
lightColor?: string;
|
|
1478
|
-
darkColor?: string;
|
|
1479
|
-
};
|
|
1480
|
-
|
|
1481
|
-
export function ThemedView({
|
|
1482
|
-
style,
|
|
1483
|
-
lightColor,
|
|
1484
|
-
darkColor,
|
|
1485
|
-
...otherProps
|
|
1486
|
-
}: ThemedViewProps) {
|
|
1487
|
-
const backgroundColor = useThemeColor(
|
|
1488
|
-
{ light: lightColor, dark: darkColor },
|
|
1489
|
-
"background",
|
|
1490
|
-
);
|
|
1491
|
-
|
|
1492
|
-
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
```
|
|
1496
|
-
|
|
1497
1438
|
## components/thread-list/ThreadListDrawer.tsx
|
|
1498
1439
|
|
|
1499
1440
|
```tsx
|
|
1500
|
-
import {
|
|
1441
|
+
import { View, Text, Pressable, StyleSheet } from "react-native";
|
|
1501
1442
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
1502
|
-
import {
|
|
1503
|
-
|
|
1443
|
+
import {
|
|
1444
|
+
ThreadListPrimitive,
|
|
1445
|
+
ThreadListItemByIndexProvider,
|
|
1446
|
+
useAui,
|
|
1447
|
+
} from "@assistant-ui/react-native";
|
|
1504
1448
|
import type { DrawerContentComponentProps } from "expo-router/build/react-navigation/drawer/types";
|
|
1449
|
+
import { ThreadListItem } from "./ThreadListItem";
|
|
1450
|
+
import { Icon } from "@/components/ui/icon";
|
|
1451
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
1452
|
+
import { Radius } from "@/constants/theme";
|
|
1453
|
+
import { haptics } from "@/lib/haptics";
|
|
1505
1454
|
|
|
1506
1455
|
export function ThreadListDrawer({ navigation }: DrawerContentComponentProps) {
|
|
1456
|
+
const { colors } = useTheme();
|
|
1507
1457
|
const aui = useAui();
|
|
1508
|
-
const threadIds = useAuiState((s) => s.threads.threadIds);
|
|
1509
|
-
const mainThreadId = useAuiState((s) => s.threads.mainThreadId);
|
|
1510
|
-
const threadItems = useAuiState((s) => s.threads.threadItems);
|
|
1511
1458
|
const insets = useSafeAreaInsets();
|
|
1512
|
-
const isDark = useColorScheme() === "dark";
|
|
1513
1459
|
|
|
1514
1460
|
return (
|
|
1515
1461
|
<View
|
|
1516
1462
|
style={[
|
|
1517
1463
|
styles.container,
|
|
1518
|
-
{
|
|
1519
|
-
backgroundColor: isDark
|
|
1520
|
-
? "rgba(28, 28, 30, 0.85)"
|
|
1521
|
-
: "rgba(242, 242, 247, 0.85)",
|
|
1522
|
-
paddingTop: insets.top,
|
|
1523
|
-
},
|
|
1464
|
+
{ backgroundColor: colors.background, paddingTop: insets.top + 12 },
|
|
1524
1465
|
]}
|
|
1525
1466
|
>
|
|
1526
|
-
<
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1467
|
+
<ThreadListPrimitive.Root style={styles.root}>
|
|
1468
|
+
<Pressable
|
|
1469
|
+
onPressIn={haptics.selection}
|
|
1470
|
+
onPress={() => {
|
|
1471
|
+
aui.threads().switchToNewThread();
|
|
1472
|
+
navigation.closeDrawer();
|
|
1473
|
+
}}
|
|
1474
|
+
style={({ pressed }) => [
|
|
1475
|
+
styles.newButton,
|
|
1476
|
+
{ backgroundColor: pressed ? colors.muted : "transparent" },
|
|
1477
|
+
]}
|
|
1478
|
+
>
|
|
1479
|
+
<Icon name="compose" size={18} color={colors.foreground} />
|
|
1480
|
+
<Text style={[styles.newLabel, { color: colors.foreground }]}>
|
|
1481
|
+
New chat
|
|
1482
|
+
</Text>
|
|
1483
|
+
</Pressable>
|
|
1484
|
+
|
|
1485
|
+
<Text style={[styles.sectionLabel, { color: colors.mutedForeground }]}>
|
|
1486
|
+
Recent
|
|
1487
|
+
</Text>
|
|
1488
|
+
|
|
1489
|
+
<ThreadListPrimitive.Items
|
|
1490
|
+
renderItem={({ index }) => (
|
|
1491
|
+
<ThreadListItemByIndexProvider index={index} archived={false}>
|
|
1492
|
+
<ThreadListItem onSelect={() => navigation.closeDrawer()} />
|
|
1493
|
+
</ThreadListItemByIndexProvider>
|
|
1494
|
+
)}
|
|
1495
|
+
style={styles.list}
|
|
1496
|
+
contentContainerStyle={styles.listContent}
|
|
1497
|
+
showsVerticalScrollIndicator={false}
|
|
1498
|
+
/>
|
|
1499
|
+
</ThreadListPrimitive.Root>
|
|
1545
1500
|
</View>
|
|
1546
1501
|
);
|
|
1547
1502
|
}
|
|
@@ -1550,8 +1505,36 @@ const styles = StyleSheet.create({
|
|
|
1550
1505
|
container: {
|
|
1551
1506
|
flex: 1,
|
|
1552
1507
|
},
|
|
1508
|
+
root: {
|
|
1509
|
+
flex: 1,
|
|
1510
|
+
},
|
|
1511
|
+
newButton: {
|
|
1512
|
+
flexDirection: "row",
|
|
1513
|
+
alignItems: "center",
|
|
1514
|
+
gap: 10,
|
|
1515
|
+
height: 40,
|
|
1516
|
+
paddingHorizontal: 12,
|
|
1517
|
+
marginHorizontal: 8,
|
|
1518
|
+
marginBottom: 4,
|
|
1519
|
+
borderRadius: Radius.md,
|
|
1520
|
+
},
|
|
1521
|
+
newLabel: {
|
|
1522
|
+
fontSize: 15,
|
|
1523
|
+
fontWeight: "500",
|
|
1524
|
+
letterSpacing: -0.2,
|
|
1525
|
+
},
|
|
1526
|
+
sectionLabel: {
|
|
1527
|
+
fontSize: 12,
|
|
1528
|
+
fontWeight: "500",
|
|
1529
|
+
paddingHorizontal: 20,
|
|
1530
|
+
paddingTop: 12,
|
|
1531
|
+
paddingBottom: 6,
|
|
1532
|
+
},
|
|
1553
1533
|
list: {
|
|
1554
|
-
|
|
1534
|
+
flex: 1,
|
|
1535
|
+
},
|
|
1536
|
+
listContent: {
|
|
1537
|
+
paddingBottom: 8,
|
|
1555
1538
|
},
|
|
1556
1539
|
});
|
|
1557
1540
|
|
|
@@ -1560,211 +1543,168 @@ const styles = StyleSheet.create({
|
|
|
1560
1543
|
## components/thread-list/ThreadListItem.tsx
|
|
1561
1544
|
|
|
1562
1545
|
```tsx
|
|
1546
|
+
import { Text, Pressable, StyleSheet } from "react-native";
|
|
1563
1547
|
import {
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
} from "
|
|
1570
|
-
|
|
1571
|
-
type ThreadListItemProps = {
|
|
1572
|
-
title: string;
|
|
1573
|
-
isActive: boolean;
|
|
1574
|
-
onPress: () => void;
|
|
1575
|
-
};
|
|
1548
|
+
ThreadListItemPrimitive,
|
|
1549
|
+
useAui,
|
|
1550
|
+
useAuiState,
|
|
1551
|
+
} from "@assistant-ui/react-native";
|
|
1552
|
+
import { useTheme } from "@/hooks/use-theme";
|
|
1553
|
+
import { Radius } from "@/constants/theme";
|
|
1554
|
+
import { haptics } from "@/lib/haptics";
|
|
1576
1555
|
|
|
1577
|
-
export function ThreadListItem({
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1556
|
+
export function ThreadListItem({ onSelect }: { onSelect: () => void }) {
|
|
1557
|
+
const { colors } = useTheme();
|
|
1558
|
+
const aui = useAui();
|
|
1559
|
+
const isActive = useAuiState(
|
|
1560
|
+
(s) => s.threads.mainThreadId === s.threadListItem.id,
|
|
1561
|
+
);
|
|
1583
1562
|
|
|
1584
|
-
|
|
1585
|
-
<
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
isActive &&
|
|
1563
|
+
return (
|
|
1564
|
+
<ThreadListItemPrimitive.Root>
|
|
1565
|
+
<Pressable
|
|
1566
|
+
onPressIn={haptics.selection}
|
|
1567
|
+
onPress={() => {
|
|
1568
|
+
aui.threadListItem().switchTo();
|
|
1569
|
+
onSelect();
|
|
1570
|
+
}}
|
|
1571
|
+
style={({ pressed }) => [
|
|
1572
|
+
styles.item,
|
|
1573
|
+
(isActive || pressed) && { backgroundColor: colors.muted },
|
|
1595
1574
|
]}
|
|
1596
1575
|
>
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
</View>
|
|
1600
|
-
);
|
|
1601
|
-
|
|
1602
|
-
if (isActive) {
|
|
1603
|
-
return (
|
|
1604
|
-
<Pressable onPress={onPress} style={styles.itemOuter}>
|
|
1605
|
-
<View
|
|
1576
|
+
<Text
|
|
1577
|
+
numberOfLines={1}
|
|
1606
1578
|
style={[
|
|
1607
|
-
styles.
|
|
1579
|
+
styles.title,
|
|
1608
1580
|
{
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
: "rgba(209, 209, 214, 0.6)",
|
|
1581
|
+
color: colors.foreground,
|
|
1582
|
+
fontWeight: isActive ? "600" : "400",
|
|
1612
1583
|
},
|
|
1613
1584
|
]}
|
|
1614
1585
|
>
|
|
1615
|
-
|
|
1616
|
-
</
|
|
1586
|
+
<ThreadListItemPrimitive.Title fallback="New chat" />
|
|
1587
|
+
</Text>
|
|
1617
1588
|
</Pressable>
|
|
1618
|
-
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
return (
|
|
1622
|
-
<Pressable
|
|
1623
|
-
onPress={onPress}
|
|
1624
|
-
style={({ pressed }) => [
|
|
1625
|
-
styles.itemOuter,
|
|
1626
|
-
styles.itemPadding,
|
|
1627
|
-
pressed && {
|
|
1628
|
-
backgroundColor: isDark ? "#3a3a3c" : "#d1d1d6",
|
|
1629
|
-
borderRadius: 10,
|
|
1630
|
-
},
|
|
1631
|
-
]}
|
|
1632
|
-
>
|
|
1633
|
-
{content}
|
|
1634
|
-
</Pressable>
|
|
1589
|
+
</ThreadListItemPrimitive.Root>
|
|
1635
1590
|
);
|
|
1636
1591
|
}
|
|
1637
1592
|
|
|
1638
1593
|
const styles = StyleSheet.create({
|
|
1639
|
-
|
|
1594
|
+
item: {
|
|
1595
|
+
height: 38,
|
|
1596
|
+
justifyContent: "center",
|
|
1597
|
+
paddingHorizontal: 12,
|
|
1640
1598
|
marginHorizontal: 8,
|
|
1641
|
-
|
|
1642
|
-
},
|
|
1643
|
-
itemPadding: {
|
|
1644
|
-
paddingVertical: 12,
|
|
1645
|
-
paddingHorizontal: 16,
|
|
1646
|
-
},
|
|
1647
|
-
glassItem: {
|
|
1648
|
-
paddingVertical: 12,
|
|
1649
|
-
paddingHorizontal: 16,
|
|
1650
|
-
borderRadius: 10,
|
|
1651
|
-
overflow: "hidden",
|
|
1652
|
-
},
|
|
1653
|
-
row: {
|
|
1654
|
-
flexDirection: "row",
|
|
1655
|
-
alignItems: "center",
|
|
1656
|
-
},
|
|
1657
|
-
indicator: {
|
|
1658
|
-
width: 4,
|
|
1659
|
-
height: 20,
|
|
1660
|
-
borderRadius: 2,
|
|
1661
|
-
marginRight: 10,
|
|
1599
|
+
borderRadius: Radius.md,
|
|
1662
1600
|
},
|
|
1663
1601
|
title: {
|
|
1664
1602
|
fontSize: 15,
|
|
1665
|
-
|
|
1666
|
-
},
|
|
1667
|
-
titleActive: {
|
|
1668
|
-
fontWeight: "600",
|
|
1603
|
+
letterSpacing: -0.2,
|
|
1669
1604
|
},
|
|
1670
1605
|
});
|
|
1671
1606
|
|
|
1672
1607
|
```
|
|
1673
1608
|
|
|
1674
|
-
## components/ui/icon-
|
|
1609
|
+
## components/ui/icon-mappings.ts
|
|
1610
|
+
|
|
1611
|
+
```typescript
|
|
1612
|
+
import type { ComponentProps } from "react";
|
|
1613
|
+
import type MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
1614
|
+
import type { SymbolViewProps, SymbolWeight } from "expo-symbols";
|
|
1615
|
+
|
|
1616
|
+
export type IconName =
|
|
1617
|
+
| "compose"
|
|
1618
|
+
| "send"
|
|
1619
|
+
| "stop"
|
|
1620
|
+
| "add"
|
|
1621
|
+
| "remove"
|
|
1622
|
+
| "copy"
|
|
1623
|
+
| "check"
|
|
1624
|
+
| "reload"
|
|
1625
|
+
| "chevron-left"
|
|
1626
|
+
| "chevron-right"
|
|
1627
|
+
| "menu"
|
|
1628
|
+
| "location";
|
|
1629
|
+
|
|
1630
|
+
export type IconProps = {
|
|
1631
|
+
name: IconName;
|
|
1632
|
+
size?: number;
|
|
1633
|
+
color: string;
|
|
1634
|
+
weight?: SymbolWeight;
|
|
1635
|
+
};
|
|
1636
|
+
|
|
1637
|
+
/** SF Symbols rendered natively on iOS via `expo-symbols`. */
|
|
1638
|
+
export const SF_SYMBOLS: Record<IconName, SymbolViewProps["name"]> = {
|
|
1639
|
+
compose: "square.and.pencil",
|
|
1640
|
+
send: "arrow.up",
|
|
1641
|
+
stop: "stop.fill",
|
|
1642
|
+
add: "plus",
|
|
1643
|
+
remove: "xmark",
|
|
1644
|
+
copy: "doc.on.doc",
|
|
1645
|
+
check: "checkmark",
|
|
1646
|
+
reload: "arrow.clockwise",
|
|
1647
|
+
"chevron-left": "chevron.left",
|
|
1648
|
+
"chevron-right": "chevron.right",
|
|
1649
|
+
menu: "line.3.horizontal",
|
|
1650
|
+
location: "location.fill",
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
/** Material Icons fallback for Android and web. */
|
|
1654
|
+
export const MATERIAL_ICONS: Record<
|
|
1655
|
+
IconName,
|
|
1656
|
+
ComponentProps<typeof MaterialIcons>["name"]
|
|
1657
|
+
> = {
|
|
1658
|
+
compose: "edit",
|
|
1659
|
+
send: "arrow-upward",
|
|
1660
|
+
stop: "stop",
|
|
1661
|
+
add: "add",
|
|
1662
|
+
remove: "close",
|
|
1663
|
+
copy: "content-copy",
|
|
1664
|
+
check: "check",
|
|
1665
|
+
reload: "refresh",
|
|
1666
|
+
"chevron-left": "chevron-left",
|
|
1667
|
+
"chevron-right": "chevron-right",
|
|
1668
|
+
menu: "menu",
|
|
1669
|
+
location: "place",
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
```
|
|
1673
|
+
|
|
1674
|
+
## components/ui/icon.ios.tsx
|
|
1675
1675
|
|
|
1676
1676
|
```tsx
|
|
1677
|
-
import {
|
|
1678
|
-
|
|
1679
|
-
type SymbolViewProps,
|
|
1680
|
-
type SymbolWeight,
|
|
1681
|
-
} from "expo-symbols";
|
|
1682
|
-
import type { StyleProp, ViewStyle } from "react-native";
|
|
1677
|
+
import { SymbolView } from "expo-symbols";
|
|
1678
|
+
import { SF_SYMBOLS, type IconProps } from "./icon-mappings";
|
|
1683
1679
|
|
|
1684
|
-
export function
|
|
1680
|
+
export function Icon({
|
|
1685
1681
|
name,
|
|
1686
1682
|
size = 24,
|
|
1687
1683
|
color,
|
|
1688
|
-
style,
|
|
1689
1684
|
weight = "regular",
|
|
1690
|
-
}: {
|
|
1691
|
-
name: SymbolViewProps["name"];
|
|
1692
|
-
size?: number;
|
|
1693
|
-
color: string;
|
|
1694
|
-
style?: StyleProp<ViewStyle>;
|
|
1695
|
-
weight?: SymbolWeight;
|
|
1696
|
-
}) {
|
|
1685
|
+
}: IconProps) {
|
|
1697
1686
|
return (
|
|
1698
1687
|
<SymbolView
|
|
1699
|
-
|
|
1688
|
+
name={SF_SYMBOLS[name]}
|
|
1700
1689
|
tintColor={color}
|
|
1690
|
+
weight={weight}
|
|
1701
1691
|
resizeMode="scaleAspectFit"
|
|
1702
|
-
|
|
1703
|
-
style={[
|
|
1704
|
-
{
|
|
1705
|
-
width: size,
|
|
1706
|
-
height: size,
|
|
1707
|
-
},
|
|
1708
|
-
style,
|
|
1709
|
-
]}
|
|
1692
|
+
style={{ width: size, height: size }}
|
|
1710
1693
|
/>
|
|
1711
1694
|
);
|
|
1712
1695
|
}
|
|
1713
1696
|
|
|
1714
1697
|
```
|
|
1715
1698
|
|
|
1716
|
-
## components/ui/icon
|
|
1699
|
+
## components/ui/icon.tsx
|
|
1717
1700
|
|
|
1718
1701
|
```tsx
|
|
1719
|
-
// Fallback for using MaterialIcons on Android and web.
|
|
1720
|
-
|
|
1721
1702
|
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
1722
|
-
import
|
|
1723
|
-
import type { ComponentProps } from "react";
|
|
1724
|
-
import type { OpaqueColorValue, StyleProp, TextStyle } from "react-native";
|
|
1725
|
-
|
|
1726
|
-
type IconMapping = Record<
|
|
1727
|
-
SFSymbol,
|
|
1728
|
-
ComponentProps<typeof MaterialIcons>["name"]
|
|
1729
|
-
>;
|
|
1730
|
-
type IconSymbolName = keyof typeof MAPPING;
|
|
1731
|
-
|
|
1732
|
-
/**
|
|
1733
|
-
* Add your SF Symbols to Material Icons mappings here.
|
|
1734
|
-
* - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
|
|
1735
|
-
* - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
|
|
1736
|
-
*/
|
|
1737
|
-
const MAPPING = {
|
|
1738
|
-
"house.fill": "home",
|
|
1739
|
-
"paperplane.fill": "send",
|
|
1740
|
-
"chevron.left.forwardslash.chevron.right": "code",
|
|
1741
|
-
"chevron.right": "chevron-right",
|
|
1742
|
-
} as IconMapping;
|
|
1703
|
+
import { MATERIAL_ICONS, type IconProps } from "./icon-mappings";
|
|
1743
1704
|
|
|
1744
|
-
|
|
1745
|
-
* An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
|
|
1746
|
-
* This ensures a consistent look across platforms, and optimal resource usage.
|
|
1747
|
-
* Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
|
|
1748
|
-
*/
|
|
1749
|
-
export function IconSymbol({
|
|
1750
|
-
name,
|
|
1751
|
-
size = 24,
|
|
1752
|
-
color,
|
|
1753
|
-
style,
|
|
1754
|
-
}: {
|
|
1755
|
-
name: IconSymbolName;
|
|
1756
|
-
size?: number;
|
|
1757
|
-
color: string | OpaqueColorValue;
|
|
1758
|
-
style?: StyleProp<TextStyle>;
|
|
1759
|
-
weight?: SymbolWeight;
|
|
1760
|
-
}) {
|
|
1705
|
+
export function Icon({ name, size = 24, color }: IconProps) {
|
|
1761
1706
|
return (
|
|
1762
|
-
<MaterialIcons
|
|
1763
|
-
color={color}
|
|
1764
|
-
size={size}
|
|
1765
|
-
name={MAPPING[name]}
|
|
1766
|
-
style={style}
|
|
1767
|
-
/>
|
|
1707
|
+
<MaterialIcons name={MATERIAL_ICONS[name]} size={size} color={color} />
|
|
1768
1708
|
);
|
|
1769
1709
|
}
|
|
1770
1710
|
|
|
@@ -1774,59 +1714,83 @@ export function IconSymbol({
|
|
|
1774
1714
|
|
|
1775
1715
|
```typescript
|
|
1776
1716
|
/**
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1717
|
+
* Design tokens for the assistant-ui Expo example.
|
|
1718
|
+
*
|
|
1719
|
+
* These mirror the canonical web kit (`packages/ui`): a neutral zinc palette,
|
|
1720
|
+
* a clean ChatGPT-grade look with subtle hairline borders and no heavy glass or
|
|
1721
|
+
* shadow. Values are the sRGB equivalents of the web `oklch` tokens.
|
|
1779
1722
|
*/
|
|
1780
1723
|
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1724
|
+
export type Palette = {
|
|
1725
|
+
background: string;
|
|
1726
|
+
foreground: string;
|
|
1727
|
+
card: string;
|
|
1728
|
+
cardForeground: string;
|
|
1729
|
+
muted: string;
|
|
1730
|
+
mutedForeground: string;
|
|
1731
|
+
accent: string;
|
|
1732
|
+
border: string;
|
|
1733
|
+
primary: string;
|
|
1734
|
+
primaryForeground: string;
|
|
1735
|
+
composer: string;
|
|
1736
|
+
destructive: string;
|
|
1737
|
+
destructiveForeground: string;
|
|
1738
|
+
destructiveSurface: string;
|
|
1739
|
+
ring: string;
|
|
1740
|
+
};
|
|
1785
1741
|
|
|
1786
|
-
export const Colors = {
|
|
1742
|
+
export const Colors: { light: Palette; dark: Palette } = {
|
|
1787
1743
|
light: {
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1744
|
+
background: "#ffffff",
|
|
1745
|
+
foreground: "#18181b",
|
|
1746
|
+
card: "#ffffff",
|
|
1747
|
+
cardForeground: "#18181b",
|
|
1748
|
+
muted: "#f4f4f5",
|
|
1749
|
+
mutedForeground: "#71717a",
|
|
1750
|
+
accent: "#f4f4f5",
|
|
1751
|
+
border: "#e4e4e7",
|
|
1752
|
+
primary: "#18181b",
|
|
1753
|
+
primaryForeground: "#fafafa",
|
|
1754
|
+
composer: "#fafafa",
|
|
1755
|
+
destructive: "#dc2626",
|
|
1756
|
+
destructiveForeground: "#ffffff",
|
|
1757
|
+
destructiveSurface: "rgba(220, 38, 38, 0.08)",
|
|
1758
|
+
ring: "#a1a1aa",
|
|
1794
1759
|
},
|
|
1795
1760
|
dark: {
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1761
|
+
background: "#18181b",
|
|
1762
|
+
foreground: "#fafafa",
|
|
1763
|
+
card: "#27272a",
|
|
1764
|
+
cardForeground: "#fafafa",
|
|
1765
|
+
muted: "#3f3f46",
|
|
1766
|
+
mutedForeground: "#a1a1aa",
|
|
1767
|
+
accent: "#3f3f46",
|
|
1768
|
+
border: "rgba(255, 255, 255, 0.1)",
|
|
1769
|
+
primary: "#e4e4e7",
|
|
1770
|
+
primaryForeground: "#18181b",
|
|
1771
|
+
composer: "#232326",
|
|
1772
|
+
destructive: "#f87171",
|
|
1773
|
+
destructiveForeground: "#18181b",
|
|
1774
|
+
destructiveSurface: "rgba(248, 113, 113, 0.12)",
|
|
1775
|
+
ring: "#52525b",
|
|
1802
1776
|
},
|
|
1803
1777
|
};
|
|
1804
1778
|
|
|
1805
|
-
export const
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
mono: "monospace",
|
|
1821
|
-
},
|
|
1822
|
-
web: {
|
|
1823
|
-
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
1824
|
-
serif: "Georgia, 'Times New Roman', serif",
|
|
1825
|
-
rounded:
|
|
1826
|
-
"'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
|
|
1827
|
-
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
1828
|
-
},
|
|
1829
|
-
});
|
|
1779
|
+
export const Radius = {
|
|
1780
|
+
sm: 6,
|
|
1781
|
+
md: 8,
|
|
1782
|
+
lg: 10,
|
|
1783
|
+
card: 16,
|
|
1784
|
+
bubble: 18,
|
|
1785
|
+
composer: 24,
|
|
1786
|
+
attachment: 14,
|
|
1787
|
+
pill: 999,
|
|
1788
|
+
} as const;
|
|
1789
|
+
|
|
1790
|
+
export const Spacing = {
|
|
1791
|
+
threadMaxWidth: 768,
|
|
1792
|
+
gutter: 16,
|
|
1793
|
+
} as const;
|
|
1830
1794
|
|
|
1831
1795
|
```
|
|
1832
1796
|
|
|
@@ -1889,34 +1853,42 @@ export function useColorScheme() {
|
|
|
1889
1853
|
|
|
1890
1854
|
```
|
|
1891
1855
|
|
|
1892
|
-
## hooks/use-theme
|
|
1856
|
+
## hooks/use-theme.ts
|
|
1893
1857
|
|
|
1894
1858
|
```typescript
|
|
1895
|
-
|
|
1896
|
-
* Learn more about light and dark modes:
|
|
1897
|
-
* https://docs.expo.dev/guides/color-schemes/
|
|
1898
|
-
*/
|
|
1899
|
-
|
|
1900
|
-
import { Colors } from "@/constants/theme";
|
|
1859
|
+
import { Colors, type Palette } from "@/constants/theme";
|
|
1901
1860
|
import { useColorScheme } from "@/hooks/use-color-scheme";
|
|
1902
1861
|
|
|
1903
|
-
export function
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
) {
|
|
1907
|
-
const scheme = useColorScheme();
|
|
1908
|
-
const theme: "light" | "dark" = scheme === "dark" ? "dark" : "light";
|
|
1909
|
-
const colorFromProps = props[theme];
|
|
1910
|
-
|
|
1911
|
-
if (colorFromProps) {
|
|
1912
|
-
return colorFromProps;
|
|
1913
|
-
} else {
|
|
1914
|
-
return Colors[theme][colorName];
|
|
1915
|
-
}
|
|
1862
|
+
export function useTheme(): { isDark: boolean; colors: Palette } {
|
|
1863
|
+
const isDark = useColorScheme() === "dark";
|
|
1864
|
+
return { isDark, colors: isDark ? Colors.dark : Colors.light };
|
|
1916
1865
|
}
|
|
1917
1866
|
|
|
1918
1867
|
```
|
|
1919
1868
|
|
|
1869
|
+
## lib/haptics.ts
|
|
1870
|
+
|
|
1871
|
+
```typescript
|
|
1872
|
+
import { Platform } from "react-native";
|
|
1873
|
+
import * as Haptics from "expo-haptics";
|
|
1874
|
+
|
|
1875
|
+
const enabled = Platform.OS === "ios" || Platform.OS === "android";
|
|
1876
|
+
|
|
1877
|
+
export const haptics = {
|
|
1878
|
+
selection() {
|
|
1879
|
+
if (enabled) void Haptics.selectionAsync();
|
|
1880
|
+
},
|
|
1881
|
+
light() {
|
|
1882
|
+
if (enabled) void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
1883
|
+
},
|
|
1884
|
+
success() {
|
|
1885
|
+
if (enabled)
|
|
1886
|
+
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
1887
|
+
},
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1920
1892
|
## metro.config.js
|
|
1921
1893
|
|
|
1922
1894
|
```javascript
|
|
@@ -1981,20 +1953,23 @@ module.exports = withAui(config);
|
|
|
1981
1953
|
"export:web": "expo export --platform web && node scripts/flatten-assets.mjs"
|
|
1982
1954
|
},
|
|
1983
1955
|
"dependencies": {
|
|
1984
|
-
"@ai-sdk/openai": "^3.0.
|
|
1985
|
-
"@ai-sdk/react": "^3.0.
|
|
1956
|
+
"@ai-sdk/openai": "^3.0.73",
|
|
1957
|
+
"@ai-sdk/react": "^3.0.210",
|
|
1986
1958
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
1987
1959
|
"@assistant-ui/react-native": "workspace:*",
|
|
1988
1960
|
"@expo/vector-icons": "^15.1.1",
|
|
1989
1961
|
"@react-navigation/drawer": "7.9.4",
|
|
1990
1962
|
"@react-navigation/native": "7.1.33",
|
|
1991
|
-
"ai": "^6.0.
|
|
1992
|
-
"expo": "~56.0.
|
|
1963
|
+
"ai": "^6.0.208",
|
|
1964
|
+
"expo": "~56.0.12",
|
|
1965
|
+
"expo-clipboard": "~56.0.4",
|
|
1993
1966
|
"expo-constants": "~56.0.18",
|
|
1994
|
-
"expo-font": "~56.0.
|
|
1995
|
-
"expo-
|
|
1967
|
+
"expo-font": "~56.0.7",
|
|
1968
|
+
"expo-haptics": "~56.0.3",
|
|
1969
|
+
"expo-image": "~56.0.11",
|
|
1970
|
+
"expo-image-picker": "~56.0.18",
|
|
1996
1971
|
"expo-linking": "~56.0.14",
|
|
1997
|
-
"expo-router": "~56.2.
|
|
1972
|
+
"expo-router": "~56.2.11",
|
|
1998
1973
|
"expo-server": "~56.0.5",
|
|
1999
1974
|
"expo-splash-screen": "~56.0.10",
|
|
2000
1975
|
"expo-status-bar": "~56.0.4",
|
|
@@ -2003,7 +1978,7 @@ module.exports = withAui(config);
|
|
|
2003
1978
|
"react": "19.2.3",
|
|
2004
1979
|
"react-dom": "19.2.3",
|
|
2005
1980
|
"react-native": "0.85.3",
|
|
2006
|
-
"react-native-gesture-handler": "~3.0.
|
|
1981
|
+
"react-native-gesture-handler": "~3.0.2",
|
|
2007
1982
|
"react-native-reanimated": "4.3.1",
|
|
2008
1983
|
"react-native-safe-area-context": "~5.8.0",
|
|
2009
1984
|
"react-native-screens": "4.25.2",
|
|
@@ -2013,6 +1988,7 @@ module.exports = withAui(config);
|
|
|
2013
1988
|
},
|
|
2014
1989
|
"devDependencies": {
|
|
2015
1990
|
"@assistant-ui/metro": "workspace:*",
|
|
1991
|
+
"@babel/core": "^7.29.7",
|
|
2016
1992
|
"@types/react": "~19.2.17",
|
|
2017
1993
|
"typescript": "~6.0.3"
|
|
2018
1994
|
},
|
|
@@ -2024,56 +2000,57 @@ module.exports = withAui(config);
|
|
|
2024
2000
|
## README.md
|
|
2025
2001
|
|
|
2026
2002
|
```markdown
|
|
2027
|
-
#
|
|
2003
|
+
# assistant-ui + Expo
|
|
2004
|
+
|
|
2005
|
+
A native chat app built with [assistant-ui](https://www.assistant-ui.com) and [Expo](https://expo.dev). It runs on iOS, Android, and the web from a single codebase, and is styled to match the assistant-ui web kit: a clean, neutral, ChatGPT-grade look with subtle hairline borders.
|
|
2006
|
+
|
|
2007
|
+
The chat is powered by `@assistant-ui/react-native` with the AI SDK runtime (`@assistant-ui/react-ai-sdk`). The example leans on native Expo APIs throughout:
|
|
2008
|
+
|
|
2009
|
+
- **SF Symbols** (`expo-symbols`) for native iconography on iOS, with a Material Icons fallback on Android and the web.
|
|
2010
|
+
- **Haptics** (`expo-haptics`) for tactile feedback on send, stop, and selection.
|
|
2011
|
+
- **Image picker** (`expo-image-picker`) and **expo-image** for attachments.
|
|
2012
|
+
- **Clipboard** (`expo-clipboard`) so the copy action works natively.
|
|
2013
|
+
- A native **drawer** (`@react-navigation/drawer`) for the thread list, with a swipe gesture to switch conversations.
|
|
2028
2014
|
|
|
2029
|
-
|
|
2015
|
+
It follows the assistant-ui component conventions: `MessagePrimitive.Parts` for message rendering, `AuiIf` for declarative state-driven UI, the `ThreadList` / `ThreadListItem` primitives for the drawer, and a `"use generative"` toolkit (`components/assistant-ui/tools.tsx`) that renders weather cards inline.
|
|
2030
2016
|
|
|
2031
2017
|
## Get started
|
|
2032
2018
|
|
|
2033
|
-
1. Install dependencies
|
|
2019
|
+
1. Install dependencies from the repository root:
|
|
2034
2020
|
|
|
2035
2021
|
```bash
|
|
2036
|
-
|
|
2022
|
+
pnpm install
|
|
2037
2023
|
```
|
|
2038
2024
|
|
|
2039
|
-
2.
|
|
2025
|
+
2. Configure the chat backend. Copy `.env.example` to `.env` and set your key:
|
|
2040
2026
|
|
|
2041
2027
|
```bash
|
|
2042
|
-
|
|
2028
|
+
cp .env.example .env
|
|
2043
2029
|
```
|
|
2044
2030
|
|
|
2045
|
-
|
|
2031
|
+
The bundled API route (`app/api/chat+api.ts`) needs `OPENAI_API_KEY`. To point the app at a separately hosted backend instead, set `EXPO_PUBLIC_CHAT_ENDPOINT_URL`.
|
|
2046
2032
|
|
|
2047
|
-
|
|
2048
|
-
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
|
2049
|
-
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
|
2050
|
-
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
|
2033
|
+
3. Start the app:
|
|
2051
2034
|
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2035
|
+
```bash
|
|
2036
|
+
pnpm --filter with-expo start
|
|
2037
|
+
```
|
|
2055
2038
|
|
|
2056
|
-
|
|
2039
|
+
From there you can open it in the iOS simulator, an Android emulator, or the browser.
|
|
2057
2040
|
|
|
2058
|
-
|
|
2059
|
-
npm run reset-project
|
|
2060
|
-
```
|
|
2041
|
+
## Project structure
|
|
2061
2042
|
|
|
2062
|
-
|
|
2043
|
+
- `app/_layout.tsx` wires the runtime, the toolkit, and the drawer navigation.
|
|
2044
|
+
- `app/index.tsx` renders the `Thread`.
|
|
2045
|
+
- `components/assistant-ui/` holds the composer, message, action bar, branch picker, and tool UIs.
|
|
2046
|
+
- `components/thread-list/` holds the drawer thread list.
|
|
2047
|
+
- `constants/theme.ts` and `hooks/use-theme.ts` define the shared design tokens.
|
|
2048
|
+
- `components/ui/icon.tsx` is the cross-platform icon (SF Symbols on iOS, Material Icons elsewhere).
|
|
2063
2049
|
|
|
2064
2050
|
## Learn more
|
|
2065
2051
|
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
|
2069
|
-
- [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.
|
|
2070
|
-
|
|
2071
|
-
## Join the community
|
|
2072
|
-
|
|
2073
|
-
Join our community of developers creating universal apps.
|
|
2074
|
-
|
|
2075
|
-
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
|
2076
|
-
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
|
2052
|
+
- [assistant-ui documentation](https://www.assistant-ui.com/docs)
|
|
2053
|
+
- [Expo documentation](https://docs.expo.dev)
|
|
2077
2054
|
|
|
2078
2055
|
```
|
|
2079
2056
|
|