create-better-t-stack 3.9.0 → 3.11.0-pr749.0

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.
Files changed (182) hide show
  1. package/README.md +2 -1
  2. package/bin/create-better-t-stack +98 -0
  3. package/package.json +69 -59
  4. package/src/api.ts +203 -0
  5. package/src/cli.ts +185 -0
  6. package/src/constants.ts +270 -0
  7. package/src/helpers/addons/addons-setup.ts +201 -0
  8. package/src/helpers/addons/examples-setup.ts +137 -0
  9. package/src/helpers/addons/fumadocs-setup.ts +99 -0
  10. package/src/helpers/addons/oxlint-setup.ts +36 -0
  11. package/src/helpers/addons/ruler-setup.ts +135 -0
  12. package/src/helpers/addons/starlight-setup.ts +45 -0
  13. package/src/helpers/addons/tauri-setup.ts +90 -0
  14. package/src/helpers/addons/tui-setup.ts +64 -0
  15. package/src/helpers/addons/ultracite-setup.ts +228 -0
  16. package/src/helpers/addons/vite-pwa-setup.ts +59 -0
  17. package/src/helpers/addons/wxt-setup.ts +86 -0
  18. package/src/helpers/core/add-addons.ts +85 -0
  19. package/src/helpers/core/add-deployment.ts +102 -0
  20. package/src/helpers/core/api-setup.ts +280 -0
  21. package/src/helpers/core/auth-setup.ts +203 -0
  22. package/src/helpers/core/backend-setup.ts +69 -0
  23. package/src/helpers/core/command-handlers.ts +354 -0
  24. package/src/helpers/core/convex-codegen.ts +14 -0
  25. package/src/helpers/core/create-project.ts +134 -0
  26. package/src/helpers/core/create-readme.ts +694 -0
  27. package/src/helpers/core/db-setup.ts +184 -0
  28. package/src/helpers/core/detect-project-config.ts +41 -0
  29. package/src/helpers/core/env-setup.ts +481 -0
  30. package/src/helpers/core/git.ts +23 -0
  31. package/src/helpers/core/install-dependencies.ts +29 -0
  32. package/src/helpers/core/payments-setup.ts +48 -0
  33. package/src/helpers/core/post-installation.ts +403 -0
  34. package/src/helpers/core/project-config.ts +250 -0
  35. package/src/helpers/core/runtime-setup.ts +76 -0
  36. package/src/helpers/core/template-manager.ts +917 -0
  37. package/src/helpers/core/workspace-setup.ts +184 -0
  38. package/src/helpers/database-providers/d1-setup.ts +28 -0
  39. package/src/helpers/database-providers/docker-compose-setup.ts +50 -0
  40. package/src/helpers/database-providers/mongodb-atlas-setup.ts +182 -0
  41. package/src/helpers/database-providers/neon-setup.ts +240 -0
  42. package/src/helpers/database-providers/planetscale-setup.ts +78 -0
  43. package/src/helpers/database-providers/prisma-postgres-setup.ts +193 -0
  44. package/src/helpers/database-providers/supabase-setup.ts +196 -0
  45. package/src/helpers/database-providers/turso-setup.ts +309 -0
  46. package/src/helpers/deployment/alchemy/alchemy-combined-setup.ts +80 -0
  47. package/src/helpers/deployment/alchemy/alchemy-next-setup.ts +52 -0
  48. package/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts +105 -0
  49. package/src/helpers/deployment/alchemy/alchemy-react-router-setup.ts +33 -0
  50. package/src/helpers/deployment/alchemy/alchemy-solid-setup.ts +33 -0
  51. package/src/helpers/deployment/alchemy/alchemy-svelte-setup.ts +99 -0
  52. package/src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts +34 -0
  53. package/src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts +99 -0
  54. package/src/helpers/deployment/alchemy/env-dts-setup.ts +76 -0
  55. package/src/helpers/deployment/alchemy/index.ts +7 -0
  56. package/src/helpers/deployment/server-deploy-setup.ts +55 -0
  57. package/src/helpers/deployment/web-deploy-setup.ts +58 -0
  58. package/src/index.ts +51 -0
  59. package/src/prompts/addons.ts +200 -0
  60. package/src/prompts/api.ts +49 -0
  61. package/src/prompts/auth.ts +84 -0
  62. package/src/prompts/backend.ts +83 -0
  63. package/src/prompts/config-prompts.ts +138 -0
  64. package/src/prompts/database-setup.ts +112 -0
  65. package/src/prompts/database.ts +57 -0
  66. package/src/prompts/examples.ts +60 -0
  67. package/src/prompts/frontend.ts +118 -0
  68. package/src/prompts/git.ts +16 -0
  69. package/src/prompts/install.ts +16 -0
  70. package/src/prompts/orm.ts +53 -0
  71. package/src/prompts/package-manager.ts +32 -0
  72. package/src/prompts/payments.ts +50 -0
  73. package/src/prompts/project-name.ts +86 -0
  74. package/src/prompts/runtime.ts +47 -0
  75. package/src/prompts/server-deploy.ts +91 -0
  76. package/src/prompts/web-deploy.ts +107 -0
  77. package/src/tui/app.tsx +1062 -0
  78. package/src/types.ts +70 -0
  79. package/src/utils/add-package-deps.ts +57 -0
  80. package/src/utils/analytics.ts +39 -0
  81. package/src/utils/better-auth-plugin-setup.ts +71 -0
  82. package/src/utils/bts-config.ts +122 -0
  83. package/src/utils/command-exists.ts +16 -0
  84. package/src/utils/compatibility-rules.ts +337 -0
  85. package/src/utils/compatibility.ts +11 -0
  86. package/src/utils/config-processing.ts +130 -0
  87. package/src/utils/config-validation.ts +470 -0
  88. package/src/utils/display-config.ts +96 -0
  89. package/src/utils/docker-utils.ts +70 -0
  90. package/src/utils/errors.ts +30 -0
  91. package/src/utils/file-formatter.ts +11 -0
  92. package/src/utils/generate-reproducible-command.ts +53 -0
  93. package/src/utils/get-latest-cli-version.ts +27 -0
  94. package/src/utils/get-package-manager.ts +13 -0
  95. package/src/utils/open-url.ts +18 -0
  96. package/src/utils/package-runner.ts +23 -0
  97. package/src/utils/project-directory.ts +102 -0
  98. package/src/utils/project-name-validation.ts +43 -0
  99. package/src/utils/render-title.ts +48 -0
  100. package/src/utils/setup-catalogs.ts +192 -0
  101. package/src/utils/sponsors.ts +101 -0
  102. package/src/utils/telemetry.ts +19 -0
  103. package/src/utils/template-processor.ts +64 -0
  104. package/src/utils/templates.ts +94 -0
  105. package/src/utils/ts-morph.ts +26 -0
  106. package/src/validation.ts +117 -0
  107. package/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs +5 -7
  108. package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +17 -17
  109. package/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +4 -4
  110. package/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs +2 -2
  111. package/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs +10 -10
  112. package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs +13 -5
  113. package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs +14 -12
  114. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs +13 -16
  115. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs +11 -5
  116. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs +4 -4
  117. package/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs +1 -1
  118. package/templates/auth/better-auth/web/react/next/src/components/user-menu.tsx.hbs +17 -15
  119. package/templates/auth/better-auth/web/react/react-router/src/components/user-menu.tsx.hbs +16 -15
  120. package/templates/auth/better-auth/web/react/tanstack-router/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
  121. package/templates/auth/better-auth/web/react/tanstack-start/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
  122. package/templates/backend/convex/packages/backend/convex/README.md +4 -4
  123. package/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +17 -0
  124. package/templates/backend/convex/packages/backend/convex/tsconfig.json.hbs +1 -1
  125. package/templates/examples/ai/convex/packages/backend/convex/agent.ts.hbs +9 -0
  126. package/templates/examples/ai/convex/packages/backend/convex/chat.ts.hbs +67 -0
  127. package/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs +301 -3
  128. package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +296 -10
  129. package/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs +180 -1
  130. package/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs +172 -9
  131. package/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs +156 -6
  132. package/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs +156 -4
  133. package/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +159 -6
  134. package/templates/frontend/react/next/package.json.hbs +8 -7
  135. package/templates/frontend/react/next/src/app/layout.tsx.hbs +28 -1
  136. package/templates/frontend/react/next/src/components/mode-toggle.tsx.hbs +4 -6
  137. package/templates/frontend/react/next/src/components/providers.tsx.hbs +14 -4
  138. package/templates/frontend/react/react-router/package.json.hbs +2 -1
  139. package/templates/frontend/react/{tanstack-router/src/components/mode-toggle.tsx → react-router/src/components/mode-toggle.tsx.hbs} +4 -6
  140. package/templates/frontend/react/tanstack-router/package.json.hbs +2 -1
  141. package/templates/frontend/react/{react-router/src/components/mode-toggle.tsx → tanstack-router/src/components/mode-toggle.tsx.hbs} +4 -6
  142. package/templates/frontend/react/tanstack-start/package.json.hbs +2 -1
  143. package/templates/frontend/react/tanstack-start/src/router.tsx.hbs +6 -0
  144. package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +13 -14
  145. package/templates/frontend/react/tanstack-start/vite.config.ts.hbs +5 -0
  146. package/templates/frontend/react/web-base/components.json +5 -2
  147. package/templates/frontend/react/web-base/src/components/ui/button.tsx.hbs +57 -0
  148. package/templates/frontend/react/web-base/src/components/ui/card.tsx.hbs +103 -0
  149. package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx.hbs +26 -0
  150. package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx.hbs +262 -0
  151. package/templates/frontend/react/web-base/src/components/ui/input.tsx.hbs +20 -0
  152. package/templates/frontend/react/web-base/src/components/ui/label.tsx.hbs +20 -0
  153. package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx.hbs +13 -0
  154. package/templates/frontend/react/web-base/src/components/ui/sonner.tsx.hbs +44 -0
  155. package/templates/frontend/react/web-base/src/index.css.hbs +58 -64
  156. package/dist/cli.d.mts +0 -1
  157. package/dist/cli.mjs +0 -8
  158. package/dist/index.d.mts +0 -347
  159. package/dist/index.mjs +0 -4
  160. package/dist/src-DLvUK0Qf.mjs +0 -7069
  161. package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +0 -7
  162. package/templates/examples/ai/web/react/base/src/components/response.tsx.hbs +0 -22
  163. package/templates/frontend/react/web-base/src/components/ui/button.tsx +0 -56
  164. package/templates/frontend/react/web-base/src/components/ui/card.tsx +0 -75
  165. package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx +0 -27
  166. package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx +0 -228
  167. package/templates/frontend/react/web-base/src/components/ui/input.tsx +0 -21
  168. package/templates/frontend/react/web-base/src/components/ui/label.tsx +0 -19
  169. package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx +0 -13
  170. package/templates/frontend/react/web-base/src/components/ui/sonner.tsx +0 -25
  171. /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  172. /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  173. /package/templates/auth/better-auth/web/react/tanstack-router/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  174. /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  175. /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  176. /package/templates/auth/better-auth/web/react/tanstack-start/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  177. /package/templates/auth/better-auth/web/solid/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  178. /package/templates/auth/better-auth/web/solid/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  179. /package/templates/auth/better-auth/web/solid/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  180. /package/templates/frontend/react/react-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
  181. /package/templates/frontend/react/tanstack-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
  182. /package/templates/frontend/react/web-base/src/lib/{utils.ts → utils.ts.hbs} +0 -0
