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
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.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",
|
|
7
7
|
"author": "Aman Varshney",
|
|
8
8
|
"bin": {
|
|
9
|
-
"create-better-t-stack": "dist/
|
|
9
|
+
"create-better-t-stack": "dist/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"templates",
|
|
@@ -49,9 +49,16 @@
|
|
|
49
49
|
"dev": "tsdown --watch",
|
|
50
50
|
"check-types": "tsc --noEmit",
|
|
51
51
|
"check": "biome check --write .",
|
|
52
|
-
"test": "vitest run",
|
|
52
|
+
"test": "bun run build && vitest run",
|
|
53
|
+
"test:with-build": "bun run build && WITH_BUILD=1 vitest run",
|
|
53
54
|
"prepublishOnly": "npm run build"
|
|
54
55
|
},
|
|
56
|
+
"exports": {
|
|
57
|
+
".": {
|
|
58
|
+
"types": "./dist/index.d.ts",
|
|
59
|
+
"import": "./dist/index.js"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
55
62
|
"dependencies": {
|
|
56
63
|
"@clack/prompts": "^0.11.0",
|
|
57
64
|
"consola": "^3.4.2",
|
|
@@ -64,12 +71,13 @@
|
|
|
64
71
|
"picocolors": "^1.1.1",
|
|
65
72
|
"trpc-cli": "^0.10.2",
|
|
66
73
|
"ts-morph": "^26.0.0",
|
|
67
|
-
"zod": "^4.0.
|
|
74
|
+
"zod": "^4.0.17"
|
|
68
75
|
},
|
|
69
76
|
"devDependencies": {
|
|
70
77
|
"@types/fs-extra": "^11.0.4",
|
|
71
|
-
"@types/node": "^24.2.
|
|
72
|
-
"tsdown": "^0.
|
|
73
|
-
"typescript": "^5.9.2"
|
|
78
|
+
"@types/node": "^24.2.1",
|
|
79
|
+
"tsdown": "^0.14.1",
|
|
80
|
+
"typescript": "^5.9.2",
|
|
81
|
+
"vitest": "^3.2.4"
|
|
74
82
|
}
|
|
75
83
|
}
|
|
@@ -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>
|