| .. | .. |
|---|
| 1 | | -import React, { useCallback } from "react"; |
|---|
| 1 | +import React, { useCallback, useState } from "react"; |
|---|
| 2 | 2 | import { Pressable, Text, View } from "react-native"; |
|---|
| 3 | 3 | import { SafeAreaView } from "react-native-safe-area-context"; |
|---|
| 4 | 4 | import { router } from "expo-router"; |
|---|
| .. | .. |
|---|
| 6 | 6 | import { useConnection } from "../contexts/ConnectionContext"; |
|---|
| 7 | 7 | import { MessageList } from "../components/chat/MessageList"; |
|---|
| 8 | 8 | import { InputBar } from "../components/chat/InputBar"; |
|---|
| 9 | | -import { CommandBar } from "../components/chat/CommandBar"; |
|---|
| 9 | +import { CommandBar, TextModeCommandBar } from "../components/chat/CommandBar"; |
|---|
| 10 | 10 | import { StatusDot } from "../components/ui/StatusDot"; |
|---|
| 11 | +import { SessionPicker } from "../components/SessionPicker"; |
|---|
| 12 | +import { playAudio } from "../services/audio"; |
|---|
| 11 | 13 | |
|---|
| 12 | 14 | export default function ChatScreen() { |
|---|
| 13 | | - const { messages, sendTextMessage, sendVoiceMessage, clearMessages } = |
|---|
| 15 | + const { messages, sendTextMessage, clearMessages, requestScreenshot } = |
|---|
| 14 | 16 | useChat(); |
|---|
| 15 | 17 | const { status } = useConnection(); |
|---|
| 18 | + const [isTextMode, setIsTextMode] = useState(false); |
|---|
| 19 | + const [showSessions, setShowSessions] = useState(false); |
|---|
| 16 | 20 | |
|---|
| 17 | | - const handleCommand = useCallback( |
|---|
| 18 | | - (command: string) => { |
|---|
| 19 | | - if (command === "/clear") { |
|---|
| 20 | | - clearMessages(); |
|---|
| 21 | + const handleSessions = useCallback(() => { |
|---|
| 22 | + setShowSessions(true); |
|---|
| 23 | + }, []); |
|---|
| 24 | + |
|---|
| 25 | + const handleScreenshot = useCallback(() => { |
|---|
| 26 | + requestScreenshot(); |
|---|
| 27 | + router.push("/navigate"); |
|---|
| 28 | + }, [requestScreenshot]); |
|---|
| 29 | + |
|---|
| 30 | + const handleHelp = useCallback(() => { |
|---|
| 31 | + sendTextMessage("/h"); |
|---|
| 32 | + }, [sendTextMessage]); |
|---|
| 33 | + |
|---|
| 34 | + const handleNavigate = useCallback(() => { |
|---|
| 35 | + router.push("/navigate"); |
|---|
| 36 | + }, []); |
|---|
| 37 | + |
|---|
| 38 | + const handleClear = useCallback(() => { |
|---|
| 39 | + clearMessages(); |
|---|
| 40 | + }, [clearMessages]); |
|---|
| 41 | + |
|---|
| 42 | + const handleReplay = useCallback(() => { |
|---|
| 43 | + for (let i = messages.length - 1; i >= 0; i--) { |
|---|
| 44 | + const msg = messages[i]; |
|---|
| 45 | + if (msg.role === "assistant") { |
|---|
| 46 | + if (msg.audioUri) { |
|---|
| 47 | + playAudio(msg.audioUri).catch(() => {}); |
|---|
| 48 | + } |
|---|
| 21 | 49 | return; |
|---|
| 22 | 50 | } |
|---|
| 23 | | - sendTextMessage(command); |
|---|
| 24 | | - }, |
|---|
| 25 | | - [sendTextMessage, clearMessages] |
|---|
| 26 | | - ); |
|---|
| 27 | | - |
|---|
| 28 | | - const handleSendVoice = useCallback( |
|---|
| 29 | | - (audioUri: string, durationMs: number) => { |
|---|
| 30 | | - sendVoiceMessage(audioUri, durationMs); |
|---|
| 31 | | - }, |
|---|
| 32 | | - [sendVoiceMessage] |
|---|
| 33 | | - ); |
|---|
| 51 | + } |
|---|
| 52 | + }, [messages]); |
|---|
| 34 | 53 | |
|---|
| 35 | 54 | return ( |
|---|
| 36 | | - <SafeAreaView className="flex-1 bg-pai-bg" edges={["top", "bottom"]}> |
|---|
| 55 | + <SafeAreaView style={{ flex: 1, backgroundColor: "#0A0A0F" }} edges={["top", "bottom"]}> |
|---|
| 37 | 56 | {/* Header */} |
|---|
| 38 | | - <View className="flex-row items-center justify-between px-4 py-3 border-b border-pai-border"> |
|---|
| 39 | | - <Text className="text-pai-text text-xl font-bold tracking-tight"> |
|---|
| 40 | | - PAILot |
|---|
| 41 | | - </Text> |
|---|
| 42 | | - <View className="flex-row items-center gap-3"> |
|---|
| 43 | | - <StatusDot status={status} size={10} /> |
|---|
| 44 | | - <Text className="text-pai-text-secondary text-xs"> |
|---|
| 45 | | - {status === "connected" |
|---|
| 46 | | - ? "Connected" |
|---|
| 47 | | - : status === "connecting" |
|---|
| 48 | | - ? "Connecting..." |
|---|
| 49 | | - : "Offline"} |
|---|
| 50 | | - </Text> |
|---|
| 51 | | - <Pressable |
|---|
| 52 | | - onPress={() => router.push("/settings")} |
|---|
| 53 | | - className="w-9 h-9 items-center justify-center rounded-full bg-pai-bg-tertiary" |
|---|
| 54 | | - hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }} |
|---|
| 57 | + <View |
|---|
| 58 | + style={{ |
|---|
| 59 | + flexDirection: "row", |
|---|
| 60 | + alignItems: "center", |
|---|
| 61 | + justifyContent: "space-between", |
|---|
| 62 | + paddingHorizontal: 16, |
|---|
| 63 | + paddingVertical: 12, |
|---|
| 64 | + borderBottomWidth: 1, |
|---|
| 65 | + borderBottomColor: "#2E2E45", |
|---|
| 66 | + }} |
|---|
| 67 | + > |
|---|
| 68 | + <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}> |
|---|
| 69 | + <Text |
|---|
| 70 | + style={{ |
|---|
| 71 | + color: "#E8E8F0", |
|---|
| 72 | + fontSize: 22, |
|---|
| 73 | + fontWeight: "800", |
|---|
| 74 | + letterSpacing: -0.5, |
|---|
| 75 | + }} |
|---|
| 55 | 76 | > |
|---|
| 56 | | - <Text className="text-base">⚙️</Text> |
|---|
| 57 | | - </Pressable> |
|---|
| 77 | + PAILot |
|---|
| 78 | + </Text> |
|---|
| 79 | + <StatusDot status={status} size={8} /> |
|---|
| 58 | 80 | </View> |
|---|
| 81 | + |
|---|
| 82 | + <Pressable |
|---|
| 83 | + onPress={() => router.push("/settings")} |
|---|
| 84 | + hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }} |
|---|
| 85 | + style={{ |
|---|
| 86 | + width: 36, |
|---|
| 87 | + height: 36, |
|---|
| 88 | + alignItems: "center", |
|---|
| 89 | + justifyContent: "center", |
|---|
| 90 | + borderRadius: 18, |
|---|
| 91 | + backgroundColor: "#1E1E2E", |
|---|
| 92 | + }} |
|---|
| 93 | + > |
|---|
| 94 | + <Text style={{ fontSize: 15 }}>⚙️</Text> |
|---|
| 95 | + </Pressable> |
|---|
| 59 | 96 | </View> |
|---|
| 60 | 97 | |
|---|
| 61 | 98 | {/* Message list */} |
|---|
| 62 | | - <View className="flex-1"> |
|---|
| 99 | + <View style={{ flex: 1 }}> |
|---|
| 63 | 100 | {messages.length === 0 ? ( |
|---|
| 64 | | - <View className="flex-1 items-center justify-center gap-3"> |
|---|
| 65 | | - <Text className="text-5xl">🛩</Text> |
|---|
| 66 | | - <Text className="text-pai-text text-xl font-semibold"> |
|---|
| 67 | | - PAILot |
|---|
| 68 | | - </Text> |
|---|
| 69 | | - <Text className="text-pai-text-muted text-sm text-center px-8"> |
|---|
| 70 | | - Voice-first AI communicator.{"\n"}Hold the mic button to talk. |
|---|
| 71 | | - </Text> |
|---|
| 101 | + <View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 16 }}> |
|---|
| 102 | + <View |
|---|
| 103 | + style={{ |
|---|
| 104 | + width: 80, |
|---|
| 105 | + height: 80, |
|---|
| 106 | + borderRadius: 40, |
|---|
| 107 | + backgroundColor: "#1E1E2E", |
|---|
| 108 | + alignItems: "center", |
|---|
| 109 | + justifyContent: "center", |
|---|
| 110 | + borderWidth: 1, |
|---|
| 111 | + borderColor: "#2E2E45", |
|---|
| 112 | + }} |
|---|
| 113 | + > |
|---|
| 114 | + <Text style={{ fontSize: 36 }}>🛩</Text> |
|---|
| 115 | + </View> |
|---|
| 116 | + <View style={{ alignItems: "center", gap: 6 }}> |
|---|
| 117 | + <Text style={{ color: "#E8E8F0", fontSize: 20, fontWeight: "700" }}> |
|---|
| 118 | + PAILot |
|---|
| 119 | + </Text> |
|---|
| 120 | + <Text |
|---|
| 121 | + style={{ |
|---|
| 122 | + color: "#5A5A78", |
|---|
| 123 | + fontSize: 14, |
|---|
| 124 | + textAlign: "center", |
|---|
| 125 | + paddingHorizontal: 40, |
|---|
| 126 | + lineHeight: 20, |
|---|
| 127 | + }} |
|---|
| 128 | + > |
|---|
| 129 | + Voice-first AI communicator.{"\n"}Tap the mic to start talking. |
|---|
| 130 | + </Text> |
|---|
| 131 | + </View> |
|---|
| 72 | 132 | </View> |
|---|
| 73 | 133 | ) : ( |
|---|
| 74 | 134 | <MessageList messages={messages} /> |
|---|
| .. | .. |
|---|
| 76 | 136 | </View> |
|---|
| 77 | 137 | |
|---|
| 78 | 138 | {/* Command bar */} |
|---|
| 79 | | - <CommandBar onCommand={handleCommand} /> |
|---|
| 139 | + {isTextMode ? ( |
|---|
| 140 | + <TextModeCommandBar |
|---|
| 141 | + onSessions={handleSessions} |
|---|
| 142 | + onScreenshot={handleScreenshot} |
|---|
| 143 | + onNavigate={handleNavigate} |
|---|
| 144 | + onClear={handleClear} |
|---|
| 145 | + /> |
|---|
| 146 | + ) : ( |
|---|
| 147 | + <CommandBar |
|---|
| 148 | + onSessions={handleSessions} |
|---|
| 149 | + onScreenshot={handleScreenshot} |
|---|
| 150 | + onHelp={handleHelp} |
|---|
| 151 | + /> |
|---|
| 152 | + )} |
|---|
| 80 | 153 | |
|---|
| 81 | 154 | {/* Input bar */} |
|---|
| 82 | | - <InputBar onSendText={sendTextMessage} onSendVoice={handleSendVoice} /> |
|---|
| 155 | + <InputBar |
|---|
| 156 | + onSendText={sendTextMessage} |
|---|
| 157 | + onReplay={handleReplay} |
|---|
| 158 | + isTextMode={isTextMode} |
|---|
| 159 | + onToggleMode={() => setIsTextMode((v) => !v)} |
|---|
| 160 | + /> |
|---|
| 161 | + |
|---|
| 162 | + {/* Session picker modal */} |
|---|
| 163 | + <SessionPicker |
|---|
| 164 | + visible={showSessions} |
|---|
| 165 | + onClose={() => setShowSessions(false)} |
|---|
| 166 | + /> |
|---|
| 83 | 167 | </SafeAreaView> |
|---|
| 84 | 168 | ); |
|---|
| 85 | 169 | } |
|---|