@@ -1,3 +1,301 @@
1
+ {{#if (eq backend "convex")}}
2
+ import { Ionicons } from "@expo/vector-icons";
3
+ import {
4
+ useUIMessages,
5
+ useSmoothText,
6
+ type UIMessage,
7
+ } from "@convex-dev/agent/react";
8
+ import { api } from "@{{projectName}}/backend/convex/_generated/api";
9
+ import { useMutation } from "convex/react";
10
+ import { useRef, useEffect, useState } from "react";
11
+ import {
12
+ View,
13
+ Text,
14
+ TextInput,
15
+ TouchableOpacity,
16
+ ScrollView,
17
+ KeyboardAvoidingView,
18
+ Platform,
19
+ StyleSheet,
20
+ ActivityIndicator,
21
+ } from "react-native";
22
+
23
+ import { Container } from "@/components/container";
24
+ import { useColorScheme } from "@/lib/use-color-scheme";
25
+ import { NAV_THEME } from "@/lib/constants";
26
+
27
+ function MessageContent({
28
+ text,
29
+ isStreaming,
30
+ textColor,
31
+ }: {
32
+ text: string;
33
+ isStreaming: boolean;
34
+ textColor: string;
35
+ }) {
36
+ const [visibleText] = useSmoothText(text, {
37
+ startStreaming: isStreaming,
38
+ });
39
+ return <Text style={[styles.messageText, { color: textColor }]}>{visibleText}</Text>;
40
+ }
41
+
42
+ export default function AIScreen() {
43
+ const { colorScheme } = useColorScheme();
44
+ const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
45
+ const [input, setInput] = useState("");
46
+ const [threadId, setThreadId] = useState<string | null>(null);
47
+ const [isLoading, setIsLoading] = useState(false);
48
+ const scrollViewRef = useRef<ScrollView>(null);
49
+
50
+ const createThread = useMutation(api.chat.createNewThread);
51
+ const sendMessage = useMutation(api.chat.sendMessage);
52
+
53
+ const { results: messages } = useUIMessages(
54
+ api.chat.listMessages,
55
+ threadId ? { threadId } : "skip",
56
+ { initialNumItems: 50, stream: true },
57
+ );
58
+
59
+ const hasStreamingMessage = messages?.some(
60
+ (m: UIMessage) => m.status === "streaming",
61
+ );
62
+
63
+ useEffect(() => {
64
+ scrollViewRef.current?.scrollToEnd({ animated: true });
65
+ }, [messages]);
66
+
67
+ async function onSubmit() {
68
+ const value = input.trim();
69
+ if (!value || isLoading) return;
70
+
71
+ setIsLoading(true);
72
+ setInput("");
73
+
74
+ try {
75
+ let currentThreadId = threadId;
76
+ if (!currentThreadId) {
77
+ currentThreadId = await createThread();
78
+ setThreadId(currentThreadId);
79
+ }
80
+
81
+ await sendMessage({ threadId: currentThreadId, prompt: value });
82
+ } catch (error) {
83
+ console.error("Failed to send message:", error);
84
+ } finally {
85
+ setIsLoading(false);
86
+ }
87
+ }
88
+
89
+ return (
90
+ <Container>
91
+ <KeyboardAvoidingView
92
+ style={styles.container}
93
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
94
+ >
95
+ <View style={styles.content}>
96
+ <View style={styles.header}>
97
+ <Text style={[styles.headerTitle, { color: theme.text }]}>
98
+ AI Chat
99
+ </Text>
100
+ <Text style={[styles.headerSubtitle, { color: theme.text, opacity: 0.7 }]}>
101
+ Chat with our AI assistant
102
+ </Text>
103
+ </View>
104
+ <ScrollView
105
+ ref={scrollViewRef}
106
+ style={styles.scrollView}
107
+ showsVerticalScrollIndicator={false}
108
+ >
109
+ {!messages || messages.length === 0 ? (
110
+ <View style={styles.emptyContainer}>
111
+ <Text style={[styles.emptyText, { color: theme.text, opacity: 0.7 }]}>
112
+ Ask me anything to get started!
113
+ </Text>
114
+ </View>
115
+ ) : (
116
+ <View style={styles.messagesList}>
117
+ {messages.map((message: UIMessage) => (
118
+ <View
119
+ key={message.key}
120
+ style={[
121
+ styles.messageCard,
122
+ {
123
+ backgroundColor: message.role === "user"
124
+ ? theme.primary + "20"
125
+ : theme.card,
126
+ borderColor: theme.border,
127
+ alignSelf: message.role === "user" ? "flex-end" : "flex-start",
128
+ marginLeft: message.role === "user" ? 32 : 0,
129
+ marginRight: message.role === "user" ? 0 : 32,
130
+ },
131
+ ]}
132
+ >
133
+ <Text style={[styles.messageRole, { color: theme.text }]}>
134
+ {message.role === "user" ? "You" : "AI Assistant"}
135
+ </Text>
136
+ <MessageContent
137
+ text={message.text ?? ""}
138
+ isStreaming={message.status === "streaming"}
139
+ textColor={theme.text}
140
+ />
141
+ </View>
142
+ ))}
143
+ {isLoading && !hasStreamingMessage && (
144
+ <View
145
+ style={[
146
+ styles.messageCard,
147
+ {
148
+ backgroundColor: theme.card,
149
+ borderColor: theme.border,
150
+ alignSelf: "flex-start",
151
+ marginRight: 32,
152
+ },
153
+ ]}
154
+ >
155
+ <Text style={[styles.messageRole, { color: theme.text }]}>
156
+ AI Assistant
157
+ </Text>
158
+ <View style={styles.loadingContainer}>
159
+ <ActivityIndicator size="small" color={theme.primary} />
160
+ <Text style={[styles.loadingText, { color: theme.text, opacity: 0.7 }]}>
161
+ Thinking...
162
+ </Text>
163
+ </View>
164
+ </View>
165
+ )}
166
+ </View>
167
+ )}
168
+ </ScrollView>
169
+ <View style={[styles.inputContainer, { borderTopColor: theme.border }]}>
170
+ <View style={styles.inputRow}>
171
+ <TextInput
172
+ value={input}
173
+ onChangeText={setInput}
174
+ placeholder="Type your message..."
175
+ placeholderTextColor={theme.text}
176
+ style={[
177
+ styles.input,
178
+ {
179
+ color: theme.text,
180
+ borderColor: theme.border,
181
+ backgroundColor: theme.background,
182
+ },
183
+ ]}
184
+ onSubmitEditing={(e) => {
185
+ e.preventDefault();
186
+ onSubmit();
187
+ }}
188
+ editable={!isLoading}
189
+ autoFocus={true}
190
+ multiline
191
+ />
192
+ <TouchableOpacity
193
+ onPress={onSubmit}
194
+ disabled={!input.trim() || isLoading}
195
+ style={[
196
+ styles.sendButton,
197
+ {
198
+ backgroundColor: input.trim() && !isLoading ? theme.primary : theme.border,
199
+ opacity: input.trim() && !isLoading ? 1 : 0.5,
200
+ },
201
+ ]}
202
+ >
203
+ <Ionicons
204
+ name="send"
205
+ size={20}
206
+ color="#ffffff"
207
+ />
208
+ </TouchableOpacity>
209
+ </View>
210
+ </View>
211
+ </View>
212
+ </KeyboardAvoidingView>
213
+ </Container>
214
+ );
215
+ }
216
+
217
+ const styles = StyleSheet.create({
218
+ container: {
219
+ flex: 1,
220
+ },
221
+ content: {
222
+ flex: 1,
223
+ padding: 16,
224
+ },
225
+ header: {
226
+ marginBottom: 16,
227
+ },
228
+ headerTitle: {
229
+ fontSize: 20,
230
+ fontWeight: "bold",
231
+ marginBottom: 4,
232
+ },
233
+ headerSubtitle: {
234
+ fontSize: 14,
235
+ },
236
+ scrollView: {
237
+ flex: 1,
238
+ marginBottom: 16,
239
+ },
240
+ emptyContainer: {
241
+ flex: 1,
242
+ justifyContent: "center",
243
+ alignItems: "center",
244
+ },
245
+ emptyText: {
246
+ fontSize: 16,
247
+ textAlign: "center",
248
+ },
249
+ messagesList: {
250
+ gap: 8,
251
+ paddingBottom: 16,
252
+ },
253
+ messageCard: {
254
+ borderWidth: 1,
255
+ padding: 12,
256
+ maxWidth: "80%",
257
+ },
258
+ messageRole: {
259
+ fontSize: 12,
260
+ fontWeight: "bold",
261
+ marginBottom: 4,
262
+ },
263
+ messageText: {
264
+ fontSize: 14,
265
+ lineHeight: 20,
266
+ },
267
+ loadingContainer: {
268
+ flexDirection: "row",
269
+ alignItems: "center",
270
+ gap: 8,
271
+ },
272
+ loadingText: {
273
+ fontSize: 14,
274
+ },
275
+ inputContainer: {
276
+ borderTopWidth: 1,
277
+ paddingTop: 12,
278
+ },
279
+ inputRow: {
280
+ flexDirection: "row",
281
+ alignItems: "flex-end",
282
+ gap: 8,
283
+ },
284
+ input: {
285
+ flex: 1,
286
+ borderWidth: 1,
287
+ padding: 8,
288
+ fontSize: 14,
289
+ minHeight: 36,
290
+ maxHeight: 100,
291
+ },
292
+ sendButton: {
293
+ padding: 8,
294
+ justifyContent: "center",
295
+ alignItems: "center",
296
+ },
297
+ });
298
+ {{else}}
1
299
  import { useRef, useEffect, useState } from "react";
2
300
  import {
3
301
  View,
@@ -104,8 +402,8 @@ export default function AIScreen() {
104
402
  style={[
105
403
  styles.messageCard,
106
404
  {
107
- backgroundColor: message.role === "user"
108
- ? theme.primary + "20"
405
+ backgroundColor: message.role === "user"
406
+ ? theme.primary + "20"
109
407
  : theme.card,
110
408
  borderColor: theme.border,
111
409
  alignSelf: message.role === "user" ? "flex-end" : "flex-start",
@@ -284,4 +582,4 @@ const styles = StyleSheet.create({
284
582
  textAlign: "center",
285
583
  },
286
584
  });
287
-
585
+ {{/if}}
@@ -1,3 +1,297 @@
1
+ {{#if (eq backend "convex")}}
2
+ import { Ionicons } from "@expo/vector-icons";
3
+ import {
4
+ useUIMessages,
5
+ useSmoothText,
6
+ type UIMessage,
7
+ } from "@convex-dev/agent/react";
8
+ import { api } from "@{{projectName}}/backend/convex/_generated/api";
9
+ import { useMutation } from "convex/react";
10
+ import React, { useRef, useEffect, useState } from "react";
11
+ import {
12
+ View,
13
+ Text,
14
+ TextInput,
15
+ TouchableOpacity,
16
+ ScrollView,
17
+ KeyboardAvoidingView,
18
+ Platform,
19
+ ActivityIndicator,
20
+ } from "react-native";
21
+ import { StyleSheet, useUnistyles } from "react-native-unistyles";
22
+
23
+ import { Container } from "@/components/container";
24
+
25
+ function MessageContent({
26
+ text,
27
+ isStreaming,
28
+ style,
29
+ }: {
30
+ text: string;
31
+ isStreaming: boolean;
32
+ style: object;
33
+ }) {
34
+ const [visibleText] = useSmoothText(text, {
35
+ startStreaming: isStreaming,
36
+ });
37
+ return <Text style={style}>{visibleText}</Text>;
38
+ }
39
+
40
+ export default function AIScreen() {
41
+ const { theme } = useUnistyles();
42
+ const [input, setInput] = useState("");
43
+ const [threadId, setThreadId] = useState<string | null>(null);
44
+ const [isLoading, setIsLoading] = useState(false);
45
+ const scrollViewRef = useRef<ScrollView>(null);
46
+
47
+ const createThread = useMutation(api.chat.createNewThread);
48
+ const sendMessage = useMutation(api.chat.sendMessage);
49
+
50
+ const { results: messages } = useUIMessages(
51
+ api.chat.listMessages,
52
+ threadId ? { threadId } : "skip",
53
+ { initialNumItems: 50, stream: true },
54
+ );
55
+
56
+ const hasStreamingMessage = messages?.some(
57
+ (m: UIMessage) => m.status === "streaming",
58
+ );
59
+
60
+ useEffect(() => {
61
+ scrollViewRef.current?.scrollToEnd({ animated: true });
62
+ }, [messages]);
63
+
64
+ const onSubmit = async () => {
65
+ const value = input.trim();
66
+ if (!value || isLoading) return;
67
+
68
+ setIsLoading(true);
69
+ setInput("");
70
+
71
+ try {
72
+ let currentThreadId = threadId;
73
+ if (!currentThreadId) {
74
+ currentThreadId = await createThread();
75
+ setThreadId(currentThreadId);
76
+ }
77
+
78
+ await sendMessage({ threadId: currentThreadId, prompt: value });
79
+ } catch (error) {
80
+ console.error("Failed to send message:", error);
81
+ } finally {
82
+ setIsLoading(false);
83
+ }
84
+ };
85
+
86
+ return (
87
+ <Container>
88
+ <KeyboardAvoidingView
89
+ style={styles.container}
90
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
91
+ >
92
+ <View style={styles.content}>
93
+ <View style={styles.header}>
94
+ <Text style={styles.headerTitle}>AI Chat</Text>
95
+ <Text style={styles.headerSubtitle}>
96
+ Chat with our AI assistant
97
+ </Text>
98
+ </View>
99
+
100
+ <ScrollView
101
+ ref={scrollViewRef}
102
+ style={styles.messagesContainer}
103
+ showsVerticalScrollIndicator={false}
104
+ >
105
+ {!messages || messages.length === 0 ? (
106
+ <View style={styles.emptyContainer}>
107
+ <Text style={styles.emptyText}>
108
+ Ask me anything to get started!
109
+ </Text>
110
+ </View>
111
+ ) : (
112
+ <View style={styles.messagesWrapper}>
113
+ {messages.map((message: UIMessage) => (
114
+ <View
115
+ key={message.key}
116
+ style={[
117
+ styles.messageContainer,
118
+ message.role === "user"
119
+ ? styles.userMessage
120
+ : styles.assistantMessage,
121
+ ]}
122
+ >
123
+ <Text style={styles.messageRole}>
124
+ {message.role === "user" ? "You" : "AI Assistant"}
125
+ </Text>
126
+ <MessageContent
127
+ text={message.text ?? ""}
128
+ isStreaming={message.status === "streaming"}
129
+ style={styles.messageContent}
130
+ />
131
+ </View>
132
+ ))}
133
+ {isLoading && !hasStreamingMessage && (
134
+ <View style={[styles.messageContainer, styles.assistantMessage]}>
135
+ <Text style={styles.messageRole}>AI Assistant</Text>
136
+ <View style={styles.loadingContainer}>
137
+ <ActivityIndicator size="small" color={theme.colors.primary} />
138
+ <Text style={styles.loadingText}>Thinking...</Text>
139
+ </View>
140
+ </View>
141
+ )}
142
+ </View>
143
+ )}
144
+ </ScrollView>
145
+
146
+ <View style={styles.inputSection}>
147
+ <View style={styles.inputContainer}>
148
+ <TextInput
149
+ value={input}
150
+ onChangeText={setInput}
151
+ placeholder="Type your message..."
152
+ placeholderTextColor={theme.colors.border}
153
+ style={styles.textInput}
154
+ onSubmitEditing={(e) => {
155
+ e.preventDefault();
156
+ onSubmit();
157
+ }}
158
+ editable={!isLoading}
159
+ autoFocus={true}
160
+ />
161
+ <TouchableOpacity
162
+ onPress={onSubmit}
163
+ disabled={!input.trim() || isLoading}
164
+ style={[
165
+ styles.sendButton,
166
+ (!input.trim() || isLoading) && styles.sendButtonDisabled,
167
+ ]}
168
+ >
169
+ <Ionicons
170
+ name="send"
171
+ size={20}
172
+ color={
173
+ input.trim() && !isLoading
174
+ ? theme.colors.background
175
+ : theme.colors.border
176
+ }
177
+ />
178
+ </TouchableOpacity>
179
+ </View>
180
+ </View>
181
+ </View>
182
+ </KeyboardAvoidingView>
183
+ </Container>
184
+ );
185
+ }
186
+
187
+ const styles = StyleSheet.create((theme) => ({
188
+ container: {
189
+ flex: 1,
190
+ },
191
+ content: {
192
+ flex: 1,
193
+ paddingHorizontal: theme.spacing.md,
194
+ paddingVertical: theme.spacing.lg,
195
+ },
196
+ header: {
197
+ marginBottom: theme.spacing.lg,
198
+ },
199
+ headerTitle: {
200
+ fontSize: 28,
201
+ fontWeight: "bold",
202
+ color: theme.colors.typography,
203
+ marginBottom: theme.spacing.sm,
204
+ },
205
+ headerSubtitle: {
206
+ fontSize: 16,
207
+ color: theme.colors.typography,
208
+ },
209
+ messagesContainer: {
210
+ flex: 1,
211
+ marginBottom: theme.spacing.md,
212
+ },
213
+ emptyContainer: {
214
+ flex: 1,
215
+ justifyContent: "center",
216
+ alignItems: "center",
217
+ },
218
+ emptyText: {
219
+ textAlign: "center",
220
+ color: theme.colors.typography,
221
+ fontSize: 18,
222
+ },
223
+ messagesWrapper: {
224
+ gap: theme.spacing.md,
225
+ },
226
+ messageContainer: {
227
+ padding: theme.spacing.md,
228
+ borderRadius: 8,
229
+ },
230
+ userMessage: {
231
+ backgroundColor: theme.colors.primary + "20",
232
+ marginLeft: theme.spacing.xl,
233
+ alignSelf: "flex-end",
234
+ },
235
+ assistantMessage: {
236
+ backgroundColor: theme.colors.background,
237
+ marginRight: theme.spacing.xl,
238
+ borderWidth: 1,
239
+ borderColor: theme.colors.border,
240
+ },
241
+ messageRole: {
242
+ fontSize: 14,
243
+ fontWeight: "600",
244
+ marginBottom: theme.spacing.sm,
245
+ color: theme.colors.typography,
246
+ },
247
+ messageContent: {
248
+ color: theme.colors.typography,
249
+ lineHeight: 20,
250
+ },
251
+ loadingContainer: {
252
+ flexDirection: "row",
253
+ alignItems: "center",
254
+ gap: theme.spacing.sm,
255
+ },
256
+ loadingText: {
257
+ color: theme.colors.typography,
258
+ opacity: 0.7,
259
+ },
260
+ inputSection: {
261
+ borderTopWidth: 1,
262
+ borderTopColor: theme.colors.border,
263
+ paddingTop: theme.spacing.md,
264
+ },
265
+ inputContainer: {
266
+ flexDirection: "row",
267
+ alignItems: "flex-end",
268
+ gap: theme.spacing.sm,
269
+ },
270
+ textInput: {
271
+ flex: 1,
272
+ borderWidth: 1,
273
+ borderColor: theme.colors.border,
274
+ borderRadius: 8,
275
+ paddingHorizontal: theme.spacing.md,
276
+ paddingVertical: theme.spacing.sm,
277
+ color: theme.colors.typography,
278
+ backgroundColor: theme.colors.background,
279
+ fontSize: 16,
280
+ minHeight: 40,
281
+ maxHeight: 120,
282
+ },
283
+ sendButton: {
284
+ backgroundColor: theme.colors.primary,
285
+ padding: theme.spacing.sm,
286
+ borderRadius: 8,
287
+ justifyContent: "center",
288
+ alignItems: "center",
289
+ },
290
+ sendButtonDisabled: {
291
+ backgroundColor: theme.colors.border,
292
+ },
293
+ }));
294
+ {{else}}
1
295
  import React, { useRef, useEffect, useState } from "react";
2
296
  import {
3
297
  View,
@@ -256,15 +550,6 @@ const styles = StyleSheet.create((theme) => ({
256
550
  color: theme.colors.typography,
257
551
  lineHeight: 20,
258
552
  },
259
- toolInvocations: {
260
- fontSize: 12,
261
- color: theme.colors.typography,
262
- fontFamily: "monospace",
263
- backgroundColor: theme.colors.border + "40",
264
- padding: theme.spacing.sm,
265
- borderRadius: 4,
266
- marginTop: theme.spacing.sm,
267
- },
268
553
  inputSection: {
269
554
  borderTopWidth: 1,
270
555
  borderTopColor: theme.colors.border,
@@ -298,4 +583,5 @@ const styles = StyleSheet.create((theme) => ({
298
583
  sendButtonDisabled: {
299
584
  backgroundColor: theme.colors.border,
300
585
  },
301
- }));
586
+ }));
587
+ {{/if}}