create-better-t-stack 3.9.0 → 3.11.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.
Files changed (77) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.mjs +1 -1
  3. package/dist/index.d.mts +7 -3
  4. package/dist/index.mjs +1 -1
  5. package/dist/{src-DLvUK0Qf.mjs → src-XVvJUQ_h.mjs} +270 -93
  6. package/package.json +44 -44
  7. package/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs +5 -7
  8. package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +17 -17
  9. package/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +4 -4
  10. package/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs +2 -2
  11. package/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs +10 -10
  12. package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs +13 -5
  13. package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs +14 -12
  14. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs +13 -16
  15. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs +11 -5
  16. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs +4 -4
  17. package/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs +1 -1
  18. package/templates/auth/better-auth/web/react/next/src/components/user-menu.tsx.hbs +17 -15
  19. package/templates/auth/better-auth/web/react/react-router/src/components/user-menu.tsx.hbs +16 -15
  20. package/templates/auth/better-auth/web/react/tanstack-router/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
  21. package/templates/auth/better-auth/web/react/tanstack-start/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
  22. package/templates/backend/convex/packages/backend/convex/README.md +4 -4
  23. package/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +17 -0
  24. package/templates/backend/convex/packages/backend/convex/tsconfig.json.hbs +1 -1
  25. package/templates/examples/ai/convex/packages/backend/convex/agent.ts.hbs +9 -0
  26. package/templates/examples/ai/convex/packages/backend/convex/chat.ts.hbs +67 -0
  27. package/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs +301 -3
  28. package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +296 -10
  29. package/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs +180 -1
  30. package/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs +172 -9
  31. package/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs +156 -6
  32. package/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs +156 -4
  33. package/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +159 -6
  34. package/templates/frontend/react/next/package.json.hbs +8 -7
  35. package/templates/frontend/react/next/src/app/layout.tsx.hbs +28 -1
  36. package/templates/frontend/react/next/src/components/mode-toggle.tsx.hbs +4 -6
  37. package/templates/frontend/react/next/src/components/providers.tsx.hbs +14 -4
  38. package/templates/frontend/react/react-router/package.json.hbs +2 -1
  39. package/templates/frontend/react/{tanstack-router/src/components/mode-toggle.tsx → react-router/src/components/mode-toggle.tsx.hbs} +4 -6
  40. package/templates/frontend/react/tanstack-router/package.json.hbs +2 -1
  41. package/templates/frontend/react/{react-router/src/components/mode-toggle.tsx → tanstack-router/src/components/mode-toggle.tsx.hbs} +4 -6
  42. package/templates/frontend/react/tanstack-start/package.json.hbs +2 -1
  43. package/templates/frontend/react/tanstack-start/src/router.tsx.hbs +6 -0
  44. package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +13 -14
  45. package/templates/frontend/react/tanstack-start/vite.config.ts.hbs +5 -0
  46. package/templates/frontend/react/web-base/components.json +5 -2
  47. package/templates/frontend/react/web-base/src/components/ui/button.tsx.hbs +57 -0
  48. package/templates/frontend/react/web-base/src/components/ui/card.tsx.hbs +103 -0
  49. package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx.hbs +26 -0
  50. package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx.hbs +262 -0
  51. package/templates/frontend/react/web-base/src/components/ui/input.tsx.hbs +20 -0
  52. package/templates/frontend/react/web-base/src/components/ui/label.tsx.hbs +20 -0
  53. package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx.hbs +13 -0
  54. package/templates/frontend/react/web-base/src/components/ui/sonner.tsx.hbs +44 -0
  55. package/templates/frontend/react/web-base/src/index.css.hbs +58 -64
  56. package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +0 -7
  57. package/templates/examples/ai/web/react/base/src/components/response.tsx.hbs +0 -22
  58. package/templates/frontend/react/web-base/src/components/ui/button.tsx +0 -56
  59. package/templates/frontend/react/web-base/src/components/ui/card.tsx +0 -75
  60. package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx +0 -27
  61. package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx +0 -228
  62. package/templates/frontend/react/web-base/src/components/ui/input.tsx +0 -21
  63. package/templates/frontend/react/web-base/src/components/ui/label.tsx +0 -19
  64. package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx +0 -13
  65. package/templates/frontend/react/web-base/src/components/ui/sonner.tsx +0 -25
  66. /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  67. /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  68. /package/templates/auth/better-auth/web/react/tanstack-router/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  69. /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  70. /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  71. /package/templates/auth/better-auth/web/react/tanstack-start/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  72. /package/templates/auth/better-auth/web/solid/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  73. /package/templates/auth/better-auth/web/solid/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  74. /package/templates/auth/better-auth/web/solid/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  75. /package/templates/frontend/react/react-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
  76. /package/templates/frontend/react/tanstack-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
  77. /package/templates/frontend/react/web-base/src/lib/{utils.ts → utils.ts.hbs} +0 -0
