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,181 @@
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 { Card, useThemeColor } from "heroui-native";
11
+ import { useRef, useEffect, useState } from "react";
12
+ import {
13
+ View,
14
+ Text,
15
+ TextInput,
16
+ Pressable,
17
+ ScrollView,
18
+ KeyboardAvoidingView,
19
+ Platform,
20
+ ActivityIndicator,
21
+ } from "react-native";
22
+
23
+ import { Container } from "@/components/container";
24
+
25
+ function MessageContent({
26
+ text,
27
+ isStreaming,
28
+ }: {
29
+ text: string;
30
+ isStreaming: boolean;
31
+ }) {
32
+ const [visibleText] = useSmoothText(text, {
33
+ startStreaming: isStreaming,
34
+ });
35
+ return <Text className="text-foreground leading-relaxed">{visibleText}</Text>;
36
+ }
37
+
38
+ export default function AIScreen() {
39
+ const [input, setInput] = useState("");
40
+ const [threadId, setThreadId] = useState<string | null>(null);
41
+ const [isLoading, setIsLoading] = useState(false);
42
+ const scrollViewRef = useRef<ScrollView>(null);
43
+ const mutedColor = useThemeColor("muted");
44
+ const accentColor = useThemeColor("accent");
45
+ const foregroundColor = useThemeColor("foreground");
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
+ className="flex-1"
90
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
91
+ >
92
+ <View className="flex-1 px-4 py-6">
93
+ <View className="mb-6">
94
+ <Text className="text-foreground text-2xl font-bold mb-2">
95
+ AI Chat
96
+ </Text>
97
+ <Text className="text-muted">Chat with our AI assistant</Text>
98
+ </View>
99
+ <ScrollView
100
+ ref={scrollViewRef}
101
+ className="flex-1 mb-4"
102
+ showsVerticalScrollIndicator={false}
103
+ >
104
+ {!messages || messages.length === 0 ? (
105
+ <View className="flex-1 justify-center items-center">
106
+ <Text className="text-center text-muted text-lg">
107
+ Ask me anything to get started!
108
+ </Text>
109
+ </View>
110
+ ) : (
111
+ <View className="gap-3">
112
+ {messages.map((message: UIMessage) => (
113
+ <Card
114
+ key={message.key}
115
+ variant="secondary"
116
+ className={`p-3 ${message.role === "user" ? "ml-8 bg-accent/10" : "mr-8"}`}
117
+ >
118
+ <Text className="text-sm font-semibold mb-1 text-foreground">
119
+ {message.role === "user" ? "You" : "AI Assistant"}
120
+ </Text>
121
+ <MessageContent
122
+ text={message.text ?? ""}
123
+ isStreaming={message.status === "streaming"}
124
+ />
125
+ </Card>
126
+ ))}
127
+ {isLoading && !hasStreamingMessage && (
128
+ <Card variant="secondary" className="p-3 mr-8">
129
+ <Text className="text-sm font-semibold mb-1 text-foreground">
130
+ AI Assistant
131
+ </Text>
132
+ <View className="flex-row items-center gap-2">
133
+ <ActivityIndicator size="small" color={accentColor} />
134
+ <Text className="text-muted">Thinking...</Text>
135
+ </View>
136
+ </Card>
137
+ )}
138
+ </View>
139
+ )}
140
+ </ScrollView>
141
+ <View className="border-t border-divider pt-4">
142
+ <View className="flex-row items-end gap-2">
143
+ <TextInput
144
+ value={input}
145
+ onChangeText={setInput}
146
+ placeholder="Type your message..."
147
+ placeholderTextColor={mutedColor}
148
+ className="flex-1 border border-divider rounded-lg px-3 py-2 text-foreground bg-surface min-h-10 max-h-30"
149
+ onSubmitEditing={(e) => {
150
+ e.preventDefault();
151
+ onSubmit();
152
+ }}
153
+ editable={!isLoading}
154
+ autoFocus={true}
155
+ />
156
+ <Pressable
157
+ onPress={onSubmit}
158
+ disabled={!input.trim() || isLoading}
159
+ className={`p-2 rounded-lg active:opacity-70 ${
160
+ input.trim() && !isLoading ? "bg-accent" : "bg-surface"
161
+ }`}
162
+ >
163
+ <Ionicons
164
+ name="send"
165
+ size={20}
166
+ color={
167
+ input.trim() && !isLoading ? foregroundColor : mutedColor
168
+ }
169
+ />
170
+ </Pressable>
171
+ </View>
172
+ </View>
173
+ </View>
174
+ </KeyboardAvoidingView>
175
+ </Container>
176
+ );
177
+ }
178
+ {{else}}
1
179
  import { useRef, useEffect, useState } from "react";
