create-better-t-stack 3.10.0 → 3.11.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.
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +7 -3
- package/dist/index.mjs +1 -1
- package/dist/{src-QkFdHtZE.mjs → src-XVvJUQ_h.mjs} +257 -83
- package/package.json +8 -8
- package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +1 -1
- package/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +17 -0
- 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/web-base/src/index.css.hbs +1 -1
- 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
|
@@ -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}}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { type ComponentProps, memo } from "react";
|
|
4
|
-
import { Streamdown } from "streamdown";
|
|
5
|
-
import { cn } from "@/lib/utils";
|
|
6
|
-
|
|
7
|
-
type ResponseProps = ComponentProps<typeof Streamdown>;
|
|
8
|
-
|
|
9
|
-
export const Response = memo(
|
|
10
|
-
({ className, ...props }: ResponseProps) => (
|
|
11
|
-
<Streamdown
|
|
12
|
-
className={cn(
|
|
13
|
-
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
|
14
|
-
className,
|
|
15
|
-
)}
|
|
16
|
-
{...props}
|
|
17
|
-
/>
|
|
18
|
-
),
|
|
19
|
-
(prevProps, nextProps) => prevProps.children === nextProps.children,
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
Response.displayName = "Response";
|