add ai and todo example templates for native frontends (#293)

This commit is contained in:
Aman Varshney
2025-06-02 16:30:53 +05:30
committed by GitHub
parent 9dbeea8983
commit 7851d0636d
42 changed files with 1606 additions and 536 deletions

View File

@@ -0,0 +1,155 @@
import { useRef, useEffect } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
KeyboardAvoidingView,
Platform,
} from "react-native";
import { useChat } from "@ai-sdk/react";
import { fetch as expoFetch } from "expo/fetch";
import { Ionicons } from "@expo/vector-icons";
import { Container } from "@/components/container";
// Utility function to generate API URLs
const generateAPIUrl = (relativePath: string) => {
const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL;
if (!serverUrl) {
throw new Error("EXPO_PUBLIC_SERVER_URL environment variable is not defined");
}
const path = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
return serverUrl.concat(path);
};
export default function AIScreen() {
const { messages, input, handleInputChange, handleSubmit, error } = useChat({
fetch: expoFetch as unknown as typeof globalThis.fetch,
api: generateAPIUrl('/ai'),
onError: error => console.error(error, 'AI Chat Error'),
maxSteps: 5,
});
const scrollViewRef = useRef<ScrollView>(null);
useEffect(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, [messages]);
const onSubmit = () => {
if (input.trim()) {
handleSubmit();
}
};
if (error) {
return (
<Container>
<View className="flex-1 justify-center items-center px-4">
<Text className="text-destructive text-center text-lg mb-4">
Error: {error.message}
</Text>
<Text className="text-muted-foreground text-center">
Please check your connection and try again.
</Text>
</View>
</Container>
);
}
return (
<Container>
<KeyboardAvoidingView
className="flex-1"
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View className="flex-1 px-4 py-6">
<View className="mb-6">
<Text className="text-foreground text-2xl font-bold mb-2">
AI Chat
</Text>
<Text className="text-muted-foreground">
Chat with our AI assistant
</Text>
</View>
<ScrollView
ref={scrollViewRef}
className="flex-1 mb-4"
showsVerticalScrollIndicator={false}
>
{messages.length === 0 ? (
<View className="flex-1 justify-center items-center">
<Text className="text-center text-muted-foreground text-lg">
Ask me anything to get started!
</Text>
</View>
) : (
<View className="space-y-4">
{messages.map((message) => (
<View
key={message.id}
className={`p-3 rounded-lg ${
message.role === "user"
? "bg-primary/10 ml-8"
: "bg-card mr-8 border border-border"
}`}
>
<Text className="text-sm font-semibold mb-1 text-foreground">
{message.role === "user" ? "You" : "AI Assistant"}
</Text>
<Text className="text-foreground leading-relaxed">
{message.content}
</Text>
</View>
))}
</View>
)}
</ScrollView>
<View className="border-t border-border pt-4">
<View className="flex-row items-end space-x-2">
<TextInput
value={input}
onChange={(e) =>
handleInputChange({
...e,
target: {
...e.target,
value: e.nativeEvent.text,
},
} as unknown as React.ChangeEvent<HTMLInputElement>)
}
placeholder="Type your message..."
placeholderTextColor="#6b7280"
className="flex-1 border border-border rounded-md px-3 py-2 text-foreground bg-background min-h-[40px] max-h-[120px]"
onSubmitEditing={(e) => {
handleSubmit(e);
e.preventDefault();
}}
autoFocus={true}
/>
<TouchableOpacity
onPress={onSubmit}
disabled={!input.trim()}
className={`p-2 rounded-md ${
input.trim()
? "bg-primary"
: "bg-muted"
}`}
>
<Ionicons
name="send"
size={20}
color={input.trim() ? "#ffffff" : "#6b7280"}
/>
</TouchableOpacity>
</View>
</View>
</View>
</KeyboardAvoidingView>
</Container>
);
}

View File

@@ -0,0 +1,25 @@
import structuredClone from "@ungap/structured-clone";
import { Platform } from "react-native";
if (Platform.OS !== "web") {
const setupPolyfills = async () => {
const { polyfillGlobal } = await import(
"react-native/Libraries/Utilities/PolyfillFunctions"
);
const { TextEncoderStream, TextDecoderStream } = await import(
"@stardazed/streams-text-encoding"
);
if (!("structuredClone" in global)) {
polyfillGlobal("structuredClone", () => structuredClone);
}
polyfillGlobal("TextEncoderStream", () => TextEncoderStream);
polyfillGlobal("TextDecoderStream", () => TextDecoderStream);
};
setupPolyfills();
}
export {};

View File

