create-better-t-stack 3.10.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/bin/create-better-t-stack +94 -0
- package/package.json +27 -31
- package/scripts/postinstall.mjs +129 -0
- 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/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-QkFdHtZE.mjs +0 -7072
- 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,3 +1,181 @@
|
|
|
1
|
+
{{#if (eq backend "convex")}}
|
|
2
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
3
|
+
import {
|
|
4
|
+
useUIMessages,
|
|
5
|
+
useSmoothText,
|
|
6
|
+
type UIMessage,
|
|
7
|
+
} from "@convex-dev/agent/react";
|
|
8
|
+
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
|
9
|
+
import { useMutation } from "convex/react";
|
|
10
|
+
import { Card, useThemeColor } from "heroui-native";
|
|
11
|
+
import { useRef, useEffect, useState } from "react";
|
|
12
|
+
import {
|
|
13
|
+
View,
|
|
14
|
+
Text,
|
|
15
|
+
TextInput,
|
|
16
|
+
Pressable,
|
|
17
|
+
ScrollView,
|
|
18
|
+
KeyboardAvoidingView,
|
|
19
|
+
Platform,
|
|
20
|
+
ActivityIndicator,
|
|
21
|
+
} from "react-native";
|
|
22
|
+
|
|
23
|
+
import { Container } from "@/components/container";
|
|
24
|
+
|
|
25
|
+
function MessageContent({
|
|
26
|
+
text,
|
|
27
|
+
isStreaming,
|
|
28
|
+
}: {
|
|
29
|
+
text: string;
|
|
30
|
+
isStreaming: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const [visibleText] = useSmoothText(text, {
|
|
33
|
+
startStreaming: isStreaming,
|
|
34
|
+
});
|
|
35
|
+
return <Text className="text-foreground leading-relaxed">{visibleText}</Text>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function AIScreen() {
|
|
39
|
+
const [input, setInput] = useState("");
|
|
40
|
+
const [threadId, setThreadId] = useState<string | null>(null);
|
|
41
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
42
|
+
const scrollViewRef = useRef<ScrollView>(null);
|
|
43
|
+
const mutedColor = useThemeColor("muted");
|
|
44
|
+
const accentColor = useThemeColor("accent");
|
|
45
|
+
const foregroundColor = useThemeColor("foreground");
|
|
46
|
+
|
|
47
|
+
const createThread = useMutation(api.chat.createNewThread);
|
|
48
|
+
const sendMessage = useMutation(api.chat.sendMessage);
|
|
49
|
+
|
|
50
|
+
const { results: messages } = useUIMessages(
|
|
51
|
+
api.chat.listMessages,
|
|
52
|
+
threadId ? { threadId } : "skip",
|
|
53
|
+
{ initialNumItems: 50, stream: true },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const hasStreamingMessage = messages?.some(
|
|
57
|
+
(m: UIMessage) => m.status === "streaming",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
scrollViewRef.current?.scrollToEnd({ animated: true });
|
|
62
|
+
}, [messages]);
|
|
63
|
+
|
|
64
|
+
const onSubmit = async () => {
|
|
65
|
+
const value = input.trim();
|
|
66
|
+
if (!value || isLoading) return;
|
|
67
|
+
|
|
68
|
+
setIsLoading(true);
|
|
69
|
+
setInput("");
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
let currentThreadId = threadId;
|
|
73
|
+
if (!currentThreadId) {
|
|
74
|
+
currentThreadId = await createThread();
|
|
75
|
+
setThreadId(currentThreadId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await sendMessage({ threadId: currentThreadId, prompt: value });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("Failed to send message:", error);
|
|
81
|
+
} finally {
|
|
82
|
+
setIsLoading(false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Container>
|
|
88
|
+
<KeyboardAvoidingView
|
|
89
|
+
className="flex-1"
|
|
90
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
91
|
+
>
|
|
92
|
+
<View className="flex-1 px-4 py-6">
|
|
93
|
+
<View className="mb-6">
|
|
94
|
+
<Text className="text-foreground text-2xl font-bold mb-2">
|
|
95
|
+
AI Chat
|
|
96
|
+
</Text>
|
|
97
|
+
<Text className="text-muted">Chat with our AI assistant</Text>
|
|
98
|
+
</View>
|
|
99
|
+
<ScrollView
|
|
100
|
+
ref={scrollViewRef}
|
|
101
|
+
className="flex-1 mb-4"
|
|
102
|
+
showsVerticalScrollIndicator={false}
|
|
103
|
+
>
|
|
104
|
+
{!messages || messages.length === 0 ? (
|
|
105
|
+
<View className="flex-1 justify-center items-center">
|
|
106
|
+
<Text className="text-center text-muted text-lg">
|
|
107
|
+
Ask me anything to get started!
|
|
108
|
+
</Text>
|
|
109
|
+
</View>
|
|
110
|
+
) : (
|
|
111
|
+
<View className="gap-3">
|
|
112
|
+
{messages.map((message: UIMessage) => (
|
|
113
|
+
<Card
|
|
114
|
+
key={message.key}
|
|
115
|
+
variant="secondary"
|
|
116
|
+
className={`p-3 ${message.role === "user" ? "ml-8 bg-accent/10" : "mr-8"}`}
|
|
117
|
+
>
|
|
118
|
+
<Text className="text-sm font-semibold mb-1 text-foreground">
|
|
119
|
+
{message.role === "user" ? "You" : "AI Assistant"}
|
|
120
|
+
</Text>
|
|
121
|
+
<MessageContent
|
|
122
|
+
text={message.text ?? ""}
|
|
123
|
+
isStreaming={message.status === "streaming"}
|
|
124
|
+
/>
|
|
125
|
+
</Card>
|
|
126
|
+
))}
|
|
127
|
+
{isLoading && !hasStreamingMessage && (
|
|
128
|
+
<Card variant="secondary" className="p-3 mr-8">
|
|
129
|
+
<Text className="text-sm font-semibold mb-1 text-foreground">
|
|
130
|
+
AI Assistant
|
|
131
|
+
</Text>
|
|
132
|
+
<View className="flex-row items-center gap-2">
|
|
133
|
+
<ActivityIndicator size="small" color={accentColor} />
|
|
134
|
+
<Text className="text-muted">Thinking...</Text>
|
|
135
|
+
</View>
|
|
136
|
+
</Card>
|
|
137
|
+
)}
|
|
138
|
+
</View>
|
|
139
|
+
)}
|
|
140
|
+
</ScrollView>
|
|
141
|
+
<View className="border-t border-divider pt-4">
|
|
142
|
+
<View className="flex-row items-end gap-2">
|
|
143
|
+
<TextInput
|
|
144
|
+
value={input}
|
|
145
|
+
onChangeText={setInput}
|
|
146
|
+
placeholder="Type your message..."
|
|
147
|
+
placeholderTextColor={mutedColor}
|
|
148
|
+
className="flex-1 border border-divider rounded-lg px-3 py-2 text-foreground bg-surface min-h-10 max-h-30"
|
|
149
|
+
onSubmitEditing={(e) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
onSubmit();
|
|
152
|
+
}}
|
|
153
|
+
editable={!isLoading}
|
|
154
|
+
autoFocus={true}
|
|
155
|
+
/>
|
|
156
|
+
<Pressable
|
|
157
|
+
onPress={onSubmit}
|
|
158
|
+
disabled={!input.trim() || isLoading}
|
|
159
|
+
className={`p-2 rounded-lg active:opacity-70 ${
|
|
160
|
+
input.trim() && !isLoading ? "bg-accent" : "bg-surface"
|
|
161
|
+
}`}
|
|
162
|
+
>
|
|
163
|
+
<Ionicons
|
|
164
|
+
name="send"
|
|
165
|
+
size={20}
|
|
166
|
+
color={
|
|
167
|
+
input.trim() && !isLoading ? foregroundColor : mutedColor
|
|
168
|
+
}
|
|
169
|
+
/>
|
|
170
|
+
</Pressable>
|
|
171
|
+
</View>
|
|
172
|
+
</View>
|
|
173
|
+
</View>
|
|
174
|
+
</KeyboardAvoidingView>
|
|
175
|
+
</Container>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
{{else}}
|
|
1
179
|
import { useRef, useEffect, useState } from "react";
|
|
2
180
|
import {
|
|
3
181
|
View,
|
|
@@ -168,4 +346,5 @@ export default function AIScreen() {
|
|
|
168
346
|
</KeyboardAvoidingView>
|
|
169
347
|
</Container>
|
|
170
348
|
);
|
|
171
|
-
}
|
|
349
|
+
}
|
|
350
|
+
{{/if}}
|
|
@@ -1,21 +1,170 @@
|
|
|
1
|
+
{{#if (eq backend "convex")}}
|
|
1
2
|
"use client";
|
|
2
3
|
|
|
3
|
-
import {
|
|
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}}
|