@reliverse/rse
Version:
@reliverse/rse is your all-in-one companion for bootstrapping and improving any kind of projects (especially web apps built with frameworks like Next.js) — whether you're kicking off something new or upgrading an existing app. It is also a little AI-power
1,542 lines • 111 kB
JavaScript
export const DLER_TPL_EXAMPLES = {
name: "examples",
description: "Template generated from 31 files",
updatedAt: "2025-06-17T20:33:59.686Z",
config: {
files: {
"examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "cc30ba62a9"
},
content: `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>
);
} `,
type: "text"
},
"examples/ai/native/nativewind/polyfills.js": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "9866d77e7e"
},
content: `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 {};
`,
type: "text"
},
"examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "3eab7496a4"
},
content: `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,
},
}));
`,
type: "text"
},
"examples/ai/native/unistyles/polyfills.js": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "9866d77e7e"
},
content: `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 {};
`,
type: "text"
},
"examples/ai/server/next/src/app/ai/route.ts": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "eee0d6a73d"
},
content: `import { google } from '@ai-sdk/google';
import { streamText } from 'ai';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: google('gemini-2.0-flash'),
messages,
});
return result.toDataStreamResponse();
}
`,
type: "text"
},
"examples/ai/web/nuxt/app/pages/ai.vue": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "7dd81ef739"
},
content: `<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import { nextTick, ref, watch } from 'vue'
const config = useRuntimeConfig()
const serverUrl = config.public.serverURL
const { messages, input, handleSubmit } = useChat({
api: \`\${serverUrl}/ai\`,
})
const messagesEndRef = ref<null | HTMLDivElement>(null)
watch(messages, async () => {
await nextTick()
messagesEndRef.value?.scrollIntoView({ behavior: 'smooth' })
})
function getMessageText(message: any) {
return message.parts
.filter((part: any) => part.type === 'text')
.map((part: any) => part.text)
.join('')
}
<\/script>
<template>
<div class="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
<div class="overflow-y-auto space-y-4 pb-4">
<div v-if="messages.length === 0" class="text-center text-muted-foreground mt-8">
Ask me anything to get started!
</div>
<div
v-for="message in messages"
:key="message.id"
:class="[
'p-3 rounded-lg',
message.role === 'user' ? 'bg-primary/10 ml-8' : 'bg-secondary/20 mr-8'
]"
>
<p class="text-sm font-semibold mb-1">
{{ message.role === 'user' ? 'You' : 'AI Assistant' }}
</p>
<div class="whitespace-pre-wrap">{{ getMessageText(message) }}</div>
</div>
<div ref="messagesEndRef" />
</div>
<form @submit.prevent="handleSubmit" class="w-full flex items-center space-x-2 pt-2 border-t">
<UInput
name="prompt"
v-model="input"
placeholder="Type your message..."
class="flex-1"
autocomplete="off"
autofocus
/>
<UButton type="submit" color="primary" size="md" square>
<UIcon name="i-lucide-send" class="w-5 h-5" />
</UButton>
</form>
</div>
</template>
`,
type: "text"
},
"examples/ai/web/react/next/src/app/ai/page.tsx": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "0c366c76eb"
},
content: `"use client"
import { useChat } from "@ai-sdk/react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Send } from "lucide-react";
import { useRef, useEffect } from "react";
export default function AIPage() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: \`\${process.env.NEXT_PUBLIC_SERVER_URL}/ai\`,
});
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
<div className="overflow-y-auto space-y-4 pb-4">
{messages.length === 0 ? (
<div className="text-center text-muted-foreground mt-8">
Ask me anything to get started!
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={\`p-3 rounded-lg \${
message.role === "user"
? "bg-primary/10 ml-8"
: "bg-secondary/20 mr-8"
}\`}
>
<p className="text-sm font-semibold mb-1">
{message.role === "user" ? "You" : "AI Assistant"}
</p>
<div className="whitespace-pre-wrap">{message.content}</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
<form
onSubmit={handleSubmit}
className="w-full flex items-center space-x-2 pt-2 border-t"
>
<Input
name="prompt"
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="flex-1"
autoComplete="off"
autoFocus
/>
<Button type="submit" size="icon">
<Send size={18} />
</Button>
</form>
</div>
);
}
`,
type: "text"
},
"examples/ai/web/react/react-router/src/routes/ai.tsx": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "47e1850967"
},
content: `import { useChat } from "@ai-sdk/react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Send } from "lucide-react";
import { useRef, useEffect } from "react";
export default function AI() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: \`\${import.meta.env.VITE_SERVER_URL}/ai\`,
});
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
<div className="overflow-y-auto space-y-4 pb-4">
{messages.length === 0 ? (
<div className="text-center text-muted-foreground mt-8">
Ask me anything to get started!
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={\`p-3 rounded-lg \${
message.role === "user"
? "bg-primary/10 ml-8"
: "bg-secondary/20 mr-8"
}\`}
>
<p className="text-sm font-semibold mb-1">
{message.role === "user" ? "You" : "AI Assistant"}
</p>
<div className="whitespace-pre-wrap">{message.content}</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
<form
onSubmit={handleSubmit}
className="w-full flex items-center space-x-2 pt-2 border-t"
>
<Input
name="prompt"
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="flex-1"
autoComplete="off"
autoFocus
/>
<Button type="submit" size="icon">
<Send size={18} />
</Button>
</form>
</div>
);
}
`,
type: "text"
},
"examples/ai/web/react/tanstack-router/src/routes/ai.tsx": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "64b5db59bb"
},
content: `import { createFileRoute } from "@tanstack/react-router";
import { useChat } from "@ai-sdk/react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Send } from "lucide-react";
import { useRef, useEffect } from "react";
export const Route = createFileRoute("/ai")({
component: RouteComponent,
});
function RouteComponent() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: \`\${import.meta.env.VITE_SERVER_URL}/ai\`,
});
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
<div className="overflow-y-auto space-y-4 pb-4">
{messages.length === 0 ? (
<div className="text-center text-muted-foreground mt-8">
Ask me anything to get started!
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={\`p-3 rounded-lg \${
message.role === "user"
? "bg-primary/10 ml-8"
: "bg-secondary/20 mr-8"
}\`}
>
<p className="text-sm font-semibold mb-1">
{message.role === "user" ? "You" : "AI Assistant"}
</p>
<div className="whitespace-pre-wrap">{message.content}</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
<form
onSubmit={handleSubmit}
className="w-full flex items-center space-x-2 pt-2 border-t"
>
<Input
name="prompt"
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="flex-1"
autoComplete="off"
autoFocus
/>
<Button type="submit" size="icon">
<Send size={18} />
</Button>
</form>
</div>
);
}
`,
type: "text"
},
"examples/ai/web/react/tanstack-start/src/routes/ai.tsx": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "64b5db59bb"
},
content: `import { createFileRoute } from "@tanstack/react-router";
import { useChat } from "@ai-sdk/react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Send } from "lucide-react";
import { useRef, useEffect } from "react";
export const Route = createFileRoute("/ai")({
component: RouteComponent,
});
function RouteComponent() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: \`\${import.meta.env.VITE_SERVER_URL}/ai\`,
});
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
<div className="overflow-y-auto space-y-4 pb-4">
{messages.length === 0 ? (
<div className="text-center text-muted-foreground mt-8">
Ask me anything to get started!
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={\`p-3 rounded-lg \${
message.role === "user"
? "bg-primary/10 ml-8"
: "bg-secondary/20 mr-8"
}\`}
>
<p className="text-sm font-semibold mb-1">
{message.role === "user" ? "You" : "AI Assistant"}
</p>
<div className="whitespace-pre-wrap">{message.content}</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
<form
onSubmit={handleSubmit}
className="w-full flex items-center space-x-2 pt-2 border-t"
>
<Input
name="prompt"
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="flex-1"
autoComplete="off"
autoFocus
/>
<Button type="submit" size="icon">
<Send size={18} />
</Button>
</form>
</div>
);
}
`,
type: "text"
},
"examples/ai/web/svelte/src/routes/ai/+page.svelte": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "a6b5e9020b"
},
content: `<script lang="ts">
import { PUBLIC_SERVER_URL } from '$env/static/public';
import { Chat } from '@ai-sdk/svelte';
const chat = new Chat({
api: \`\${PUBLIC_SERVER_URL}/ai\`,
});
let messagesEndElement: HTMLDivElement | null = null;
$effect(() => {
const messageCount = chat.messages.length;
if (messageCount > 0) {
setTimeout(() => {
messagesEndElement?.scrollIntoView({ behavior: 'smooth' });
}, 0);
}
});
<\/script>
<div class="mx-auto grid h-full w-full max-w-2xl grid-rows-[1fr_auto] overflow-hidden p-4">
<div class="mb-4 space-y-4 overflow-y-auto pb-4">
{#if chat.messages.length === 0}
<div class="mt-8 text-center text-neutral-500">Ask the AI anything to get started!</div>
{/if}
{#each chat.messages as message (message.id)}
<div
class="w-fit max-w-[85%] rounded-lg p-3 text-sm md:text-base"
class:ml-auto={message.role === 'user'}
class:bg-indigo-600={message.role === 'user'}
class:text-white={message.role === 'user'}
class:bg-neutral-700={message.role === 'assistant'}
class:text-neutral-100={message.role === 'assistant'}
>
<p
class="mb-1 text-xs font-semibold uppercase tracking-wide"
class:text-indigo-200={message.role === 'user'}
class:text-neutral-400={message.role === 'assistant'}
>
{message.role === 'user' ? 'You' : 'AI Assistant'}
</p>
<div class="whitespace-pre-wrap break-words">
{#each message.parts as part, partIndex (partIndex)}
{#if part.type === 'text'}
{part.text}
{:else if part.type === 'tool-invocation'}
<pre class="mt-2 rounded bg-neutral-800 p-2 text-xs text-neutral-300"
>{JSON.stringify(part.toolInvocation, null, 2)}</pre
>
{/if}
{/each}
</div>
</div>
{/each}
<div bind:this={messagesEndElement}></div>
</div>
<form
onsubmit={chat.handleSubmit}
class="flex w-full items-center space-x-2 border-t border-neutral-700 pt-4"
>
<input
name="prompt"
bind:value={chat.input}
placeholder="Type your message..."
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"
autocomplete="off"
onkeydown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
chat.handleSubmit(e);
}
}}
/>
<button
type="submit"
disabled={!chat.input.trim()}
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"
aria-label="Send message"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m22 2-7 20-4-9-9-4Z" /><path d="M22 2 11 13" />
</svg>
</button>
</form>
</div>
`,
type: "text"
},
"examples/todo/native/nativewind/app/(drawer)/todos.tsx.hbs": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "2bb32caaba"
},
content: `import { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
ActivityIndicator,
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
{{#if (eq backend "convex")}}
import { useMutation, useQuery } from "convex/react";
import { api } from "@{{projectName}}/backend/convex/_generated/api";
import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
{{else}}
import { useMutation, useQuery } from "@tanstack/react-query";
{{/if}}
import { Container } from "@/components/container";
{{#unless (eq backend "convex")}}
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
{{/unless}}
export default function TodosScreen() {
const [newTodoText, setNewTodoText] = useState("");
{{#if (eq backend "convex")}}
const todos = useQuery(api.todos.getAll);
const createTodoMutation = useMutation(api.todos.create);
const toggleTodoMutation = useMutation(api.todos.toggle);
const deleteTodoMutation = useMutation(api.todos.deleteTodo);
const handleAddTodo = async () => {
const text = newTodoText.trim();
if (!text) return;
await createTodoMutation({ text });
setNewTodoText("");
};
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
toggleTodoMutation({ id, completed: !currentCompleted });
};
const handleDeleteTodo = (id: Id<"todos">) => {
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: () => deleteTodoMutation({ id }),
},
]);
};
{{else}}
{{#if (eq api "orpc")}}
const todos = useQuery(orpc.todo.getAll.queryOptions());
const createMutation = useMutation(
orpc.todo.create.mutationOptions({
onSuccess: () => {
todos.refetch();
setNewTodoText("");
},
}),
);
const toggleMutation = useMutation(
orpc.todo.toggle.mutationOptions({
onSuccess: () => { todos.refetch() },
}),
);
const deleteMutation = useMutation(
orpc.todo.delete.mutationOptions({
onSuccess: () => { todos.refetch() },
}),
);
{{/if}}
{{#if (eq api "trpc")}}
const todos = useQuery(trpc.todo.getAll.queryOptions());
const createMutation = useMutation(
trpc.todo.create.mutationOptions({
onSuccess: () => {
todos.refetch();
setNewTodoText("");
},
}),
);
const toggleMutation = useMutation(
trpc.todo.toggle.mutationOptions({
onSuccess: () => { todos.refetch() },
}),
);
const deleteMutation = useMutation(
trpc.todo.delete.mutationOptions({
onSuccess: () => { todos.refetch() },
}),
);
{{/if}}
const handleAddTodo = () => {
if (newTodoText.trim()) {
createMutation.mutate({ text: newTodoText });
}
};
const handleToggleTodo = (id: number, completed: boolean) => {
toggleMutation.mutate({ id, completed: !completed });
};
const handleDeleteTodo = (id: number) => {
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: () => deleteMutation.mutate({ id }),
},
]);
};
{{/if}}
return (
<Container>
<ScrollView className="flex-1">
<View className="px-4 py-6">
<View className="mb-6 rounded-lg border border-border p-4 bg-card">
<Text className="text-foreground text-2xl font-bold mb-2">
Todo List
</Text>
<Text className="text-muted-foreground mb-4">
Manage your tasks efficiently
</Text>
<View className="mb-6">
<View className="flex-row items-center space-x-2 mb-2">
<TextInput
value={newTodoText}
onChangeText={setNewTodoText}
placeholder="Add a new task..."
placeholderTextColor="#6b7280"
{{#if (eq backend "convex")}}
{{else}}
editable={!createMutation.isPending}
{{/if}}
className="flex-1 border border-border rounded-md px-3 py-2 text-foreground bg-background"
onSubmitEditing={handleAddTodo}
returnKeyType="done"
/>
<TouchableOpacity
onPress={handleAddTodo}
{{#if (eq backend "convex")}}
disabled={!newTodoText.trim()}
{{else}}
disabled={createMutation.isPending || !newTodoText.trim()}
{{/if}}
className={\`px-4 py-2 rounded-md \${
{{#if (eq backend "convex")}}
!newTodoText.trim()
{{else}}
createMutation.isPending || !newTodoText.trim()
{{/if}}
? "bg-muted"
: "bg-primary"
}\`}
>
{{#if (eq backend "convex")}}
<Text className="text-white font-medium">Add</Text>
{{else}}
{createMutation.isPending ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text className="text-white font-medium">Add</Text>
)}
{{/if}}
</TouchableOpacity>
</View>
</View>
{{#if (eq backend "convex")}}
{todos === undefined ? (
<View className="flex justify-center py-8">
<ActivityIndicator size="large" color="#3b82f6" />
</View>
) : todos.length === 0 ? (
<Text className="py-8 text-center text-muted-foreground">
No todos yet. Add one above!
</Text>
) : (
<View className="space-y-2">
{todos.map((todo) => (
<View
key={todo._id}
className="flex-row items-center justify-between rounded-md border border-border p-3 bg-background"
>
<View className="flex-row items-center flex-1">
<TouchableOpacity
onPress={() =>
handleToggleTodo(todo._id, todo.completed)
}
className="mr-3"
>
<Ionicons
name={todo.completed ? "checkbox" : "square-outline"}
size={24}
color={todo.completed ? "#22c55e" : "#6b7280"}
/>
</TouchableOpacity>
<Text
className={\`flex-1 \${
todo.completed
? "line-through text-muted-foreground"
: "text-foreground"
}\`}
>
{todo.text}
</Text>
</View>
<TouchableOpacity
onPress={() => handleDeleteTodo(todo._id)}
className="ml-2 p-1"
>
<Ionicons
name="trash-outline"
size={20}
color="#ef4444"
/>
</TouchableOpacity>
</View>
))}
</View>
)}
{{else}}
{todos.isLoading ? (
<View className="flex justify-center py-8">
<ActivityIndicator size="large" color="#3b82f6" />
</View>
) : todos.data?.length === 0 ? (
<Text className="py-8 text-center text-muted-foreground">
No todos yet. Add one above!
</Text>
) : (
<View className="space-y-2">
{todos.data?.map((todo) => (
<View
key={todo.id}
className="flex-row items-center justify-between rounded-md border border-border p-3 bg-background"
>
<View className="flex-row items-center flex-1">
<TouchableOpacity
onPress={() =>
handleToggleTodo(todo.id, todo.completed)
}
className="mr-3"
>
<Ionicons
name={todo.completed ? "checkbox" : "square-outline"}
size={24}
color={todo.completed ? "#22c55e" : "#6b7280"}
/>
</TouchableOpacity>
<Text
className={\`flex-1 \${
todo.completed
? "line-through text-muted-foreground"
: "text-foreground"
}\`}
>
{todo.text}
</Text>
</View>
<TouchableOpacity
onPress={() => handleDeleteTodo(todo.id)}
className="ml-2 p-1"
>
<Ionicons
name="trash-outline"
size={20}
color="#ef4444"
/>
</TouchableOpacity>
</View>
))}
</View>
)}
{{/if}}
</View>
</View>
</ScrollView>
</Container>
);
}
`,
type: "text"
},
"examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs": {
metadata: {
updatedAt: "2025-06-17T06:06:35.000Z",
updatedHash: "26a877bbb6"
},
content: `import { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
ActivityIndicator,
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
{{#if (eq backend "convex")}}
import { useMutation, useQuery } from "convex/react";
import { api } from "@{{projectName}}/backend/convex/_generated/api";
import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel";
{{else}}
import { useMutation, useQuery } from "@tanstack/react-query";
{{/if}}
import { Container } from "@/components/container";
{{#unless (eq backend "convex")}}
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
{{/unless}}
export default function TodosScreen() {
const [newTodoText, setNewTodoText] = useState("");
const { theme } = useUnistyles();
{{#if (eq backend "convex")}}
const todos = useQuery(api.todos.getAll);
const createTodoMutation = useMutation(api.todos.create);
const toggleTodoMutation = useMutation(api.todos.toggle);
const deleteTodoMutation = useMutation(api.todos.deleteTodo);
const handleAddTodo = async () => {
const text = newTodoText.trim();
if (!text) return;
await createTodoMutation({ text });
setNewTodoText("");
};
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
toggleTodoMutation({ id, completed: !currentCompleted });
};
const handleDeleteTodo = (id: Id<"todos">) => {
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: () => deleteTodoMutation({ id }),
},
]);
};
{{else}}
{{#if (eq api "orpc")}}
const todos = useQuery(orpc.todo.getAll.queryOptions());
const createMutation = useMutation(
orpc.todo.create.mutationOptions({
onSuccess: () => {
todos.refetch();
setNewTodoText("");
},
})
);
const toggleMutation = useMutation(
orpc.todo.toggle.mutationOptions({
onSuccess: () => { todos.refetch() },
})
);
const deleteMutation = useMutation(
orpc.todo.delete.mutationOptions({
onSuccess: () => { todos.refetch() },
})
);
{{/if}}
{{#if (eq api "trpc")}}
const todos = useQuery(trpc.todo.getAll.queryOptions());
const createMutation = useMutation(
trpc.todo.create.mutationOptions({
onSuccess: () => {
todos.refetch();
setNewTodoText("");
},
})
);
const toggleMutation = useMutation(
trpc.todo.toggle.mutationOptions({
onSuccess: () => { todos.refetch() },
})
);
const deleteMutation = useMutation(
trpc.todo.delete.mutationOptions({
onSuccess: () => { todos.refetch() },
})
);
{{/if}}
const handleAddTodo = () => {
if (newTodoText.trim()) {
createMutation.mutate({ text: newTodoText });
}
};
const handleToggleTodo = (id: number, completed: boolean) => {
toggleMutation.mutate({ id, completed: !completed });
};
const handleDeleteTodo = (id: number) => {
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: () => deleteMutation.mutate({ id }),
},
]);
};
{{/if}}
const isLoading = {{#if (eq backend "convex")}}!todos{{else}}todos.isLoading{{/if}};
const isCreating = {{#if (eq backend "convex")}}false{{else}}createMutation.isPending{{/if}};
const primaryButtonTextColor = theme.colors.background;
return (
<Container>
<ScrollView style={styles.scrollView}>
<View style={styles.headerContainer}>
<Text style={styles.headerTitle}>Todo List</Text>
<Text style={styles.headerSubtitle}>
Manage your tasks efficiently
</Text>
<View style={styles.inputContainer}>
<TextInput
value={newTodoText}
onChangeText={setNewTodoText}
placeholder="Add a new task..."
placeholderTextColor={theme.colors.border}
editable={!isCreating}
style={styles.textInput}
onSubmitEditing={handleAddTodo}
returnKeyType="done"
/>
<TouchableOpacity
onPress={handleAddTodo}
disabled={isCreating || !newTodoText.trim()}
style={[
styles.addButton,
(isCreating || !newTodoText.trim()) && styles.addButtonDisabled,
]}
>
{isCreating ? (
<ActivityIndicator size="small" color={primaryButtonTextColor} />
) : (
<Ionicons
name="add"
size={24}
color={primaryButtonTextColor}
/>
)}
</TouchableOpacity>
</View>
</View>
{isLoading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<Text style={styles.loadingText}>Loading todos...</Text>
</View>
)}
{{#if (eq backend "convex")}}
{todos && todos.length === 0 && !isLoading && (
<Text style={styles.emptyText}>No todos yet. Add one!</Text>
)}
{todos?.map((todo) => (
<View key={todo._id} style={styles.todoItem}>
<TouchableOpacity
onPress={() => handleToggleTodo(todo._id, todo.completed)}
style={styles.todoContent}
>
<Ionicons
name={todo.completed ? "checkbox" : "square-outline"}
size={24}
color={todo.completed ? theme.colors.primary : theme.colors.typography}
style={styles.checkbox}
/>
<Text
style={[
styles.todoText,
todo.completed && styles.todoTextCompleted,
]}
>
{todo.text}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleDeleteTodo(todo._id)}>
<Ionicons name="trash-outline" size={24} color={theme.colors.destructive} />
</TouchableOpacity>
</View>
))}
{{else}}
{todos.data && todos.data.length === 0 && !isLoading && (
<Text style={styles.emptyText}>No todos yet. Add one!</Text>
)}
{todos.data?.map((todo: { id: number; text: string; completed: boolean }) => (
<View key={todo.id} style={styles.todoItem}>
<TouchableOpacity
onPress={() => handleToggleTodo(todo.id, todo.completed)}
style={styles.todoContent}
>
<Ionicons
name={todo.completed ? "checkbox" : "square-outline"}
size={24}
color={todo.completed ? theme.colors.primary : theme.colors.typography}
style={styles.checkbox}
/>
<Text
style={[
styles.todoText,
todo.completed && styles.todoTextCompleted,
]}
>
{todo.text}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleDeleteTodo(todo.id)}>
<Ionicons name="trash-outline" size={24} color={theme.colors.destructive} />
</TouchableOpacity>
</View>
))}
{{/if}}
</ScrollView>
</Container>
);
}
const styles = StyleSheet.create((theme) => ({
scrollView: {
flex: 1,
},
headerContainer: {
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.lg,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
backgroundColor: theme.colors.background,
},
headerTitle: {
fontSize: 28,
fontWeight: "bold",
color: theme.colors.typography,
marginBottom: theme.spacing.sm,
},
headerSubtitle: {
fontSize: 16,
color: theme.colors.typography,
marginBottom: theme.spacing.md,
},
inputContainer: {
flexDirection: "row",
alignItems: "center",
marginBottom: theme.spacing.md,
},
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,
marginRight: theme.spacing.sm,
fontSize: 16,
},
addButton: {
backgroundColor: theme.colors.primary,
padding: theme.spacing.sm,
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
},
addButtonDisabled: {
backgroundColor: theme.colors.border,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: theme.spacing.lg,
},
loadingText: {
marginTop: theme.spacing.sm,
fontSize: 16,
color: theme.colors.typography,
},
emptyText: {
textAlign: "center",
marginTop: theme.spacing.xl,
fontSize: 16,
color: theme.colors.typography,
},
todoItem: {
flexDirection: "row",
justifyContent