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.
@@ -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}}
@@ -2,7 +2,7 @@
2
2
  @import 'tw-animate-css';
3
3
  @import 'shadcn/tailwind.css';
4
4
  {{#if (includes examples "ai")}}
5
- @source "../node_modules/streamdown/dist/index.js";
5
+ @source "../node_modules/streamdown/dist/*.js";
6
6
  {{/if}}
7
7
 
8
8
  @custom-variant dark (&:is(.dark *));
@@ -1,7 +0,0 @@
1
- import { defineApp } from "convex/server";
2
- import betterAuth from "@convex-dev/better-auth/convex.config";
3
-
4
- const app = defineApp();
5
- app.use(betterAuth);
6
-
7
- export default app;
@@ -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";