create-better-t-stack 3.10.0 → 3.11.0-pr749.7e7198c

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.
@@ -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}}
@@ -1,3 +1,297 @@
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 React, { useRef, useEffect, useState } from "react";
11
+ import {
12
+ View,
13
+ Text,
14
+ TextInput,
15
+ TouchableOpacity,
16
+ ScrollView,
17
+ KeyboardAvoidingView,
18
+ Platform,
19
+ ActivityIndicator,
20
+ } from "react-native";
21
+ import { StyleSheet, useUnistyles } from "react-native-unistyles";
22
+
23
+ import { Container } from "@/components/container";
24
+
25
+ function MessageContent({
26
+ text,
27
+ isStreaming,
28
+ style,
29
+ }: {
30
+ text: string;
31
+ isStreaming: boolean;
32
+ style: object;
33
+ }) {
34
+ const [visibleText] = useSmoothText(text, {
35
+ startStreaming: isStreaming,
36
+ });
37
+ return <Text style={style}>{visibleText}</Text>;
38
+ }
39
+
40
+ export default function AIScreen() {
41
+ const { theme } = useUnistyles();
42
+ const [input, setInput] = useState("");
43
+ const [threadId, setThreadId] = useState<string | null>(null);
44
+ const [isLoading, setIsLoading] = useState(false);
45
+ const scrollViewRef = useRef<ScrollView>(null);
46
+
47
+ const createThread = useMutation(api.chat.createNewThread);
48
+ const sendMessage = useMutation(api.chat.sendMessage);
49
+
50
+ const { results: messages } = useUIMessages(
51
+ api.chat.listMessages,
52
+ threadId ? { threadId } : "skip",
53
+ { initialNumItems: 50, stream: true },
54
+ );
55
+
56
+ const hasStreamingMessage = messages?.some(
57
+ (m: UIMessage) => m.status === "streaming",
58
+ );
59
+
60
+ useEffect(() => {
61
+ scrollViewRef.current?.scrollToEnd({ animated: true });
62
+ }, [messages]);
63
+
64
+ const onSubmit = async () => {
65
+ const value = input.trim();
66
+ if (!value || isLoading) return;
67
+
68
+ setIsLoading(true);
69
+ setInput("");
70
+
71
+ try {
72
+ let currentThreadId = threadId;
73
+ if (!currentThreadId) {
74
+ currentThreadId = await createThread();
75
+ setThreadId(currentThreadId);
76
+ }
77
+
78
+ await sendMessage({ threadId: currentThreadId, prompt: value });
79
+ } catch (error) {
80
+ console.error("Failed to send message:", error);
81
+ } finally {
82
+ setIsLoading(false);
83
+ }
84
+ };
85
+
86
+ return (
87
+ <Container>
88
+ <KeyboardAvoidingView
89
+ style={styles.container}
90
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
91
+ >
92
+ <View style={styles.content}>
93
+ <View style={styles.header}>
94
+ <Text style={styles.headerTitle}>AI Chat</Text>
95
+ <Text style={styles.headerSubtitle}>
96
+ Chat with our AI assistant
97
+ </Text>
98
+ </View>
99
+
100
+ <ScrollView
101
+ ref={scrollViewRef}
102
+ style={styles.messagesContainer}
103
+ showsVerticalScrollIndicator={false}
104
+ >
105
+ {!messages || messages.length === 0 ? (
106
+ <View style={styles.emptyContainer}>
107
+ <Text style={styles.emptyText}>
108
+ Ask me anything to get started!
109
+ </Text>
110
+ </View>
111
+ ) : (
112
+ <View style={styles.messagesWrapper}>
113
+ {messages.map((message: UIMessage) => (
114
+ <View
115
+ key={message.key}
116
+ style={[
117
+ styles.messageContainer,
118
+ message.role === "user"
119
+ ? styles.userMessage
120
+ : styles.assistantMessage,
121
+ ]}
122
+ >
123
+ <Text style={styles.messageRole}>
124
+ {message.role === "user" ? "You" : "AI Assistant"}
125
+ </Text>
126
+ <MessageContent
127
+ text={message.text ?? ""}
128
+ isStreaming={message.status === "streaming"}
129
+ style={styles.messageContent}
130
+ />
131
+ </View>
132
+ ))}
133
+ {isLoading && !hasStreamingMessage && (
134
+ <View style={[styles.messageContainer, styles.assistantMessage]}>
135
+ <Text style={styles.messageRole}>AI Assistant</Text>
136
+ <View style={styles.loadingContainer}>
137
+ <ActivityIndicator size="small" color={theme.colors.primary} />
138
+ <Text style={styles.loadingText}>Thinking...</Text>
139
+ </View>
140
+ </View>
141
+ )}
142
+ </View>
143
+ )}
144
+ </ScrollView>
145
+
146
+ <View style={styles.inputSection}>
147
+ <View style={styles.inputContainer}>
148
+ <TextInput
149
+ value={input}
150
+ onChangeText={setInput}
151
+ placeholder="Type your message..."
152
+ placeholderTextColor={theme.colors.border}
153
+ style={styles.textInput}
154
+ onSubmitEditing={(e) => {
155
+ e.preventDefault();
156
+ onSubmit();
157
+ }}
158
+ editable={!isLoading}
159
+ autoFocus={true}
160
+ />
161
+ <TouchableOpacity
162
+ onPress={onSubmit}
163
+ disabled={!input.trim() || isLoading}
164
+ style={[
165
+ styles.sendButton,
166
+ (!input.trim() || isLoading) && styles.sendButtonDisabled,
167
+ ]}
168
+ >
169
+ <Ionicons
170
+ name="send"
171
+ size={20}
172
+ color={
173
+ input.trim() && !isLoading
174
+ ? theme.colors.background
175
+ : theme.colors.border
176
+ }
177
+ />
178
+ </TouchableOpacity>
179
+ </View>
180
+ </View>
181
+ </View>
182
+ </KeyboardAvoidingView>
183
+ </Container>
184
+ );
185
+ }
186
+
187
+ const styles = StyleSheet.create((theme) => ({
188
+ container: {
189
+ flex: 1,
190
+ },
191
+ content: {
192
+ flex: 1,
193
+ paddingHorizontal: theme.spacing.md,
194
+ paddingVertical: theme.spacing.lg,
195
+ },
196
+ header: {
197
+ marginBottom: theme.spacing.lg,
198
+ },
199
+ headerTitle: {
200
+ fontSize: 28,
201
+ fontWeight: "bold",
202
+ color: theme.colors.typography,
203
+ marginBottom: theme.spacing.sm,
204
+ },
205
+ headerSubtitle: {
206
+ fontSize: 16,
207
+ color: theme.colors.typography,
208
+ },
209
+ messagesContainer: {
210
+ flex: 1,
211
+ marginBottom: theme.spacing.md,
212
+ },
213
+ emptyContainer: {
214
+ flex: 1,
215
+ justifyContent: "center",
216
+ alignItems: "center",
217
+ },
218
+ emptyText: {
219
+ textAlign: "center",
220
+ color: theme.colors.typography,
221
+ fontSize: 18,
222
+ },
223
+ messagesWrapper: {
224
+ gap: theme.spacing.md,
225
+ },
226
+ messageContainer: {
227
+ padding: theme.spacing.md,
228
+ borderRadius: 8,
229
+ },
230
+ userMessage: {
231
+ backgroundColor: theme.colors.primary + "20",
232
+ marginLeft: theme.spacing.xl,
233
+ alignSelf: "flex-end",
234
+ },
235
+ assistantMessage: {
236
+ backgroundColor: theme.colors.background,
237
+ marginRight: theme.spacing.xl,
238
+ borderWidth: 1,
239
+ borderColor: theme.colors.border,
240
+ },
241
+ messageRole: {
242
+ fontSize: 14,
243
+ fontWeight: "600",
244
+ marginBottom: theme.spacing.sm,
245
+ color: theme.colors.typography,
246
+ },
247
+ messageContent: {
248
+ color: theme.colors.typography,
249
+ lineHeight: 20,
250
+ },
251
+ loadingContainer: {
252
+ flexDirection: "row",
253
+ alignItems: "center",
254
+ gap: theme.spacing.sm,
255
+ },
256
+ loadingText: {
257
+ color: theme.colors.typography,
258
+ opacity: 0.7,
259
+ },
260
+ inputSection: {
261
+ borderTopWidth: 1,
262
+ borderTopColor: theme.colors.border,
263
+ paddingTop: theme.spacing.md,
264
+ },
265
+ inputContainer: {
266
+ flexDirection: "row",
267
+ alignItems: "flex-end",
268
+ gap: theme.spacing.sm,
269
+ },
270
+ textInput: {
271
+ flex: 1,
272
+ borderWidth: 1,
273
+ borderColor: theme.colors.border,
274
+ borderRadius: 8,
275
+ paddingHorizontal: theme.spacing.md,
276
+ paddingVertical: theme.spacing.sm,
277
+ color: theme.colors.typography,
278
+ backgroundColor: theme.colors.background,
279
+ fontSize: 16,
280
+ minHeight: 40,
281
+ maxHeight: 120,
282
+ },
283
+ sendButton: {
284
+ backgroundColor: theme.colors.primary,
285
+ padding: theme.spacing.sm,
286
+ borderRadius: 8,
287
+ justifyContent: "center",
288
+ alignItems: "center",
289
+ },
290
+ sendButtonDisabled: {
291
+ backgroundColor: theme.colors.border,
292
+ },
293
+ }));
294
+ {{else}}
1
295
  import React, { useRef, useEffect, useState } from "react";
2
296
  import {
3
297
  View,
@@ -256,15 +550,6 @@ const styles = StyleSheet.create((theme) => ({
256
550
  color: theme.colors.typography,
257
551
  lineHeight: 20,
258
552
  },
259
- toolInvocations: {
260
- fontSize: 12,
261
- color: theme.colors.typography,
262
- fontFamily: "monospace",
263
- backgroundColor: theme.colors.border + "40",
264
- padding: theme.spacing.sm,
265
- borderRadius: 4,
266
- marginTop: theme.spacing.sm,
267
- },
268
553
  inputSection: {
269
554
  borderTopWidth: 1,
270
555
  borderTopColor: theme.colors.border,
@@ -298,4 +583,5 @@ const styles = StyleSheet.create((theme) => ({
298
583
  sendButtonDisabled: {
299
584
  backgroundColor: theme.colors.border,
300
585
  },
301
- }));
586
+ }));
587
+ {{/if}}