@@ -1,16 +1,18 @@
1
+ import { Link, useNavigate } from "@tanstack/react-router";
2
+
1
3
  import {
2
4
  DropdownMenu,
3
5
  DropdownMenuContent,
6
+ DropdownMenuGroup,
4
7
  DropdownMenuItem,
5
8
  DropdownMenuLabel,
6
9
  DropdownMenuSeparator,
7
10
  DropdownMenuTrigger,
8
11
  } from "@/components/ui/dropdown-menu";
9
12
  import { authClient } from "@/lib/auth-client";
10
- import { useNavigate } from "@tanstack/react-router";
13
+
11
14
  import { Button } from "./ui/button";
12
15
  import { Skeleton } from "./ui/skeleton";
13
- import { Link } from "@tanstack/react-router";
14
16
 
15
17
  export default function UserMenu() {
16
18
  const navigate = useNavigate();
@@ -22,25 +24,24 @@ export default function UserMenu() {
22
24
 
23
25
  if (!session) {
24
26
  return (
25
- <Button variant="outline" asChild>
26
- <Link to="/login">Sign In</Link>
27
- </Button>
27
+ <Link to="/login">
28
+ <Button variant="outline">Sign In</Button>
29
+ </Link>
28
30
  );
29
31
  }
30
32
 
31
33
  return (
32
34
  <DropdownMenu>
33
- <DropdownMenuTrigger asChild>
34
- <Button variant="outline">{session.user.name}</Button>
35
+ <DropdownMenuTrigger render={<Button variant="outline" />}>
36
+ {session.user.name}
35
37
  </DropdownMenuTrigger>
36
38
  <DropdownMenuContent className="bg-card">
37
- <DropdownMenuLabel>My Account</DropdownMenuLabel>
38
- <DropdownMenuSeparator />
39
- <DropdownMenuItem>{session.user.email}</DropdownMenuItem>
40
- <DropdownMenuItem asChild>
41
- <Button
39
+ <DropdownMenuGroup>
40
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
41
+ <DropdownMenuSeparator />
42
+ <DropdownMenuItem>{session.user.email}</DropdownMenuItem>
43
+ <DropdownMenuItem
42
44
  variant="destructive"
43
- className="w-full"
44
45
  onClick={() => {
45
46
  authClient.signOut({
46
47
  fetchOptions: {
@@ -54,8 +55,8 @@ export default function UserMenu() {
54
55
  }}
55
56
  >
56
57
  Sign Out
57
- </Button>
58
- </DropdownMenuItem>
58
+ </DropdownMenuItem>
59
+ </DropdownMenuGroup>
59
60
  </DropdownMenuContent>
60
61
  </DropdownMenu>
61
62
  );
@@ -1,16 +1,18 @@
1
+ import { Link, useNavigate } from "@tanstack/react-router";
2
+
1
3
  import {
2
4
  DropdownMenu,
3
5
  DropdownMenuContent,
6
+ DropdownMenuGroup,
4
7
  DropdownMenuItem,
5
8
  DropdownMenuLabel,
6
9
  DropdownMenuSeparator,
7
10
  DropdownMenuTrigger,
8
11
  } from "@/components/ui/dropdown-menu";
9
12
  import { authClient } from "@/lib/auth-client";
10
- import { useNavigate } from "@tanstack/react-router";
13
+
11
14
  import { Button } from "./ui/button";
12
15
  import { Skeleton } from "./ui/skeleton";
13
- import { Link } from "@tanstack/react-router";
14
16
 
15
17
  export default function UserMenu() {
16
18
  const navigate = useNavigate();
@@ -22,25 +24,24 @@ export default function UserMenu() {
22
24
 
23
25
  if (!session) {
24
26
  return (
25
- <Button variant="outline" asChild>
26
- <Link to="/login">Sign In</Link>
27
- </Button>
27
+ <Link to="/login">
28
+ <Button variant="outline">Sign In</Button>
29
+ </Link>
28
30
  );
29
31
  }
30
32
 
31
33
  return (
32
34
  <DropdownMenu>
33
- <DropdownMenuTrigger asChild>
34
- <Button variant="outline">{session.user.name}</Button>
35
+ <DropdownMenuTrigger render={<Button variant="outline" />}>
36
+ {session.user.name}
35
37
  </DropdownMenuTrigger>
36
38
  <DropdownMenuContent className="bg-card">
37
- <DropdownMenuLabel>My Account</DropdownMenuLabel>
38
- <DropdownMenuSeparator />
39
- <DropdownMenuItem>{session.user.email}</DropdownMenuItem>
40
- <DropdownMenuItem asChild>
41
- <Button
39
+ <DropdownMenuGroup>
40
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
41
+ <DropdownMenuSeparator />
42
+ <DropdownMenuItem>{session.user.email}</DropdownMenuItem>
43
+ <DropdownMenuItem
42
44
  variant="destructive"
43
- className="w-full"
44
45
  onClick={() => {
45
46
  authClient.signOut({
46
47
  fetchOptions: {
@@ -54,8 +55,8 @@ export default function UserMenu() {
54
55
  }}
55
56
  >
56
57
  Sign Out
57
- </Button>
58
- </DropdownMenuItem>
58
+ </DropdownMenuItem>
59
+ </DropdownMenuGroup>
59
60
  </DropdownMenuContent>
60
61
  </DropdownMenu>
61
62
  );
@@ -6,7 +6,7 @@ See https://docs.convex.dev/functions for more.
6
6
  A query function that takes two arguments looks like:
7
7
 
8
8
  ```ts
9
- // functions.js
9
+ // convex/myFunctions.ts
10
10
  import { query } from "./_generated/server";
11
11
  import { v } from "convex/values";
12
12
 
@@ -36,7 +36,7 @@ export const myQueryFunction = query({
36
36
  Using this query function in a React component looks like:
37
37
 
38
38
  ```ts
39
- const data = useQuery(api.functions.myQueryFunction, {
39
+ const data = useQuery(api.myFunctions.myQueryFunction, {
40
40
  first: 10,
41
41
  second: "hello",
42
42
  });
@@ -45,7 +45,7 @@ const data = useQuery(api.functions.myQueryFunction, {
45
45
  A mutation function looks like:
46
46
 
47
47
  ```ts
48
- // functions.js
48
+ // convex/myFunctions.ts
49
49
  import { mutation } from "./_generated/server";
50
50
  import { v } from "convex/values";
51
51
 
@@ -73,7 +73,7 @@ export const myMutationFunction = mutation({
73
73
  Using this mutation function in a React component looks like:
74
74
 
75
75
  ```ts
76
- const mutation = useMutation(api.functions.myMutationFunction);
76
+ const mutation = useMutation(api.myFunctions.myMutationFunction);
77
77
  function handleButtonPress() {
78
78
  // fire and forget, the most common way to use mutations
79
79
  mutation({ first: "Hello!", second: "me" });
@@ -0,0 +1,17 @@
1
+ import { defineApp } from "convex/server";
2
+ {{#if (eq auth "better-auth")}}
3
+ import betterAuth from "@convex-dev/better-auth/convex.config";
4
+ {{/if}}
5
+ {{#if (includes examples "ai")}}
6
+ import agent from "@convex-dev/agent/convex.config";
7
+ {{/if}}
8
+
9
+ const app = defineApp();
10
+ {{#if (eq auth "better-auth")}}
11
+ app.use(betterAuth);
12
+ {{/if}}
13
+ {{#if (includes examples "ai")}}
14
+ app.use(agent);
15
+ {{/if}}
16
+
17
+ export default app;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  /* This TypeScript project config describes the environment that
3
3
  * Convex functions run in and is used to typecheck them.
4
- * You can modify it, but some settings required to use Convex.
4
+ * You can modify it, but some settings are required to use Convex.
5
5
  */
6
6
  "compilerOptions": {
7
7
  /* These settings are not required by Convex and can be modified. */
@@ -0,0 +1,9 @@
1
+ import { Agent } from "@convex-dev/agent";
2
+ import { google } from "@ai-sdk/google";
3
+ import { components } from "./_generated/api";
4
+
5
+ export const chatAgent = new Agent(components.agent, {
6
+ name: "Chat Agent",
7
+ languageModel: google("gemini-2.5-flash"),
8
+ instructions: "You are a helpful AI assistant. Be concise and friendly in your responses.",
9
+ });
@@ -0,0 +1,67 @@
1
+ import {
2
+ createThread,
3
+ listUIMessages,
4
+ saveMessage,
5
+ syncStreams,
6
+ vStreamArgs,
7
+ } from "@convex-dev/agent";
8
+ import { paginationOptsValidator } from "convex/server";
9
+ import { v } from "convex/values";
10
+
11
+ import { components, internal } from "./_generated/api";
12
+ import { internalAction, mutation, query } from "./_generated/server";
13
+ import { chatAgent } from "./agent";
14
+
15
+ export const createNewThread = mutation({
16
+ args: {},
17
+ handler: async (ctx) => {
18
+ const threadId = await createThread(ctx, components.agent, {});
19
+ return threadId;
20
+ },
21
+ });
22
+
23
+ export const listMessages = query({
24
+ args: {
25
+ threadId: v.string(),
26
+ paginationOpts: paginationOptsValidator,
27
+ streamArgs: vStreamArgs,
28
+ },
29
+ handler: async (ctx, args) => {
30
+ const paginated = await listUIMessages(ctx, components.agent, args);
31
+ const streams = await syncStreams(ctx, components.agent, args);
32
+ return { ...paginated, streams };
33
+ },
34
+ });
35
+
36
+ export const sendMessage = mutation({
37
+ args: {
38
+ threadId: v.string(),
39
+ prompt: v.string(),
40
+ },
41
+ handler: async (ctx, { threadId, prompt }) => {
42
+ const { messageId } = await saveMessage(ctx, components.agent, {
43
+ threadId,
44
+ prompt,
45
+ });
46
+ await ctx.scheduler.runAfter(0, internal.chat.generateResponseAsync, {
47
+ threadId,
48
+ promptMessageId: messageId,
49
+ });
50
+ return messageId;
51
+ },
52
+ });
53
+
54
+ export const generateResponseAsync = internalAction({
55
+ args: {
56
+ threadId: v.string(),
57
+ promptMessageId: v.string(),
58
+ },
59
+ handler: async (ctx, { threadId, promptMessageId }) => {
60
+ await chatAgent.streamText(
61
+ ctx,
62
+ { threadId },
63
+ { promptMessageId },
64
+ { saveStreamDeltas: true },
65
+ );
66
+ },
67
+ });
@@ -1,3 +1,301 @@
1
+ {{#if (eq backend "convex")}}
2
+ import { Ionicons } from "@expo/vector-icons";
3
+ import {
4
+ useUIMessages,
5
+ useSmoothText,
6
+ type UIMessage,
7
+ } from "@convex-dev/agent/react";
8
+ import { api } from "@{{projectName}}/backend/convex/_generated/api";
9
+ import { useMutation } from "convex/react";
10
+ import { useRef, useEffect, useState } from "react";
11
+ import {
12
+ View,
13
+ Text,
14
+ TextInput,
15
+ TouchableOpacity,
16
+ ScrollView,
17
+ KeyboardAvoidingView,
18
+ Platform,
19
+ StyleSheet,
20
+ ActivityIndicator,
21
+ } from "react-native";
22
+
23
+ import { Container } from "@/components/container";
24
+ import { useColorScheme } from "@/lib/use-color-scheme";
25
+ import { NAV_THEME } from "@/lib/constants";
26
+
27
+ function MessageContent({
28
+ text,
29
+ isStreaming,
30
+ textColor,
31
+ }: {
32
+ text: string;
33
+ isStreaming: boolean;
34
+ textColor: string;
35
+ }) {
36
+ const [visibleText] = useSmoothText(text, {
37
+ startStreaming: isStreaming,
38
+ });
39
+ return <Text style={[styles.messageText, { color: textColor }]}>{visibleText}</Text>;
40
+ }
41
+
42
+ export default function AIScreen() {
43
+ const { colorScheme } = useColorScheme();
44
+ const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light;
45
+ const [input, setInput] = useState("");
46
+ const [threadId, setThreadId] = useState<string | null>(null);
47
+ const [isLoading, setIsLoading] = useState(false);
48
+ const scrollViewRef = useRef<ScrollView>(null);
49
+
50
+ const createThread = useMutation(api.chat.createNewThread);
51
+ const sendMessage = useMutation(api.chat.sendMessage);
52
+
53
+ const { results: messages } = useUIMessages(
54
+ api.chat.listMessages,
55
+ threadId ? { threadId } : "skip",
56
+ { initialNumItems: 50, stream: true },
57
+ );
58
+
59
+ const hasStreamingMessage = messages?.some(
60
+ (m: UIMessage) => m.status === "streaming",
61
+ );
62
+
63
+ useEffect(() => {
64
+ scrollViewRef.current?.scrollToEnd({ animated: true });
65
+ }, [messages]);
66
+
67
+ async function onSubmit() {
68
+ const value = input.trim();
69
+ if (!value || isLoading) return;
70
+
71
+ setIsLoading(true);
72
+ setInput("");
73
+
74
+ try {
75
+ let currentThreadId = threadId;
76
+ if (!currentThreadId) {
77
+ currentThreadId = await createThread();
78
+ setThreadId(currentThreadId);
79
+ }
80
+
81
+ await sendMessage({ threadId: currentThreadId, prompt: value });
82
+ } catch (error) {
83
+ console.error("Failed to send message:", error);
84
+ } finally {
85
+ setIsLoading(false);
86
+ }
87
+ }
88
+
89
+ return (
90
+ <Container>
91
+ <KeyboardAvoidingView
92
+ style={styles.container}
93
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
94
+ >
95
+ <View style={styles.content}>
96
+ <View style={styles.header}>
97
+ <Text style={[styles.headerTitle, { color: theme.text }]}>
98
+ AI Chat
99
+ </Text>
100
+ <Text style={[styles.headerSubtitle, { color: theme.text, opacity: 0.7 }]}>
101
+ Chat with our AI assistant
102
+ </Text>
103
+ </View>
104
+ <ScrollView
105
+ ref={scrollViewRef}
106
+ style={styles.scrollView}
107
+ showsVerticalScrollIndicator={false}
108
+ >
109
+ {!messages || messages.length === 0 ? (
110
+ <View style={styles.emptyContainer}>
111
+ <Text style={[styles.emptyText, { color: theme.text, opacity: 0.7 }]}>
112
+ Ask me anything to get started!
113
+ </Text>
114
+ </View>
115
+ ) : (
116
+ <View style={styles.messagesList}>
117
+ {messages.map((message: UIMessage) => (
118
+ <View
119
+ key={message.key}
120
+ style={[
121
+ styles.messageCard,
122
+ {
123
+ backgroundColor: message.role === "user"
124
+ ? theme.primary + "20"
125
+ : theme.card,
126
+ borderColor: theme.border,
127
+ alignSelf: message.role === "user" ? "flex-end" : "flex-start",
128
+ marginLeft: message.role === "user" ? 32 : 0,
129
+ marginRight: message.role === "user" ? 0 : 32,
130
+ },
131
+ ]}
132
+ >
133
+ <Text style={[styles.messageRole, { color: theme.text }]}>
134
+ {message.role === "user" ? "You" : "AI Assistant"}
135
+ </Text>
136
+ <MessageContent
137
+ text={message.text ?? ""}
138
+ isStreaming={message.status === "streaming"}
139
+ textColor={theme.text}
140
+ />
141
+ </View>
142
+ ))}
143
+ {isLoading && !hasStreamingMessage && (
144
+ <View
145
+ style={[
146
+ styles.messageCard,
147
+ {
148
+ backgroundColor: theme.card,
149
+ borderColor: theme.border,
150
+ alignSelf: "flex-start",
151
+ marginRight: 32,
152
+ },
153
+ ]}
154
+ >
155
+ <Text style={[styles.messageRole, { color: theme.text }]}>
156
+ AI Assistant
157
+ </Text>
158
+ <View style={styles.loadingContainer}>
159
+ <ActivityIndicator size="small" color={theme.primary} />
160
+ <Text style={[styles.loadingText, { color: theme.text, opacity: 0.7 }]}>
161
+ Thinking...
162
+ </Text>
163
+ </View>
164
+ </View>
165
+ )}
166
+ </View>
167
+ )}
168
+ </ScrollView>
169
+ <View style={[styles.inputContainer, { borderTopColor: theme.border }]}>
170
+ <View style={styles.inputRow}>
171
+ <TextInput
172
+ value={input}
173
+ onChangeText={setInput}
174
+ placeholder="Type your message..."
175
+ placeholderTextColor={theme.text}
176
+ style={[
177
+ styles.input,
178
+ {
179
+ color: theme.text,
180
+ borderColor: theme.border,
181
+ backgroundColor: theme.background,
182
+ },
183
+ ]}
184
+ onSubmitEditing={(e) => {
185
+ e.preventDefault();
186
+ onSubmit();
187
+ }}
188
+ editable={!isLoading}
189
+ autoFocus={true}
190
+ multiline
191
+ />
192
+ <TouchableOpacity
193
+ onPress={onSubmit}
194
+ disabled={!input.trim() || isLoading}
195
+ style={[
196
+ styles.sendButton,
197
+ {
198
+ backgroundColor: input.trim() && !isLoading ? theme.primary : theme.border,
199
+ opacity: input.trim() && !isLoading ? 1 : 0.5,
200
+ },
201
+ ]}
202
+ >
203
+ <Ionicons
204
+ name="send"
205
+ size={20}
206
+ color="#ffffff"
207
+ />
208
+ </TouchableOpacity>
209
+ </View>
210
+ </View>
211
+ </View>
212
+ </KeyboardAvoidingView>
213
+ </Container>
214
+ );
215
+ }
216
+
217
+ const styles = StyleSheet.create({
218
+ container: {
219
+ flex: 1,
220
+ },
221
+ content: {
222
+ flex: 1,
223
+ padding: 16,
224
+ },
225
+ header: {
226
+ marginBottom: 16,
227
+ },
228
+ headerTitle: {
229
+ fontSize: 20,
230
+ fontWeight: "bold",
231
+ marginBottom: 4,
232
+ },
233
+ headerSubtitle: {
234
+ fontSize: 14,
235
+ },
236
+ scrollView: {
237
+ flex: 1,
238
+ marginBottom: 16,
239
+ },
240
+ emptyContainer: {
241
+ flex: 1,
242
+ justifyContent: "center",
243
+ alignItems: "center",
244
+ },
245
+ emptyText: {
246
+ fontSize: 16,
247
+ textAlign: "center",
248
+ },
249
+ messagesList: {
250
+ gap: 8,
251
+ paddingBottom: 16,
252
+ },
253
+ messageCard: {
254
+ borderWidth: 1,
255
+ padding: 12,
256
+ maxWidth: "80%",
257
+ },
258
+ messageRole: {
259
+ fontSize: 12,
260
+ fontWeight: "bold",
261
+ marginBottom: 4,
262
+ },
263
+ messageText: {
264
+ fontSize: 14,
265
+ lineHeight: 20,
266
+ },
267
+ loadingContainer: {
268
+ flexDirection: "row",
269
+ alignItems: "center",
270
+ gap: 8,
271
+ },
272
+ loadingText: {
273
+ fontSize: 14,
274
+ },
275
+ inputContainer: {
276
+ borderTopWidth: 1,
277
+ paddingTop: 12,
278
+ },
279
+ inputRow: {
280
+ flexDirection: "row",
281
+ alignItems: "flex-end",
282
+ gap: 8,
283
+ },
284
+ input: {
285
+ flex: 1,
286
+ borderWidth: 1,
287
+ padding: 8,
288
+ fontSize: 14,
289
+ minHeight: 36,
290
+ maxHeight: 100,
291
+ },
292
+ sendButton: {
293
+ padding: 8,
294
+ justifyContent: "center",
295
+ alignItems: "center",
296
+ },
297
+ });
298
+ {{else}}
1
299
  import { useRef, useEffect, useState } from "react";
2
300
  import {
3
301
  View,
@@ -104,8 +402,8 @@ export default function AIScreen() {
104
402
  style={[
105
403
  styles.messageCard,
106
404
  {
107
- backgroundColor: message.role === "user"
108
- ? theme.primary + "20"
405
+ backgroundColor: message.role === "user"
406
+ ? theme.primary + "20"
109
407
  : theme.card,
110
408
  borderColor: theme.border,
111
409
  alignSelf: message.role === "user" ? "flex-end" : "flex-start",
@@ -284,4 +582,4 @@ const styles = StyleSheet.create({
284
582
  textAlign: "center",
285
583
  },
286
584
  });
287
-
585
+ {{/if}}