create-better-t-stack 2.30.0 → 2.32.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,15 +1,18 @@
1
- "use client"
1
+ "use client";
2
2
 
3
3
  import { useChat } from "@ai-sdk/react";
4
+ import { DefaultChatTransport } from "ai";
4
5
  import { Input } from "@/components/ui/input";
5
6
  import { Button } from "@/components/ui/button";
6
7
  import { Send } from "lucide-react";
7
- import { useRef, useEffect } from "react";
8
-
8
+ import { useRef, useEffect, useState } from "react";
9
9
 
10
10
  export default function AIPage() {
11
- const { messages, input, handleInputChange, handleSubmit } = useChat({
12
- api: `${process.env.NEXT_PUBLIC_SERVER_URL}/ai`,
11
+ const [input, setInput] = useState("");
12
+ const { messages, sendMessage } = useChat({
13
+ transport: new DefaultChatTransport({
14
+ api: `${process.env.NEXT_PUBLIC_SERVER_URL}/ai`,
15
+ }),
13
16
  });
14
17
 
15
18
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -18,6 +21,14 @@ export default function AIPage() {
18
21
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
19
22
  }, [messages]);
20
23
 
24
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
25
+ e.preventDefault();
26
+ const text = input.trim();
27
+ if (!text) return;
28
+ sendMessage({ text });
29
+ setInput("");
30
+ };
31
+
21
32
  return (
22
33
  <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
23
34
  <div className="overflow-y-auto space-y-4 pb-4">
@@ -38,7 +49,16 @@ export default function AIPage() {
38
49
  <p className="text-sm font-semibold mb-1">
39
50
  {message.role === "user" ? "You" : "AI Assistant"}
40
51
  </p>
41
- <div className="whitespace-pre-wrap">{message.content}</div>
52
+ {message.parts?.map((part, index) => {
53
+ if (part.type === "text") {
54
+ return (
55
+ <div key={index} className="whitespace-pre-wrap">
56
+ {part.text}
57
+ </div>
58
+ );
59
+ }
60
+ return null;
61
+ })}
42
62
  </div>
43
63
  ))
44
64
  )}
@@ -52,7 +72,7 @@ export default function AIPage() {
52
72
  <Input
53
73
  name="prompt"
54
74
  value={input}
55
- onChange={handleInputChange}
75
+ onChange={(e) => setInput(e.target.value)}
56
76
  placeholder="Type your message..."
57
77
  className="flex-1"
58
78
  autoComplete="off"
@@ -64,4 +84,4 @@ export default function AIPage() {
64
84
  </form>
65
85
  </div>
66
86
  );
67
- }
87
+ }
@@ -1,12 +1,16 @@
1
+ import React, { useRef, useEffect, useState } from "react";
1
2
  import { useChat } from "@ai-sdk/react";
3
+ import { DefaultChatTransport } from "ai";
2
4
  import { Input } from "@/components/ui/input";
3
5
  import { Button } from "@/components/ui/button";
4
6
  import { Send } from "lucide-react";
5
- import { useRef, useEffect } from "react";
6
7
 
7
- export default function AI() {
8
- const { messages, input, handleInputChange, handleSubmit } = useChat({
9
- api: `${import.meta.env.VITE_SERVER_URL}/ai`,
8
+ const AI: React.FC = () => {
9
+ const [input, setInput] = useState("");
10
+ const { messages, sendMessage } = useChat({
11
+ transport: new DefaultChatTransport({
12
+ api: `${import.meta.env.VITE_SERVER_URL}/ai`,
13
+ }),
10
14
  });
11
15
 
12
16
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -15,6 +19,14 @@ export default function AI() {
15
19
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
16
20
  }, [messages]);
17
21
 
22
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
23
+ e.preventDefault();
24
+ const text = input.trim();
25
+ if (!text) return;
26
+ sendMessage({ text });
27
+ setInput("");
28
+ };
29
+
18
30
  return (
19
31
  <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
20
32
  <div className="overflow-y-auto space-y-4 pb-4">
@@ -35,7 +47,16 @@ export default function AI() {
35
47
  <p className="text-sm font-semibold mb-1">
36
48
  {message.role === "user" ? "You" : "AI Assistant"}
37
49
  </p>
38
- <div className="whitespace-pre-wrap">{message.content}</div>
50
+ {message.parts?.map((part, index) => {
51
+ if (part.type === "text") {
52
+ return (
53
+ <div key={index} className="whitespace-pre-wrap">
54
+ {part.text}
55
+ </div>
56
+ );
57
+ }
58
+ return null;
59
+ })}
39
60
  </div>
40
61
  ))
41
62
  )}
