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,21 +1,170 @@
|
|
|
1
|
+
{{#if (eq backend "convex")}}
|
|
1
2
|
"use client";
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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}}
|
|
@@ -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}}
|