From a0f39302919fbacf7a0d407f01b1a50413ea6f70 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Mon, 02 Mar 2026 23:15:13 +0100
Subject: [PATCH] feat: on-device speech recognition, navigation screen, session picker

---
 app/chat.tsx |  182 +++++++++++++++++++++++++++++++++------------
 1 files changed, 133 insertions(+), 49 deletions(-)

diff --git a/app/chat.tsx b/app/chat.tsx
index cd0e2af..d773e98 100644
--- a/app/chat.tsx
+++ b/app/chat.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from "react";
+import React, { useCallback, useState } from "react";
 import { Pressable, Text, View } from "react-native";
 import { SafeAreaView } from "react-native-safe-area-context";
 import { router } from "expo-router";
@@ -6,69 +6,129 @@
 import { useConnection } from "../contexts/ConnectionContext";
 import { MessageList } from "../components/chat/MessageList";
 import { InputBar } from "../components/chat/InputBar";
-import { CommandBar } from "../components/chat/CommandBar";
+import { CommandBar, TextModeCommandBar } from "../components/chat/CommandBar";
 import { StatusDot } from "../components/ui/StatusDot";
+import { SessionPicker } from "../components/SessionPicker";
+import { playAudio } from "../services/audio";
 
 export default function ChatScreen() {
-  const { messages, sendTextMessage, sendVoiceMessage, clearMessages } =
+  const { messages, sendTextMessage, clearMessages, requestScreenshot } =
     useChat();
   const { status } = useConnection();
+  const [isTextMode, setIsTextMode] = useState(false);
+  const [showSessions, setShowSessions] = useState(false);
 
-  const handleCommand = useCallback(
-    (command: string) => {
-      if (command === "/clear") {
-        clearMessages();
+  const handleSessions = useCallback(() => {
+    setShowSessions(true);
+  }, []);
+
+  const handleScreenshot = useCallback(() => {
+    requestScreenshot();
+    router.push("/navigate");
+  }, [requestScreenshot]);
+
+  const handleHelp = useCallback(() => {
+    sendTextMessage("/h");
+  }, [sendTextMessage]);
+
+  const handleNavigate = useCallback(() => {
+    router.push("/navigate");
+  }, []);
+
+  const handleClear = useCallback(() => {
+    clearMessages();
+  }, [clearMessages]);
+
+  const handleReplay = useCallback(() => {
+    for (let i = messages.length - 1; i >= 0; i--) {
+      const msg = messages[i];
+      if (msg.role === "assistant") {
+        if (msg.audioUri) {
+          playAudio(msg.audioUri).catch(() => {});
+        }
         return;
       }
-      sendTextMessage(command);
-    },
-    [sendTextMessage, clearMessages]
-  );
-
-  const handleSendVoice = useCallback(
-    (audioUri: string, durationMs: number) => {
-      sendVoiceMessage(audioUri, durationMs);
-    },
-    [sendVoiceMessage]
-  );
+    }
+  }, [messages]);
 
   return (
-    <SafeAreaView className="flex-1 bg-pai-bg" edges={["top", "bottom"]}>
+    <SafeAreaView style={{ flex: 1, backgroundColor: "#0A0A0F" }} edges={["top", "bottom"]}>
       {/* Header */}
-      <View className="flex-row items-center justify-between px-4 py-3 border-b border-pai-border">
-        <Text className="text-pai-text text-xl font-bold tracking-tight">
-          PAILot
-        </Text>
-        <View className="flex-row items-center gap-3">
-          <StatusDot status={status} size={10} />
-          <Text className="text-pai-text-secondary text-xs">
-            {status === "connected"
-              ? "Connected"
-              : status === "connecting"
-              ? "Connecting..."
-              : "Offline"}
-          </Text>
-          <Pressable
-            onPress={() => router.push("/settings")}
-            className="w-9 h-9 items-center justify-center rounded-full bg-pai-bg-tertiary"
-            hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
+      <View
+        style={{
+          flexDirection: "row",
+          alignItems: "center",
+          justifyContent: "space-between",
+          paddingHorizontal: 16,
+          paddingVertical: 12,
+          borderBottomWidth: 1,
+          borderBottomColor: "#2E2E45",
+        }}
+      >
+        <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
+          <Text
+            style={{
+              color: "#E8E8F0",
+              fontSize: 22,
+              fontWeight: "800",
+              letterSpacing: -0.5,
+            }}
           >
-            <Text className="text-base">⚙️</Text>
-          </Pressable>
+            PAILot
+          </Text>
+          <StatusDot status={status} size={8} />
         </View>
+
+        <Pressable
+          onPress={() => router.push("/settings")}
+          hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
+          style={{
+            width: 36,
+            height: 36,
+            alignItems: "center",
+            justifyContent: "center",
+            borderRadius: 18,
+            backgroundColor: "#1E1E2E",
+          }}
+        >
+          <Text style={{ fontSize: 15 }}>⚙️</Text>
+        </Pressable>
       </View>
 
       {/* Message list */}
-      <View className="flex-1">
+      <View style={{ flex: 1 }}>
         {messages.length === 0 ? (
-          <View className="flex-1 items-center justify-center gap-3">
-            <Text className="text-5xl">🛩</Text>
-            <Text className="text-pai-text text-xl font-semibold">
-              PAILot
-            </Text>
-            <Text className="text-pai-text-muted text-sm text-center px-8">
-              Voice-first AI communicator.{"\n"}Hold the mic button to talk.
-            </Text>
+          <View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 16 }}>
+            <View
+              style={{
+                width: 80,
+                height: 80,
+                borderRadius: 40,
+                backgroundColor: "#1E1E2E",
+                alignItems: "center",
+                justifyContent: "center",
+                borderWidth: 1,
+                borderColor: "#2E2E45",
+              }}
+            >
+              <Text style={{ fontSize: 36 }}>🛩</Text>
+            </View>
+            <View style={{ alignItems: "center", gap: 6 }}>
+              <Text style={{ color: "#E8E8F0", fontSize: 20, fontWeight: "700" }}>
+                PAILot
+              </Text>
+              <Text
+                style={{
+                  color: "#5A5A78",
+                  fontSize: 14,
+                  textAlign: "center",
+                  paddingHorizontal: 40,
+                  lineHeight: 20,
+                }}
+              >
+                Voice-first AI communicator.{"\n"}Tap the mic to start talking.
+              </Text>
+            </View>
           </View>
         ) : (
           <MessageList messages={messages} />
@@ -76,10 +136,34 @@
       </View>
 
       {/* Command bar */}
-      <CommandBar onCommand={handleCommand} />
+      {isTextMode ? (
+        <TextModeCommandBar
+          onSessions={handleSessions}
+          onScreenshot={handleScreenshot}
+          onNavigate={handleNavigate}
+          onClear={handleClear}
+        />
+      ) : (
+        <CommandBar
+          onSessions={handleSessions}
+          onScreenshot={handleScreenshot}
+          onHelp={handleHelp}
+        />
+      )}
 
       {/* Input bar */}
-      <InputBar onSendText={sendTextMessage} onSendVoice={handleSendVoice} />
+      <InputBar
+        onSendText={sendTextMessage}
+        onReplay={handleReplay}
+        isTextMode={isTextMode}
+        onToggleMode={() => setIsTextMode((v) => !v)}
+      />
+
+      {/* Session picker modal */}
+      <SessionPicker
+        visible={showSessions}
+        onClose={() => setShowSessions(false)}
+      />
     </SafeAreaView>
   );
 }

--
Gitblit v1.3.1