/** * AI Town — Chat Interface with AI Characters * ============================================== * RAG-powered conversation with persistent memory. */ import React, { useState, useRef, useEffect } from "react"; import { View, Text, TextInput, TouchableOpacity, FlatList, KeyboardAvoidingView, Platform, ActivityIndicator } from "react-native"; import { useAction, useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; import { Id } from "@/convex/_generated/dataModel"; interface ChatScreenProps { characterId: Id<"characters">; userId: string; onBack: () => void; } export function ChatScreen({ characterId, userId, onBack }: ChatScreenProps) { const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const flatListRef = useRef(null); // Realtime data from Convex const character = useQuery(api.characters.get, { characterId }); const chatHistory = useQuery(api.characters.getUserChat, { userId, characterId }); const chatWithCharacter = useAction(api.characters.chatWithCharacter); const messages = chatHistory?.messages || []; const sendMessage = async () => { if (!input.trim() || isLoading) return; const userMessage = input.trim(); setInput(""); setIsLoading(true); try { const result = await chatWithCharacter({ userId, characterId, message: userMessage, }); // Response is automatically saved and will appear via realtime query } catch (error) { console.error("Chat error:", error); } finally { setIsLoading(false); } }; useEffect(() => { // Scroll to bottom on new message if (messages.length > 0) { setTimeout(() => flatListRef.current?.scrollToEnd({ animated: true }), 100); } }, [messages.length]); if (!character) { return ( ); } return ( {/* Header */} ← Back {character.avatar} {character.name} {character.mood} • {character.currentAction} {/* Messages */} i.toString()} contentContainerStyle={{ padding: 16, paddingBottom: 8 }} renderItem={({ item }) => ( {item.content} {item.role === "user" ? "You" : character.name} •{" "} {new Date(item.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} )} ListEmptyComponent={ {character.avatar} Chat with {character.name} {character.name} is a {character.occupation}. They're currently{" "} {character.currentAction}. Start a conversation! {["Hello!", "What are you up to?", "Tell me about yourself", "How's your day?"].map( (suggestion, i) => ( setInput(suggestion)} className="bg-gray-800 px-3 py-1.5 rounded-full border border-gray-700" > {suggestion} ) )} } /> {/* Typing indicator */} {isLoading && ( {character.name} is thinking... )} {/* Input */} ); }