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,147 @@
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 { createFileRoute } from "@tanstack/react-router";
9
+ import { useMutation } from "convex/react";
10
+ import { Send, Loader2 } from "lucide-react";
11
+ import { useRef, useEffect, useState } from "react";
12
+ import { Streamdown } from "streamdown";
13
+
14
+ import { Button } from "@/components/ui/button";
15
+ import { Input } from "@/components/ui/input";
16
+
17
+ export const Route = createFileRoute("/ai")({
18
+ component: RouteComponent,
19
+ });
20
+
21
+ function MessageContent({
22
+ text,
23
+ isStreaming,
24
+ }: {
25
+ text: string;
26
+ isStreaming: boolean;
27
+ }) {
28
+ const [visibleText] = useSmoothText(text, {
29
+ startStreaming: isStreaming,
30
+ });
31
+ return <Streamdown>{visibleText}</Streamdown>;
32
+ }
33
+
34
+ function RouteComponent() {
35
+ const [input, setInput] = useState("");
36
+ const [threadId, setThreadId] = useState<string | null>(null);
37
+ const [isLoading, setIsLoading] = useState(false);
38
+ const messagesEndRef = useRef<HTMLDivElement>(null);
39
+
40
+ const createThread = useMutation(api.chat.createNewThread);
41
+ const sendMessage = useMutation(api.chat.sendMessage);
42
+
43
+ const { results: messages } = useUIMessages(
44
+ api.chat.listMessages,
45
+ threadId ? { threadId } : "skip",
46
+ { initialNumItems: 50, stream: true },
47
+ );
48
+
49
+ useEffect(() => {
50
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
51
+ }, [messages]);
52
+
53
+ const hasStreamingMessage = messages?.some(
54
+ (m: UIMessage) => m.status === "streaming",
55
+ );
56
+
57
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
58
+ e.preventDefault();
59
+ const text = input.trim();
60
+ if (!text || isLoading) return;
61
+
62
+ setIsLoading(true);
63
+ setInput("");
64
+
65
+ try {
66
+ let currentThreadId = threadId;
67
+ if (!currentThreadId) {
68
+ currentThreadId = await createThread();
69
+ setThreadId(currentThreadId);
70
+ }
71
+
72
+ await sendMessage({ threadId: currentThreadId, prompt: text });
73
+ } catch (error) {
74
+ console.error("Failed to send message:", error);
75
+ } finally {
76
+ setIsLoading(false);
77
+ }
78
+ };
79
+
80
+ return (
81
+ <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
82
+ <div className="overflow-y-auto space-y-4 pb-4">
83
+ {!messages || messages.length === 0 ? (
84
+ <div className="text-center text-muted-foreground mt-8">
85
+ Ask me anything to get started!
86
+ </div>
87
+ ) : (
88
+ messages.map((message: UIMessage) => (
89
+ <div
90
+ key={message.key}
91
+ className={`p-3 rounded-lg ${
92
+ message.role === "user"
93
+ ? "bg-primary/10 ml-8"
94
+ : "bg-secondary/20 mr-8"
95
+ }`}
96
+ >
97
+ <p className="text-sm font-semibold mb-1">
98
+ {message.role === "user" ? "You" : "AI Assistant"}
99
+ </p>
100
+ <MessageContent
101
+ text={message.text ?? ""}
102
+ isStreaming={message.status === "streaming"}
103
+ />
104
+ </div>
105
+ ))
106
+ )}
107
+ {isLoading && !hasStreamingMessage && (
108
+ <div className="p-3 rounded-lg bg-secondary/20 mr-8">
109
+ <p className="text-sm font-semibold mb-1">AI Assistant</p>
110
+ <div className="flex items-center gap-2 text-muted-foreground">
111
+ <Loader2 className="h-4 w-4 animate-spin" />
112
+ <span>Thinking...</span>
113
+ </div>
114
+ </div>
115
+ )}
116
+ <div ref={messagesEndRef} />
117
+ </div>
118
+
119
+ <form
120
+ onSubmit={handleSubmit}
121
+ className="w-full flex items-center space-x-2 pt-2 border-t"
122
+ >
123
+ <Input
124
+ name="prompt"
125
+ value={input}
126
+ onChange={(e) => setInput(e.target.value)}
127
+ placeholder="Type your message..."
128
+ className="flex-1"
129
+ autoComplete="off"
130
+ autoFocus
131
+ disabled={isLoading}
132
+ />
133
+ <Button type="submit" size="icon" disabled={isLoading || !input.trim()}>
134
+ {isLoading ? (
135
+ <Loader2 className="h-4 w-4 animate-spin" />
136
+ ) : (
137
+ <Send size={18} />
138
+ )}
139
+ </Button>
140
+ </form>
141
+ </div>
142
+ );
143
+ }
144
+ {{else}}
1
145
  import { createFileRoute } from "@tanstack/react-router";