@@ -0,0 +1,279 @@
import { useRef, useEffect } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
KeyboardAvoidingView,
Platform,
} from "react-native";
import { useChat } from "@ai-sdk/react";
import { fetch as expoFetch } from "expo/fetch";
import { Ionicons } from "@expo/vector-icons";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Container } from "@/components/container";
const generateAPIUrl = (relativePath: string) => {
const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL;
if (!serverUrl) {
throw new Error(
"EXPO_PUBLIC_SERVER_URL environment variable is not defined",
);
}
const path = relativePath.startsWith("/") ? relativePath : `/${relativePath}`;
return serverUrl.concat(path);
};
export default function AIScreen() {
const { theme } = useUnistyles();
const { messages, input, handleInputChange, handleSubmit, error } = useChat({
fetch: expoFetch as unknown as typeof globalThis.fetch,
api: generateAPIUrl("/ai"),
onError: (error) => console.error(error, "AI Chat Error"),
maxSteps: 5,
});
const scrollViewRef = useRef<ScrollView>(null);
useEffect(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, [messages]);
const onSubmit = () => {
if (input.trim()) {
handleSubmit();
}
};
if (error) {
return (
<Container>
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Error: {error.message}</Text>
<Text style={styles.errorSubtext}>
Please check your connection and try again.
</Text>
</View>
</Container>
);
}
return (
<Container>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.headerTitle}>AI Chat</Text>
<Text style={styles.headerSubtitle}>
Chat with our AI assistant
</Text>
</View>
<ScrollView
ref={scrollViewRef}
style={styles.messagesContainer}
showsVerticalScrollIndicator={false}
>
{messages.length === 0 ? (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>
Ask me anything to get started!
</Text>
</View>
) : (
<View style={styles.messagesWrapper}>
{messages.map((message) => (
<View
key={message.id}
style={[
styles.messageContainer,
message.role === "user"
? styles.userMessage
: styles.assistantMessage,
]}
>
<Text style={styles.messageRole}>
{message.role === "user" ? "You" : "AI Assistant"}
</Text>
<Text style={styles.messageContent}>{message.content}</Text>
</View>
))}
</View>
)}
</ScrollView>
<View style={styles.inputSection}>
<View style={styles.inputContainer}>
<TextInput
value={input}
onChange={(e) =>
handleInputChange({
...e,
target: {
...e.target,
value: e.nativeEvent.text,
},
} as unknown as React.ChangeEvent<HTMLInputElement>)
}
placeholder="Type your message..."
placeholderTextColor={theme.colors.border}
style={styles.textInput}
onSubmitEditing={(e) => {
handleSubmit(e);
e.preventDefault();
}}
autoFocus={true}
/>
<TouchableOpacity
onPress={onSubmit}
disabled={!input.trim()}
style={[
styles.sendButton,
!input.trim() && styles.sendButtonDisabled,
]}
>
<Ionicons
name="send"
size={20}
color={
input.trim() ? theme.colors.background : theme.colors.border
}
/>
</TouchableOpacity>
</View>
</View>
</View>
</KeyboardAvoidingView>
</Container>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.lg,
},
errorContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: theme.spacing.md,
},
errorText: {
color: theme.colors.destructive,
textAlign: "center",
fontSize: 18,
marginBottom: theme.spacing.md,
},
errorSubtext: {
color: theme.colors.typography,
textAlign: "center",
fontSize: 16,
},
header: {
marginBottom: theme.spacing.lg,
},
headerTitle: {
fontSize: 28,
fontWeight: "bold",
color: theme.colors.typography,
marginBottom: theme.spacing.sm,
},
headerSubtitle: {
fontSize: 16,
color: theme.colors.typography,
},
messagesContainer: {
flex: 1,
marginBottom: theme.spacing.md,
},
emptyContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
emptyText: {
textAlign: "center",
color: theme.colors.typography,
fontSize: 18,
},
messagesWrapper: {
gap: theme.spacing.md,
},
messageContainer: {
padding: theme.spacing.md,
borderRadius: 8,
},
userMessage: {
backgroundColor: theme.colors.primary + "20",
marginLeft: theme.spacing.xl,
alignSelf: "flex-end",
},
assistantMessage: {
backgroundColor: theme.colors.background,
marginRight: theme.spacing.xl,
borderWidth: 1,
borderColor: theme.colors.border,
},
messageRole: {
fontSize: 14,
fontWeight: "600",
marginBottom: theme.spacing.sm,
color: theme.colors.typography,
},
messageContent: {
color: theme.colors.typography,
lineHeight: 20,
},
toolInvocations: {
fontSize: 12,
color: theme.colors.typography,
fontFamily: "monospace",
backgroundColor: theme.colors.border + "40",
padding: theme.spacing.sm,
borderRadius: 4,
marginTop: theme.spacing.sm,
},
inputSection: {
borderTopWidth: 1,
borderTopColor: theme.colors.border,
paddingTop: theme.spacing.md,
},
inputContainer: {
flexDirection: "row",
alignItems: "flex-end",
gap: theme.spacing.sm,
},
textInput: {
flex: 1,
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: 8,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
color: theme.colors.typography,
backgroundColor: theme.colors.background,
fontSize: 16,
minHeight: 40,
maxHeight: 120,
},
sendButton: {
backgroundColor: theme.colors.primary,
padding: theme.spacing.sm,
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
},
sendButtonDisabled: {
backgroundColor: theme.colors.border,
},
}));

View File

@@ -0,0 +1,25 @@
import structuredClone from "@ungap/structured-clone";
import { Platform } from "react-native";
if (Platform.OS !== "web") {
const setupPolyfills = async () => {
const { polyfillGlobal } = await import(
"react-native/Libraries/Utilities/PolyfillFunctions"
);
const { TextEncoderStream, TextDecoderStream } = await import(
"@stardazed/streams-text-encoding"
);
if (!("structuredClone" in global)) {
polyfillGlobal("structuredClone", () => structuredClone);
}
polyfillGlobal("TextEncoderStream", () => TextEncoderStream);
polyfillGlobal("TextDecoderStream", () => TextDecoderStream);
};
setupPolyfills();
}
export {};