@@ -49,7 +70,7 @@ export default function AI() {
49
70
  <Input
50
71
  name="prompt"
51
72
  value={input}
52
- onChange={handleInputChange}
73
+ onChange={(e) => setInput(e.target.value)}
53
74
  placeholder="Type your message..."
54
75
  className="flex-1"
55
76
  autoComplete="off"
@@ -61,4 +82,6 @@ export default function AI() {
61
82
  </form>
62
83
  </div>
63
84
  );
64
- }
85
+ };
86
+
87
+ export default AI;
@@ -1,17 +1,21 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { useChat } from "@ai-sdk/react";
3
+ import { DefaultChatTransport } from "ai";
3
4
  import { Input } from "@/components/ui/input";
4
5
  import { Button } from "@/components/ui/button";
5
6
  import { Send } from "lucide-react";
6
- import { useRef, useEffect } from "react";
7
+ import { useRef, useEffect, useState } from "react";
7
8
 
8
9
  export const Route = createFileRoute("/ai")({
9
10
  component: RouteComponent,
10
11
  });
11
12
 
12
13
  function RouteComponent() {
13
- const { messages, input, handleInputChange, handleSubmit } = useChat({
14
- api: `${import.meta.env.VITE_SERVER_URL}/ai`,
14
+ const [input, setInput] = useState("");
15
+ const { messages, sendMessage } = useChat({
16
+ transport: new DefaultChatTransport({
17
+ api: `${import.meta.env.VITE_SERVER_URL}/ai`,
18
+ }),
15
19
  });
16
20
 
17
21
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -20,6 +24,14 @@ function RouteComponent() {
20
24
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
21
25
  }, [messages]);
22
26
 
27
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
28
+ e.preventDefault();
29
+ const text = input.trim();
30
+ if (!text) return;
31
+ sendMessage({ text });
32
+ setInput("");
33
+ };
34
+
23
35
  return (
24
36
  <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
25
37
  <div className="overflow-y-auto space-y-4 pb-4">
@@ -40,7 +52,16 @@ function RouteComponent() {
40
52
  <p className="text-sm font-semibold mb-1">
41
53
  {message.role === "user" ? "You" : "AI Assistant"}
42
54
  </p>
43
- <div className="whitespace-pre-wrap">{message.content}</div>
55
+ {message.parts?.map((part, index) => {
56
+ if (part.type === "text") {
57
+ return (
58
+ <div key={index} className="whitespace-pre-wrap">
59
+ {part.text}
60
+ </div>
61
+ );
62
+ }
63
+ return null;
64
+ })}
44
65
  </div>
45
66
  ))
46
67
  )}
@@ -54,7 +75,7 @@ function RouteComponent() {
54
75
  <Input
55
76
  name="prompt"
56
77
  value={input}
57
- onChange={handleInputChange}
78
+ onChange={(e) => setInput(e.target.value)}
58
79
  placeholder="Type your message..."
59
80
  className="flex-1"
60
81
  autoComplete="off"
@@ -1,17 +1,21 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { useChat } from "@ai-sdk/react";
3
+ import { DefaultChatTransport } from "ai";
3
4
  import { Input } from "@/components/ui/input";
4
5
  import { Button } from "@/components/ui/button";
5
6
  import { Send } from "lucide-react";
6
- import { useRef, useEffect } from "react";
7
+ import { useRef, useEffect, useState } from "react";
7
8
 
8
9
  export const Route = createFileRoute("/ai")({
9
10
  component: RouteComponent,
10
11
  });
11
12
 
12
13
  function RouteComponent() {
13
- const { messages, input, handleInputChange, handleSubmit } = useChat({
14
- api: `${import.meta.env.VITE_SERVER_URL}/ai`,
14
+ const [input, setInput] = useState("");
15
+ const { messages, sendMessage } = useChat({
16
+ transport: new DefaultChatTransport({
17
+ api: `${import.meta.env.VITE_SERVER_URL}/ai`,
18
+ }),
15
19
  });
16
20
 
17
21
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -20,6 +24,14 @@ function RouteComponent() {
20
24
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
21
25
  }, [messages]);
22
26
 
27
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
28
+ e.preventDefault();
29
+ const text = input.trim();
30
+ if (!text) return;
31
+ sendMessage({ text });
32
+ setInput("");
33
+ };
34
+
23
35
  return (
24
36
  <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
25
37
  <div className="overflow-y-auto space-y-4 pb-4">
@@ -40,7 +52,16 @@ function RouteComponent() {
40
52
  <p className="text-sm font-semibold mb-1">
41
53
  {message.role === "user" ? "You" : "AI Assistant"}
42
54
  </p>
43
- <div className="whitespace-pre-wrap">{message.content}</div>
55
+ {message.parts?.map((part, index) => {
56
+ if (part.type === "text") {
57
+ return (
58
+ <div key={index} className="whitespace-pre-wrap">
59
+ {part.text}
60
+ </div>
61
+ );
62
+ }
63
+ return null;
64
+ })}
44
65
  </div>
45
66
  ))
46
67
  )}