2
146
  import { useChat } from "@ai-sdk/react";
3
147
  import { DefaultChatTransport } from "ai";
@@ -5,7 +149,7 @@ import { Input } from "@/components/ui/input";
5
149
  import { Button } from "@/components/ui/button";
6
150
  import { Send } from "lucide-react";
7
151
  import { useRef, useEffect, useState } from "react";
8
- import { Response } from "@/components/response";
152
+ import { Streamdown } from "streamdown";
9
153
 
10
154
  export const Route = createFileRoute("/ai")({
11
155
  component: RouteComponent,
@@ -13,9 +157,9 @@ export const Route = createFileRoute("/ai")({
13
157
 
14
158
  function RouteComponent() {
15
159
  const [input, setInput] = useState("");
16
- const { messages, sendMessage } = useChat({
160
+ const { messages, sendMessage, status } = useChat({
17
161
  transport: new DefaultChatTransport({
18
- api: `${import.meta.env.VITE_SERVER_URL}/ai`,
162
+ api: {{#if (eq backend "self")}}"/api/ai"{{else}}`${import.meta.env.VITE_SERVER_URL}/ai`{{/if}},
19
163
  }),
20
164
  });
21
165
 
@@ -55,7 +199,14 @@ function RouteComponent() {
55
199
  </p>
56
200
  {message.parts?.map((part, index) => {
57
201
  if (part.type === "text") {
58
- return <Response key={index}>{part.text}</Response>;
202
+ return (
203
+ <Streamdown
204
+ key={index}
205
+ isAnimating={status === "streaming" && message.role === "assistant"}
206
+ >
207
+ {part.text}
208
+ </Streamdown>
209
+ );
59
210
  }
60
211
  return null;
61
212
  })}
@@ -85,3 +236,4 @@ function RouteComponent() {
85
236
  </div>
86
237
  );
87
238
  }
239
+ {{/if}}
@@ -1,11 +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 { createFileRoute } from "@tanstack/react-router";
9
+ import { useMutation } from "convex/react";
10
+ import { Send, Loader2 } from "lucide-react";
11
+ import { useRef, useEffect, useState } from "react";
12
+ import { Streamdown } from "streamdown";
13
+
14
+ import { Button } from "@/components/ui/button";
15
+ import { Input } from "@/components/ui/input";
16
+
17
+ export const Route = createFileRoute("/ai")({
18
+ component: RouteComponent,
19
+ });
20
+
21
+ function MessageContent({
22
+ text,
23
+ isStreaming,
24
+ }: {
25
+ text: string;
26
+ isStreaming: boolean;
27
+ }) {
28
+ const [visibleText] = useSmoothText(text, {
29
+ startStreaming: isStreaming,
30
+ });
31
+ return <Streamdown>{visibleText}</Streamdown>;
32
+ }
33
+
34
+ function RouteComponent() {
35
+ const [input, setInput] = useState("");
36
+ const [threadId, setThreadId] = useState<string | null>(null);
37
+ const [isLoading, setIsLoading] = useState(false);
38
+ const messagesEndRef = useRef<HTMLDivElement>(null);
39
+
40
+ const createThread = useMutation(api.chat.createNewThread);
41
+ const sendMessage = useMutation(api.chat.sendMessage);
42
+
43
+ const { results: messages } = useUIMessages(
44
+ api.chat.listMessages,
45
+ threadId ? { threadId } : "skip",
46
+ { initialNumItems: 50, stream: true },
47
+ );
48
+
49
+ useEffect(() => {
50
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
51
+ }, [messages]);
52
+
53
+ const hasStreamingMessage = messages?.some(
54
+ (m: UIMessage) => m.status === "streaming",
55
+ );
56
+
57
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
58
+ e.preventDefault();
59
+ const text = input.trim();
60
+ if (!text || isLoading) return;
61
+
62
+ setIsLoading(true);
63
+ setInput("");
64
+
65
+ try {
66
+ let currentThreadId = threadId;
67
+ if (!currentThreadId) {
68
+ currentThreadId = await createThread();
69
+ setThreadId(currentThreadId);
70
+ }
71
+
72
+ await sendMessage({ threadId: currentThreadId, prompt: text });
73
+ } catch (error) {
74
+ console.error("Failed to send message:", error);
75
+ } finally {
76
+ setIsLoading(false);
77
+ }
78
+ };
79
+
80
+ return (
81
+ <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
82
+ <div className="overflow-y-auto space-y-4 pb-4">
83
+ {!messages || messages.length === 0 ? (
84
+ <div className="text-center text-muted-foreground mt-8">
85
+ Ask me anything to get started!
86
+ </div>
87
+ ) : (
88
+ messages.map((message: UIMessage) => (
89
+ <div
90
+ key={message.key}
91
+ className={`p-3 rounded-lg ${
92
+ message.role === "user"
93
+ ? "bg-primary/10 ml-8"
94
+ : "bg-secondary/20 mr-8"
95
+ }`}
96
+ >
97
+ <p className="text-sm font-semibold mb-1">
98
+ {message.role === "user" ? "You" : "AI Assistant"}
99
+ </p>
100
+ <MessageContent
101
+ text={message.text ?? ""}
102
+ isStreaming={message.status === "streaming"}
103
+ />
104
+ </div>
105
+ ))
106
+ )}
107
+ {isLoading && !hasStreamingMessage && (
108
+ <div className="p-3 rounded-lg bg-secondary/20 mr-8">
109
+ <p className="text-sm font-semibold mb-1">AI Assistant</p>
110
+ <div className="flex items-center gap-2 text-muted-foreground">
111
+ <Loader2 className="h-4 w-4 animate-spin" />
112
+ <span>Thinking...</span>
113
+ </div>
114
+ </div>
115
+ )}
116
+ <div ref={messagesEndRef} />
117
+ </div>
118
+
119
+ <form
120
+ onSubmit={handleSubmit}
121
+ className="w-full flex items-center space-x-2 pt-2 border-t"
122
+ >
123
+ <Input
124
+ name="prompt"
125
+ value={input}
126
+ onChange={(e) => setInput(e.target.value)}
127
+ placeholder="Type your message..."
128
+ className="flex-1"
129
+ autoComplete="off"
130
+ autoFocus
131
+ disabled={isLoading}
132
+ />
133
+ <Button type="submit" size="icon" disabled={isLoading || !input.trim()}>
134
+ {isLoading ? (
135
+ <Loader2 className="h-4 w-4 animate-spin" />
136
+ ) : (
137
+ <Send size={18} />
138
+ )}
139
+ </Button>
140
+ </form>
141
+ </div>
142
+ );
143
+ }
144
+ {{else}}
1
145
  import { createFileRoute } from "@tanstack/react-router";
2
146
  import { useChat } from "@ai-sdk/react";
3
147
  import { DefaultChatTransport } from "ai";
4
- import { Input } from "@/components/ui/input";
5
- import { Button } from "@/components/ui/button";
6
148
  import { Send } from "lucide-react";
7
149
  import { useRef, useEffect, useState } from "react";
8
- import { Response } from "@/components/response";
150
+ import { Streamdown } from "streamdown";
151
+
152
+ import { Button } from "@/components/ui/button";
153
+ import { Input } from "@/components/ui/input";
9
154
 
10
155
  export const Route = createFileRoute("/ai")({
11
156
  component: RouteComponent,
@@ -13,7 +158,7 @@ export const Route = createFileRoute("/ai")({
13
158
 
14
159
  function RouteComponent() {
15
160
  const [input, setInput] = useState("");
16
- const { messages, sendMessage } = useChat({
161
+ const { messages, sendMessage, status } = useChat({
17
162
  transport: new DefaultChatTransport({
18
163
  api: {{#if (eq backend "self")}}"/api/ai"{{else}}`${import.meta.env.VITE_SERVER_URL}/ai`{{/if}},
19
164
  }),
@@ -55,7 +200,14 @@ function RouteComponent() {
55
200
  </p>
56
201
  {message.parts?.map((part, index) => {
57
202
  if (part.type === "text") {
58
- return <Response key={index}>{part.text}</Response>;
203
+ return (
204
+ <Streamdown
205
+ key={index}
206
+ isAnimating={status === "streaming" && message.role === "assistant"}
207
+ >
208
+ {part.text}
209
+ </Streamdown>
210
+ );
59
211
  }
60
212
  return null;
61
213
  })}
@@ -84,4 +236,5 @@ function RouteComponent() {
84
236
  </form>
85
237
  </div>
86
238
  );
87
- }
239
+ }
240
+ {{/if}}
@@ -3,20 +3,21 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "dev": "next dev",
6
+ "dev": "next dev --port 3001",
7
7
  "build": "next build",
8
8
  "start": "next start"
9
9
  },
10
10
  "dependencies": {
11
- "radix-ui": "^1.4.2",
11
+ "@base-ui/react": "^1.0.0",
12
+ "shadcn": "^3.6.2",
12
13
  "@tanstack/react-form": "^1.27.3",
13
14
  "class-variance-authority": "^0.7.1",
14
15
  "clsx": "^2.1.1",
15
16
  "lucide-react": "^0.546.0",
16
- "next": "^16.0.10",
17
+ "next": "^16.1.0",
17
18
  "next-themes": "^0.4.6",
18
- "react": "19.2.3",
19
- "react-dom": "19.2.3",
19
+ "react": "^19.2.3",
20
+ "react-dom": "^19.2.3",
20
21
  "sonner": "^2.0.5",
21
22
  "tailwind-merge": "^3.3.1",
22
23
  "tw-animate-css": "^1.3.4",
@@ -25,8 +26,8 @@
25
26
  "devDependencies": {
26
27
  "@tailwindcss/postcss": "^4.1.10",
27
28
  "@types/node": "^20",
28
- "@types/react": "19.2.7",
29
- "@types/react-dom": "19.2.3",
29
+ "@types/react": "^19.2.7",
30
+ "@types/react-dom": "^19.2.3",
30
31
  "tailwindcss": "^4.1.10",
31
32
  "typescript": "^5"
32
33
  }
@@ -2,7 +2,10 @@ import type { Metadata } from "next";
2
2
  import { Geist, Geist_Mono } from "next/font/google";
3
3
  import "../index.css";
4
4
  {{#if (eq auth "clerk")}}{{#if (eq backend "convex")}}import { ClerkProvider } from "@clerk/nextjs";
5
- {{/if}}{{/if}}import Providers from "@/components/providers";
5
+ {{/if}}{{/if}}{{#if (and (eq backend "convex") (eq auth "better-auth"))}}
6
+ import { getToken } from "@/lib/auth-server";
7
+ {{/if}}
8
+ import Providers from "@/components/providers";
6
9
  import Header from "@/components/header";
7
10
 
8
11
  const geistSans = Geist({
@@ -20,6 +23,29 @@ export const metadata: Metadata = {
20
23
  description: "{{projectName}}",
21
24
  };
22
25
 
26
+ {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
27
+ export default async function RootLayout({
28
+ children,
29
+ }: Readonly<{
30
+ children: React.ReactNode;
31
+ }>) {
32
+ const token = await getToken();
33
+ return (
34
+ <html lang="en" suppressHydrationWarning>
35
+ <body
36
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
37
+ >
38
+ <Providers initialToken={token}>
39
+ <div className="grid grid-rows-[auto_1fr] h-svh">
40
+ <Header />
41
+ {children}
42
+ </div>
43
+ </Providers>
44
+ </body>
45
+ </html>
46
+ );
47
+ }
48
+ {{else}}
23
49
  export default function RootLayout({
24
50
  children,
25
51
  }: Readonly<{
@@ -47,3 +73,4 @@ export default function RootLayout({
47
73
  </html>
48
74
  );
49
75
  }
76
+ {{/if}}
@@ -16,12 +16,10 @@ export function ModeToggle() {
16
16
 
17
17
  return (
18
18
  <DropdownMenu>
19
- <DropdownMenuTrigger asChild>
20
- <Button variant="outline" size="icon">
21
- <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
22
- <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
23
- <span className="sr-only">Toggle theme</span>
24
- </Button>
19
+ <DropdownMenuTrigger render={<Button variant="outline" size="icon" />}>
20
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
21
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
22
+ <span className="sr-only">Toggle theme</span>
25
23
  </DropdownMenuTrigger>
26
24
  <DropdownMenuContent align="end">
27
25
  <DropdownMenuItem onClick={() => setTheme("light")}>
@@ -6,7 +6,7 @@ import { useAuth } from "@clerk/nextjs";
6
6
  import { ConvexReactClient } from "convex/react";
7
7
  import { ConvexProviderWithClerk } from "convex/react-clerk";
8
8
  {{else if (eq auth "better-auth")}}
9
- import { ConvexProvider, ConvexReactClient } from "convex/react";
9
+ import { ConvexReactClient } from "convex/react";
10
10
  import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
11
11
  import { authClient } from "@/lib/auth-client";
12
12
  {{else}}
@@ -32,9 +32,15 @@ const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
32
32
  {{/if}}
33
33
 
34
34
  export default function Providers({
35
- children
35
+ children,
36
+ {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
37
+ initialToken,
38
+ {{/if}}
36
39
  }: {
37
- children: React.ReactNode
40
+ children: React.ReactNode;
41
+ {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
42
+ initialToken?: string | null;
43
+ {{/if}}
38
44
  }) {
39
45
  return (
40
46
  <ThemeProvider
@@ -49,7 +55,11 @@ export default function Providers({
49
55
  {children}
50
56
  </ConvexProviderWithClerk>
51
57
  {{else if (eq auth "better-auth")}}
52
- <ConvexBetterAuthProvider client={convex} authClient={authClient}>
58
+ <ConvexBetterAuthProvider
59
+ client={convex}
60
+ authClient={authClient}
61
+ initialToken={initialToken}
62
+ >
53
63
  {children}
54
64
  </ConvexBetterAuthProvider>
55
65
  {{else}}
@@ -9,7 +9,8 @@
9
9
  "typecheck": "react-router typegen && tsc"
10
10
  },
11
11
  "dependencies": {
12
- "radix-ui": "^1.4.2",
12
+ "@base-ui/react": "^1.0.0",
13
+ "shadcn": "^3.6.2",
13
14
  "@react-router/fs-routes": "^7.10.1",
14
15
  "@react-router/node": "^7.10.1",
15
16
  "@react-router/serve": "^7.10.1",
@@ -14,12 +14,10 @@ export function ModeToggle() {
14
14
 
15
15
  return (
16
16
  <DropdownMenu>
17
- <DropdownMenuTrigger asChild>
18
- <Button variant="outline" size="icon">
19
- <Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
20
- <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
21
- <span className="sr-only">Toggle theme</span>
22
- </Button>
17
+ <DropdownMenuTrigger render={<Button variant="outline" size="icon" />}>
18
+ <Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
19
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
20
+ <span className="sr-only">Toggle theme</span>
23
21
  </DropdownMenuTrigger>
24
22
  <DropdownMenuContent align="end">
25
23
  <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@hookform/resolvers": "^5.1.1",
15
- "radix-ui": "^1.4.2",
15
+ "@base-ui/react": "^1.0.0",
16
+ "shadcn": "^3.6.2",
16
17
  "@tanstack/react-form": "^1.12.3",
17
18
  "@tailwindcss/vite": "^4.0.15",
18
19
  "@tanstack/react-router": "^1.141.1",
@@ -14,12 +14,10 @@ export function ModeToggle() {
14
14
 
15
15
  return (
16
16
  <DropdownMenu>
17
- <DropdownMenuTrigger asChild>
18
- <Button variant="outline" size="icon">
19
- <Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
20
- <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
21
- <span className="sr-only">Toggle theme</span>
22
- </Button>
17
+ <DropdownMenuTrigger render={<Button variant="outline" size="icon" />}>
18
+ <Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
19
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
20
+ <span className="sr-only">Toggle theme</span>
23
21
  </DropdownMenuTrigger>
24
22
  <DropdownMenuContent align="end">
25
23
  <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
@@ -8,7 +8,8 @@
8
8
  "dev": "vite dev"
9
9
  },
10
10
  "dependencies": {
11
- "radix-ui": "^1.4.2",
11
+ "@base-ui/react": "^1.0.0",
12
+ "shadcn": "^3.6.2",
12
13
  "@tanstack/react-form": "^1.23.5",
13
14
  "@tailwindcss/vite": "^4.1.8",
14
15
  "@tanstack/react-query": "^5.80.6",
@@ -35,7 +35,13 @@ export function getRouter() {
35
35
  unsavedChangesWarning: false,
36
36
  });
37
37
 
38
+ {{#if (eq auth "better-auth")}}
39
+ const convexQueryClient = new ConvexQueryClient(convex, {
40
+ expectAuth: true,
41
+ });
42
+ {{else}}
38
43
  const convexQueryClient = new ConvexQueryClient(convex);
44
+ {{/if}}
39
45
 
40
46
  const queryClient: QueryClient = new QueryClient({
41
47
  defaultOptions: {
@@ -37,20 +37,12 @@ const fetchClerkAuth = createServerFn({ method: "GET" }).handler(async () => {
37
37
  });
38
38
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
39
39
  import { createServerFn } from "@tanstack/react-start";
40
- import { getRequest, getCookie } from "@tanstack/react-start/server";
41
40
  import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
42
- import { fetchSession, getCookieName } from "@convex-dev/better-auth/react-start";
43
41
  import { authClient } from "@/lib/auth-client";
44
- import { createAuth } from "@{{projectName}}/backend/convex/auth";
42
+ import { getToken } from "@/lib/auth-server";
45
43
 
46
- const fetchAuth = createServerFn({ method: "GET" }).handler(async () => {
47
- const { session } = await fetchSession(getRequest());
48
- const sessionCookieName = getCookieName(createAuth);
49
- const token = getCookie(sessionCookieName);
50
- return {
51
- userId: session?.user.id,
52
- token,
53
- };
44
+ const getAuth = createServerFn({ method: "GET" }).handler(async () => {
45
+ return await getToken();
54
46
  });
55
47
  {{/if}}
56
48
 
@@ -113,11 +105,14 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
113
105
  },
114
106
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
115
107
  beforeLoad: async (ctx) => {
116
- const { userId, token } = await fetchAuth();
108
+ const token = await getAuth();
117
109
  if (token) {
118
110
  ctx.context.convexQueryClient.serverHttpClient?.setAuth(token);
119
111
  }
120
- return { userId, token };
112
+ return {
113
+ isAuthenticated: !!token,
114
+ token,
115
+ };
121
116
  },
122
117
  {{/if}}
123
118
  });
@@ -148,7 +143,11 @@ function RootDocument() {
148
143
  {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
149
144
  const context = useRouteContext({ from: Route.id });
150
145
  return (
151
- <ConvexBetterAuthProvider client={context.convexClient} authClient={authClient}>
146
+ <ConvexBetterAuthProvider
147
+ client={context.convexClient}
148
+ authClient={authClient}
149
+ initialToken={context.token}
150
+ >
152
151
  <html lang="en" className="dark">
153
152
  <head>
154
153
  <HeadContent />
@@ -14,4 +14,9 @@ export default defineConfig({
14
14
  server: {
15
15
  port: 3001,
16
16
  },
17
+ {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
18
+ ssr: {
19
+ noExternal: ["@convex-dev/better-auth"],
20
+ },
21
+ {{/if}}
17
22
  });