create-better-t-stack 2.14.4 → 2.15.1
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 +20 -16
- package/package.json +1 -1
- package/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs +155 -0
- package/templates/examples/ai/native/nativewind/polyfills.js +25 -0
- package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +279 -0
- package/templates/examples/ai/native/unistyles/polyfills.js +25 -0
- package/templates/examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs +340 -0
- package/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx +24 -7
- package/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx +15 -13
- package/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx +15 -13
- package/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs +25 -9
- package/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs +43 -30
- package/templates/frontend/native/nativewind/app/+not-found.tsx +20 -9
- package/templates/frontend/native/nativewind/app/_layout.tsx.hbs +3 -0
- package/templates/frontend/native/nativewind/app/modal.tsx +9 -7
- package/templates/frontend/native/nativewind/components/container.tsx +2 -3
- package/templates/frontend/native/nativewind/components/header-button.tsx +24 -29
- package/templates/frontend/native/nativewind/components/tabbar-icon.tsx +3 -10
- package/templates/frontend/native/nativewind/global.css +36 -11
- package/templates/frontend/native/nativewind/lib/constants.ts +8 -8
- package/templates/frontend/native/nativewind/{package.json → package.json.hbs} +4 -0
- package/templates/frontend/native/nativewind/tailwind.config.js +27 -0
- package/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx +9 -4
- package/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx +25 -17
- package/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx +26 -18
- package/templates/frontend/native/unistyles/app/(drawer)/{_layout.tsx → _layout.tsx.hbs} +35 -7
- package/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs +121 -58
- package/templates/frontend/native/unistyles/app/+not-found.tsx +45 -14
- package/templates/frontend/native/unistyles/app/_layout.tsx.hbs +9 -6
- package/templates/frontend/native/unistyles/app/modal.tsx +18 -14
- package/templates/frontend/native/unistyles/components/container.tsx +3 -8
- package/templates/frontend/native/unistyles/components/header-button.tsx +33 -28
- package/templates/frontend/native/unistyles/components/tabbar-icon.tsx +3 -10
- package/templates/frontend/native/unistyles/{package.json → package.json.hbs} +5 -1
- package/templates/frontend/native/unistyles/theme.ts +82 -19
- package/templates/frontend/native/unistyles/unistyles.ts +4 -4
- /package/templates/examples/todo/native/nativewind/app/{todo.tsx.hbs → (drawer)/todos.tsx.hbs} +0 -0
package/dist/index.js
CHANGED
|
@@ -1686,7 +1686,7 @@ async function setupExamples(config) {
|
|
|
1686
1686
|
const serverDirExists = await fs.pathExists(serverDir);
|
|
1687
1687
|
const hasNuxt = frontend.includes("nuxt");
|
|
1688
1688
|
const hasSvelte = frontend.includes("svelte");
|
|
1689
|
-
const hasReact = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
|
|
1689
|
+
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");
|
|
1690
1690
|
if (clientDirExists) {
|
|
1691
1691
|
const dependencies = ["ai"];
|
|
1692
1692
|
if (hasNuxt) dependencies.push("@ai-sdk/vue");
|
|
@@ -2039,7 +2039,9 @@ function getNativeInstructions(isConvex) {
|
|
|
2039
2039
|
const exampleUrl = isConvex ? "https://<YOUR_CONVEX_URL>" : "http://<YOUR_LOCAL_IP>:3000";
|
|
2040
2040
|
const envFileName = ".env";
|
|
2041
2041
|
const ipNote = isConvex ? "your Convex deployment URL (find after running 'dev:setup')" : "your local IP address";
|
|
2042
|
-
|
|
2042
|
+
let instructions = `${pc.yellow("NOTE:")} For Expo connectivity issues, update apps/native/${envFileName} \nwith ${ipNote}:\n${`${envVar}=${exampleUrl}`}\n`;
|
|
2043
|
+
if (isConvex) instructions += `\n${pc.yellow("IMPORTANT:")} When using local development with Convex and native apps, ensure you use your local IP address \ninstead of localhost or 127.0.0.1 for proper connectivity.\n`;
|
|
2044
|
+
return instructions;
|
|
2043
2045
|
}
|
|
2044
2046
|
function getLintingInstructions(runCmd) {
|
|
2045
2047
|
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
|
|
@@ -2511,6 +2513,8 @@ async function setupExamplesTemplate(projectDir, context) {
|
|
|
2511
2513
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2512
2514
|
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
|
2513
2515
|
const webAppDirExists = await fs.pathExists(webAppDir);
|
|
2516
|
+
const nativeAppDir = path.join(projectDir, "apps/native");
|
|
2517
|
+
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
|
2514
2518
|
const hasReactWeb = context.frontend.some((f) => [
|
|
2515
2519
|
"tanstack-router",
|
|
2516
2520
|
"react-router",
|
|
@@ -2578,6 +2582,17 @@ async function setupExamplesTemplate(projectDir, context) {
|
|
|
2578
2582
|
if (await fs.pathExists(exampleWebSolidSrc)) await processAndCopyFiles("**/*", exampleWebSolidSrc, webAppDir, context, false);
|
|
2579
2583
|
}
|
|
2580
2584
|
}
|
|
2585
|
+
if (nativeAppDirExists) {
|
|
2586
|
+
const hasNativeWind = context.frontend.includes("native-nativewind");
|
|
2587
|
+
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
2588
|
+
if (hasNativeWind || hasUnistyles) {
|
|
2589
|
+
let nativeFramework = "";
|
|
2590
|
+
if (hasNativeWind) nativeFramework = "nativewind";
|
|
2591
|
+
else if (hasUnistyles) nativeFramework = "unistyles";
|
|
2592
|
+
const exampleNativeSrc = path.join(exampleBaseDir, `native/${nativeFramework}`);
|
|
2593
|
+
if (await fs.pathExists(exampleNativeSrc)) await processAndCopyFiles("**/*", exampleNativeSrc, nativeAppDir, context, false);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2581
2596
|
}
|
|
2582
2597
|
}
|
|
2583
2598
|
async function handleExtras(projectDir, context) {
|
|
@@ -2940,19 +2955,8 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
|
2940
2955
|
if (backend === "convex") return ["todo"];
|
|
2941
2956
|
if (backend === "none") return [];
|
|
2942
2957
|
if (database === "none") return [];
|
|
2943
|
-
const onlyNative = frontends && frontends.length === 1 && (frontends[0] === "native-nativewind" || frontends[0] === "native-unistyles");
|
|
2944
|
-
if (onlyNative) return [];
|
|
2945
|
-
const hasWebFrontend = frontends?.some((f) => [
|
|
2946
|
-
"react-router",
|
|
2947
|
-
"tanstack-router",
|
|
2948
|
-
"tanstack-start",
|
|
2949
|
-
"next",
|
|
2950
|
-
"nuxt",
|
|
2951
|
-
"svelte",
|
|
2952
|
-
"solid"
|
|
2953
|
-
].includes(f)) ?? false;
|
|
2954
2958
|
const noFrontendSelected = !frontends || frontends.length === 0;
|
|
2955
|
-
if (
|
|
2959
|
+
if (noFrontendSelected) return [];
|
|
2956
2960
|
let response = [];
|
|
2957
2961
|
const options = [{
|
|
2958
2962
|
value: "todo",
|
|
@@ -3043,7 +3047,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
3043
3047
|
return true;
|
|
3044
3048
|
});
|
|
3045
3049
|
const webFramework = await select({
|
|
3046
|
-
message: "Choose
|
|
3050
|
+
message: "Choose web",
|
|
3047
3051
|
options: webOptions,
|
|
3048
3052
|
initialValue: DEFAULT_CONFIG.frontend[0]
|
|
3049
3053
|
});
|
|
@@ -3055,7 +3059,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
3055
3059
|
}
|
|
3056
3060
|
if (frontendTypes.includes("native")) {
|
|
3057
3061
|
const nativeFramework = await select({
|
|
3058
|
-
message: "Choose native
|
|
3062
|
+
message: "Choose native",
|
|
3059
3063
|
options: [{
|
|
3060
3064
|
value: "native-nativewind",
|
|
3061
3065
|
label: "NativeWind",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.1",
|
|
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",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useRef, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
ScrollView,
|
|
8
|
+
KeyboardAvoidingView,
|
|
9
|
+
Platform,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { useChat } from "@ai-sdk/react";
|
|
12
|
+
import { fetch as expoFetch } from "expo/fetch";
|
|
13
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
14
|
+
import { Container } from "@/components/container";
|
|
15
|
+
|
|
16
|
+
// Utility function to generate API URLs
|
|
17
|
+
const generateAPIUrl = (relativePath: string) => {
|
|
18
|
+
const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL;
|
|
19
|
+
if (!serverUrl) {
|
|
20
|
+
throw new Error("EXPO_PUBLIC_SERVER_URL environment variable is not defined");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const path = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
24
|
+
return serverUrl.concat(path);
|
|
25
|
+
};
|
|
26
|
+
|
|
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'),
|
|
31
|
+
onError: error => console.error(error, 'AI Chat Error'),
|
|
32
|
+
maxSteps: 5,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const scrollViewRef = useRef<ScrollView>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
scrollViewRef.current?.scrollToEnd({ animated: true });
|
|
39
|
+
}, [messages]);
|
|
40
|
+
|
|
41
|
+
const onSubmit = () => {
|
|
42
|
+
if (input.trim()) {
|
|
43
|
+
handleSubmit();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (error) {
|
|
48
|
+
return (
|
|
49
|
+
<Container>
|
|
50
|
+
<View className="flex-1 justify-center items-center px-4">
|
|
51
|
+
<Text className="text-destructive text-center text-lg mb-4">
|
|
52
|
+
Error: {error.message}
|
|
53
|
+
</Text>
|
|
54
|
+
<Text className="text-muted-foreground text-center">
|
|
55
|
+
Please check your connection and try again.
|
|
56
|
+
</Text>
|
|
57
|
+
</View>
|
|
58
|
+
</Container>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Container>
|
|
64
|
+
<KeyboardAvoidingView
|
|
65
|
+
className="flex-1"
|
|
66
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
67
|
+
>
|
|
68
|
+
<View className="flex-1 px-4 py-6">
|
|
69
|
+
<View className="mb-6">
|
|
70
|
+
<Text className="text-foreground text-2xl font-bold mb-2">
|
|
71
|
+
AI Chat
|
|
72
|
+
</Text>
|
|
73
|
+
<Text className="text-muted-foreground">
|
|
74
|
+
Chat with our AI assistant
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
77
|
+
|
|
78
|
+
<ScrollView
|
|
79
|
+
ref={scrollViewRef}
|
|
80
|
+
className="flex-1 mb-4"
|
|
81
|
+
showsVerticalScrollIndicator={false}
|
|
82
|
+
>
|
|
83
|
+
{messages.length === 0 ? (
|
|
84
|
+
<View className="flex-1 justify-center items-center">
|
|
85
|
+
<Text className="text-center text-muted-foreground text-lg">
|
|
86
|
+
Ask me anything to get started!
|
|
87
|
+
</Text>
|
|
88
|
+
</View>
|
|
89
|
+
) : (
|
|
90
|
+
<View className="space-y-4">
|
|
91
|
+
{messages.map((message) => (
|
|
92
|
+
<View
|
|
93
|
+
key={message.id}
|
|
94
|
+
className={`p-3 rounded-lg ${
|
|
95
|
+
message.role === "user"
|
|
96
|
+
? "bg-primary/10 ml-8"
|
|
97
|
+
: "bg-card mr-8 border border-border"
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
<Text className="text-sm font-semibold mb-1 text-foreground">
|
|
101
|
+
{message.role === "user" ? "You" : "AI Assistant"}
|
|
102
|
+
</Text>
|
|
103
|
+
<Text className="text-foreground leading-relaxed">
|
|
104
|
+
{message.content}
|
|
105
|
+
</Text>
|
|
106
|
+
</View>
|
|
107
|
+
))}
|
|
108
|
+
</View>
|
|
109
|
+
)}
|
|
110
|
+
</ScrollView>
|
|
111
|
+
|
|
112
|
+
<View className="border-t border-border pt-4">
|
|
113
|
+
<View className="flex-row items-end space-x-2">
|
|
114
|
+
<TextInput
|
|
115
|
+
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
|
+
}
|
|
125
|
+
placeholder="Type your message..."
|
|
126
|
+
placeholderTextColor="#6b7280"
|
|
127
|
+
className="flex-1 border border-border rounded-md px-3 py-2 text-foreground bg-background min-h-[40px] max-h-[120px]"
|
|
128
|
+
onSubmitEditing={(e) => {
|
|
129
|
+
handleSubmit(e);
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
}}
|
|
132
|
+
autoFocus={true}
|
|
133
|
+
/>
|
|
134
|
+
<TouchableOpacity
|
|
135
|
+
onPress={onSubmit}
|
|
136
|
+
disabled={!input.trim()}
|
|
137
|
+
className={`p-2 rounded-md ${
|
|
138
|
+
input.trim()
|
|
139
|
+
? "bg-primary"
|
|
140
|
+
: "bg-muted"
|
|
141
|
+
}`}
|
|
142
|
+
>
|
|
143
|
+
<Ionicons
|
|
144
|
+
name="send"
|
|
145
|
+
size={20}
|
|
146
|
+
color={input.trim() ? "#ffffff" : "#6b7280"}
|
|
147
|
+
/>
|
|
148
|
+
</TouchableOpacity>
|
|
149
|
+
</View>
|
|
150
|
+
</View>
|
|
151
|
+
</View>
|
|
152
|
+
</KeyboardAvoidingView>
|
|
153
|
+
</Container>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import structuredClone from "@ungap/structured-clone";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
if (Platform.OS !== "web") {
|
|
5
|
+
const setupPolyfills = async () => {
|
|
6
|
+
const { polyfillGlobal } = await import(
|
|
7
|
+
"react-native/Libraries/Utilities/PolyfillFunctions"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const { TextEncoderStream, TextDecoderStream } = await import(
|
|
11
|
+
"@stardazed/streams-text-encoding"
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (!("structuredClone" in global)) {
|
|
15
|
+
polyfillGlobal("structuredClone", () => structuredClone);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
polyfillGlobal("TextEncoderStream", () => TextEncoderStream);
|
|
19
|
+
polyfillGlobal("TextDecoderStream", () => TextDecoderStream);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
setupPolyfills();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { useRef, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
ScrollView,
|
|
8
|
+
KeyboardAvoidingView,
|
|
9
|
+
Platform,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { useChat } from "@ai-sdk/react";
|
|
12
|
+
import { fetch as expoFetch } from "expo/fetch";
|
|
13
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
14
|
+
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
15
|
+
import { Container } from "@/components/container";
|
|
16
|
+
|
|
17
|
+
const generateAPIUrl = (relativePath: string) => {
|
|
18
|
+
const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL;
|
|
19
|
+
if (!serverUrl) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"EXPO_PUBLIC_SERVER_URL environment variable is not defined",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const path = relativePath.startsWith("/") ? relativePath : `/${relativePath}`;
|
|
26
|
+
return serverUrl.concat(path);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default function AIScreen() {
|
|
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"),
|
|
34
|
+
onError: (error) => console.error(error, "AI Chat Error"),
|
|
35
|
+
maxSteps: 5,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const scrollViewRef = useRef<ScrollView>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
scrollViewRef.current?.scrollToEnd({ animated: true });
|
|
42
|
+
}, [messages]);
|
|
43
|
+
|
|
44
|
+
const onSubmit = () => {
|
|
45
|
+
if (input.trim()) {
|
|
46
|
+
handleSubmit();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (error) {
|
|
51
|
+
return (
|
|
52
|
+
<Container>
|
|
53
|
+
<View style={styles.errorContainer}>
|
|
54
|
+
<Text style={styles.errorText}>Error: {error.message}</Text>
|
|
55
|
+
<Text style={styles.errorSubtext}>
|
|
56
|
+
Please check your connection and try again.
|
|
57
|
+
</Text>
|
|
58
|
+
</View>
|
|
59
|
+
</Container>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Container>
|
|
65
|
+
<KeyboardAvoidingView
|
|
66
|
+
style={styles.container}
|
|
67
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
68
|
+
>
|
|
69
|
+
<View style={styles.content}>
|
|
70
|
+
<View style={styles.header}>
|
|
71
|
+
<Text style={styles.headerTitle}>AI Chat</Text>
|
|
72
|
+
<Text style={styles.headerSubtitle}>
|
|
73
|
+
Chat with our AI assistant
|
|
74
|
+
</Text>
|
|
75
|
+
</View>
|
|
76
|
+
|
|
77
|
+
<ScrollView
|
|
78
|
+
ref={scrollViewRef}
|
|
79
|
+
style={styles.messagesContainer}
|
|
80
|
+
showsVerticalScrollIndicator={false}
|
|
81
|
+
>
|
|
82
|
+
{messages.length === 0 ? (
|
|
83
|
+
<View style={styles.emptyContainer}>
|
|
84
|
+
<Text style={styles.emptyText}>
|
|
85
|
+
Ask me anything to get started!
|
|
86
|
+
</Text>
|
|
87
|
+
</View>
|
|
88
|
+
) : (
|
|
89
|
+
<View style={styles.messagesWrapper}>
|
|
90
|
+
{messages.map((message) => (
|
|
91
|
+
<View
|
|
92
|
+
key={message.id}
|
|
93
|
+
style={[
|
|
94
|
+
styles.messageContainer,
|
|
95
|
+
message.role === "user"
|
|
96
|
+
? styles.userMessage
|
|
97
|
+
: styles.assistantMessage,
|
|
98
|
+
]}
|
|
99
|
+
>
|
|
100
|
+
<Text style={styles.messageRole}>
|
|
101
|
+
{message.role === "user" ? "You" : "AI Assistant"}
|
|
102
|
+
</Text>
|
|
103
|
+
<Text style={styles.messageContent}>{message.content}</Text>
|
|
104
|
+
</View>
|
|
105
|
+
))}
|
|
106
|
+
</View>
|
|
107
|
+
)}
|
|
108
|
+
</ScrollView>
|
|
109
|
+
|
|
110
|
+
<View style={styles.inputSection}>
|
|
111
|
+
<View style={styles.inputContainer}>
|
|
112
|
+
<TextInput
|
|
113
|
+
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
|
+
}
|
|
123
|
+
placeholder="Type your message..."
|
|
124
|
+
placeholderTextColor={theme.colors.border}
|
|
125
|
+
style={styles.textInput}
|
|
126
|
+
onSubmitEditing={(e) => {
|
|
127
|
+
handleSubmit(e);
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
}}
|
|
130
|
+
autoFocus={true}
|
|
131
|
+
/>
|
|
132
|
+
<TouchableOpacity
|
|
133
|
+
onPress={onSubmit}
|
|
134
|
+
disabled={!input.trim()}
|
|
135
|
+
style={[
|
|
136
|
+
styles.sendButton,
|
|
137
|
+
!input.trim() && styles.sendButtonDisabled,
|
|
138
|
+
]}
|
|
139
|
+
>
|
|
140
|
+
<Ionicons
|
|
141
|
+
name="send"
|
|
142
|
+
size={20}
|
|
143
|
+
color={
|
|
144
|
+
input.trim() ? theme.colors.background : theme.colors.border
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
</TouchableOpacity>
|
|
148
|
+
</View>
|
|
149
|
+
</View>
|
|
150
|
+
</View>
|
|
151
|
+
</KeyboardAvoidingView>
|
|
152
|
+
</Container>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const styles = StyleSheet.create((theme) => ({
|
|
157
|
+
container: {
|
|
158
|
+
flex: 1,
|
|
159
|
+
},
|
|
160
|
+
content: {
|
|
161
|
+
flex: 1,
|
|
162
|
+
paddingHorizontal: theme.spacing.md,
|
|
163
|
+
paddingVertical: theme.spacing.lg,
|
|
164
|
+
},
|
|
165
|
+
errorContainer: {
|
|
166
|
+
flex: 1,
|
|
167
|
+
justifyContent: "center",
|
|
168
|
+
alignItems: "center",
|
|
169
|
+
paddingHorizontal: theme.spacing.md,
|
|
170
|
+
},
|
|
171
|
+
errorText: {
|
|
172
|
+
color: theme.colors.destructive,
|
|
173
|
+
textAlign: "center",
|
|
174
|
+
fontSize: 18,
|
|
175
|
+
marginBottom: theme.spacing.md,
|
|
176
|
+
},
|
|
177
|
+
errorSubtext: {
|
|
178
|
+
color: theme.colors.typography,
|
|
179
|
+
textAlign: "center",
|
|
180
|
+
fontSize: 16,
|
|
181
|
+
},
|
|
182
|
+
header: {
|
|
183
|
+
marginBottom: theme.spacing.lg,
|
|
184
|
+
},
|
|
185
|
+
headerTitle: {
|
|
186
|
+
fontSize: 28,
|
|
187
|
+
fontWeight: "bold",
|
|
188
|
+
color: theme.colors.typography,
|
|
189
|
+
marginBottom: theme.spacing.sm,
|
|
190
|
+
},
|
|
191
|
+
headerSubtitle: {
|
|
192
|
+
fontSize: 16,
|
|
193
|
+
color: theme.colors.typography,
|
|
194
|
+
},
|
|
195
|
+
messagesContainer: {
|
|
196
|
+
flex: 1,
|
|
197
|
+
marginBottom: theme.spacing.md,
|
|
198
|
+
},
|
|
199
|
+
emptyContainer: {
|
|
200
|
+
flex: 1,
|
|
201
|
+
justifyContent: "center",
|
|
202
|
+
alignItems: "center",
|
|
203
|
+
},
|
|
204
|
+
emptyText: {
|
|
205
|
+
textAlign: "center",
|
|
206
|
+
color: theme.colors.typography,
|
|
207
|
+
fontSize: 18,
|
|
208
|
+
},
|
|
209
|
+
messagesWrapper: {
|
|
210
|
+
gap: theme.spacing.md,
|
|
211
|
+
},
|
|
212
|
+
messageContainer: {
|
|
213
|
+
padding: theme.spacing.md,
|
|
214
|
+
borderRadius: 8,
|
|
215
|
+
},
|
|
216
|
+
userMessage: {
|
|
217
|
+
backgroundColor: theme.colors.primary + "20",
|
|
218
|
+
marginLeft: theme.spacing.xl,
|
|
219
|
+
alignSelf: "flex-end",
|
|
220
|
+
},
|
|
221
|
+
assistantMessage: {
|
|
222
|
+
backgroundColor: theme.colors.background,
|
|
223
|
+
marginRight: theme.spacing.xl,
|
|
224
|
+
borderWidth: 1,
|
|
225
|
+
borderColor: theme.colors.border,
|
|
226
|
+
},
|
|
227
|
+
messageRole: {
|
|
228
|
+
fontSize: 14,
|
|
229
|
+
fontWeight: "600",
|
|
230
|
+
marginBottom: theme.spacing.sm,
|
|
231
|
+
color: theme.colors.typography,
|
|
232
|
+
},
|
|
233
|
+
messageContent: {
|
|
234
|
+
color: theme.colors.typography,
|
|
235
|
+
lineHeight: 20,
|
|
236
|
+
},
|
|
237
|
+
toolInvocations: {
|
|
238
|
+
fontSize: 12,
|
|
239
|
+
color: theme.colors.typography,
|
|
240
|
+
fontFamily: "monospace",
|
|
241
|
+
backgroundColor: theme.colors.border + "40",
|
|
242
|
+
padding: theme.spacing.sm,
|
|
243
|
+
borderRadius: 4,
|
|
244
|
+
marginTop: theme.spacing.sm,
|
|
245
|
+
},
|
|
246
|
+
inputSection: {
|
|
247
|
+
borderTopWidth: 1,
|
|
248
|
+
borderTopColor: theme.colors.border,
|
|
249
|
+
paddingTop: theme.spacing.md,
|
|
250
|
+
},
|
|
251
|
+
inputContainer: {
|
|
252
|
+
flexDirection: "row",
|
|
253
|
+
alignItems: "flex-end",
|
|
254
|
+
gap: theme.spacing.sm,
|
|
255
|
+
},
|
|
256
|
+
textInput: {
|
|
257
|
+
flex: 1,
|
|
258
|
+
borderWidth: 1,
|
|
259
|
+
borderColor: theme.colors.border,
|
|
260
|
+
borderRadius: 8,
|
|
261
|
+
paddingHorizontal: theme.spacing.md,
|
|
262
|
+
paddingVertical: theme.spacing.sm,
|
|
263
|
+
color: theme.colors.typography,
|
|
264
|
+
backgroundColor: theme.colors.background,
|
|
265
|
+
fontSize: 16,
|
|
266
|
+
minHeight: 40,
|
|
267
|
+
maxHeight: 120,
|
|
268
|
+
},
|
|
269
|
+
sendButton: {
|
|
270
|
+
backgroundColor: theme.colors.primary,
|
|
271
|
+
padding: theme.spacing.sm,
|
|
272
|
+
borderRadius: 8,
|
|
273
|
+
justifyContent: "center",
|
|
274
|
+
alignItems: "center",
|
|
275
|
+
},
|
|
276
|
+
sendButtonDisabled: {
|
|
277
|
+
backgroundColor: theme.colors.border,
|
|
278
|
+
},
|
|
279
|
+
}));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import structuredClone from "@ungap/structured-clone";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
if (Platform.OS !== "web") {
|
|
5
|
+
const setupPolyfills = async () => {
|
|
6
|
+
const { polyfillGlobal } = await import(
|
|
7
|
+
"react-native/Libraries/Utilities/PolyfillFunctions"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const { TextEncoderStream, TextDecoderStream } = await import(
|
|
11
|
+
"@stardazed/streams-text-encoding"
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (!("structuredClone" in global)) {
|
|
15
|
+
polyfillGlobal("structuredClone", () => structuredClone);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
polyfillGlobal("TextEncoderStream", () => TextEncoderStream);
|
|
19
|
+
polyfillGlobal("TextDecoderStream", () => TextDecoderStream);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
setupPolyfills();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {};
|