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 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: "^4.3.16",
92
- "@ai-sdk/google": "^1.2.3",
93
- "@ai-sdk/vue": "^1.2.8",
94
- "@ai-sdk/svelte": "^2.1.9",
95
- "@ai-sdk/react": "^1.2.12",
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 clientDir = path.join(projectDir, "apps/web");
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 clientDirExists = await fs.pathExists(clientDir);
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 hasReact = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start") || frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
4300
- if (clientDirExists) {
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 (hasReact) dependencies.push("@ai-sdk/react");
4307
+ else if (hasReactWeb) dependencies.push("@ai-sdk/react");
4305
4308
  await addPackageDependency({
4306
4309
  dependencies,
4307
- projectDir: clientDir
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.30.0",
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('/rpc{*path}', async (req, res, next) => {
54
+ app.use("/rpc{*path}", async (req, res, next) => {
55
55
  const { matched } = await handler.handle(req, res, {
56
- prefix: '/rpc',
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.pipeDataStreamToResponse(res);
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 type { FastifyRequest, FastifyReply } from "fastify";
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:", 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: Message[];
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.header('X-Vercel-AI-Data-Stream', 'v1');
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("/*", 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
- }));
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("/trpc/*", trpcServer({
73
- router: appRouter,
74
- createContext: (_opts, context) => {
75
- return createContext({ context });
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 messages = body.messages || [];
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
- c.header("X-Vercel-AI-Data-Stream", "v1");
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 messages = body.messages || [];
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
- c.header("X-Vercel-AI-Data-Stream", "v1");
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
- fetch: app.fetch,
122
- port: 3000,
123
- }, (info) => {
124
- console.log(`Server is running on http://localhost:${info.port}`);
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
- {{#if (eq runtime "bun")}}
130
+ {{#if (eq runtime "bun")}}
128
131
  export default app;
129
- {{/if}}
130
- {{#if (eq runtime "workers")}}
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 { messages, input, handleInputChange, handleSubmit, error } = useChat({
29
- fetch: expoFetch as unknown as typeof globalThis.fetch,
30
- api: generateAPIUrl('/ai'),
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
- if (input.trim()) {
43
- handleSubmit();
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
- <Text className="text-foreground leading-relaxed">
104
- {message.content}
105
- </Text>
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
- onChange={(e) =>
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 { messages, input, handleInputChange, handleSubmit, error } = useChat({
32
- fetch: expoFetch as unknown as typeof globalThis.fetch,
33
- api: generateAPIUrl("/ai"),
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
- if (input.trim()) {
46
- handleSubmit();
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
- <Text style={styles.messageContent}>{message.content}</Text>
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
- onChange={(e) =>
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() ? theme.colors.background : theme.colors.border
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 { useChat } from '@ai-sdk/vue'
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 { messages, input, handleSubmit } = useChat({
9
- api: `${serverUrl}/ai`,
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(messages, async () => {
15
- await nextTick()
16
- messagesEndRef.value?.scrollIntoView({ behavior: 'smooth' })
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">{{ getMessageText(message) }}</div>
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.prevent="handleSubmit" class="w-full flex items-center space-x-2 pt-2 border-t">
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 { messages, input, handleInputChange, handleSubmit } = useChat({
12
- api: `${process.env.NEXT_PUBLIC_SERVER_URL}/ai`,
11
+ const [input, setInput] = useState("");
12
+ const { messages, sendMessage } = useChat({
13
+ transport: new DefaultChatTransport({
14
+ api: `${process.env.NEXT_PUBLIC_SERVER_URL}/ai`,
15
+ }),
13
16
  });
14
17
 
15
18
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -18,6 +21,14 @@ export default function AIPage() {
18
21
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
19
22
  }, [messages]);
20
23
 
24
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
25
+ e.preventDefault();
26
+ const text = input.trim();
27
+ if (!text) return;
28
+ sendMessage({ text });
29
+ setInput("");
30
+ };
31
+
21
32
  return (
22
33
  <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
23
34
  <div className="overflow-y-auto space-y-4 pb-4">
@@ -38,7 +49,16 @@ export default function AIPage() {
38
49
  <p className="text-sm font-semibold mb-1">
39
50
  {message.role === "user" ? "You" : "AI Assistant"}
40
51
  </p>
41
- <div className="whitespace-pre-wrap">{message.content}</div>
52
+ {message.parts?.map((part, index) => {
53
+ if (part.type === "text") {
54
+ return (
55
+ <div key={index} className="whitespace-pre-wrap">
56
+ {part.text}
57
+ </div>
58
+ );
59
+ }
60
+ return null;
61
+ })}
42
62
  </div>
43
63
  ))
44
64
  )}
@@ -52,7 +72,7 @@ export default function AIPage() {
52
72
  <Input
53
73
  name="prompt"
54
74
  value={input}
55
- onChange={handleInputChange}
75
+ onChange={(e) => setInput(e.target.value)}
56
76
  placeholder="Type your message..."
57
77
  className="flex-1"
58
78
  autoComplete="off"
@@ -64,4 +84,4 @@ export default function AIPage() {
64
84
  </form>
65
85
  </div>
66
86
  );
67
- }
87
+ }
@@ -1,12 +1,16 @@
1
+ import React, { useRef, useEffect, useState } from "react";
1
2
  import { useChat } from "@ai-sdk/react";
3
+ import { DefaultChatTransport } from "ai";
2
4
  import { Input } from "@/components/ui/input";
3
5
  import { Button } from "@/components/ui/button";
4
6
  import { Send } from "lucide-react";
5
- import { useRef, useEffect } from "react";
6
7
 
7
- export default function AI() {
8
- const { messages, input, handleInputChange, handleSubmit } = useChat({
9
- api: `${import.meta.env.VITE_SERVER_URL}/ai`,
8
+ const AI: React.FC = () => {
9
+ const [input, setInput] = useState("");
10
+ const { messages, sendMessage } = useChat({
11
+ transport: new DefaultChatTransport({
12
+ api: `${import.meta.env.VITE_SERVER_URL}/ai`,
13
+ }),
10
14
  });
11
15
 
12
16
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -15,6 +19,14 @@ export default function AI() {
15
19
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
16
20
  }, [messages]);
17
21
 
22
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
23
+ e.preventDefault();
24
+ const text = input.trim();
25
+ if (!text) return;
26
+ sendMessage({ text });
27
+ setInput("");
28
+ };
29
+
18
30
  return (
19
31
  <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
20
32
  <div className="overflow-y-auto space-y-4 pb-4">
@@ -35,7 +47,16 @@ export default function AI() {
35
47
  <p className="text-sm font-semibold mb-1">
36
48
  {message.role === "user" ? "You" : "AI Assistant"}
37
49
  </p>
38
- <div className="whitespace-pre-wrap">{message.content}</div>
50
+ {message.parts?.map((part, index) => {
51
+ if (part.type === "text") {
52
+ return (
53
+ <div key={index} className="whitespace-pre-wrap">
54
+ {part.text}
55
+ </div>
56
+ );
57
+ }
58
+ return null;
59
+ })}
39
60
  </div>
40
61
  ))
41
62
  )}
@@ -49,7 +70,7 @@ export default function AI() {
49
70
  <Input
50
71
  name="prompt"
51
72
  value={input}
52
- onChange={handleInputChange}
73
+ onChange={(e) => setInput(e.target.value)}
53
74
  placeholder="Type your message..."
54
75
  className="flex-1"
55
76
  autoComplete="off"
@@ -61,4 +82,6 @@ export default function AI() {
61
82
  </form>
62
83
  </div>
63
84
  );
64
- }
85
+ };
86
+
87
+ export default AI;
@@ -1,17 +1,21 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { useChat } from "@ai-sdk/react";
3
+ import { DefaultChatTransport } from "ai";
3
4
  import { Input } from "@/components/ui/input";
4
5
  import { Button } from "@/components/ui/button";
5
6
  import { Send } from "lucide-react";
6
- import { useRef, useEffect } from "react";
7
+ import { useRef, useEffect, useState } from "react";
7
8
 
8
9
  export const Route = createFileRoute("/ai")({
9
10
  component: RouteComponent,
10
11
  });
11
12
 
12
13
  function RouteComponent() {
13
- const { messages, input, handleInputChange, handleSubmit } = useChat({
14
- api: `${import.meta.env.VITE_SERVER_URL}/ai`,
14
+ const [input, setInput] = useState("");
15
+ const { messages, sendMessage } = useChat({
16
+ transport: new DefaultChatTransport({
17
+ api: `${import.meta.env.VITE_SERVER_URL}/ai`,
18
+ }),
15
19
  });
16
20
 
17
21
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -20,6 +24,14 @@ function RouteComponent() {
20
24
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
21
25
  }, [messages]);
22
26
 
27
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
28
+ e.preventDefault();
29
+ const text = input.trim();
30
+ if (!text) return;
31
+ sendMessage({ text });
32
+ setInput("");
33
+ };
34
+
23
35
  return (
24
36
  <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
25
37
  <div className="overflow-y-auto space-y-4 pb-4">
@@ -40,7 +52,16 @@ function RouteComponent() {
40
52
  <p className="text-sm font-semibold mb-1">
41
53
  {message.role === "user" ? "You" : "AI Assistant"}
42
54
  </p>
43
- <div className="whitespace-pre-wrap">{message.content}</div>
55
+ {message.parts?.map((part, index) => {
56
+ if (part.type === "text") {
57
+ return (
58
+ <div key={index} className="whitespace-pre-wrap">
59
+ {part.text}
60
+ </div>
61
+ );
62
+ }
63
+ return null;
64
+ })}
44
65
  </div>
45
66
  ))
46
67
  )}
@@ -54,7 +75,7 @@ function RouteComponent() {
54
75
  <Input
55
76
  name="prompt"
56
77
  value={input}
57
- onChange={handleInputChange}
78
+ onChange={(e) => setInput(e.target.value)}
58
79
  placeholder="Type your message..."
59
80
  className="flex-1"
60
81
  autoComplete="off"
@@ -1,17 +1,21 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { useChat } from "@ai-sdk/react";
3
+ import { DefaultChatTransport } from "ai";
3
4
  import { Input } from "@/components/ui/input";
4
5
  import { Button } from "@/components/ui/button";
5
6
  import { Send } from "lucide-react";
6
- import { useRef, useEffect } from "react";
7
+ import { useRef, useEffect, useState } from "react";
7
8
 
8
9
  export const Route = createFileRoute("/ai")({
9
10
  component: RouteComponent,
10
11
  });
11
12
 
12
13
  function RouteComponent() {
13
- const { messages, input, handleInputChange, handleSubmit } = useChat({
14
- api: `${import.meta.env.VITE_SERVER_URL}/ai`,
14
+ const [input, setInput] = useState("");
15
+ const { messages, sendMessage } = useChat({
16
+ transport: new DefaultChatTransport({
17
+ api: `${import.meta.env.VITE_SERVER_URL}/ai`,
18
+ }),
15
19
  });
16
20
 
17
21
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -20,6 +24,14 @@ function RouteComponent() {
20
24
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
21
25
  }, [messages]);
22
26
 
27
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
28
+ e.preventDefault();
29
+ const text = input.trim();
30
+ if (!text) return;
31
+ sendMessage({ text });
32
+ setInput("");
33
+ };
34
+
23
35
  return (
24
36
  <div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
25
37
  <div className="overflow-y-auto space-y-4 pb-4">
@@ -40,7 +52,16 @@ function RouteComponent() {
40
52
  <p className="text-sm font-semibold mb-1">
41
53
  {message.role === "user" ? "You" : "AI Assistant"}
42
54
  </p>
43
- <div className="whitespace-pre-wrap">{message.content}</div>
55
+ {message.parts?.map((part, index) => {
56
+ if (part.type === "text") {
57
+ return (
58
+ <div key={index} className="whitespace-pre-wrap">
59
+ {part.text}
60
+ </div>
61
+ );
62
+ }
63
+ return null;
64
+ })}
44
65
  </div>
45
66
  ))
46
67
  )}
@@ -54,7 +75,7 @@ function RouteComponent() {
54
75
  <Input
55
76
  name="prompt"
56
77
  value={input}
57
- onChange={handleInputChange}
78
+ onChange={(e) => setInput(e.target.value)}
58
79
  placeholder="Type your message..."
59
80
  className="flex-1"
60
81
  autoComplete="off"
@@ -66,4 +87,4 @@ function RouteComponent() {
66
87
  </form>
67
88
  </div>
68
89
  );
69
- }
90
+ }
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ import { PUBLIC_SERVER_URL } from "$env/static/public";
3
+ import { Chat } from "@ai-sdk/svelte";
4
+ import { DefaultChatTransport } from "ai";
5
+
6
+ let input = $state("");
7
+ const chat = new Chat({
8
+ transport: new DefaultChatTransport({
9
+ api: `${PUBLIC_SERVER_URL}/ai`,
10
+ }),
11
+ });
12
+
13
+ let messagesEndElement: HTMLDivElement | null = null;
14
+
15
+ $effect(() => {
16
+ if (chat.messages.length > 0) {
17
+ setTimeout(() => {
18
+ messagesEndElement?.scrollIntoView({ behavior: "smooth" });
19
+ }, 0);
20
+ }
21
+ });
22
+
23
+ function handleSubmit(e: Event) {
24
+ e.preventDefault();
25
+ const text = input.trim();
26
+ if (!text) return;
27
+ chat.sendMessage({ text });
28
+ input = "";
29
+ }
30
+ </script>
31
+
32
+ <div
33
+ class="mx-auto grid h-full w-full max-w-2xl grid-rows-[1fr_auto] overflow-hidden p-4"
34
+ >
35
+ <div class="mb-4 space-y-4 overflow-y-auto pb-4">
36
+ {#if chat.messages.length === 0}
37
+ <div class="mt-8 text-center text-neutral-500">
38
+ Ask me anything to get started!
39
+ </div>
40
+ {/if}
41
+
42
+ {#each chat.messages as message (message.id)}
43
+ <div
44
+ class="p-3 rounded-lg w-fit max-w-[85%] text-sm md:text-base"
45
+ class:ml-auto={message.role === "user"}
46
+ class:bg-primary={message.role === "user"}
47
+ class:bg-secondary={message.role === "assistant"}
48
+ >
49
+ <p
50
+ class="mb-1 text-sm font-semibold"
51
+ class:text-indigo-600={message.role === "user"}
52
+ class:text-neutral-400={message.role === "assistant"}
53
+ >
54
+ {message.role === "user" ? "You" : "AI Assistant"}
55
+ </p>
56
+ <div class="whitespace-pre-wrap break-words">
57
+ {#each message.parts as part, partIndex (partIndex)}
58
+ {#if part.type === "text"}
59
+ {part.text}
60
+ {/if}
61
+ {/each}
62
+ </div>
63
+ </div>
64
+ {/each}
65
+ <div bind:this={messagesEndElement}></div>
66
+ </div>
67
+
68
+ <form
69
+ onsubmit={handleSubmit}
70
+ class="w-full flex items-center space-x-2 pt-2 border-t"
71
+ >
72
+ <input
73
+ name="prompt"
74
+ bind:value={input}
75
+ placeholder="Type your message..."
76
+ class="flex-1 rounded border border-neutral-600 bg-neutral-800 px-3 py-2 text-neutral-100 placeholder-neutral-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:opacity-50"
77
+ autocomplete="off"
78
+ onkeydown={(e) => {
79
+ if (e.key === "Enter" && !e.shiftKey) {
80
+ e.preventDefault();
81
+ handleSubmit(e);
82
+ }
83
+ }}
84
+ />
85
+ <button
86
+ type="submit"
87
+ disabled={!input.trim()}
88
+ class="inline-flex h-10 w-10 items-center justify-center rounded bg-indigo-600 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-neutral-900 disabled:cursor-not-allowed disabled:opacity-50"
89
+ aria-label="Send message"
90
+ >
91
+ <svg
92
+ xmlns="http://www.w3.org/2000/svg"
93
+ width="18"
94
+ height="18"
95
+ viewBox="0 0 24 24"
96
+ fill="none"
97
+ stroke="currentColor"
98
+ stroke-width="2"
99
+ stroke-linecap="round"
100
+ stroke-linejoin="round"
101
+ >
102
+ <path d="m22 2-7 20-4-9-9-4Z" />
103
+ <path d="M22 2 11 13" />
104
+ </svg>
105
+ </button>
106
+ </form>
107
+ </div>
@@ -1,7 +1,7 @@
1
- {{#if (or (includes frontend "nuxt") (includes frontend "native-nativewind"))}}
2
- # [install]
1
+ [install]
2
+ {{#if (or (or (includes frontend "nuxt") (includes frontend "native-nativewind")) (includes frontend
3
+ "native-unistyles"))}}
3
4
  # linker = "isolated"
4
5
  {{else}}
5
- [install]
6
6
  linker = "isolated"
7
- {{/if}}
7
+ {{/if}}
@@ -1,15 +0,0 @@
1
- import { google } from '@ai-sdk/google';
2
- import { streamText } from 'ai';
3
-
4
- export const maxDuration = 30;
5
-
6
- export async function POST(req: Request) {
7
- const { messages } = await req.json();
8
-
9
- const result = streamText({
10
- model: google('gemini-2.0-flash'),
11
- messages,
12
- });
13
-
14
- return result.toDataStreamResponse();
15
- }
@@ -1,98 +0,0 @@
1
- <script lang="ts">
2
- import { PUBLIC_SERVER_URL } from '$env/static/public';
3
- import { Chat } from '@ai-sdk/svelte';
4
-
5
- const chat = new Chat({
6
- api: `${PUBLIC_SERVER_URL}/ai`,
7
- });
8
-
9
- let messagesEndElement: HTMLDivElement | null = null;
10
-
11
- $effect(() => {
12
- const messageCount = chat.messages.length;
13
- if (messageCount > 0) {
14
- setTimeout(() => {
15
- messagesEndElement?.scrollIntoView({ behavior: 'smooth' });
16
- }, 0);
17
- }
18
- });
19
-
20
- </script>
21
-
22
- <div class="mx-auto grid h-full w-full max-w-2xl grid-rows-[1fr_auto] overflow-hidden p-4">
23
- <div class="mb-4 space-y-4 overflow-y-auto pb-4">
24
- {#if chat.messages.length === 0}
25
- <div class="mt-8 text-center text-neutral-500">Ask the AI anything to get started!</div>
26
- {/if}
27
-
28
- {#each chat.messages as message (message.id)}
29
- <div
30
- class="w-fit max-w-[85%] rounded-lg p-3 text-sm md:text-base"
31
- class:ml-auto={message.role === 'user'}
32
- class:bg-indigo-600={message.role === 'user'}
33
- class:text-white={message.role === 'user'}
34
- class:bg-neutral-700={message.role === 'assistant'}
35
- class:text-neutral-100={message.role === 'assistant'}
36
- >
37
- <p
38
- class="mb-1 text-xs font-semibold uppercase tracking-wide"
39
- class:text-indigo-200={message.role === 'user'}
40
- class:text-neutral-400={message.role === 'assistant'}
41
- >
42
- {message.role === 'user' ? 'You' : 'AI Assistant'}
43
- </p>
44
- <div class="whitespace-pre-wrap break-words">
45
- {#each message.parts as part, partIndex (partIndex)}
46
- {#if part.type === 'text'}
47
- {part.text}
48
- {:else if part.type === 'tool-invocation'}
49
- <pre class="mt-2 rounded bg-neutral-800 p-2 text-xs text-neutral-300"
50
- >{JSON.stringify(part.toolInvocation, null, 2)}</pre
51
- >
52
- {/if}
53
- {/each}
54
- </div>
55
- </div>
56
- {/each}
57
- <div bind:this={messagesEndElement}></div>
58
- </div>
59
-
60
- <form
61
- onsubmit={chat.handleSubmit}
62
- class="flex w-full items-center space-x-2 border-t border-neutral-700 pt-4"
63
- >
64
- <input
65
- name="prompt"
66
- bind:value={chat.input}
67
- placeholder="Type your message..."
68
- class="flex-1 rounded border border-neutral-600 bg-neutral-800 px-3 py-2 text-neutral-100 placeholder-neutral-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:opacity-50"
69
- autocomplete="off"
70
- onkeydown={(e) => {
71
- if (e.key === 'Enter' && !e.shiftKey) {
72
- e.preventDefault();
73
- chat.handleSubmit(e);
74
- }
75
- }}
76
- />
77
- <button
78
- type="submit"
79
- disabled={!chat.input.trim()}
80
- class="inline-flex h-10 w-10 items-center justify-center rounded bg-indigo-600 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-neutral-900 disabled:cursor-not-allowed disabled:opacity-50"
81
- aria-label="Send message"
82
- >
83
- <svg
84
- xmlns="http://www.w3.org/2000/svg"
85
- width="18"
86
- height="18"
87
- viewBox="0 0 24 24"
88
- fill="none"
89
- stroke="currentColor"
90
- stroke-width="2"
91
- stroke-linecap="round"
92
- stroke-linejoin="round"
93
- >
94
- <path d="m22 2-7 20-4-9-9-4Z" /><path d="M22 2 11 13" />
95
- </svg>
96
- </button>
97
- </form>
98
- </div>