@@ -54,7 +75,7 @@ function RouteComponent() {
54
75
  <Input
55
76
  name="prompt"
56
77
  value={input}
57
- onChange={handleInputChange}
78
+ onChange={(e) => setInput(e.target.value)}
58
79
  placeholder="Type your message..."
59
80
  className="flex-1"
60
81
  autoComplete="off"
@@ -66,4 +87,4 @@ function RouteComponent() {
66
87
  </form>
67
88
  </div>
68
89
  );
69
- }
90
+ }
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ import { PUBLIC_SERVER_URL } from "$env/static/public";
3
+ import { Chat } from "@ai-sdk/svelte";
4
+ import { DefaultChatTransport } from "ai";
5
+
6
+ let input = $state("");
7
+ const chat = new Chat({
8
+ transport: new DefaultChatTransport({
9
+ api: `${PUBLIC_SERVER_URL}/ai`,
10
+ }),
11
+ });
12
+
13
+ let messagesEndElement: HTMLDivElement | null = null;
14
+
15
+ $effect(() => {
16
+ if (chat.messages.length > 0) {
17
+ setTimeout(() => {
18
+ messagesEndElement?.scrollIntoView({ behavior: "smooth" });
19
+ }, 0);
20
+ }
21
+ });
22
+
23
+ function handleSubmit(e: Event) {
24
+ e.preventDefault();
25
+ const text = input.trim();
26
+ if (!text) return;
27
+ chat.sendMessage({ text });
28
+ input = "";
29
+ }
30
+ </script>
31
+
32
+ <div
33
+ class="mx-auto grid h-full w-full max-w-2xl grid-rows-[1fr_auto] overflow-hidden p-4"
34
+ >
35
+ <div class="mb-4 space-y-4 overflow-y-auto pb-4">
36
+ {#if chat.messages.length === 0}
37
+ <div class="mt-8 text-center text-neutral-500">
38
+ Ask me anything to get started!
39
+ </div>
40
+ {/if}
41
+
42
+ {#each chat.messages as message (message.id)}
43
+ <div
44
+ class="p-3 rounded-lg w-fit max-w-[85%] text-sm md:text-base"
45
+ class:ml-auto={message.role === "user"}
46
+ class:bg-primary={message.role === "user"}
47
+ class:bg-secondary={message.role === "assistant"}
48
+ >
49
+ <p
50
+ class="mb-1 text-sm font-semibold"
51
+ class:text-indigo-600={message.role === "user"}
52
+ class:text-neutral-400={message.role === "assistant"}
53
+ >
54
+ {message.role === "user" ? "You" : "AI Assistant"}
55
+ </p>
56
+ <div class="whitespace-pre-wrap break-words">
57
+ {#each message.parts as part, partIndex (partIndex)}
58
+ {#if part.type === "text"}
59
+ {part.text}
60
+ {/if}
61
+ {/each}
62
+ </div>
63
+ </div>
64
+ {/each}
65
+ <div bind:this={messagesEndElement}></div>
66
+ </div>
67
+
68
+ <form
69
+ onsubmit={handleSubmit}
70
+ class="w-full flex items-center space-x-2 pt-2 border-t"
71
+ >
72
+ <input
73
+ name="prompt"
74
+ bind:value={input}
75
+ placeholder="Type your message..."
76
+ class="flex-1 rounded border border-neutral-600 bg-neutral-800 px-3 py-2 text-neutral-100 placeholder-neutral-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:opacity-50"
77
+ autocomplete="off"
78
+ onkeydown={(e) => {
79
+ if (e.key === "Enter" && !e.shiftKey) {
80
+ e.preventDefault();
81
+ handleSubmit(e);
82
+ }
83
+ }}
84
+ />
85
+ <button
86
+ type="submit"
87
+ disabled={!input.trim()}
88
+ class="inline-flex h-10 w-10 items-center justify-center rounded bg-indigo-600 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-neutral-900 disabled:cursor-not-allowed disabled:opacity-50"
89
+ aria-label="Send message"
90
+ >
91
+ <svg
92
+ xmlns="http://www.w3.org/2000/svg"
93
+ width="18"
94
+ height="18"
95
+ viewBox="0 0 24 24"
96
+ fill="none"
97
+ stroke="currentColor"
98
+ stroke-width="2"
99
+ stroke-linecap="round"
100
+ stroke-linejoin="round"
101
+ >
102
+ <path d="m22 2-7 20-4-9-9-4Z" />
103
+ <path d="M22 2 11 13" />
104
+ </svg>
105
+ </button>
106
+ </form>
107
+ </div>
@@ -1,7 +1,7 @@
1
- {{#if (or (includes frontend "nuxt") (includes frontend "native-nativewind"))}}
2
- # [install]
1
+ [install]
2
+ {{#if (or (or (includes frontend "nuxt") (includes frontend "native-nativewind")) (includes frontend
3
+ "native-unistyles"))}}
3
4
  # linker = "isolated"
4
5
  {{else}}
5
- [install]
6
6
  linker = "isolated"
7
- {{/if}}
7
+ {{/if}}
@@ -1,15 +0,0 @@
1
- import { google } from '@ai-sdk/google';
2
- import { streamText } from 'ai';
3
-
4
- export const maxDuration = 30;
5
-
6
- export async function POST(req: Request) {
7
- const { messages } = await req.json();
8
-
9
- const result = streamText({
10
- model: google('gemini-2.0-flash'),
11
- messages,
12
- });
13
-
14
- return result.toDataStreamResponse();
15
- }
@@ -1,98 +0,0 @@
1
- <script lang="ts">
2
- import { PUBLIC_SERVER_URL } from '$env/static/public';
3
- import { Chat } from '@ai-sdk/svelte';
4
-
5
- const chat = new Chat({
6
- api: `${PUBLIC_SERVER_URL}/ai`,
7
- });
8
-
9
- let messagesEndElement: HTMLDivElement | null = null;
10
-
11
- $effect(() => {
12
- const messageCount = chat.messages.length;
13
- if (messageCount > 0) {
14
- setTimeout(() => {
15
- messagesEndElement?.scrollIntoView({ behavior: 'smooth' });
16
- }, 0);
17
- }
18
- });
19
-
20
- </script>
21
-
22
- <div class="mx-auto grid h-full w-full max-w-2xl grid-rows-[1fr_auto] overflow-hidden p-4">
23
- <div class="mb-4 space-y-4 overflow-y-auto pb-4">
24
- {#if chat.messages.length === 0}
25
- <div class="mt-8 text-center text-neutral-500">Ask the AI anything to get started!</div>
26
- {/if}
27
-
28
- {#each chat.messages as message (message.id)}
29
- <div
30
- class="w-fit max-w-[85%] rounded-lg p-3 text-sm md:text-base"
31
- class:ml-auto={message.role === 'user'}
32
- class:bg-indigo-600={message.role === 'user'}
33
- class:text-white={message.role === 'user'}
34
- class:bg-neutral-700={message.role === 'assistant'}
35
- class:text-neutral-100={message.role === 'assistant'}
36
- >
37
- <p
38
- class="mb-1 text-xs font-semibold uppercase tracking-wide"
39
- class:text-indigo-200={message.role === 'user'}
40
- class:text-neutral-400={message.role === 'assistant'}
41
- >
42
- {message.role === 'user' ? 'You' : 'AI Assistant'}
43
- </p>
44
- <div class="whitespace-pre-wrap break-words">
45
- {#each message.parts as part, partIndex (partIndex)}
46
- {#if part.type === 'text'}
47
- {part.text}
48
- {:else if part.type === 'tool-invocation'}
49
- <pre class="mt-2 rounded bg-neutral-800 p-2 text-xs text-neutral-300"
50
- >{JSON.stringify(part.toolInvocation, null, 2)}</pre
51
- >
52
- {/if}
53
- {/each}
54
- </div>
55
- </div>
56
- {/each}
57
- <div bind:this={messagesEndElement}></div>
58
- </div>
59
-
60
- <form
61
- onsubmit={chat.handleSubmit}
62
- class="flex w-full items-center space-x-2 border-t border-neutral-700 pt-4"
63
- >
64
- <input
65
- name="prompt"
66
- bind:value={chat.input}
67
- placeholder="Type your message..."
68
- class="flex-1 rounded border border-neutral-600 bg-neutral-800 px-3 py-2 text-neutral-100 placeholder-neutral-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:opacity-50"
69
- autocomplete="off"
70
- onkeydown={(e) => {
71
- if (e.key === 'Enter' && !e.shiftKey) {
72
- e.preventDefault();
73
- chat.handleSubmit(e);
74
- }
75
- }}
76
- />
77
- <button
78
- type="submit"
79
- disabled={!chat.input.trim()}
80
- class="inline-flex h-10 w-10 items-center justify-center rounded bg-indigo-600 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-neutral-900 disabled:cursor-not-allowed disabled:opacity-50"
81
- aria-label="Send message"
82
- >
83
- <svg
84
- xmlns="http://www.w3.org/2000/svg"
85
- width="18"
86
- height="18"
87
- viewBox="0 0 24 24"
88
- fill="none"
89
- stroke="currentColor"
90
- stroke-width="2"
91
- stroke-linecap="round"
92
- stroke-linejoin="round"
93
- >
94
- <path d="m22 2-7 20-4-9-9-4Z" /><path d="M22 2 11 13" />
95
- </svg>
96
- </button>
97
- </form>
98
- </div>