create-better-t-stack 2.30.0 → 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 +18 -11
- package/package.json +1 -1
- 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
package/dist/index.js
CHANGED
|
@@ -88,11 +88,11 @@ const dependencyVersionMap = {
|
|
|
88
88
|
fastify: "^5.3.3",
|
|
89
89
|
"@fastify/cors": "^11.0.1",
|
|
90
90
|
turbo: "^2.5.4",
|
|
91
|
-
ai: "^
|
|
92
|
-
"@ai-sdk/google": "^
|
|
93
|
-
"@ai-sdk/vue": "^
|
|
94
|
-
"@ai-sdk/svelte": "^
|
|
95
|
-
"@ai-sdk/react": "^
|
|
91
|
+
ai: "^5.0.9",
|
|
92
|
+
"@ai-sdk/google": "^2.0.3",
|
|
93
|
+
"@ai-sdk/vue": "^2.0.9",
|
|
94
|
+
"@ai-sdk/svelte": "^3.0.9",
|
|
95
|
+
"@ai-sdk/react": "^2.0.9",
|
|
96
96
|
"@orpc/server": "^1.5.0",
|
|
97
97
|
"@orpc/client": "^1.5.0",
|
|
98
98
|
"@orpc/tanstack-query": "^1.5.0",
|
|
@@ -4290,23 +4290,30 @@ async function setupExamples(config) {
|
|
|
4290
4290
|
const { examples, frontend, backend, projectDir } = config;
|
|
4291
4291
|
if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
|
|
4292
4292
|
if (examples.includes("ai")) {
|
|
4293
|
-
const
|
|
4293
|
+
const webClientDir = path.join(projectDir, "apps/web");
|
|
4294
|
+
const nativeClientDir = path.join(projectDir, "apps/native");
|
|
4294
4295
|
const serverDir = path.join(projectDir, "apps/server");
|
|
4295
|
-
const
|
|
4296
|
+
const webClientDirExists = await fs.pathExists(webClientDir);
|
|
4297
|
+
const nativeClientDirExists = await fs.pathExists(nativeClientDir);
|
|
4296
4298
|
const serverDirExists = await fs.pathExists(serverDir);
|
|
4297
4299
|
const hasNuxt = frontend.includes("nuxt");
|
|
4298
4300
|
const hasSvelte = frontend.includes("svelte");
|
|
4299
|
-
const
|
|
4300
|
-
|
|
4301
|
+
const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
|
|
4302
|
+
const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
4303
|
+
if (webClientDirExists) {
|
|
4301
4304
|
const dependencies = ["ai"];
|
|
4302
4305
|
if (hasNuxt) dependencies.push("@ai-sdk/vue");
|
|
4303
4306
|
else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
|
|
4304
|
-
else if (
|
|
4307
|
+
else if (hasReactWeb) dependencies.push("@ai-sdk/react");
|
|
4305
4308
|
await addPackageDependency({
|
|
4306
4309
|
dependencies,
|
|
4307
|
-
projectDir:
|
|
4310
|
+
projectDir: webClientDir
|
|
4308
4311
|
});
|
|
4309
4312
|
}
|
|
4313
|
+
if (nativeClientDirExists && hasReactNative) await addPackageDependency({
|
|
4314
|
+
dependencies: ["ai", "@ai-sdk/react"],
|
|
4315
|
+
projectDir: nativeClientDir
|
|
4316
|
+
});
|
|
4310
4317
|
if (serverDirExists && backend !== "none") await addPackageDependency({
|
|
4311
4318
|
dependencies: ["ai", "@ai-sdk/google"],
|
|
4312
4319
|
projectDir: serverDir
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.0",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,7 @@ import { createContext } from "./lib/context";
|
|
|
14
14
|
import cors from "cors";
|
|
15
15
|
import express from "express";
|
|
16
16
|
{{#if (includes examples "ai")}}
|
|
17
|
-
import { streamText } from "ai";
|
|
17
|
+
import { streamText, type UIMessage, convertToModelMessages } from "ai";
|
|
18
18
|
import { google } from "@ai-sdk/google";
|
|
19
19
|
{{/if}}
|
|
20
20
|
{{#if auth}}
|
|
@@ -44,16 +44,16 @@ app.use(
|
|
|
44
44
|
"/trpc",
|
|
45
45
|
createExpressMiddleware({
|
|
46
46
|
router: appRouter,
|
|
47
|
-
createContext
|
|
47
|
+
createContext,
|
|
48
48
|
})
|
|
49
49
|
);
|
|
50
50
|
{{/if}}
|
|
51
51
|
|
|
52
52
|
{{#if (eq api "orpc")}}
|
|
53
53
|
const handler = new RPCHandler(appRouter);
|
|
54
|
-
app.use(
|
|
54
|
+
app.use("/rpc{*path}", async (req, res, next) => {
|
|
55
55
|
const { matched } = await handler.handle(req, res, {
|
|
56
|
-
prefix:
|
|
56
|
+
prefix: "/rpc",
|
|
57
57
|
{{#if auth}}
|
|
58
58
|
context: await createContext({ req }),
|
|
59
59
|
{{else}}
|
|
@@ -65,16 +65,16 @@ app.use('/rpc{*path}', async (req, res, next) => {
|
|
|
65
65
|
});
|
|
66
66
|
{{/if}}
|
|
67
67
|
|
|
68
|
-
app.use(express.json())
|
|
68
|
+
app.use(express.json());
|
|
69
69
|
|
|
70
70
|
{{#if (includes examples "ai")}}
|
|
71
71
|
app.post("/ai", async (req, res) => {
|
|
72
|
-
const { messages = [] } = req.body || {};
|
|
72
|
+
const { messages = [] } = (req.body || {}) as { messages: UIMessage[] };
|
|
73
73
|
const result = streamText({
|
|
74
74
|
model: google("gemini-1.5-flash"),
|
|
75
|
-
messages,
|
|
75
|
+
messages: convertToModelMessages(messages),
|
|
76
76
|
});
|
|
77
|
-
result.
|
|
77
|
+
result.pipeUIMessageStreamToResponse(res);
|
|
78
78
|
});
|
|
79
79
|
{{/if}}
|
|
80
80
|
|
|
@@ -85,4 +85,4 @@ app.get("/", (_req, res) => {
|
|
|
85
85
|
const port = process.env.PORT || 3000;
|
|
86
86
|
app.listen(port, () => {
|
|
87
87
|
console.log(`Server is running on port ${port}`);
|
|
88
|
-
});
|
|
88
|
+
});
|
|
@@ -19,8 +19,7 @@ import { createContext } from "./lib/context";
|
|
|
19
19
|
{{/if}}
|
|
20
20
|
|
|
21
21
|
{{#if (includes examples "ai")}}
|
|
22
|
-
import
|
|
23
|
-
import { streamText, type Message } from "ai";
|
|
22
|
+
import { streamText, type UIMessage, convertToModelMessages } from "ai";
|
|
24
23
|
import { google } from "@ai-sdk/google";
|
|
25
24
|
{{/if}}
|
|
26
25
|
|
|
@@ -99,7 +98,7 @@ fastify.route({
|
|
|
99
98
|
response.headers.forEach((value, key) => reply.header(key, value));
|
|
100
99
|
reply.send(response.body ? await response.text() : null);
|
|
101
100
|
} catch (error) {
|
|
102
|
-
fastify.log.error("Authentication Error:"
|
|
101
|
+
fastify.log.error({ err: error }, "Authentication Error:");
|
|
103
102
|
reply.status(500).send({
|
|
104
103
|
error: "Internal authentication error",
|
|
105
104
|
code: "AUTH_FAILURE"
|
|
@@ -125,26 +124,24 @@ fastify.register(fastifyTRPCPlugin, {
|
|
|
125
124
|
{{#if (includes examples "ai")}}
|
|
126
125
|
interface AiRequestBody {
|
|
127
126
|
id?: string;
|
|
128
|
-
messages:
|
|
127
|
+
messages: UIMessage[];
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
fastify.post('/ai', async function (request, reply) {
|
|
131
|
+
// there are some issues with the ai sdk and fastify, docs: https://ai-sdk.dev/cookbook/api-servers/fastify
|
|
132
132
|
const { messages } = request.body as AiRequestBody;
|
|
133
133
|
const result = streamText({
|
|
134
134
|
model: google('gemini-1.5-flash'),
|
|
135
|
-
messages,
|
|
135
|
+
messages: convertToModelMessages(messages),
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
reply.
|
|
139
|
-
reply.header('Content-Type', 'text/plain; charset=utf-8');
|
|
140
|
-
|
|
141
|
-
return reply.send(result.toDataStream());
|
|
138
|
+
return result.pipeUIMessageStreamToResponse(reply.raw);
|
|
142
139
|
});
|
|
143
140
|
{{/if}}
|
|
144
141
|
|
|
145
142
|
fastify.get('/', async () => {
|
|
146
|
-
return 'OK'
|
|
147
|
-
})
|
|
143
|
+
return 'OK';
|
|
144
|
+
});
|
|
148
145
|
|
|
149
146
|
fastify.listen({ port: 3000 }, (err) => {
|
|
150
147
|
if (err) {
|
|
@@ -152,4 +149,4 @@ fastify.listen({ port: 3000 }, (err) => {
|
|
|
152
149
|
process.exit(1);
|
|
153
150
|
}
|
|
154
151
|
console.log("Server running on port 3000");
|
|
155
|
-
});
|
|
152
|
+
});
|
|
@@ -21,32 +21,33 @@ import { Hono } from "hono";
|
|
|
21
21
|
import { cors } from "hono/cors";
|
|
22
22
|
import { logger } from "hono/logger";
|
|
23
23
|
{{#if (and (includes examples "ai") (or (eq runtime "bun") (eq runtime "node")))}}
|
|
24
|
-
import { streamText } from "ai";
|
|
24
|
+
import { streamText, convertToModelMessages } from "ai";
|
|
25
25
|
import { google } from "@ai-sdk/google";
|
|
26
|
-
import { stream } from "hono/streaming";
|
|
27
26
|
{{/if}}
|
|
28
27
|
{{#if (and (includes examples "ai") (eq runtime "workers"))}}
|
|
29
|
-
import { streamText } from "ai";
|
|
30
|
-
import { stream } from "hono/streaming";
|
|
28
|
+
import { streamText, convertToModelMessages } from "ai";
|
|
31
29
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
32
30
|
{{/if}}
|
|
33
31
|
|
|
34
32
|
const app = new Hono();
|
|
35
33
|
|
|
36
34
|
app.use(logger());
|
|
37
|
-
app.use(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
app.use(
|
|
36
|
+
"/*",
|
|
37
|
+
cors({
|
|
38
|
+
{{#if (or (eq runtime "bun") (eq runtime "node"))}}
|
|
39
|
+
origin: process.env.CORS_ORIGIN || "",
|
|
40
|
+
{{/if}}
|
|
41
|
+
{{#if (eq runtime "workers")}}
|
|
42
|
+
origin: env.CORS_ORIGIN || "",
|
|
43
|
+
{{/if}}
|
|
44
|
+
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
45
|
+
{{#if auth}}
|
|
46
|
+
allowHeaders: ["Content-Type", "Authorization"],
|
|
47
|
+
credentials: true,
|
|
48
|
+
{{/if}}
|
|
49
|
+
})
|
|
50
|
+
);
|
|
50
51
|
|
|
51
52
|
{{#if auth}}
|
|
52
53
|
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
|
|
@@ -69,44 +70,43 @@ app.use("/rpc/*", async (c, next) => {
|
|
|
69
70
|
{{/if}}
|
|
70
71
|
|
|
71
72
|
{{#if (eq api "trpc")}}
|
|
72
|
-
app.use(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
})
|
|
73
|
+
app.use(
|
|
74
|
+
"/trpc/*",
|
|
75
|
+
trpcServer({
|
|
76
|
+
router: appRouter,
|
|
77
|
+
createContext: (_opts, context) => {
|
|
78
|
+
return createContext({ context });
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
);
|
|
78
82
|
{{/if}}
|
|
79
83
|
|
|
80
84
|
{{#if (and (includes examples "ai") (or (eq runtime "bun") (eq runtime "node")))}}
|
|
81
85
|
app.post("/ai", async (c) => {
|
|
82
86
|
const body = await c.req.json();
|
|
83
|
-
const
|
|
87
|
+
const uiMessages = body.messages || [];
|
|
84
88
|
const result = streamText({
|
|
85
89
|
model: google("gemini-1.5-flash"),
|
|
86
|
-
messages,
|
|
90
|
+
messages: convertToModelMessages(uiMessages),
|
|
87
91
|
});
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
c.header("Content-Type", "text/plain; charset=utf-8");
|
|
91
|
-
return stream(c, (stream) => stream.pipe(result.toDataStream()));
|
|
93
|
+
return result.toUIMessageStreamResponse();
|
|
92
94
|
});
|
|
93
95
|
{{/if}}
|
|
94
96
|
|
|
95
97
|
{{#if (and (includes examples "ai") (eq runtime "workers"))}}
|
|
96
98
|
app.post("/ai", async (c) => {
|
|
97
99
|
const body = await c.req.json();
|
|
98
|
-
const
|
|
100
|
+
const uiMessages = body.messages || [];
|
|
99
101
|
const google = createGoogleGenerativeAI({
|
|
100
102
|
apiKey: env.GOOGLE_GENERATIVE_AI_API_KEY,
|
|
101
103
|
});
|
|
102
104
|
const result = streamText({
|
|
103
105
|
model: google("gemini-1.5-flash"),
|
|
104
|
-
messages,
|
|
106
|
+
messages: convertToModelMessages(uiMessages),
|
|
105
107
|
});
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
c.header("Content-Type", "text/plain; charset=utf-8");
|
|
109
|
-
return stream(c, (stream) => stream.pipe(result.toDataStream()));
|
|
109
|
+
return result.toUIMessageStreamResponse();
|
|
110
110
|
});
|
|
111
111
|
{{/if}}
|
|
112
112
|
|
|
@@ -117,17 +117,20 @@ app.get("/", (c) => {
|
|
|
117
117
|
{{#if (eq runtime "node")}}
|
|
118
118
|
import { serve } from "@hono/node-server";
|
|
119
119
|
|
|
120
|
-
serve(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
serve(
|
|
121
|
+
{
|
|
122
|
+
fetch: app.fetch,
|
|
123
|
+
port: 3000,
|
|
124
|
+
},
|
|
125
|
+
(info) => {
|
|
126
|
+
console.log(`Server is running on http://localhost:${info.port}`);
|
|
127
|
+
}
|
|
128
|
+
);
|
|
126
129
|
{{else}}
|
|
127
|
-
|
|
130
|
+
{{#if (eq runtime "bun")}}
|
|
128
131
|
export default app;
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
{{/if}}
|
|
133
|
+
{{#if (eq runtime "workers")}}
|
|
131
134
|
export default app;
|
|
132
|
-
{{/if}}
|
|
133
135
|
{{/if}}
|
|
136
|
+
{{/if}}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useEffect } from "react";
|
|
1
|
+
import { useRef, useEffect, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
Platform,
|
|
10
10
|
} from "react-native";
|
|
11
11
|
import { useChat } from "@ai-sdk/react";
|
|
12
|
+
import { DefaultChatTransport } from "ai";
|
|
12
13
|
import { fetch as expoFetch } from "expo/fetch";
|
|
13
14
|
import { Ionicons } from "@expo/vector-icons";
|
|
14
15
|
import { Container } from "@/components/container";
|
|
15
16
|
|
|
16
|
-
// Utility function to generate API URLs
|
|
17
17
|
const generateAPIUrl = (relativePath: string) => {
|
|
18
18
|
const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL;
|
|
19
19
|
if (!serverUrl) {
|
|
@@ -25,11 +25,13 @@ const generateAPIUrl = (relativePath: string) => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
export default function AIScreen() {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const [input, setInput] = useState("");
|
|
29
|
+
const { messages, error, sendMessage } = useChat({
|
|
30
|
+
transport: new DefaultChatTransport({
|
|
31
|
+
fetch: expoFetch as unknown as typeof globalThis.fetch,
|
|
32
|
+
api: generateAPIUrl('/ai'),
|
|
33
|
+
}),
|
|
31
34
|
onError: error => console.error(error, 'AI Chat Error'),
|
|
32
|
-
maxSteps: 5,
|
|
33
35
|
});
|
|
34
36
|
|
|
35
37
|
const scrollViewRef = useRef<ScrollView>(null);
|
|
@@ -39,8 +41,10 @@ export default function AIScreen() {
|
|
|
39
41
|
}, [messages]);
|
|
40
42
|
|
|
41
43
|
const onSubmit = () => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const value = input.trim();
|
|
45
|
+
if (value) {
|
|
46
|
+
sendMessage({ text: value });
|
|
47
|
+
setInput("");
|
|
44
48
|
}
|
|
45
49
|
};
|
|
46
50
|
|
|
@@ -100,9 +104,28 @@ export default function AIScreen() {
|
|
|
100
104
|
<Text className="text-sm font-semibold mb-1 text-foreground">
|
|
101
105
|
{message.role === "user" ? "You" : "AI Assistant"}
|
|
102
106
|
</Text>
|
|
103
|
-
<
|
|
104
|
-
{message.
|
|
105
|
-
|
|
107
|
+
<View className="space-y-1">
|
|
108
|
+
{message.parts.map((part, i) => {
|
|
109
|
+
if (part.type === 'text') {
|
|
110
|
+
return (
|
|
111
|
+
<Text
|
|
112
|
+
key={`${message.id}-${i}`}
|
|
113
|
+
className="text-foreground leading-relaxed"
|
|
114
|
+
>
|
|
115
|
+
{part.text}
|
|
116
|
+
</Text>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return (
|
|
120
|
+
<Text
|
|
121
|
+
key={`${message.id}-${i}`}
|
|
122
|
+
className="text-foreground leading-relaxed"
|
|
123
|
+
>
|
|
124
|
+
{JSON.stringify(part)}
|
|
125
|
+
</Text>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</View>
|
|
106
129
|
</View>
|
|
107
130
|
))}
|
|
108
131
|
</View>
|
|
@@ -113,21 +136,13 @@ export default function AIScreen() {
|
|
|
113
136
|
<View className="flex-row items-end space-x-2">
|
|
114
137
|
<TextInput
|
|
115
138
|
value={input}
|
|
116
|
-
|
|
117
|
-
handleInputChange({
|
|
118
|
-
...e,
|
|
119
|
-
target: {
|
|
120
|
-
...e.target,
|
|
121
|
-
value: e.nativeEvent.text,
|
|
122
|
-
},
|
|
123
|
-
} as unknown as React.ChangeEvent<HTMLInputElement>)
|
|
124
|
-
}
|
|
139
|
+
onChangeText={setInput}
|
|
125
140
|
placeholder="Type your message..."
|
|
126
141
|
placeholderTextColor="#6b7280"
|
|
127
142
|
className="flex-1 border border-border rounded-md px-3 py-2 text-foreground bg-background min-h-[40px] max-h-[120px]"
|
|
128
143
|
onSubmitEditing={(e) => {
|
|
129
|
-
handleSubmit(e);
|
|
130
144
|
e.preventDefault();
|
|
145
|
+
onSubmit();
|
|
131
146
|
}}
|
|
132
147
|
autoFocus={true}
|
|
133
148
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useEffect } from "react";
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Platform,
|
|
10
10
|
} from "react-native";
|
|
11
11
|
import { useChat } from "@ai-sdk/react";
|
|
12
|
+
import { DefaultChatTransport } from "ai";
|
|
12
13
|
import { fetch as expoFetch } from "expo/fetch";
|
|
13
14
|
import { Ionicons } from "@expo/vector-icons";
|
|
14
15
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
@@ -18,21 +19,22 @@ const generateAPIUrl = (relativePath: string) => {
|
|
|
18
19
|
const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL;
|
|
19
20
|
if (!serverUrl) {
|
|
20
21
|
throw new Error(
|
|
21
|
-
"EXPO_PUBLIC_SERVER_URL environment variable is not defined"
|
|
22
|
+
"EXPO_PUBLIC_SERVER_URL environment variable is not defined"
|
|
22
23
|
);
|
|
23
24
|
}
|
|
24
|
-
|
|
25
25
|
const path = relativePath.startsWith("/") ? relativePath : `/${relativePath}`;
|
|
26
26
|
return serverUrl.concat(path);
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export default function AIScreen() {
|
|
30
30
|
const { theme } = useUnistyles();
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const [input, setInput] = useState("");
|
|
32
|
+
const { messages, error, sendMessage } = useChat({
|
|
33
|
+
transport: new DefaultChatTransport({
|
|
34
|
+
fetch: expoFetch as unknown as typeof globalThis.fetch,
|
|
35
|
+
api: generateAPIUrl("/ai"),
|
|
36
|
+
}),
|
|
34
37
|
onError: (error) => console.error(error, "AI Chat Error"),
|
|
35
|
-
maxSteps: 5,
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
const scrollViewRef = useRef<ScrollView>(null);
|
|
@@ -42,8 +44,10 @@ export default function AIScreen() {
|
|
|
42
44
|
}, [messages]);
|
|
43
45
|
|
|
44
46
|
const onSubmit = () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
const value = input.trim();
|
|
48
|
+
if (value) {
|
|
49
|
+
sendMessage({ text: value });
|
|
50
|
+
setInput("");
|
|
47
51
|
}
|
|
48
52
|
};
|
|
49
53
|
|
|
@@ -100,7 +104,28 @@ export default function AIScreen() {
|
|
|
100
104
|
<Text style={styles.messageRole}>
|
|
101
105
|
{message.role === "user" ? "You" : "AI Assistant"}
|
|
102
106
|
</Text>
|
|
103
|
-
<
|
|
107
|
+
<View style={styles.messageContentWrapper}>
|
|
108
|
+
{message.parts.map((part, i) => {
|
|
109
|
+
if (part.type === "text") {
|
|
110
|
+
return (
|
|
111
|
+
<Text
|
|
112
|
+
key={`${message.id}-${i}`}
|
|
113
|
+
style={styles.messageContent}
|
|
114
|
+
>
|
|
115
|
+
{part.text}
|
|
116
|
+
</Text>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return (
|
|
120
|
+
<Text
|
|
121
|
+
key={`${message.id}-${i}`}
|
|
122
|
+
style={styles.messageContent}
|
|
123
|
+
>
|
|
124
|
+
{JSON.stringify(part)}
|
|
125
|
+
</Text>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</View>
|
|
104
129
|
</View>
|
|
105
130
|
))}
|
|
106
131
|
</View>
|
|
@@ -111,21 +136,13 @@ export default function AIScreen() {
|
|
|
111
136
|
<View style={styles.inputContainer}>
|
|
112
137
|
<TextInput
|
|
113
138
|
value={input}
|
|
114
|
-
|
|
115
|
-
handleInputChange({
|
|
116
|
-
...e,
|
|
117
|
-
target: {
|
|
118
|
-
...e.target,
|
|
119
|
-
value: e.nativeEvent.text,
|
|
120
|
-
},
|
|
121
|
-
} as unknown as React.ChangeEvent<HTMLInputElement>)
|
|
122
|
-
}
|
|
139
|
+
onChangeText={setInput}
|
|
123
140
|
placeholder="Type your message..."
|
|
124
141
|
placeholderTextColor={theme.colors.border}
|
|
125
142
|
style={styles.textInput}
|
|
126
143
|
onSubmitEditing={(e) => {
|
|
127
|
-
handleSubmit(e);
|
|
128
144
|
e.preventDefault();
|
|
145
|
+
onSubmit();
|
|
129
146
|
}}
|
|
130
147
|
autoFocus={true}
|
|
131
148
|
/>
|
|
@@ -141,7 +158,9 @@ export default function AIScreen() {
|
|
|
141
158
|
name="send"
|
|
142
159
|
size={20}
|
|
143
160
|
color={
|
|
144
|
-
input.trim()
|
|
161
|
+
input.trim()
|
|
162
|
+
? theme.colors.background
|
|
163
|
+
: theme.colors.border
|
|
145
164
|
}
|
|
146
165
|
/>
|
|
147
166
|
</TouchableOpacity>
|
|
@@ -230,6 +249,9 @@ const styles = StyleSheet.create((theme) => ({
|
|
|
230
249
|
marginBottom: theme.spacing.sm,
|
|
231
250
|
color: theme.colors.typography,
|
|
232
251
|
},
|
|
252
|
+
messageContentWrapper: {
|
|
253
|
+
gap: theme.spacing.xs,
|
|
254
|
+
},
|
|
233
255
|
messageContent: {
|
|
234
256
|
color: theme.colors.typography,
|
|
235
257
|
lineHeight: 20,
|
|
@@ -276,4 +298,4 @@ const styles = StyleSheet.create((theme) => ({
|
|
|
276
298
|
sendButtonDisabled: {
|
|
277
299
|
backgroundColor: theme.colors.border,
|
|
278
300
|
},
|
|
279
|
-
}));
|
|
301
|
+
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { google } from '@ai-sdk/google';
|
|
2
|
+
import { streamText, type UIMessage, convertToModelMessages } from 'ai';
|
|
3
|
+
|
|
4
|
+
export const maxDuration = 30;
|
|
5
|
+
|
|
6
|
+
export async function POST(req: Request) {
|
|
7
|
+
const { messages }: { messages: UIMessage[] } = await req.json();
|
|
8
|
+
|
|
9
|
+
const result = streamText({
|
|
10
|
+
model: google('gemini-2.0-flash'),
|
|
11
|
+
messages: convertToModelMessages(messages),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return result.toUIMessageStreamResponse();
|
|
15
|
+
}
|
|
@@ -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>
|