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