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
---
components/chat/InputBar.tsx | 157 +++++++++++++++++++++++++++++++++++++++++----------
1 files changed, 125 insertions(+), 32 deletions(-)
diff --git a/components/chat/InputBar.tsx b/components/chat/InputBar.tsx
index 71a576a..6f1bc35 100644
--- a/components/chat/InputBar.tsx
+++ b/components/chat/InputBar.tsx
@@ -6,16 +6,23 @@
TextInput,
View,
} from "react-native";
+import * as Haptics from "expo-haptics";
import { VoiceButton } from "./VoiceButton";
interface InputBarProps {
onSendText: (text: string) => void;
- onSendVoice: (audioUri: string, durationMs: number) => void;
+ onReplay: () => void;
+ isTextMode: boolean;
+ onToggleMode: () => void;
}
-export function InputBar({ onSendText, onSendVoice }: InputBarProps) {
+export function InputBar({
+ onSendText,
+ onReplay,
+ isTextMode,
+ onToggleMode,
+}: InputBarProps) {
const [text, setText] = useState("");
- const [isVoiceMode, setIsVoiceMode] = useState(false);
const inputRef = useRef<TextInput>(null);
const handleSend = useCallback(() => {
@@ -25,42 +32,108 @@
setText("");
}, [text, onSendText]);
- const toggleMode = useCallback(() => {
- setIsVoiceMode((prev) => {
- if (prev) {
- // Switching to text mode — focus input after mode switch
- setTimeout(() => inputRef.current?.focus(), 100);
- } else {
- Keyboard.dismiss();
- }
- return !prev;
- });
- }, []);
-
- if (isVoiceMode) {
+ if (!isTextMode) {
+ // Voice mode: [Replay] [Talk] [Aa]
return (
- <View className="border-t border-pai-border bg-pai-bg">
- {/* Mode toggle */}
+ <View
+ style={{
+ flexDirection: "row",
+ gap: 10,
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ paddingBottom: 6,
+ borderTopWidth: 1,
+ borderTopColor: "#2E2E45",
+ alignItems: "center",
+ }}
+ >
+ {/* Replay last message */}
<Pressable
- onPress={toggleMode}
- className="absolute top-3 right-4 z-10 w-10 h-10 items-center justify-center"
+ onPress={() => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ onReplay();
+ }}
>
- <Text className="text-2xl">⌨️</Text>
+ <View
+ style={{
+ width: 68,
+ height: 68,
+ borderRadius: 34,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#1A2E1A",
+ borderWidth: 1.5,
+ borderColor: "#3A6A3A",
+ }}
+ >
+ <Text style={{ fontSize: 24 }}>▶</Text>
+ <Text style={{ color: "#8ABF8A", fontSize: 10, marginTop: 1, fontWeight: "600" }}>Replay</Text>
+ </View>
</Pressable>
- <VoiceButton onVoiceMessage={onSendVoice} />
+ {/* Talk button — center, biggest */}
+ <View style={{ flex: 1, alignItems: "center" }}>
+ <VoiceButton onTranscript={onSendText} />
+ </View>
+
+ {/* Text mode toggle */}
+ <Pressable
+ onPress={() => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ onToggleMode();
+ setTimeout(() => inputRef.current?.focus(), 150);
+ }}
+ >
+ <View
+ style={{
+ width: 68,
+ height: 68,
+ borderRadius: 34,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#1A1A3E",
+ borderWidth: 1.5,
+ borderColor: "#3A3A7A",
+ }}
+ >
+ <Text style={{ fontSize: 22, color: "#9898D0", fontWeight: "700" }}>Aa</Text>
+ </View>
+ </Pressable>
</View>
);
}
+ // Text mode: [Mic] [TextInput] [Send]
return (
- <View className="border-t border-pai-border bg-pai-bg px-3 py-2 flex-row items-end gap-2">
+ <View
+ style={{
+ flexDirection: "row",
+ gap: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderTopWidth: 1,
+ borderTopColor: "#2E2E45",
+ alignItems: "flex-end",
+ }}
+ >
{/* Voice mode toggle */}
<Pressable
- onPress={toggleMode}
- className="w-10 h-10 items-center justify-center rounded-full bg-pai-bg-tertiary mb-0.5"
+ onPress={() => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ Keyboard.dismiss();
+ onToggleMode();
+ }}
+ style={{
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#1E1E2E",
+ marginBottom: 2,
+ }}
>
- <Text className="text-xl">🎤</Text>
+ <Text style={{ fontSize: 20 }}>🎤</Text>
</Pressable>
{/* Text input */}
@@ -75,19 +148,39 @@
onSubmitEditing={handleSend}
returnKeyType="send"
blurOnSubmit
- className="flex-1 bg-pai-bg-tertiary rounded-2xl px-4 py-2.5 text-pai-text text-base"
- style={{ maxHeight: 120 }}
+ style={{
+ flex: 1,
+ backgroundColor: "#1E1E2E",
+ borderRadius: 20,
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ maxHeight: 120,
+ color: "#E8E8F0",
+ fontSize: 16,
+ }}
/>
{/* Send button */}
<Pressable
onPress={handleSend}
disabled={!text.trim()}
- className={`w-10 h-10 rounded-full items-center justify-center mb-0.5 ${
- text.trim() ? "bg-pai-accent" : "bg-pai-bg-tertiary"
- }`}
+ style={{
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ alignItems: "center",
+ justifyContent: "center",
+ marginBottom: 2,
+ backgroundColor: text.trim() ? "#4A9EFF" : "#1E1E2E",
+ }}
>
- <Text className={`text-xl ${text.trim() ? "text-white" : "text-pai-text-muted"}`}>
+ <Text
+ style={{
+ fontSize: 18,
+ fontWeight: "bold",
+ color: text.trim() ? "#FFFFFF" : "#5A5A78",
+ }}
+ >
↑
</Text>
</Pressable>
--
Gitblit v1.3.1