create-better-t-stack 2.29.4 → 2.31.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/index.js +256 -347
- package/package.json +1 -1
- package/templates/addons/vibe-rules/.bts/rules.md.hbs +132 -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,20 +1,35 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { Chat } from '@ai-sdk/vue'
|
|
3
|
+
import { DefaultChatTransport } from 'ai'
|
|
3
4
|
import { nextTick, ref, watch } from 'vue'
|
|
4
5
|
|
|
5
6
|
const config = useRuntimeConfig()
|
|
6
7
|
const serverUrl = config.public.serverURL
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
|
|
9
|
+
const input = ref('')
|
|
10
|
+
const chat = new Chat({
|
|
11
|
+
transport: new DefaultChatTransport({
|
|
12
|
+
api: `${serverUrl}/ai`,
|
|
13
|
+
}),
|
|
10
14
|
})
|
|
11
15
|
|
|
12
16
|
const messagesEndRef = ref<null | HTMLDivElement>(null)
|
|
13
17
|
|
|
14
|
-
watch(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
watch(
|
|
19
|
+
() => chat.messages,
|
|
20
|
+
async () => {
|
|
21
|
+
await nextTick()
|
|
22
|
+
messagesEndRef.value?.scrollIntoView({ behavior: 'smooth' })
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const handleSubmit = (e: Event) => {
|
|
27
|
+
e.preventDefault()
|
|
28
|
+
const text = input.value.trim()
|
|
29
|
+
if (!text) return
|
|
30
|
+
chat.sendMessage({ text })
|
|
31
|
+
input.value = ''
|
|
32
|
+
}
|
|
18
33
|
|
|
19
34
|
function getMessageText(message: any) {
|
|
20
35
|
return message.parts
|
|
@@ -27,11 +42,11 @@ function getMessageText(message: any) {
|
|
|
27
42
|
<template>
|
|
28
43
|
<div class="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
|
|
29
44
|
<div class="overflow-y-auto space-y-4 pb-4">
|
|
30
|
-
<div v-if="messages.length === 0" class="text-center text-muted-foreground mt-8">
|
|
45
|
+
<div v-if="chat.messages.length === 0" class="text-center text-muted-foreground mt-8">
|
|
31
46
|
Ask me anything to get started!
|
|
32
47
|
</div>
|
|
33
48
|
<div
|
|
34
|
-
v-for="message in messages"
|
|
49
|
+
v-for="message in chat.messages"
|
|
35
50
|
:key="message.id"
|
|
36
51
|
:class="[
|
|
37
52
|
'p-3 rounded-lg',
|
|
@@ -39,14 +54,14 @@ function getMessageText(message: any) {
|
|
|
39
54
|
]"
|
|
40
55
|
>
|
|
41
56
|
<p class="text-sm font-semibold mb-1">
|
|
42
|
-
{{ message.role === 'user' ? 'You' : 'AI Assistant' }}
|
|
57
|
+
\{{ message.role === 'user' ? 'You' : 'AI Assistant' }}
|
|
43
58
|
</p>
|
|
44
|
-
<div class="whitespace-pre-wrap"
|
|
59
|
+
<div class="whitespace-pre-wrap">\{{ getMessageText(message) }}</div>
|
|
45
60
|
</div>
|
|
46
|
-
<div ref="messagesEndRef"
|
|
61
|
+
<div ref="messagesEndRef"></div>
|
|
47
62
|
</div>
|
|
48
63
|
|
|
49
|
-
<form @submit
|
|
64
|
+
<form @submit="handleSubmit" class="w-full flex items-center space-x-2 pt-2 border-t">
|
|
50
65
|
<UInput
|
|
51
66
|
name="prompt"
|
|
52
67
|
v-model="input"
|
|
@@ -60,4 +75,4 @@ function getMessageText(message: any) {
|
|
|
60
75
|
</UButton>
|
|
61
76
|
</form>
|
|
62
77
|
</div>
|
|
63
|
-
</template>
|
|
78
|
+
</template>
|
|
@@ -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>
|