2
180
  import {
3
181
  View,
@@ -168,4 +346,5 @@ export default function AIScreen() {
168
346
  </KeyboardAvoidingView>
169
347
  </Container>
170
348
  );
171
- }
349
+ }
350
+ {{/if}}
@@ -1,21 +1,170 @@
1
+ {{#if (eq backend "convex")}}
1
2
  "use client";
2
3
 
3
- import { useChat } from "@ai-sdk/react";
4
- import { DefaultChatTransport } from "ai";
5
- import { Send } from "lucide-react";
4
+ import { api } from "@{{projectName}}/backend/convex/_generated/api";
5
+ import {
6
+ useUIMessages,
7
+ useSmoothText,
8
+ type UIMessage,
9
+ } from "@convex-dev/agent/react";
10
+ import { useMutation } from "convex/react";
11
+ import { Send, Loader2 } from "lucide-react";
6
12
  {{#if (eq webDeploy "alchemy")}}
7
13
  import dynamic from "next/dynamic";
14
+
15
+ const Streamdown = dynamic(
16
+ () => import("streamdown").then((mod) => ({ default: mod.Streamdown })),
17
+ {
18
+ loading: () => (
19
+ <div className="flex h-full items-center justify-center">
20
+ <div className="text-muted-foreground">Loading response...</div>
21
+ </div>
22
+ ),
23
+ ssr: false,
24
+ }
25
+ );
8
26
  {{else}}
9
- import { Response } from "@/components/response";
27
+ import { Streamdown } from "streamdown";
10
28
  {{/if}}
11
29
  import { useEffect, useRef, useState } from "react";
30
+
12
31
  import { Button } from "@/components/ui/button";
13
32
  import { Input } from "@/components/ui/input";
14
33
 
34
+ function MessageContent({
35
+ text,
36
+ isStreaming,
37
+ }: {
38
+ text: string;
39
+ isStreaming: boolean;
40
+ }) {
41
+ const [visibleText] = useSmoothText(text, {
42
+ startStreaming: isStreaming,
43
+ });
44
+ return <Streamdown>{visibleText}</Streamdown>;
45
+ }
46
+
47
+ export default function AIPage() {
48
+ const [input, setInput] = useState("");
49
+ const [threadId, setThreadId] = useState<string | null>(null);
50
+ const [isLoading, setIsLoading] = useState(false);
51
+ const messagesEndRef = useRef<HTMLDivElement>(null);
52
+
53
+ const createThread = useMutation(api.chat.createNewThread);
54
+ const sendMessage = useMutation(api.chat.sendMessage);
55
+
56
+ const { results: messages } = useUIMessages(
57
+ api.chat.listMessages,
58
+ threadId ? { threadId } : "skip",
59
+ { initialNumItems: 50, stream: true },
60
+ );
61
+
62
+ useEffect(() => {
63
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
64
+ }, [messages]);
65
+
66
+ const hasStreamingMessage = messages?.some(
67
+ (m: UIMessage) => m.status === "streaming",
68
+ );
69
+
70
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
71
+ e.preventDefault();
72
+ const text = input.trim();
73
+ if (!text || isLoading) return;
74
+
75
+ setIsLoading(true);
76
+ setInput("");
77
+
78
+ try {
79
+ let currentThreadId = threadId;
80
+ if (!currentThreadId) {
81
+ currentThreadId = await createThread();
82
+ setThreadId(currentThreadId);
83
+ }
84
+
85
+ await sendMessage({ threadId: currentThreadId, prompt: text });
86
+ } catch (error) {
87
+ console.error("Failed to send message:", error);
88
+ } finally {
89
+ setIsLoading(false);
90
+ }
91
+ };
92
+
93
+ return (
94
+ <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
95
+ <div className="overflow-y-auto space-y-4 pb-4">
96
+ {!messages || messages.length === 0 ? (
97
+ <div className="text-center text-muted-foreground mt-8">
98
+ Ask me anything to get started!
99
+ </div>
100
+ ) : (
101
+ messages.map((message: UIMessage) => (
102
+ <div
103
+ key={message.key}
104
+ className={`p-3 rounded-lg ${
105
+ message.role === "user"
106
+ ? "bg-primary/10 ml-8"
107
+ : "bg-secondary/20 mr-8"
108
+ }`}
109
+ >
110
+ <p className="text-sm font-semibold mb-1">
111
+ {message.role === "user" ? "You" : "AI Assistant"}
112
+ </p>
113
+ <MessageContent
114
+ text={message.text ?? ""}
115
+ isStreaming={message.status === "streaming"}
116
+ />
117
+ </div>
118
+ ))
119
+ )}
120
+ {isLoading && !hasStreamingMessage && (
121
+ <div className="p-3 rounded-lg bg-secondary/20 mr-8">
122
+ <p className="text-sm font-semibold mb-1">AI Assistant</p>
123
+ <div className="flex items-center gap-2 text-muted-foreground">
124
+ <Loader2 className="h-4 w-4 animate-spin" />
125
+ <span>Thinking...</span>
126
+ </div>
127
+ </div>
128
+ )}
129
+ <div ref={messagesEndRef} />
130
+ </div>
131
+
132
+ <form
133
+ onSubmit={handleSubmit}
134
+ className="w-full flex items-center space-x-2 pt-2 border-t"
135
+ >
136
+ <Input
137
+ name="prompt"
138
+ value={input}
139
+ onChange={(e) => setInput(e.target.value)}
140
+ placeholder="Type your message..."
141
+ className="flex-1"
142
+ autoComplete="off"
143
+ autoFocus
144
+ disabled={isLoading}
145
+ />
146
+ <Button type="submit" size="icon" disabled={isLoading || !input.trim()}>
147
+ {isLoading ? (
148
+ <Loader2 className="h-4 w-4 animate-spin" />
149
+ ) : (
150
+ <Send size={18} />
151
+ )}
152
+ </Button>
153
+ </form>
154
+ </div>
155
+ );
156
+ }
157
+ {{else}}
158
+ "use client";
159
+
160
+ import { useChat } from "@ai-sdk/react";
161
+ import { DefaultChatTransport } from "ai";
162
+ import { Send } from "lucide-react";
15
163
  {{#if (eq webDeploy "alchemy")}}
16
- const Response = dynamic(
17
- () =>
18
- import("@/components/response").then((mod) => ({ default: mod.Response })),
164
+ import dynamic from "next/dynamic";
165
+
166
+ const Streamdown = dynamic(
167
+ () => import("streamdown").then((mod) => ({ default: mod.Streamdown })),
19
168
  {
20
169
  loading: () => (
21
170
  <div className="flex h-full items-center justify-center">
@@ -25,11 +174,17 @@ const Response = dynamic(
25
174
  ssr: false,
26
175
  }
27
176
  );
177
+ {{else}}
178
+ import { Streamdown } from "streamdown";
28
179
  {{/if}}
180
+ import { useEffect, useRef, useState } from "react";
181
+
182
+ import { Button } from "@/components/ui/button";
183
+ import { Input } from "@/components/ui/input";
29
184
 
30
185
  export default function AIPage() {
31
186
  const [input, setInput] = useState("");
32
- const { messages, sendMessage } = useChat({
187
+ const { messages, sendMessage, status } = useChat({
33
188
  transport: new DefaultChatTransport({
34
189
  api: {{#if (eq backend "self")}}"/api/ai"{{else}}`${process.env.NEXT_PUBLIC_SERVER_URL}/ai`{{/if}},
35
190
  }),
@@ -71,7 +226,14 @@ export default function AIPage() {
71
226
  </p>
72
227
  {message.parts?.map((part, index) => {
73
228
  if (part.type === "text") {
74
- return <Response key={index}>{part.text}</Response>;
229
+ return (
230
+ <Streamdown
231
+ key={index}
232
+ isAnimating={status === "streaming" && message.role === "assistant"}
233
+ >
234
+ {part.text}
235
+ </Streamdown>
236
+ );
75
237
  }
76
238
  return null;
77
239
  })}
@@ -101,3 +263,4 @@ export default function AIPage() {
101
263
  </div>
102
264
  );
103
265
  }
266
+ {{/if}}
@@ -1,14 +1,156 @@
1
+ {{#if (eq backend "convex")}}
2
+ import { api } from "@{{projectName}}/backend/convex/_generated/api";
3
+ import {
4
+ useUIMessages,
5
+ useSmoothText,
6
+ type UIMessage,
7
+ } from "@convex-dev/agent/react";
8
+ import { useMutation } from "convex/react";
9
+ import { Send, Loader2 } from "lucide-react";
10
+ import React, { useRef, useEffect, useState } from "react";
11
+ import { Streamdown } from "streamdown";
12
+
13
+ import { Button } from "@/components/ui/button";
14
+ import { Input } from "@/components/ui/input";
15
+
16
+ function MessageContent({
17
+ text,
18
+ isStreaming,
19
+ }: {
20
+ text: string;
21
+ isStreaming: boolean;
22
+ }) {
23
+ const [visibleText] = useSmoothText(text, {
24
+ startStreaming: isStreaming,
25
+ });
26
+ return <Streamdown>{visibleText}</Streamdown>;
27
+ }
28
+
29
+ const AI: React.FC = () => {
30
+ const [input, setInput] = useState("");
31
+ const [threadId, setThreadId] = useState<string | null>(null);
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const messagesEndRef = useRef<HTMLDivElement>(null);
34
+
35
+ const createThread = useMutation(api.chat.createNewThread);
36
+ const sendMessage = useMutation(api.chat.sendMessage);
37
+
38
+ const { results: messages } = useUIMessages(
39
+ api.chat.listMessages,
40
+ threadId ? { threadId } : "skip",
41
+ { initialNumItems: 50, stream: true },
42
+ );
43
+
44
+ useEffect(() => {
45
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
46
+ }, [messages]);
47
+
48
+ const hasStreamingMessage = messages?.some(
49
+ (m: UIMessage) => m.status === "streaming",
50
+ );
51
+
52
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
53
+ e.preventDefault();
54
+ const text = input.trim();
55
+ if (!text || isLoading) return;
56
+
57
+ setIsLoading(true);
58
+ setInput("");
59
+
60
+ try {
61
+ let currentThreadId = threadId;
62
+ if (!currentThreadId) {
63
+ currentThreadId = await createThread();
64
+ setThreadId(currentThreadId);
65
+ }
66
+
67
+ await sendMessage({ threadId: currentThreadId, prompt: text });
68
+ } catch (error) {
69
+ console.error("Failed to send message:", error);
70
+ } finally {
71
+ setIsLoading(false);
72
+ }
73
+ };
74
+
75
+ return (
76
+ <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
77
+ <div className="overflow-y-auto space-y-4 pb-4">
78
+ {!messages || messages.length === 0 ? (
79
+ <div className="text-center text-muted-foreground mt-8">
80
+ Ask me anything to get started!
81
+ </div>
82
+ ) : (
83
+ messages.map((message: UIMessage) => (
84
+ <div
85
+ key={message.key}
86
+ className={`p-3 rounded-lg ${
87
+ message.role === "user"
88
+ ? "bg-primary/10 ml-8"
89
+ : "bg-secondary/20 mr-8"
90
+ }`}
91
+ >
92
+ <p className="text-sm font-semibold mb-1">
93
+ {message.role === "user" ? "You" : "AI Assistant"}
94
+ </p>
95
+ <MessageContent
96
+ text={message.text ?? ""}
97
+ isStreaming={message.status === "streaming"}
98
+ />
99
+ </div>
100
+ ))
101
+ )}
102
+ {isLoading && !hasStreamingMessage && (
103
+ <div className="p-3 rounded-lg bg-secondary/20 mr-8">
104
+ <p className="text-sm font-semibold mb-1">AI Assistant</p>
105
+ <div className="flex items-center gap-2 text-muted-foreground">
106
+ <Loader2 className="h-4 w-4 animate-spin" />
107
+ <span>Thinking...</span>
108
+ </div>
109
+ </div>
110
+ )}
111
+ <div ref={messagesEndRef} />
112
+ </div>
113
+
114
+ <form
115
+ onSubmit={handleSubmit}
116
+ className="w-full flex items-center space-x-2 pt-2 border-t"
117
+ >
118
+ <Input
119
+ name="prompt"
120
+ value={input}
121
+ onChange={(e) => setInput(e.target.value)}
122
+ placeholder="Type your message..."
123
+ className="flex-1"
124
+ autoComplete="off"
125
+ autoFocus
126
+ disabled={isLoading}
127
+ />
128
+ <Button type="submit" size="icon" disabled={isLoading || !input.trim()}>
129
+ {isLoading ? (
130
+ <Loader2 className="h-4 w-4 animate-spin" />
131
+ ) : (
132
+ <Send size={18} />
133
+ )}
134
+ </Button>
135
+ </form>
136
+ </div>
137
+ );
138
+ };
139
+
140
+ export default AI;
141
+ {{else}}
1
142
  import React, { useRef, useEffect, useState } from "react";
2
143
  import { useChat } from "@ai-sdk/react";
3
144
  import { DefaultChatTransport } from "ai";
4
- import { Input } from "@/components/ui/input";
5
- import { Button } from "@/components/ui/button";
6
145
  import { Send } from "lucide-react";
7
- import { Response } from "@/components/response";
146
+ import { Streamdown } from "streamdown";
147
+
148
+ import { Button } from "@/components/ui/button";
149
+ import { Input } from "@/components/ui/input";
8
150
 
9
151
  const AI: React.FC = () => {
10
152
  const [input, setInput] = useState("");
11
- const { messages, sendMessage } = useChat({
153
+ const { messages, sendMessage, status } = useChat({
12
154
  transport: new DefaultChatTransport({
13
155
  api: `${import.meta.env.VITE_SERVER_URL}/ai`,
14
156
  }),
@@ -50,7 +192,14 @@ const AI: React.FC = () => {
50
192
  </p>
51
193
  {message.parts?.map((part, index) => {
52
194
  if (part.type === "text") {
53
- return <Response key={index}>{part.text}</Response>;
195
+ return (
196
+ <Streamdown
197
+ key={index}
198
+ isAnimating={status === "streaming" && message.role === "assistant"}
199
+ >
200
+ {part.text}
201
+ </Streamdown>
202
+ );
54
203
  }
55
204
  return null;
56
205
  })}
@@ -81,4 +230,5 @@ const AI: React.FC = () => {
81
230
  );
82
231
  };
83
232
 
84
- export default AI;
233
+ export default AI;
234
+ {{/if}}