create-better-t-stack 3.9.0 → 3.11.0-pr749.7e7198c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/bin/create-better-t-stack +94 -0
- package/package.json +51 -55
- package/scripts/postinstall.mjs +129 -0
- package/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs +5 -7
- package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +17 -17
- package/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +4 -4
- package/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs +2 -2
- package/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs +10 -10
- package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs +13 -5
- package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs +14 -12
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs +13 -16
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs +11 -5
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs +4 -4
- package/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs +1 -1
- package/templates/auth/better-auth/web/react/next/src/components/user-menu.tsx.hbs +17 -15
- package/templates/auth/better-auth/web/react/react-router/src/components/user-menu.tsx.hbs +16 -15
- package/templates/auth/better-auth/web/react/tanstack-router/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
- package/templates/auth/better-auth/web/react/tanstack-start/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
- package/templates/backend/convex/packages/backend/convex/README.md +4 -4
- package/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +17 -0
- package/templates/backend/convex/packages/backend/convex/tsconfig.json.hbs +1 -1
- package/templates/examples/ai/convex/packages/backend/convex/agent.ts.hbs +9 -0
- package/templates/examples/ai/convex/packages/backend/convex/chat.ts.hbs +67 -0
- package/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs +301 -3
- package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +296 -10
- package/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs +180 -1
- package/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs +172 -9
- package/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs +156 -6
- package/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs +156 -4
- package/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +159 -6
- package/templates/frontend/react/next/package.json.hbs +8 -7
- package/templates/frontend/react/next/src/app/layout.tsx.hbs +28 -1
- package/templates/frontend/react/next/src/components/mode-toggle.tsx.hbs +4 -6
- package/templates/frontend/react/next/src/components/providers.tsx.hbs +14 -4
- package/templates/frontend/react/react-router/package.json.hbs +2 -1
- package/templates/frontend/react/{tanstack-router/src/components/mode-toggle.tsx → react-router/src/components/mode-toggle.tsx.hbs} +4 -6
- package/templates/frontend/react/tanstack-router/package.json.hbs +2 -1
- package/templates/frontend/react/{react-router/src/components/mode-toggle.tsx → tanstack-router/src/components/mode-toggle.tsx.hbs} +4 -6
- package/templates/frontend/react/tanstack-start/package.json.hbs +2 -1
- package/templates/frontend/react/tanstack-start/src/router.tsx.hbs +6 -0
- package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +13 -14
- package/templates/frontend/react/tanstack-start/vite.config.ts.hbs +5 -0
- package/templates/frontend/react/web-base/components.json +5 -2
- package/templates/frontend/react/web-base/src/components/ui/button.tsx.hbs +57 -0
- package/templates/frontend/react/web-base/src/components/ui/card.tsx.hbs +103 -0
- package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx.hbs +26 -0
- package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx.hbs +262 -0
- package/templates/frontend/react/web-base/src/components/ui/input.tsx.hbs +20 -0
- package/templates/frontend/react/web-base/src/components/ui/label.tsx.hbs +20 -0
- package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx.hbs +13 -0
- package/templates/frontend/react/web-base/src/components/ui/sonner.tsx.hbs +44 -0
- package/templates/frontend/react/web-base/src/index.css.hbs +58 -64
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -8
- package/dist/index.d.mts +0 -347
- package/dist/index.mjs +0 -4
- package/dist/src-DLvUK0Qf.mjs +0 -7069
- package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +0 -7
- package/templates/examples/ai/web/react/base/src/components/response.tsx.hbs +0 -22
- package/templates/frontend/react/web-base/src/components/ui/button.tsx +0 -56
- package/templates/frontend/react/web-base/src/components/ui/card.tsx +0 -75
- package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx +0 -27
- package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx +0 -228
- package/templates/frontend/react/web-base/src/components/ui/input.tsx +0 -21
- package/templates/frontend/react/web-base/src/components/ui/label.tsx +0 -19
- package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx +0 -13
- package/templates/frontend/react/web-base/src/components/ui/sonner.tsx +0 -25
- /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-router/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/frontend/react/react-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
- /package/templates/frontend/react/tanstack-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
- /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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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}}
|
|
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
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
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 {
|
|
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
|
|
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
|
-
"
|
|
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
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
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>
|
|
@@ -14,12 +14,10 @@ export function ModeToggle() {
|
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<DropdownMenu>
|
|
17
|
-
<DropdownMenuTrigger
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
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>
|
|
@@ -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 {
|
|
42
|
+
import { getToken } from "@/lib/auth-server";
|
|
45
43
|
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
|
108
|
+
const token = await getAuth();
|
|
117
109
|
if (token) {
|
|
118
110
|
ctx.context.convexQueryClient.serverHttpClient?.setAuth(token);
|
|
119
111
|
}
|
|
120
|
-
return {
|
|
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
|
|
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 />
|