import React, { useCallback, useEffect, useRef, useState } from "react"; import { ActionSheetIOS, Alert, KeyboardAvoidingView, Platform, Pressable, Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { router } from "expo-router"; import { useChat } from "../contexts/ChatContext"; import { useConnection } from "../contexts/ConnectionContext"; import { useTheme } from "../contexts/ThemeContext"; import { MessageList } from "../components/chat/MessageList"; import { InputBar } from "../components/chat/InputBar"; import { CommandBar, TextModeCommandBar } from "../components/chat/CommandBar"; import { ImageCaptionModal } from "../components/chat/ImageCaptionModal"; import { StatusDot } from "../components/ui/StatusDot"; import { SessionDrawer } from "../components/SessionDrawer"; import { playAudio, stopPlayback, isPlaying, onPlayingChange } from "../services/audio"; interface StagedImage { base64: string; uri: string; mimeType: string; } export default function ChatScreen() { const { messages, sendTextMessage, sendVoiceMessage, sendImageMessage, deleteMessage, clearMessages, isTyping, requestScreenshot, sessions } = useChat(); const { status } = useConnection(); const { colors, mode, cycleMode } = useTheme(); const themeIcon = mode === "dark" ? "🌙" : mode === "light" ? "☀️" : "📱"; const activeSessionName = sessions.find((s) => s.isActive)?.name ?? "PAILot"; const [isTextMode, setIsTextMode] = useState(false); const [showSessions, setShowSessions] = useState(false); const [audioPlaying, setAudioPlaying] = useState(false); const [stagedImage, setStagedImage] = useState(null); useEffect(() => { return onPlayingChange((uri) => setAudioPlaying(uri !== null)); }, []); const handleScreenshot = useCallback(() => { requestScreenshot(); }, [requestScreenshot]); const handleHelp = useCallback(() => { sendTextMessage("/h"); }, [sendTextMessage]); const handleNavigate = useCallback(() => { router.push("/navigate"); }, []); const handleClear = useCallback(() => { clearMessages(); }, [clearMessages]); // Resolve a picked asset into a StagedImage const stageAsset = useCallback(async (asset: { base64?: string | null; uri: string; mimeType?: string | null }) => { const mimeType = asset.mimeType ?? (asset.uri.endsWith(".png") ? "image/png" : "image/jpeg"); let base64 = asset.base64 ?? ""; if (!base64 && asset.uri) { const { readAsStringAsync } = await import("expo-file-system/legacy"); base64 = await readAsStringAsync(asset.uri, { encoding: "base64" }); } if (base64) { setStagedImage({ base64, uri: asset.uri, mimeType }); } }, []); const pickFromLibrary = useCallback(async () => { try { const ImagePicker = await import("expo-image-picker"); const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== "granted") { Alert.alert("Permission needed", "Please allow photo library access in Settings."); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ["images"], quality: 0.7, base64: true, }); if (result.canceled || !result.assets?.[0]) return; await stageAsset(result.assets[0]); } catch (err: any) { Alert.alert("Image Error", err?.message ?? String(err)); } }, [stageAsset]); const pickFromCamera = useCallback(async () => { try { const ImagePicker = await import("expo-image-picker"); const { status } = await ImagePicker.requestCameraPermissionsAsync(); if (status !== "granted") { Alert.alert("Permission needed", "Please allow camera access in Settings."); return; } const result = await ImagePicker.launchCameraAsync({ quality: 0.7, base64: true, }); if (result.canceled || !result.assets?.[0]) return; await stageAsset(result.assets[0]); } catch (err: any) { Alert.alert("Camera Error", err?.message ?? String(err)); } }, [stageAsset]); const handlePickImage = useCallback(() => { if (Platform.OS === "ios") { ActionSheetIOS.showActionSheetWithOptions( { options: ["Cancel", "Take Photo", "Choose from Library"], cancelButtonIndex: 0, }, (index) => { if (index === 1) pickFromCamera(); else if (index === 2) pickFromLibrary(); }, ); } else { // Android: just open library (camera is accessible from there) pickFromLibrary(); } }, [pickFromCamera, pickFromLibrary]); const handleImageSend = useCallback( (caption: string) => { if (!stagedImage) return; sendImageMessage(stagedImage.base64, caption, stagedImage.mimeType); setStagedImage(null); }, [stagedImage, sendImageMessage], ); const handleReplay = useCallback(async () => { if (isPlaying()) { stopPlayback(); return; } // Find the last assistant voice message, then walk back to the first chunk in that group let lastIdx = -1; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === "assistant" && messages[i].type === "voice" && messages[i].audioUri) { lastIdx = i; break; } } if (lastIdx === -1) return; // Walk back to find the start of this chunk group let startIdx = lastIdx; while (startIdx > 0) { const prev = messages[startIdx - 1]; if (prev.role === "assistant" && prev.type === "voice" && prev.audioUri) { startIdx--; } else { break; } } // Queue all chunks from start to last await stopPlayback(); for (let i = startIdx; i <= lastIdx; i++) { const m = messages[i]; if (m.audioUri) playAudio(m.audioUri); } }, [messages]); return ( {/* Header */} setShowSessions(true)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} style={({ pressed }) => ({ width: 36, height: 36, alignItems: "center", justifyContent: "center", borderRadius: 18, backgroundColor: pressed ? colors.bgTertiary : colors.bgTertiary + "80", })} > setShowSessions(true)} style={{ flexDirection: "row", alignItems: "center", gap: 8, flex: 1 }} hitSlop={{ top: 6, bottom: 6, left: 0, right: 6 }} > {activeSessionName} ({ width: 36, height: 36, alignItems: "center", justifyContent: "center", borderRadius: 18, backgroundColor: pressed ? colors.bgTertiary : colors.bgTertiary + "80", })} > {themeIcon} router.push("/settings")} hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }} style={{ width: 36, height: 36, alignItems: "center", justifyContent: "center", borderRadius: 18, backgroundColor: colors.bgTertiary, }} > ⚙️ {/* Message list */} {messages.length === 0 ? ( 🛩 PAILot Voice-first AI communicator.{"\n"}Tap the mic to start talking. ) : ( )} {/* Command bar */} {isTextMode ? ( ) : ( )} {/* Input bar */} setIsTextMode((v) => !v)} audioPlaying={audioPlaying} /> {/* Image caption modal — WhatsApp-style full-screen preview */} setStagedImage(null)} /> {/* Session drawer — absolute overlay outside KAV */} setShowSessions(false)} /> ); }