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.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8 -0
- package/dist/index.d.ts +279 -0
- package/dist/index.js +2 -5367
- package/dist/src-BEEshIQf.js +5534 -0
- package/package.json +15 -7
- package/templates/addons/biome/biome.json.hbs +1 -0
- package/templates/addons/ultracite/biome.json.hbs +1 -0
- package/templates/backend/server/express/src/index.ts.hbs +9 -9
- package/templates/backend/server/fastify/src/index.ts.hbs +9 -12
- package/templates/backend/server/hono/src/index.ts.hbs +46 -43
- package/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs +36 -21
- package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +44 -22
- package/templates/examples/ai/server/next/src/app/ai/route.ts.hbs +15 -0
- package/templates/examples/ai/web/nuxt/app/pages/{ai.vue → ai.vue.hbs} +29 -14
- package/templates/examples/ai/web/react/next/src/app/ai/{page.tsx → page.tsx.hbs} +28 -8
- package/templates/examples/ai/web/react/react-router/src/routes/{ai.tsx → ai.tsx.hbs} +30 -7
- package/templates/examples/ai/web/react/tanstack-router/src/routes/{ai.tsx → ai.tsx.hbs} +26 -5
- package/templates/examples/ai/web/react/tanstack-start/src/routes/{ai.tsx → ai.tsx.hbs} +27 -6
- package/templates/examples/ai/web/svelte/src/routes/ai/+page.svelte.hbs +107 -0
- package/templates/extras/bunfig.toml.hbs +4 -4
- package/templates/examples/ai/server/next/src/app/ai/route.ts +0 -15
- package/templates/examples/ai/web/svelte/src/routes/ai/+page.svelte +0 -98
|
@@ -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
|
|
12
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
2
|
-
#
|
|
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>
|