feat: on-device speech recognition, navigation screen, session picker
- Replace audio recording + server-side Whisper with expo-speech-recognition
- Add navigate screen for iTerm2 remote control
- Add session picker modal for switching Claude sessions
- Restyle command bar with colored button backgrounds
- Add speech recognition and microphone permissions
2 files added
13 files modified
| .. | .. |
|---|
| 18 | 18 | "bundleIdentifier": "org.mnsoft.pailot", |
|---|
| 19 | 19 | "appleTeamId": "7KU642K5ZL", |
|---|
| 20 | 20 | "infoPlist": { |
|---|
| 21 | | - "NSMicrophoneUsageDescription": "PAILot needs microphone access to record voice messages.", |
|---|
| 21 | + "NSMicrophoneUsageDescription": "PAILot needs microphone access for voice input.", |
|---|
| 22 | + "NSSpeechRecognitionUsageDescription": "PAILot uses speech recognition to convert your voice to text.", |
|---|
| 22 | 23 | "UIBackgroundModes": [ |
|---|
| 23 | 24 | "audio" |
|---|
| 24 | 25 | ] |
|---|
| .. | .. |
|---|
| 43 | 44 | "plugins": [ |
|---|
| 44 | 45 | "expo-router", |
|---|
| 45 | 46 | [ |
|---|
| 46 | | - "expo-av", |
|---|
| 47 | + "expo-audio", |
|---|
| 47 | 48 | { |
|---|
| 48 | | - "microphonePermission": "PAILot needs microphone access to record voice messages." |
|---|
| 49 | + "microphonePermission": "PAILot needs microphone access for voice input." |
|---|
| 50 | + } |
|---|
| 51 | + ], |
|---|
| 52 | + [ |
|---|
| 53 | + "expo-speech-recognition", |
|---|
| 54 | + { |
|---|
| 55 | + "microphonePermission": "PAILot needs microphone access for voice input.", |
|---|
| 56 | + "speechRecognitionPermission": "PAILot uses speech recognition to convert your voice to text." |
|---|
| 49 | 57 | } |
|---|
| 50 | 58 | ], |
|---|
| 51 | 59 | "expo-secure-store" |
|---|
| .. | .. |
|---|
| 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 | } |
|---|
| .. | .. |
|---|
| 1 | +import React, { useEffect } from "react"; |
|---|
| 2 | +import { Image, Pressable, Text, View } from "react-native"; |
|---|
| 3 | +import { SafeAreaView } from "react-native-safe-area-context"; |
|---|
| 4 | +import { router } from "expo-router"; |
|---|
| 5 | +import * as Haptics from "expo-haptics"; |
|---|
| 6 | +import { useChat } from "../contexts/ChatContext"; |
|---|
| 7 | + |
|---|
| 8 | +interface NavButton { |
|---|
| 9 | + label: string; |
|---|
| 10 | + key: string; |
|---|
| 11 | + icon?: string; |
|---|
| 12 | + wide?: boolean; |
|---|
| 13 | +} |
|---|
| 14 | + |
|---|
| 15 | +const NAV_BUTTONS: NavButton[][] = [ |
|---|
| 16 | + [ |
|---|
| 17 | + { label: "Esc", key: "escape" }, |
|---|
| 18 | + { label: "Tab", key: "tab" }, |
|---|
| 19 | + { label: "Enter", key: "enter" }, |
|---|
| 20 | + { label: "Ctrl-C", key: "ctrl-c" }, |
|---|
| 21 | + ], |
|---|
| 22 | + [ |
|---|
| 23 | + { label: "", key: "left", icon: "←" }, |
|---|
| 24 | + { label: "", key: "up", icon: "↑" }, |
|---|
| 25 | + { label: "", key: "down", icon: "↓" }, |
|---|
| 26 | + { label: "", key: "right", icon: "→" }, |
|---|
| 27 | + ], |
|---|
| 28 | +]; |
|---|
| 29 | + |
|---|
| 30 | +export default function NavigateScreen() { |
|---|
| 31 | + const { latestScreenshot, requestScreenshot, sendNavKey } = useChat(); |
|---|
| 32 | + |
|---|
| 33 | + // Request a screenshot when entering navigation mode |
|---|
| 34 | + useEffect(() => { |
|---|
| 35 | + requestScreenshot(); |
|---|
| 36 | + }, [requestScreenshot]); |
|---|
| 37 | + |
|---|
| 38 | + function handleNavPress(key: string) { |
|---|
| 39 | + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 40 | + sendNavKey(key); |
|---|
| 41 | + } |
|---|
| 42 | + |
|---|
| 43 | + return ( |
|---|
| 44 | + <SafeAreaView style={{ flex: 1, backgroundColor: "#0A0A0F" }} edges={["top", "bottom"]}> |
|---|
| 45 | + {/* Header */} |
|---|
| 46 | + <View |
|---|
| 47 | + style={{ |
|---|
| 48 | + flexDirection: "row", |
|---|
| 49 | + alignItems: "center", |
|---|
| 50 | + justifyContent: "space-between", |
|---|
| 51 | + paddingHorizontal: 16, |
|---|
| 52 | + paddingVertical: 10, |
|---|
| 53 | + borderBottomWidth: 1, |
|---|
| 54 | + borderBottomColor: "#2E2E45", |
|---|
| 55 | + }} |
|---|
| 56 | + > |
|---|
| 57 | + <Pressable |
|---|
| 58 | + onPress={() => router.back()} |
|---|
| 59 | + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} |
|---|
| 60 | + style={{ |
|---|
| 61 | + width: 36, |
|---|
| 62 | + height: 36, |
|---|
| 63 | + alignItems: "center", |
|---|
| 64 | + justifyContent: "center", |
|---|
| 65 | + borderRadius: 18, |
|---|
| 66 | + backgroundColor: "#1E1E2E", |
|---|
| 67 | + }} |
|---|
| 68 | + > |
|---|
| 69 | + <Text style={{ color: "#E8E8F0", fontSize: 16 }}>←</Text> |
|---|
| 70 | + </Pressable> |
|---|
| 71 | + <Text style={{ color: "#E8E8F0", fontSize: 18, fontWeight: "700" }}> |
|---|
| 72 | + Navigate |
|---|
| 73 | + </Text> |
|---|
| 74 | + <Pressable |
|---|
| 75 | + onPress={() => requestScreenshot()} |
|---|
| 76 | + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} |
|---|
| 77 | + style={{ |
|---|
| 78 | + paddingHorizontal: 12, |
|---|
| 79 | + paddingVertical: 8, |
|---|
| 80 | + borderRadius: 12, |
|---|
| 81 | + backgroundColor: "#1E1E2E", |
|---|
| 82 | + }} |
|---|
| 83 | + > |
|---|
| 84 | + <Text style={{ color: "#4A9EFF", fontSize: 14, fontWeight: "600" }}> |
|---|
| 85 | + Refresh |
|---|
| 86 | + </Text> |
|---|
| 87 | + </Pressable> |
|---|
| 88 | + </View> |
|---|
| 89 | + |
|---|
| 90 | + {/* Screenshot area */} |
|---|
| 91 | + <View style={{ flex: 1, padding: 8 }}> |
|---|
| 92 | + {latestScreenshot ? ( |
|---|
| 93 | + <Image |
|---|
| 94 | + source={{ uri: `data:image/png;base64,${latestScreenshot}` }} |
|---|
| 95 | + style={{ |
|---|
| 96 | + flex: 1, |
|---|
| 97 | + borderRadius: 12, |
|---|
| 98 | + backgroundColor: "#14141F", |
|---|
| 99 | + }} |
|---|
| 100 | + resizeMode="contain" |
|---|
| 101 | + /> |
|---|
| 102 | + ) : ( |
|---|
| 103 | + <View |
|---|
| 104 | + style={{ |
|---|
| 105 | + flex: 1, |
|---|
| 106 | + alignItems: "center", |
|---|
| 107 | + justifyContent: "center", |
|---|
| 108 | + backgroundColor: "#14141F", |
|---|
| 109 | + borderRadius: 12, |
|---|
| 110 | + }} |
|---|
| 111 | + > |
|---|
| 112 | + <Text style={{ color: "#5A5A78", fontSize: 16 }}> |
|---|
| 113 | + Loading screenshot... |
|---|
| 114 | + </Text> |
|---|
| 115 | + </View> |
|---|
| 116 | + )} |
|---|
| 117 | + </View> |
|---|
| 118 | + |
|---|
| 119 | + {/* Navigation buttons */} |
|---|
| 120 | + <View |
|---|
| 121 | + style={{ |
|---|
| 122 | + paddingHorizontal: 12, |
|---|
| 123 | + paddingBottom: 8, |
|---|
| 124 | + gap: 8, |
|---|
| 125 | + }} |
|---|
| 126 | + > |
|---|
| 127 | + {NAV_BUTTONS.map((row, rowIdx) => ( |
|---|
| 128 | + <View |
|---|
| 129 | + key={rowIdx} |
|---|
| 130 | + style={{ |
|---|
| 131 | + flexDirection: "row", |
|---|
| 132 | + gap: 8, |
|---|
| 133 | + justifyContent: "center", |
|---|
| 134 | + }} |
|---|
| 135 | + > |
|---|
| 136 | + {row.map((btn) => ( |
|---|
| 137 | + <Pressable |
|---|
| 138 | + key={btn.key} |
|---|
| 139 | + onPress={() => handleNavPress(btn.key)} |
|---|
| 140 | + style={({ pressed }) => ({ |
|---|
| 141 | + flex: btn.wide ? 2 : 1, |
|---|
| 142 | + height: 52, |
|---|
| 143 | + borderRadius: 14, |
|---|
| 144 | + alignItems: "center", |
|---|
| 145 | + justifyContent: "center", |
|---|
| 146 | + backgroundColor: pressed ? "#4A9EFF" : "#1E1E2E", |
|---|
| 147 | + borderWidth: 1, |
|---|
| 148 | + borderColor: pressed ? "#4A9EFF" : "#2E2E45", |
|---|
| 149 | + })} |
|---|
| 150 | + > |
|---|
| 151 | + <Text |
|---|
| 152 | + style={{ |
|---|
| 153 | + color: "#E8E8F0", |
|---|
| 154 | + fontSize: btn.icon ? 22 : 15, |
|---|
| 155 | + fontWeight: "700", |
|---|
| 156 | + }} |
|---|
| 157 | + > |
|---|
| 158 | + {btn.icon ?? btn.label} |
|---|
| 159 | + </Text> |
|---|
| 160 | + </Pressable> |
|---|
| 161 | + ))} |
|---|
| 162 | + </View> |
|---|
| 163 | + ))} |
|---|
| 164 | + </View> |
|---|
| 165 | + </SafeAreaView> |
|---|
| 166 | + ); |
|---|
| 167 | +} |
|---|
| .. | .. |
|---|
| 20 | 20 | const { serverConfig, status, connect, disconnect, saveServerConfig } = |
|---|
| 21 | 21 | useConnection(); |
|---|
| 22 | 22 | |
|---|
| 23 | | - const [host, setHost] = useState(serverConfig?.host ?? ""); |
|---|
| 23 | + const [host, setHost] = useState(serverConfig?.host ?? "192.168.1.100"); |
|---|
| 24 | 24 | const [port, setPort] = useState( |
|---|
| 25 | | - serverConfig?.port ? String(serverConfig.port) : "" |
|---|
| 25 | + serverConfig?.port ? String(serverConfig.port) : "8765" |
|---|
| 26 | 26 | ); |
|---|
| 27 | 27 | const [saved, setSaved] = useState(false); |
|---|
| 28 | 28 | |
|---|
| .. | .. |
|---|
| 63 | 63 | keyboardShouldPersistTaps="handled" |
|---|
| 64 | 64 | > |
|---|
| 65 | 65 | {/* Header */} |
|---|
| 66 | | - <View className="flex-row items-center px-4 py-3 border-b border-pai-border"> |
|---|
| 66 | + <View |
|---|
| 67 | + style={{ |
|---|
| 68 | + flexDirection: "row", |
|---|
| 69 | + alignItems: "center", |
|---|
| 70 | + paddingHorizontal: 16, |
|---|
| 71 | + paddingVertical: 12, |
|---|
| 72 | + borderBottomWidth: 1, |
|---|
| 73 | + borderBottomColor: "#2E2E45", |
|---|
| 74 | + }} |
|---|
| 75 | + > |
|---|
| 67 | 76 | <Pressable |
|---|
| 68 | 77 | onPress={() => router.back()} |
|---|
| 69 | | - className="w-9 h-9 items-center justify-center rounded-full bg-pai-bg-tertiary mr-3" |
|---|
| 78 | + hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }} |
|---|
| 79 | + style={{ |
|---|
| 80 | + width: 36, |
|---|
| 81 | + height: 36, |
|---|
| 82 | + alignItems: "center", |
|---|
| 83 | + justifyContent: "center", |
|---|
| 84 | + borderRadius: 18, |
|---|
| 85 | + backgroundColor: "#1E1E2E", |
|---|
| 86 | + marginRight: 12, |
|---|
| 87 | + }} |
|---|
| 70 | 88 | > |
|---|
| 71 | | - <Text className="text-pai-text text-base">←</Text> |
|---|
| 89 | + <Text style={{ color: "#E8E8F0", fontSize: 16 }}>←</Text> |
|---|
| 72 | 90 | </Pressable> |
|---|
| 73 | | - <Text className="text-pai-text text-xl font-bold">Settings</Text> |
|---|
| 91 | + <Text style={{ color: "#E8E8F0", fontSize: 22, fontWeight: "800", letterSpacing: -0.5 }}> |
|---|
| 92 | + Settings |
|---|
| 93 | + </Text> |
|---|
| 74 | 94 | </View> |
|---|
| 75 | 95 | |
|---|
| 76 | 96 | <View className="px-4 mt-6"> |
|---|
| .. | .. |
|---|
| 115 | 135 | autoCapitalize="none" |
|---|
| 116 | 136 | autoCorrect={false} |
|---|
| 117 | 137 | keyboardType="url" |
|---|
| 118 | | - className="text-pai-text text-base" |
|---|
| 138 | + style={{ color: "#E8E8F0", fontSize: 16, padding: 0 }} |
|---|
| 119 | 139 | /> |
|---|
| 120 | 140 | </View> |
|---|
| 121 | 141 | |
|---|
| .. | .. |
|---|
| 130 | 150 | placeholder="8765" |
|---|
| 131 | 151 | placeholderTextColor="#5A5A78" |
|---|
| 132 | 152 | keyboardType="number-pad" |
|---|
| 133 | | - className="text-pai-text text-base" |
|---|
| 153 | + style={{ color: "#E8E8F0", fontSize: 16, padding: 0 }} |
|---|
| 134 | 154 | /> |
|---|
| 135 | 155 | </View> |
|---|
| 136 | 156 | </View> |
|---|
| .. | .. |
|---|
| 1 | +import React, { useCallback, useEffect, useState } from "react"; |
|---|
| 2 | +import { |
|---|
| 3 | + Modal, |
|---|
| 4 | + Pressable, |
|---|
| 5 | + ScrollView, |
|---|
| 6 | + Text, |
|---|
| 7 | + TextInput, |
|---|
| 8 | + View, |
|---|
| 9 | +} from "react-native"; |
|---|
| 10 | +import * as Haptics from "expo-haptics"; |
|---|
| 11 | +import { WsSession } from "../types"; |
|---|
| 12 | +import { useChat } from "../contexts/ChatContext"; |
|---|
| 13 | + |
|---|
| 14 | +interface SessionPickerProps { |
|---|
| 15 | + visible: boolean; |
|---|
| 16 | + onClose: () => void; |
|---|
| 17 | +} |
|---|
| 18 | + |
|---|
| 19 | +export function SessionPicker({ visible, onClose }: SessionPickerProps) { |
|---|
| 20 | + const { sessions, requestSessions, switchSession, renameSession } = useChat(); |
|---|
| 21 | + const [editingId, setEditingId] = useState<string | null>(null); |
|---|
| 22 | + const [editName, setEditName] = useState(""); |
|---|
| 23 | + |
|---|
| 24 | + useEffect(() => { |
|---|
| 25 | + if (visible) { |
|---|
| 26 | + requestSessions(); |
|---|
| 27 | + } |
|---|
| 28 | + }, [visible, requestSessions]); |
|---|
| 29 | + |
|---|
| 30 | + const handleSwitch = useCallback( |
|---|
| 31 | + (session: WsSession) => { |
|---|
| 32 | + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); |
|---|
| 33 | + switchSession(session.id); |
|---|
| 34 | + onClose(); |
|---|
| 35 | + }, |
|---|
| 36 | + [switchSession, onClose] |
|---|
| 37 | + ); |
|---|
| 38 | + |
|---|
| 39 | + const handleStartRename = useCallback((session: WsSession) => { |
|---|
| 40 | + setEditingId(session.id); |
|---|
| 41 | + setEditName(session.name); |
|---|
| 42 | + }, []); |
|---|
| 43 | + |
|---|
| 44 | + const handleConfirmRename = useCallback(() => { |
|---|
| 45 | + if (editingId && editName.trim()) { |
|---|
| 46 | + renameSession(editingId, editName.trim()); |
|---|
| 47 | + } |
|---|
| 48 | + setEditingId(null); |
|---|
| 49 | + setEditName(""); |
|---|
| 50 | + }, [editingId, editName, renameSession]); |
|---|
| 51 | + |
|---|
| 52 | + return ( |
|---|
| 53 | + <Modal |
|---|
| 54 | + visible={visible} |
|---|
| 55 | + animationType="slide" |
|---|
| 56 | + transparent |
|---|
| 57 | + onRequestClose={onClose} |
|---|
| 58 | + > |
|---|
| 59 | + <View |
|---|
| 60 | + style={{ |
|---|
| 61 | + flex: 1, |
|---|
| 62 | + backgroundColor: "rgba(0,0,0,0.6)", |
|---|
| 63 | + justifyContent: "flex-end", |
|---|
| 64 | + }} |
|---|
| 65 | + > |
|---|
| 66 | + <Pressable |
|---|
| 67 | + style={{ flex: 1 }} |
|---|
| 68 | + onPress={onClose} |
|---|
| 69 | + /> |
|---|
| 70 | + <View |
|---|
| 71 | + style={{ |
|---|
| 72 | + backgroundColor: "#14141F", |
|---|
| 73 | + borderTopLeftRadius: 24, |
|---|
| 74 | + borderTopRightRadius: 24, |
|---|
| 75 | + maxHeight: "70%", |
|---|
| 76 | + paddingBottom: 40, |
|---|
| 77 | + }} |
|---|
| 78 | + > |
|---|
| 79 | + {/* Handle bar */} |
|---|
| 80 | + <View style={{ alignItems: "center", paddingTop: 12, paddingBottom: 8 }}> |
|---|
| 81 | + <View |
|---|
| 82 | + style={{ |
|---|
| 83 | + width: 40, |
|---|
| 84 | + height: 4, |
|---|
| 85 | + borderRadius: 2, |
|---|
| 86 | + backgroundColor: "#2E2E45", |
|---|
| 87 | + }} |
|---|
| 88 | + /> |
|---|
| 89 | + </View> |
|---|
| 90 | + |
|---|
| 91 | + {/* Header */} |
|---|
| 92 | + <View |
|---|
| 93 | + style={{ |
|---|
| 94 | + flexDirection: "row", |
|---|
| 95 | + alignItems: "center", |
|---|
| 96 | + justifyContent: "space-between", |
|---|
| 97 | + paddingHorizontal: 20, |
|---|
| 98 | + paddingBottom: 16, |
|---|
| 99 | + }} |
|---|
| 100 | + > |
|---|
| 101 | + <Text |
|---|
| 102 | + style={{ |
|---|
| 103 | + color: "#E8E8F0", |
|---|
| 104 | + fontSize: 20, |
|---|
| 105 | + fontWeight: "700", |
|---|
| 106 | + }} |
|---|
| 107 | + > |
|---|
| 108 | + Sessions |
|---|
| 109 | + </Text> |
|---|
| 110 | + <Pressable |
|---|
| 111 | + onPress={() => requestSessions()} |
|---|
| 112 | + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} |
|---|
| 113 | + style={{ |
|---|
| 114 | + paddingHorizontal: 12, |
|---|
| 115 | + paddingVertical: 6, |
|---|
| 116 | + borderRadius: 12, |
|---|
| 117 | + backgroundColor: "#1E1E2E", |
|---|
| 118 | + }} |
|---|
| 119 | + > |
|---|
| 120 | + <Text style={{ color: "#9898B0", fontSize: 13 }}>Refresh</Text> |
|---|
| 121 | + </Pressable> |
|---|
| 122 | + </View> |
|---|
| 123 | + |
|---|
| 124 | + {/* Session list */} |
|---|
| 125 | + <ScrollView |
|---|
| 126 | + style={{ paddingHorizontal: 16 }} |
|---|
| 127 | + showsVerticalScrollIndicator={false} |
|---|
| 128 | + > |
|---|
| 129 | + {sessions.length === 0 ? ( |
|---|
| 130 | + <View style={{ alignItems: "center", paddingVertical: 32 }}> |
|---|
| 131 | + <Text style={{ color: "#5A5A78", fontSize: 15 }}> |
|---|
| 132 | + No sessions found |
|---|
| 133 | + </Text> |
|---|
| 134 | + </View> |
|---|
| 135 | + ) : ( |
|---|
| 136 | + sessions.map((session) => ( |
|---|
| 137 | + <View key={session.id} style={{ marginBottom: 8 }}> |
|---|
| 138 | + {editingId === session.id ? ( |
|---|
| 139 | + /* Rename mode */ |
|---|
| 140 | + <View |
|---|
| 141 | + style={{ |
|---|
| 142 | + backgroundColor: "#1E1E2E", |
|---|
| 143 | + borderRadius: 16, |
|---|
| 144 | + padding: 16, |
|---|
| 145 | + borderWidth: 2, |
|---|
| 146 | + borderColor: "#4A9EFF", |
|---|
| 147 | + }} |
|---|
| 148 | + > |
|---|
| 149 | + <TextInput |
|---|
| 150 | + value={editName} |
|---|
| 151 | + onChangeText={setEditName} |
|---|
| 152 | + autoFocus |
|---|
| 153 | + onSubmitEditing={handleConfirmRename} |
|---|
| 154 | + returnKeyType="done" |
|---|
| 155 | + style={{ |
|---|
| 156 | + color: "#E8E8F0", |
|---|
| 157 | + fontSize: 17, |
|---|
| 158 | + fontWeight: "600", |
|---|
| 159 | + padding: 0, |
|---|
| 160 | + marginBottom: 12, |
|---|
| 161 | + }} |
|---|
| 162 | + placeholderTextColor="#5A5A78" |
|---|
| 163 | + placeholder="Session name..." |
|---|
| 164 | + /> |
|---|
| 165 | + <View style={{ flexDirection: "row", gap: 8 }}> |
|---|
| 166 | + <Pressable |
|---|
| 167 | + onPress={handleConfirmRename} |
|---|
| 168 | + style={{ |
|---|
| 169 | + flex: 1, |
|---|
| 170 | + backgroundColor: "#4A9EFF", |
|---|
| 171 | + borderRadius: 10, |
|---|
| 172 | + paddingVertical: 10, |
|---|
| 173 | + alignItems: "center", |
|---|
| 174 | + }} |
|---|
| 175 | + > |
|---|
| 176 | + <Text style={{ color: "#FFF", fontSize: 15, fontWeight: "600" }}> |
|---|
| 177 | + Save |
|---|
| 178 | + </Text> |
|---|
| 179 | + </Pressable> |
|---|
| 180 | + <Pressable |
|---|
| 181 | + onPress={() => setEditingId(null)} |
|---|
| 182 | + style={{ |
|---|
| 183 | + flex: 1, |
|---|
| 184 | + backgroundColor: "#252538", |
|---|
| 185 | + borderRadius: 10, |
|---|
| 186 | + paddingVertical: 10, |
|---|
| 187 | + alignItems: "center", |
|---|
| 188 | + }} |
|---|
| 189 | + > |
|---|
| 190 | + <Text style={{ color: "#9898B0", fontSize: 15 }}>Cancel</Text> |
|---|
| 191 | + </Pressable> |
|---|
| 192 | + </View> |
|---|
| 193 | + </View> |
|---|
| 194 | + ) : ( |
|---|
| 195 | + /* Normal session row */ |
|---|
| 196 | + <Pressable |
|---|
| 197 | + onPress={() => handleSwitch(session)} |
|---|
| 198 | + onLongPress={() => handleStartRename(session)} |
|---|
| 199 | + style={({ pressed }) => ({ |
|---|
| 200 | + backgroundColor: pressed ? "#252538" : "#1E1E2E", |
|---|
| 201 | + borderRadius: 16, |
|---|
| 202 | + padding: 16, |
|---|
| 203 | + flexDirection: "row", |
|---|
| 204 | + alignItems: "center", |
|---|
| 205 | + borderWidth: session.isActive ? 2 : 1, |
|---|
| 206 | + borderColor: session.isActive ? "#4A9EFF" : "#2E2E45", |
|---|
| 207 | + })} |
|---|
| 208 | + > |
|---|
| 209 | + {/* Number badge */} |
|---|
| 210 | + <View |
|---|
| 211 | + style={{ |
|---|
| 212 | + width: 36, |
|---|
| 213 | + height: 36, |
|---|
| 214 | + borderRadius: 18, |
|---|
| 215 | + backgroundColor: session.isActive ? "#4A9EFF" : "#252538", |
|---|
| 216 | + alignItems: "center", |
|---|
| 217 | + justifyContent: "center", |
|---|
| 218 | + marginRight: 14, |
|---|
| 219 | + }} |
|---|
| 220 | + > |
|---|
| 221 | + <Text |
|---|
| 222 | + style={{ |
|---|
| 223 | + color: session.isActive ? "#FFF" : "#9898B0", |
|---|
| 224 | + fontSize: 16, |
|---|
| 225 | + fontWeight: "700", |
|---|
| 226 | + }} |
|---|
| 227 | + > |
|---|
| 228 | + {session.index} |
|---|
| 229 | + </Text> |
|---|
| 230 | + </View> |
|---|
| 231 | + |
|---|
| 232 | + {/* Session info */} |
|---|
| 233 | + <View style={{ flex: 1 }}> |
|---|
| 234 | + <Text |
|---|
| 235 | + style={{ |
|---|
| 236 | + color: "#E8E8F0", |
|---|
| 237 | + fontSize: 17, |
|---|
| 238 | + fontWeight: "600", |
|---|
| 239 | + }} |
|---|
| 240 | + numberOfLines={1} |
|---|
| 241 | + > |
|---|
| 242 | + {session.name} |
|---|
| 243 | + </Text> |
|---|
| 244 | + <Text |
|---|
| 245 | + style={{ |
|---|
| 246 | + color: "#5A5A78", |
|---|
| 247 | + fontSize: 12, |
|---|
| 248 | + marginTop: 2, |
|---|
| 249 | + }} |
|---|
| 250 | + > |
|---|
| 251 | + {session.type === "terminal" ? "Terminal" : "Claude"} |
|---|
| 252 | + {session.isActive ? " — active" : ""} |
|---|
| 253 | + </Text> |
|---|
| 254 | + </View> |
|---|
| 255 | + |
|---|
| 256 | + {/* Active indicator */} |
|---|
| 257 | + {session.isActive && ( |
|---|
| 258 | + <View |
|---|
| 259 | + style={{ |
|---|
| 260 | + width: 10, |
|---|
| 261 | + height: 10, |
|---|
| 262 | + borderRadius: 5, |
|---|
| 263 | + backgroundColor: "#2ED573", |
|---|
| 264 | + }} |
|---|
| 265 | + /> |
|---|
| 266 | + )} |
|---|
| 267 | + </Pressable> |
|---|
| 268 | + )} |
|---|
| 269 | + </View> |
|---|
| 270 | + )) |
|---|
| 271 | + )} |
|---|
| 272 | + |
|---|
| 273 | + {/* Hint */} |
|---|
| 274 | + <Text |
|---|
| 275 | + style={{ |
|---|
| 276 | + color: "#5A5A78", |
|---|
| 277 | + fontSize: 12, |
|---|
| 278 | + textAlign: "center", |
|---|
| 279 | + paddingVertical: 12, |
|---|
| 280 | + }} |
|---|
| 281 | + > |
|---|
| 282 | + Tap to switch — Long press to rename |
|---|
| 283 | + </Text> |
|---|
| 284 | + </ScrollView> |
|---|
| 285 | + </View> |
|---|
| 286 | + </View> |
|---|
| 287 | + </Modal> |
|---|
| 288 | + ); |
|---|
| 289 | +} |
|---|
| .. | .. |
|---|
| 1 | 1 | import React, { useState } from "react"; |
|---|
| 2 | | -import { Pressable, ScrollView, Text, View } from "react-native"; |
|---|
| 3 | | - |
|---|
| 4 | | -interface Command { |
|---|
| 5 | | - label: string; |
|---|
| 6 | | - value: string; |
|---|
| 7 | | -} |
|---|
| 8 | | - |
|---|
| 9 | | -const DEFAULT_COMMANDS: Command[] = [ |
|---|
| 10 | | - { label: "/s", value: "/s" }, |
|---|
| 11 | | - { label: "/ss", value: "/ss" }, |
|---|
| 12 | | - { label: "/clear", value: "/clear" }, |
|---|
| 13 | | - { label: "/help", value: "/help" }, |
|---|
| 14 | | - { label: "/status", value: "/status" }, |
|---|
| 15 | | -]; |
|---|
| 2 | +import { Pressable, Text, View, useWindowDimensions } from "react-native"; |
|---|
| 3 | +import * as Haptics from "expo-haptics"; |
|---|
| 16 | 4 | |
|---|
| 17 | 5 | interface CommandBarProps { |
|---|
| 18 | | - onCommand: (command: string) => void; |
|---|
| 19 | | - commands?: Command[]; |
|---|
| 6 | + onSessions: () => void; |
|---|
| 7 | + onScreenshot: () => void; |
|---|
| 8 | + onHelp: () => void; |
|---|
| 20 | 9 | } |
|---|
| 21 | 10 | |
|---|
| 22 | | -export function CommandBar({ |
|---|
| 23 | | - onCommand, |
|---|
| 24 | | - commands = DEFAULT_COMMANDS, |
|---|
| 25 | | -}: CommandBarProps) { |
|---|
| 26 | | - const [activeCommand, setActiveCommand] = useState<string | null>(null); |
|---|
| 11 | +export function CommandBar({ onSessions, onScreenshot, onHelp }: CommandBarProps) { |
|---|
| 12 | + return ( |
|---|
| 13 | + <View |
|---|
| 14 | + style={{ |
|---|
| 15 | + flexDirection: "row", |
|---|
| 16 | + paddingHorizontal: 12, |
|---|
| 17 | + paddingVertical: 6, |
|---|
| 18 | + gap: 8, |
|---|
| 19 | + }} |
|---|
| 20 | + > |
|---|
| 21 | + <CmdBtn icon="📋" label="Sessions" bg="#1A2744" border="#2E4A7A" onPress={onSessions} /> |
|---|
| 22 | + <CmdBtn icon="📸" label="Screen" bg="#1A3A2A" border="#2E6A4A" onPress={onScreenshot} /> |
|---|
| 23 | + <CmdBtn icon="❓" label="Help" bg="#3A1A2A" border="#6A2E4A" onPress={onHelp} /> |
|---|
| 24 | + </View> |
|---|
| 25 | + ); |
|---|
| 26 | +} |
|---|
| 27 | 27 | |
|---|
| 28 | | - function handlePress(command: Command) { |
|---|
| 29 | | - setActiveCommand(command.value); |
|---|
| 30 | | - onCommand(command.value); |
|---|
| 31 | | - setTimeout(() => setActiveCommand(null), 200); |
|---|
| 32 | | - } |
|---|
| 28 | +interface TextModeCommandBarProps { |
|---|
| 29 | + onSessions: () => void; |
|---|
| 30 | + onScreenshot: () => void; |
|---|
| 31 | + onNavigate: () => void; |
|---|
| 32 | + onClear: () => void; |
|---|
| 33 | +} |
|---|
| 34 | + |
|---|
| 35 | +export function TextModeCommandBar({ |
|---|
| 36 | + onSessions, |
|---|
| 37 | + onScreenshot, |
|---|
| 38 | + onNavigate, |
|---|
| 39 | + onClear, |
|---|
| 40 | +}: TextModeCommandBarProps) { |
|---|
| 41 | + return ( |
|---|
| 42 | + <View |
|---|
| 43 | + style={{ |
|---|
| 44 | + flexDirection: "row", |
|---|
| 45 | + paddingHorizontal: 12, |
|---|
| 46 | + paddingVertical: 6, |
|---|
| 47 | + gap: 8, |
|---|
| 48 | + }} |
|---|
| 49 | + > |
|---|
| 50 | + <CmdBtn icon="📋" label="Sessions" bg="#1A2744" border="#2E4A7A" onPress={onSessions} /> |
|---|
| 51 | + <CmdBtn icon="📸" label="Screen" bg="#1A3A2A" border="#2E6A4A" onPress={onScreenshot} /> |
|---|
| 52 | + <CmdBtn icon="🧭" label="Navigate" bg="#2A2A1A" border="#5A5A2E" onPress={onNavigate} /> |
|---|
| 53 | + <CmdBtn icon="🗑" label="Clear" bg="#3A1A1A" border="#6A2E2E" onPress={onClear} /> |
|---|
| 54 | + </View> |
|---|
| 55 | + ); |
|---|
| 56 | +} |
|---|
| 57 | + |
|---|
| 58 | +function CmdBtn({ |
|---|
| 59 | + icon, |
|---|
| 60 | + label, |
|---|
| 61 | + bg, |
|---|
| 62 | + border, |
|---|
| 63 | + onPress, |
|---|
| 64 | +}: { |
|---|
| 65 | + icon: string; |
|---|
| 66 | + label: string; |
|---|
| 67 | + bg: string; |
|---|
| 68 | + border: string; |
|---|
| 69 | + onPress: () => void; |
|---|
| 70 | +}) { |
|---|
| 71 | + const [pressed, setPressed] = useState(false); |
|---|
| 72 | + const { width } = useWindowDimensions(); |
|---|
| 33 | 73 | |
|---|
| 34 | 74 | return ( |
|---|
| 35 | | - <View className="border-t border-pai-border"> |
|---|
| 36 | | - <ScrollView |
|---|
| 37 | | - horizontal |
|---|
| 38 | | - showsHorizontalScrollIndicator={false} |
|---|
| 39 | | - contentContainerStyle={{ paddingHorizontal: 12, paddingVertical: 8, gap: 8 }} |
|---|
| 75 | + <View style={{ flex: 1 }}> |
|---|
| 76 | + <Pressable |
|---|
| 77 | + onPressIn={() => setPressed(true)} |
|---|
| 78 | + onPressOut={() => setPressed(false)} |
|---|
| 79 | + onPress={() => { |
|---|
| 80 | + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 81 | + onPress(); |
|---|
| 82 | + }} |
|---|
| 40 | 83 | > |
|---|
| 41 | | - {commands.map((cmd) => ( |
|---|
| 42 | | - <Pressable |
|---|
| 43 | | - key={cmd.value} |
|---|
| 44 | | - onPress={() => handlePress(cmd)} |
|---|
| 45 | | - className="rounded-full px-4 py-2" |
|---|
| 46 | | - style={({ pressed }) => ({ |
|---|
| 47 | | - backgroundColor: |
|---|
| 48 | | - activeCommand === cmd.value || pressed |
|---|
| 49 | | - ? "#4A9EFF" |
|---|
| 50 | | - : "#1E1E2E", |
|---|
| 51 | | - })} |
|---|
| 52 | | - > |
|---|
| 53 | | - <Text |
|---|
| 54 | | - className="text-sm font-medium" |
|---|
| 55 | | - style={{ |
|---|
| 56 | | - color: |
|---|
| 57 | | - activeCommand === cmd.value ? "#FFFFFF" : "#9898B0", |
|---|
| 58 | | - }} |
|---|
| 59 | | - > |
|---|
| 60 | | - {cmd.label} |
|---|
| 61 | | - </Text> |
|---|
| 62 | | - </Pressable> |
|---|
| 63 | | - ))} |
|---|
| 64 | | - </ScrollView> |
|---|
| 84 | + <View |
|---|
| 85 | + style={{ |
|---|
| 86 | + height: 68, |
|---|
| 87 | + borderRadius: 16, |
|---|
| 88 | + alignItems: "center", |
|---|
| 89 | + justifyContent: "center", |
|---|
| 90 | + backgroundColor: pressed ? "#4A9EFF" : bg, |
|---|
| 91 | + borderWidth: 1.5, |
|---|
| 92 | + borderColor: pressed ? "#4A9EFF" : border, |
|---|
| 93 | + }} |
|---|
| 94 | + > |
|---|
| 95 | + <Text style={{ fontSize: 26, marginBottom: 2 }}>{icon}</Text> |
|---|
| 96 | + <Text style={{ color: "#C8C8E0", fontSize: 13, fontWeight: "700" }}> |
|---|
| 97 | + {label} |
|---|
| 98 | + </Text> |
|---|
| 99 | + </View> |
|---|
| 100 | + </Pressable> |
|---|
| 65 | 101 | </View> |
|---|
| 66 | 102 | ); |
|---|
| 67 | 103 | } |
|---|
| .. | .. |
|---|
| 6 | 6 | TextInput, |
|---|
| 7 | 7 | View, |
|---|
| 8 | 8 | } from "react-native"; |
|---|
| 9 | +import * as Haptics from "expo-haptics"; |
|---|
| 9 | 10 | import { VoiceButton } from "./VoiceButton"; |
|---|
| 10 | 11 | |
|---|
| 11 | 12 | interface InputBarProps { |
|---|
| 12 | 13 | onSendText: (text: string) => void; |
|---|
| 13 | | - onSendVoice: (audioUri: string, durationMs: number) => void; |
|---|
| 14 | + onReplay: () => void; |
|---|
| 15 | + isTextMode: boolean; |
|---|
| 16 | + onToggleMode: () => void; |
|---|
| 14 | 17 | } |
|---|
| 15 | 18 | |
|---|
| 16 | | -export function InputBar({ onSendText, onSendVoice }: InputBarProps) { |
|---|
| 19 | +export function InputBar({ |
|---|
| 20 | + onSendText, |
|---|
| 21 | + onReplay, |
|---|
| 22 | + isTextMode, |
|---|
| 23 | + onToggleMode, |
|---|
| 24 | +}: InputBarProps) { |
|---|
| 17 | 25 | const [text, setText] = useState(""); |
|---|
| 18 | | - const [isVoiceMode, setIsVoiceMode] = useState(false); |
|---|
| 19 | 26 | const inputRef = useRef<TextInput>(null); |
|---|
| 20 | 27 | |
|---|
| 21 | 28 | const handleSend = useCallback(() => { |
|---|
| .. | .. |
|---|
| 25 | 32 | setText(""); |
|---|
| 26 | 33 | }, [text, onSendText]); |
|---|
| 27 | 34 | |
|---|
| 28 | | - const toggleMode = useCallback(() => { |
|---|
| 29 | | - setIsVoiceMode((prev) => { |
|---|
| 30 | | - if (prev) { |
|---|
| 31 | | - // Switching to text mode — focus input after mode switch |
|---|
| 32 | | - setTimeout(() => inputRef.current?.focus(), 100); |
|---|
| 33 | | - } else { |
|---|
| 34 | | - Keyboard.dismiss(); |
|---|
| 35 | | - } |
|---|
| 36 | | - return !prev; |
|---|
| 37 | | - }); |
|---|
| 38 | | - }, []); |
|---|
| 39 | | - |
|---|
| 40 | | - if (isVoiceMode) { |
|---|
| 35 | + if (!isTextMode) { |
|---|
| 36 | + // Voice mode: [Replay] [Talk] [Aa] |
|---|
| 41 | 37 | return ( |
|---|
| 42 | | - <View className="border-t border-pai-border bg-pai-bg"> |
|---|
| 43 | | - {/* Mode toggle */} |
|---|
| 38 | + <View |
|---|
| 39 | + style={{ |
|---|
| 40 | + flexDirection: "row", |
|---|
| 41 | + gap: 10, |
|---|
| 42 | + paddingHorizontal: 16, |
|---|
| 43 | + paddingVertical: 10, |
|---|
| 44 | + paddingBottom: 6, |
|---|
| 45 | + borderTopWidth: 1, |
|---|
| 46 | + borderTopColor: "#2E2E45", |
|---|
| 47 | + alignItems: "center", |
|---|
| 48 | + }} |
|---|
| 49 | + > |
|---|
| 50 | + {/* Replay last message */} |
|---|
| 44 | 51 | <Pressable |
|---|
| 45 | | - onPress={toggleMode} |
|---|
| 46 | | - className="absolute top-3 right-4 z-10 w-10 h-10 items-center justify-center" |
|---|
| 52 | + onPress={() => { |
|---|
| 53 | + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 54 | + onReplay(); |
|---|
| 55 | + }} |
|---|
| 47 | 56 | > |
|---|
| 48 | | - <Text className="text-2xl">⌨️</Text> |
|---|
| 57 | + <View |
|---|
| 58 | + style={{ |
|---|
| 59 | + width: 68, |
|---|
| 60 | + height: 68, |
|---|
| 61 | + borderRadius: 34, |
|---|
| 62 | + alignItems: "center", |
|---|
| 63 | + justifyContent: "center", |
|---|
| 64 | + backgroundColor: "#1A2E1A", |
|---|
| 65 | + borderWidth: 1.5, |
|---|
| 66 | + borderColor: "#3A6A3A", |
|---|
| 67 | + }} |
|---|
| 68 | + > |
|---|
| 69 | + <Text style={{ fontSize: 24 }}>▶</Text> |
|---|
| 70 | + <Text style={{ color: "#8ABF8A", fontSize: 10, marginTop: 1, fontWeight: "600" }}>Replay</Text> |
|---|
| 71 | + </View> |
|---|
| 49 | 72 | </Pressable> |
|---|
| 50 | 73 | |
|---|
| 51 | | - <VoiceButton onVoiceMessage={onSendVoice} /> |
|---|
| 74 | + {/* Talk button — center, biggest */} |
|---|
| 75 | + <View style={{ flex: 1, alignItems: "center" }}> |
|---|
| 76 | + <VoiceButton onTranscript={onSendText} /> |
|---|
| 77 | + </View> |
|---|
| 78 | + |
|---|
| 79 | + {/* Text mode toggle */} |
|---|
| 80 | + <Pressable |
|---|
| 81 | + onPress={() => { |
|---|
| 82 | + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 83 | + onToggleMode(); |
|---|
| 84 | + setTimeout(() => inputRef.current?.focus(), 150); |
|---|
| 85 | + }} |
|---|
| 86 | + > |
|---|
| 87 | + <View |
|---|
| 88 | + style={{ |
|---|
| 89 | + width: 68, |
|---|
| 90 | + height: 68, |
|---|
| 91 | + borderRadius: 34, |
|---|
| 92 | + alignItems: "center", |
|---|
| 93 | + justifyContent: "center", |
|---|
| 94 | + backgroundColor: "#1A1A3E", |
|---|
| 95 | + borderWidth: 1.5, |
|---|
| 96 | + borderColor: "#3A3A7A", |
|---|
| 97 | + }} |
|---|
| 98 | + > |
|---|
| 99 | + <Text style={{ fontSize: 22, color: "#9898D0", fontWeight: "700" }}>Aa</Text> |
|---|
| 100 | + </View> |
|---|
| 101 | + </Pressable> |
|---|
| 52 | 102 | </View> |
|---|
| 53 | 103 | ); |
|---|
| 54 | 104 | } |
|---|
| 55 | 105 | |
|---|
| 106 | + // Text mode: [Mic] [TextInput] [Send] |
|---|
| 56 | 107 | return ( |
|---|
| 57 | | - <View className="border-t border-pai-border bg-pai-bg px-3 py-2 flex-row items-end gap-2"> |
|---|
| 108 | + <View |
|---|
| 109 | + style={{ |
|---|
| 110 | + flexDirection: "row", |
|---|
| 111 | + gap: 8, |
|---|
| 112 | + paddingHorizontal: 12, |
|---|
| 113 | + paddingVertical: 8, |
|---|
| 114 | + borderTopWidth: 1, |
|---|
| 115 | + borderTopColor: "#2E2E45", |
|---|
| 116 | + alignItems: "flex-end", |
|---|
| 117 | + }} |
|---|
| 118 | + > |
|---|
| 58 | 119 | {/* Voice mode toggle */} |
|---|
| 59 | 120 | <Pressable |
|---|
| 60 | | - onPress={toggleMode} |
|---|
| 61 | | - className="w-10 h-10 items-center justify-center rounded-full bg-pai-bg-tertiary mb-0.5" |
|---|
| 121 | + onPress={() => { |
|---|
| 122 | + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 123 | + Keyboard.dismiss(); |
|---|
| 124 | + onToggleMode(); |
|---|
| 125 | + }} |
|---|
| 126 | + style={{ |
|---|
| 127 | + width: 40, |
|---|
| 128 | + height: 40, |
|---|
| 129 | + borderRadius: 20, |
|---|
| 130 | + alignItems: "center", |
|---|
| 131 | + justifyContent: "center", |
|---|
| 132 | + backgroundColor: "#1E1E2E", |
|---|
| 133 | + marginBottom: 2, |
|---|
| 134 | + }} |
|---|
| 62 | 135 | > |
|---|
| 63 | | - <Text className="text-xl">🎤</Text> |
|---|
| 136 | + <Text style={{ fontSize: 20 }}>🎤</Text> |
|---|
| 64 | 137 | </Pressable> |
|---|
| 65 | 138 | |
|---|
| 66 | 139 | {/* Text input */} |
|---|
| .. | .. |
|---|
| 75 | 148 | onSubmitEditing={handleSend} |
|---|
| 76 | 149 | returnKeyType="send" |
|---|
| 77 | 150 | blurOnSubmit |
|---|
| 78 | | - className="flex-1 bg-pai-bg-tertiary rounded-2xl px-4 py-2.5 text-pai-text text-base" |
|---|
| 79 | | - style={{ maxHeight: 120 }} |
|---|
| 151 | + style={{ |
|---|
| 152 | + flex: 1, |
|---|
| 153 | + backgroundColor: "#1E1E2E", |
|---|
| 154 | + borderRadius: 20, |
|---|
| 155 | + paddingHorizontal: 16, |
|---|
| 156 | + paddingVertical: 10, |
|---|
| 157 | + maxHeight: 120, |
|---|
| 158 | + color: "#E8E8F0", |
|---|
| 159 | + fontSize: 16, |
|---|
| 160 | + }} |
|---|
| 80 | 161 | /> |
|---|
| 81 | 162 | |
|---|
| 82 | 163 | {/* Send button */} |
|---|
| 83 | 164 | <Pressable |
|---|
| 84 | 165 | onPress={handleSend} |
|---|
| 85 | 166 | disabled={!text.trim()} |
|---|
| 86 | | - className={`w-10 h-10 rounded-full items-center justify-center mb-0.5 ${ |
|---|
| 87 | | - text.trim() ? "bg-pai-accent" : "bg-pai-bg-tertiary" |
|---|
| 88 | | - }`} |
|---|
| 167 | + style={{ |
|---|
| 168 | + width: 40, |
|---|
| 169 | + height: 40, |
|---|
| 170 | + borderRadius: 20, |
|---|
| 171 | + alignItems: "center", |
|---|
| 172 | + justifyContent: "center", |
|---|
| 173 | + marginBottom: 2, |
|---|
| 174 | + backgroundColor: text.trim() ? "#4A9EFF" : "#1E1E2E", |
|---|
| 175 | + }} |
|---|
| 89 | 176 | > |
|---|
| 90 | | - <Text className={`text-xl ${text.trim() ? "text-white" : "text-pai-text-muted"}`}> |
|---|
| 177 | + <Text |
|---|
| 178 | + style={{ |
|---|
| 179 | + fontSize: 18, |
|---|
| 180 | + fontWeight: "bold", |
|---|
| 181 | + color: text.trim() ? "#FFFFFF" : "#5A5A78", |
|---|
| 182 | + }} |
|---|
| 183 | + > |
|---|
| 91 | 184 | ↑ |
|---|
| 92 | 185 | </Text> |
|---|
| 93 | 186 | </Pressable> |
|---|
| .. | .. |
|---|
| 1 | 1 | import React, { useCallback, useState } from "react"; |
|---|
| 2 | | -import { Pressable, Text, View } from "react-native"; |
|---|
| 2 | +import { Image, Pressable, Text, View } from "react-native"; |
|---|
| 3 | 3 | import { Message } from "../../types"; |
|---|
| 4 | 4 | import { playAudio, stopPlayback } from "../../services/audio"; |
|---|
| 5 | 5 | |
|---|
| .. | .. |
|---|
| 57 | 57 | : "bg-pai-surface rounded-tl-sm" |
|---|
| 58 | 58 | }`} |
|---|
| 59 | 59 | > |
|---|
| 60 | | - {message.type === "voice" ? ( |
|---|
| 60 | + {message.type === "image" && message.imageBase64 ? ( |
|---|
| 61 | + /* Image message */ |
|---|
| 62 | + <View> |
|---|
| 63 | + <Image |
|---|
| 64 | + source={{ uri: `data:image/png;base64,${message.imageBase64}` }} |
|---|
| 65 | + style={{ |
|---|
| 66 | + width: 260, |
|---|
| 67 | + height: 180, |
|---|
| 68 | + borderRadius: 10, |
|---|
| 69 | + backgroundColor: "#14141F", |
|---|
| 70 | + }} |
|---|
| 71 | + resizeMode="contain" |
|---|
| 72 | + /> |
|---|
| 73 | + {message.content ? ( |
|---|
| 74 | + <Text |
|---|
| 75 | + style={{ |
|---|
| 76 | + color: isUser ? "#FFF" : "#9898B0", |
|---|
| 77 | + fontSize: 12, |
|---|
| 78 | + marginTop: 4, |
|---|
| 79 | + }} |
|---|
| 80 | + > |
|---|
| 81 | + {message.content} |
|---|
| 82 | + </Text> |
|---|
| 83 | + ) : null} |
|---|
| 84 | + </View> |
|---|
| 85 | + ) : message.type === "voice" ? ( |
|---|
| 61 | 86 | <Pressable |
|---|
| 62 | 87 | onPress={handleVoicePress} |
|---|
| 63 | 88 | className="flex-row items-center gap-3" |
|---|
| .. | .. |
|---|
| 1 | | -import React, { useCallback, useRef, useState } from "react"; |
|---|
| 1 | +import React, { useCallback, useEffect, useRef, useState } from "react"; |
|---|
| 2 | 2 | import { Animated, Pressable, Text, View } from "react-native"; |
|---|
| 3 | 3 | import * as Haptics from "expo-haptics"; |
|---|
| 4 | | -import { startRecording, stopRecording } from "../../services/audio"; |
|---|
| 5 | | -import { Audio } from "expo-av"; |
|---|
| 4 | +import { |
|---|
| 5 | + ExpoSpeechRecognitionModule, |
|---|
| 6 | + useSpeechRecognitionEvent, |
|---|
| 7 | +} from "expo-speech-recognition"; |
|---|
| 6 | 8 | |
|---|
| 7 | 9 | interface VoiceButtonProps { |
|---|
| 8 | | - onVoiceMessage: (audioUri: string, durationMs: number) => void; |
|---|
| 10 | + onTranscript: (text: string) => void; |
|---|
| 9 | 11 | } |
|---|
| 10 | 12 | |
|---|
| 11 | | -const VOICE_BUTTON_SIZE = 88; |
|---|
| 13 | +const VOICE_BUTTON_SIZE = 72; |
|---|
| 12 | 14 | |
|---|
| 13 | | -export function VoiceButton({ onVoiceMessage }: VoiceButtonProps) { |
|---|
| 14 | | - const [isRecording, setIsRecording] = useState(false); |
|---|
| 15 | | - const recordingRef = useRef<Audio.Recording | null>(null); |
|---|
| 16 | | - const scaleAnim = useRef(new Animated.Value(1)).current; |
|---|
| 15 | +/** |
|---|
| 16 | + * Tap-to-toggle voice button using on-device speech recognition. |
|---|
| 17 | + * - Tap once: start listening |
|---|
| 18 | + * - Tap again: stop and send transcript |
|---|
| 19 | + * - Long-press while listening: cancel (discard) |
|---|
| 20 | + */ |
|---|
| 21 | +export function VoiceButton({ onTranscript }: VoiceButtonProps) { |
|---|
| 22 | + const [isListening, setIsListening] = useState(false); |
|---|
| 23 | + const [transcript, setTranscript] = useState(""); |
|---|
| 17 | 24 | const pulseAnim = useRef(new Animated.Value(1)).current; |
|---|
| 25 | + const glowAnim = useRef(new Animated.Value(0)).current; |
|---|
| 18 | 26 | const pulseLoop = useRef<Animated.CompositeAnimation | null>(null); |
|---|
| 27 | + const cancelledRef = useRef(false); |
|---|
| 28 | + |
|---|
| 29 | + // Speech recognition events |
|---|
| 30 | + useSpeechRecognitionEvent("start", () => { |
|---|
| 31 | + setIsListening(true); |
|---|
| 32 | + }); |
|---|
| 33 | + |
|---|
| 34 | + useSpeechRecognitionEvent("end", () => { |
|---|
| 35 | + setIsListening(false); |
|---|
| 36 | + stopPulse(); |
|---|
| 37 | + |
|---|
| 38 | + // Send transcript if we have one and weren't cancelled |
|---|
| 39 | + if (!cancelledRef.current && transcript.trim()) { |
|---|
| 40 | + onTranscript(transcript.trim()); |
|---|
| 41 | + } |
|---|
| 42 | + setTranscript(""); |
|---|
| 43 | + cancelledRef.current = false; |
|---|
| 44 | + }); |
|---|
| 45 | + |
|---|
| 46 | + useSpeechRecognitionEvent("result", (event) => { |
|---|
| 47 | + const text = event.results[0]?.transcript ?? ""; |
|---|
| 48 | + setTranscript(text); |
|---|
| 49 | + }); |
|---|
| 50 | + |
|---|
| 51 | + useSpeechRecognitionEvent("error", (event) => { |
|---|
| 52 | + console.error("Speech recognition error:", event.error, event.message); |
|---|
| 53 | + setIsListening(false); |
|---|
| 54 | + stopPulse(); |
|---|
| 55 | + setTranscript(""); |
|---|
| 56 | + }); |
|---|
| 19 | 57 | |
|---|
| 20 | 58 | const startPulse = useCallback(() => { |
|---|
| 21 | 59 | pulseLoop.current = Animated.loop( |
|---|
| 22 | 60 | Animated.sequence([ |
|---|
| 23 | 61 | Animated.timing(pulseAnim, { |
|---|
| 24 | 62 | toValue: 1.15, |
|---|
| 25 | | - duration: 600, |
|---|
| 63 | + duration: 700, |
|---|
| 26 | 64 | useNativeDriver: true, |
|---|
| 27 | 65 | }), |
|---|
| 28 | 66 | Animated.timing(pulseAnim, { |
|---|
| 29 | 67 | toValue: 1, |
|---|
| 30 | | - duration: 600, |
|---|
| 68 | + duration: 700, |
|---|
| 31 | 69 | useNativeDriver: true, |
|---|
| 32 | 70 | }), |
|---|
| 33 | 71 | ]) |
|---|
| 34 | 72 | ); |
|---|
| 35 | 73 | pulseLoop.current.start(); |
|---|
| 36 | | - }, [pulseAnim]); |
|---|
| 74 | + Animated.timing(glowAnim, { |
|---|
| 75 | + toValue: 1, |
|---|
| 76 | + duration: 300, |
|---|
| 77 | + useNativeDriver: true, |
|---|
| 78 | + }).start(); |
|---|
| 79 | + }, [pulseAnim, glowAnim]); |
|---|
| 37 | 80 | |
|---|
| 38 | 81 | const stopPulse = useCallback(() => { |
|---|
| 39 | 82 | pulseLoop.current?.stop(); |
|---|
| 40 | 83 | pulseAnim.setValue(1); |
|---|
| 41 | | - }, [pulseAnim]); |
|---|
| 42 | | - |
|---|
| 43 | | - const handlePressIn = useCallback(async () => { |
|---|
| 44 | | - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); |
|---|
| 45 | | - |
|---|
| 46 | | - Animated.spring(scaleAnim, { |
|---|
| 47 | | - toValue: 0.92, |
|---|
| 84 | + Animated.timing(glowAnim, { |
|---|
| 85 | + toValue: 0, |
|---|
| 86 | + duration: 200, |
|---|
| 48 | 87 | useNativeDriver: true, |
|---|
| 49 | 88 | }).start(); |
|---|
| 89 | + }, [pulseAnim, glowAnim]); |
|---|
| 50 | 90 | |
|---|
| 51 | | - const recording = await startRecording(); |
|---|
| 52 | | - if (recording) { |
|---|
| 53 | | - recordingRef.current = recording; |
|---|
| 54 | | - setIsRecording(true); |
|---|
| 55 | | - startPulse(); |
|---|
| 56 | | - } |
|---|
| 57 | | - }, [scaleAnim, startPulse]); |
|---|
| 91 | + const startListening = useCallback(async () => { |
|---|
| 92 | + const result = await ExpoSpeechRecognitionModule.requestPermissionsAsync(); |
|---|
| 93 | + if (!result.granted) return; |
|---|
| 58 | 94 | |
|---|
| 59 | | - const handlePressOut = useCallback(async () => { |
|---|
| 60 | | - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 95 | + cancelledRef.current = false; |
|---|
| 96 | + setTranscript(""); |
|---|
| 97 | + startPulse(); |
|---|
| 61 | 98 | |
|---|
| 62 | | - Animated.spring(scaleAnim, { |
|---|
| 63 | | - toValue: 1, |
|---|
| 64 | | - useNativeDriver: true, |
|---|
| 65 | | - }).start(); |
|---|
| 99 | + ExpoSpeechRecognitionModule.start({ |
|---|
| 100 | + lang: "en-US", |
|---|
| 101 | + interimResults: true, |
|---|
| 102 | + continuous: true, |
|---|
| 103 | + }); |
|---|
| 104 | + }, [startPulse]); |
|---|
| 66 | 105 | |
|---|
| 106 | + const stopAndSend = useCallback(() => { |
|---|
| 67 | 107 | stopPulse(); |
|---|
| 68 | | - setIsRecording(false); |
|---|
| 108 | + cancelledRef.current = false; |
|---|
| 109 | + ExpoSpeechRecognitionModule.stop(); |
|---|
| 110 | + }, [stopPulse]); |
|---|
| 69 | 111 | |
|---|
| 70 | | - if (recordingRef.current) { |
|---|
| 71 | | - const result = await stopRecording(); |
|---|
| 72 | | - recordingRef.current = null; |
|---|
| 112 | + const cancelListening = useCallback(() => { |
|---|
| 113 | + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); |
|---|
| 114 | + stopPulse(); |
|---|
| 115 | + cancelledRef.current = true; |
|---|
| 116 | + setTranscript(""); |
|---|
| 117 | + ExpoSpeechRecognitionModule.abort(); |
|---|
| 118 | + }, [stopPulse]); |
|---|
| 73 | 119 | |
|---|
| 74 | | - if (result && result.durationMs > 500) { |
|---|
| 75 | | - onVoiceMessage(result.uri, result.durationMs); |
|---|
| 76 | | - } |
|---|
| 120 | + const handleTap = useCallback(async () => { |
|---|
| 121 | + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); |
|---|
| 122 | + if (isListening) { |
|---|
| 123 | + stopAndSend(); |
|---|
| 124 | + } else { |
|---|
| 125 | + await startListening(); |
|---|
| 77 | 126 | } |
|---|
| 78 | | - }, [scaleAnim, stopPulse, onVoiceMessage]); |
|---|
| 127 | + }, [isListening, stopAndSend, startListening]); |
|---|
| 128 | + |
|---|
| 129 | + const handleLongPress = useCallback(() => { |
|---|
| 130 | + if (isListening) { |
|---|
| 131 | + cancelListening(); |
|---|
| 132 | + } |
|---|
| 133 | + }, [isListening, cancelListening]); |
|---|
| 79 | 134 | |
|---|
| 80 | 135 | return ( |
|---|
| 81 | | - <View className="items-center justify-center py-4"> |
|---|
| 82 | | - {/* Pulse ring — only visible while recording */} |
|---|
| 136 | + <View style={{ alignItems: "center", justifyContent: "center" }}> |
|---|
| 137 | + {/* Outer pulse ring */} |
|---|
| 83 | 138 | <Animated.View |
|---|
| 84 | 139 | style={{ |
|---|
| 85 | 140 | position: "absolute", |
|---|
| 86 | 141 | width: VOICE_BUTTON_SIZE + 24, |
|---|
| 87 | 142 | height: VOICE_BUTTON_SIZE + 24, |
|---|
| 88 | 143 | borderRadius: (VOICE_BUTTON_SIZE + 24) / 2, |
|---|
| 89 | | - backgroundColor: isRecording ? "rgba(255, 159, 67, 0.15)" : "transparent", |
|---|
| 144 | + backgroundColor: isListening ? "rgba(255, 159, 67, 0.12)" : "transparent", |
|---|
| 90 | 145 | transform: [{ scale: pulseAnim }], |
|---|
| 146 | + opacity: glowAnim, |
|---|
| 91 | 147 | }} |
|---|
| 92 | 148 | /> |
|---|
| 93 | 149 | |
|---|
| 94 | 150 | {/* Button */} |
|---|
| 95 | | - <Animated.View style={{ transform: [{ scale: scaleAnim }] }}> |
|---|
| 96 | | - <Pressable |
|---|
| 97 | | - onPressIn={handlePressIn} |
|---|
| 98 | | - onPressOut={handlePressOut} |
|---|
| 151 | + <Pressable |
|---|
| 152 | + onPress={handleTap} |
|---|
| 153 | + onLongPress={handleLongPress} |
|---|
| 154 | + delayLongPress={600} |
|---|
| 155 | + > |
|---|
| 156 | + <View |
|---|
| 99 | 157 | style={{ |
|---|
| 100 | 158 | width: VOICE_BUTTON_SIZE, |
|---|
| 101 | 159 | height: VOICE_BUTTON_SIZE, |
|---|
| 102 | 160 | borderRadius: VOICE_BUTTON_SIZE / 2, |
|---|
| 103 | | - backgroundColor: isRecording ? "#FF9F43" : "#4A9EFF", |
|---|
| 161 | + backgroundColor: isListening ? "#FF9F43" : "#4A9EFF", |
|---|
| 104 | 162 | alignItems: "center", |
|---|
| 105 | 163 | justifyContent: "center", |
|---|
| 106 | | - shadowColor: isRecording ? "#FF9F43" : "#4A9EFF", |
|---|
| 164 | + shadowColor: isListening ? "#FF9F43" : "#4A9EFF", |
|---|
| 107 | 165 | shadowOffset: { width: 0, height: 4 }, |
|---|
| 108 | 166 | shadowOpacity: 0.4, |
|---|
| 109 | 167 | shadowRadius: 12, |
|---|
| 110 | 168 | elevation: 8, |
|---|
| 111 | 169 | }} |
|---|
| 112 | 170 | > |
|---|
| 113 | | - <Text style={{ fontSize: 32 }}>{isRecording ? "🎙" : "🎤"}</Text> |
|---|
| 114 | | - </Pressable> |
|---|
| 115 | | - </Animated.View> |
|---|
| 171 | + <Text style={{ fontSize: 28 }}>{isListening ? "⏹" : "🎤"}</Text> |
|---|
| 172 | + </View> |
|---|
| 173 | + </Pressable> |
|---|
| 116 | 174 | |
|---|
| 117 | | - <Text className="text-pai-text-muted text-xs mt-3"> |
|---|
| 118 | | - {isRecording ? "Release to send" : "Hold to talk"} |
|---|
| 175 | + {/* Label / transcript preview */} |
|---|
| 176 | + <Text |
|---|
| 177 | + style={{ |
|---|
| 178 | + color: isListening ? "#FF9F43" : "#5A5A78", |
|---|
| 179 | + fontSize: 11, |
|---|
| 180 | + marginTop: 4, |
|---|
| 181 | + fontWeight: isListening ? "600" : "400", |
|---|
| 182 | + maxWidth: 200, |
|---|
| 183 | + textAlign: "center", |
|---|
| 184 | + }} |
|---|
| 185 | + numberOfLines={2} |
|---|
| 186 | + > |
|---|
| 187 | + {isListening |
|---|
| 188 | + ? transcript || "Listening..." |
|---|
| 189 | + : "Tap to talk"} |
|---|
| 119 | 190 | </Text> |
|---|
| 120 | 191 | </View> |
|---|
| 121 | 192 | ); |
|---|
| .. | .. |
|---|
| 6 | 6 | useRef, |
|---|
| 7 | 7 | useState, |
|---|
| 8 | 8 | } from "react"; |
|---|
| 9 | | -import { Message, WebSocketMessage } from "../types"; |
|---|
| 9 | +import { Message, WsIncoming, WsSession } from "../types"; |
|---|
| 10 | 10 | import { useConnection } from "./ConnectionContext"; |
|---|
| 11 | | -import { playAudio } from "../services/audio"; |
|---|
| 11 | +import { playAudio, encodeAudioToBase64 } from "../services/audio"; |
|---|
| 12 | 12 | |
|---|
| 13 | 13 | function generateId(): string { |
|---|
| 14 | 14 | return Date.now().toString(36) + Math.random().toString(36).slice(2); |
|---|
| .. | .. |
|---|
| 19 | 19 | sendTextMessage: (text: string) => void; |
|---|
| 20 | 20 | sendVoiceMessage: (audioUri: string, durationMs?: number) => void; |
|---|
| 21 | 21 | clearMessages: () => void; |
|---|
| 22 | + // Session management |
|---|
| 23 | + sessions: WsSession[]; |
|---|
| 24 | + requestSessions: () => void; |
|---|
| 25 | + switchSession: (sessionId: string) => void; |
|---|
| 26 | + renameSession: (sessionId: string, name: string) => void; |
|---|
| 27 | + // Screenshot / navigation |
|---|
| 28 | + latestScreenshot: string | null; |
|---|
| 29 | + requestScreenshot: () => void; |
|---|
| 30 | + sendNavKey: (key: string) => void; |
|---|
| 22 | 31 | } |
|---|
| 23 | 32 | |
|---|
| 24 | 33 | const ChatContext = createContext<ChatContextValue | null>(null); |
|---|
| 25 | 34 | |
|---|
| 26 | 35 | export function ChatProvider({ children }: { children: React.ReactNode }) { |
|---|
| 27 | 36 | const [messages, setMessages] = useState<Message[]>([]); |
|---|
| 28 | | - const { sendTextMessage: wsSend, sendVoiceMessage: wsVoice, onMessageReceived } = useConnection(); |
|---|
| 37 | + const [sessions, setSessions] = useState<WsSession[]>([]); |
|---|
| 38 | + const [latestScreenshot, setLatestScreenshot] = useState<string | null>(null); |
|---|
| 39 | + const { |
|---|
| 40 | + sendTextMessage: wsSend, |
|---|
| 41 | + sendVoiceMessage: wsVoice, |
|---|
| 42 | + sendCommand, |
|---|
| 43 | + onMessageReceived, |
|---|
| 44 | + } = useConnection(); |
|---|
| 29 | 45 | |
|---|
| 30 | 46 | const addMessage = useCallback((msg: Message) => { |
|---|
| 31 | 47 | setMessages((prev) => [...prev, msg]); |
|---|
| .. | .. |
|---|
| 42 | 58 | |
|---|
| 43 | 59 | // Handle incoming WebSocket messages |
|---|
| 44 | 60 | useEffect(() => { |
|---|
| 45 | | - onMessageReceived.current = (data: WebSocketMessage) => { |
|---|
| 46 | | - if (data.type === "text") { |
|---|
| 47 | | - const msg: Message = { |
|---|
| 48 | | - id: generateId(), |
|---|
| 49 | | - role: "assistant", |
|---|
| 50 | | - type: "text", |
|---|
| 51 | | - content: data.content, |
|---|
| 52 | | - timestamp: Date.now(), |
|---|
| 53 | | - status: "sent", |
|---|
| 54 | | - }; |
|---|
| 55 | | - setMessages((prev) => [...prev, msg]); |
|---|
| 56 | | - } else if (data.type === "voice") { |
|---|
| 57 | | - const msg: Message = { |
|---|
| 58 | | - id: generateId(), |
|---|
| 59 | | - role: "assistant", |
|---|
| 60 | | - type: "voice", |
|---|
| 61 | | - content: data.content ?? "", |
|---|
| 62 | | - audioUri: data.audioBase64 |
|---|
| 63 | | - ? `data:audio/mp4;base64,${data.audioBase64}` |
|---|
| 64 | | - : undefined, |
|---|
| 65 | | - timestamp: Date.now(), |
|---|
| 66 | | - status: "sent", |
|---|
| 67 | | - }; |
|---|
| 68 | | - setMessages((prev) => [...prev, msg]); |
|---|
| 69 | | - |
|---|
| 70 | | - // Auto-play incoming voice messages |
|---|
| 71 | | - if (msg.audioUri) { |
|---|
| 72 | | - playAudio(msg.audioUri).catch(() => {}); |
|---|
| 61 | + onMessageReceived.current = (data: WsIncoming) => { |
|---|
| 62 | + switch (data.type) { |
|---|
| 63 | + case "text": { |
|---|
| 64 | + const msg: Message = { |
|---|
| 65 | + id: generateId(), |
|---|
| 66 | + role: "assistant", |
|---|
| 67 | + type: "text", |
|---|
| 68 | + content: data.content, |
|---|
| 69 | + timestamp: Date.now(), |
|---|
| 70 | + status: "sent", |
|---|
| 71 | + }; |
|---|
| 72 | + setMessages((prev) => [...prev, msg]); |
|---|
| 73 | + break; |
|---|
| 74 | + } |
|---|
| 75 | + case "voice": { |
|---|
| 76 | + const msg: Message = { |
|---|
| 77 | + id: generateId(), |
|---|
| 78 | + role: "assistant", |
|---|
| 79 | + type: "voice", |
|---|
| 80 | + content: data.content ?? "", |
|---|
| 81 | + audioUri: data.audioBase64 |
|---|
| 82 | + ? `data:audio/mp4;base64,${data.audioBase64}` |
|---|
| 83 | + : undefined, |
|---|
| 84 | + timestamp: Date.now(), |
|---|
| 85 | + status: "sent", |
|---|
| 86 | + }; |
|---|
| 87 | + setMessages((prev) => [...prev, msg]); |
|---|
| 88 | + if (msg.audioUri) { |
|---|
| 89 | + playAudio(msg.audioUri).catch(() => {}); |
|---|
| 90 | + } |
|---|
| 91 | + break; |
|---|
| 92 | + } |
|---|
| 93 | + case "image": { |
|---|
| 94 | + // Store as latest screenshot for navigation mode |
|---|
| 95 | + setLatestScreenshot(data.imageBase64); |
|---|
| 96 | + // Also add to chat as an image message |
|---|
| 97 | + const msg: Message = { |
|---|
| 98 | + id: generateId(), |
|---|
| 99 | + role: "assistant", |
|---|
| 100 | + type: "image", |
|---|
| 101 | + content: data.caption ?? "Screenshot", |
|---|
| 102 | + imageBase64: data.imageBase64, |
|---|
| 103 | + timestamp: Date.now(), |
|---|
| 104 | + status: "sent", |
|---|
| 105 | + }; |
|---|
| 106 | + setMessages((prev) => [...prev, msg]); |
|---|
| 107 | + break; |
|---|
| 108 | + } |
|---|
| 109 | + case "sessions": { |
|---|
| 110 | + setSessions(data.sessions); |
|---|
| 111 | + break; |
|---|
| 112 | + } |
|---|
| 113 | + case "session_switched": { |
|---|
| 114 | + const msg: Message = { |
|---|
| 115 | + id: generateId(), |
|---|
| 116 | + role: "system", |
|---|
| 117 | + type: "text", |
|---|
| 118 | + content: `Switched to ${data.name}`, |
|---|
| 119 | + timestamp: Date.now(), |
|---|
| 120 | + }; |
|---|
| 121 | + setMessages((prev) => [...prev, msg]); |
|---|
| 122 | + break; |
|---|
| 123 | + } |
|---|
| 124 | + case "session_renamed": { |
|---|
| 125 | + const msg: Message = { |
|---|
| 126 | + id: generateId(), |
|---|
| 127 | + role: "system", |
|---|
| 128 | + type: "text", |
|---|
| 129 | + content: `Renamed to ${data.name}`, |
|---|
| 130 | + timestamp: Date.now(), |
|---|
| 131 | + }; |
|---|
| 132 | + setMessages((prev) => [...prev, msg]); |
|---|
| 133 | + // Refresh sessions to show updated name |
|---|
| 134 | + sendCommand("sessions"); |
|---|
| 135 | + break; |
|---|
| 136 | + } |
|---|
| 137 | + case "error": { |
|---|
| 138 | + const msg: Message = { |
|---|
| 139 | + id: generateId(), |
|---|
| 140 | + role: "system", |
|---|
| 141 | + type: "text", |
|---|
| 142 | + content: data.message, |
|---|
| 143 | + timestamp: Date.now(), |
|---|
| 144 | + }; |
|---|
| 145 | + setMessages((prev) => [...prev, msg]); |
|---|
| 146 | + break; |
|---|
| 73 | 147 | } |
|---|
| 74 | 148 | } |
|---|
| 75 | 149 | }; |
|---|
| .. | .. |
|---|
| 77 | 151 | return () => { |
|---|
| 78 | 152 | onMessageReceived.current = null; |
|---|
| 79 | 153 | }; |
|---|
| 80 | | - }, [onMessageReceived]); |
|---|
| 154 | + }, [onMessageReceived, sendCommand]); |
|---|
| 81 | 155 | |
|---|
| 82 | 156 | const sendTextMessage = useCallback( |
|---|
| 83 | 157 | (text: string) => { |
|---|
| .. | .. |
|---|
| 91 | 165 | status: "sending", |
|---|
| 92 | 166 | }; |
|---|
| 93 | 167 | addMessage(msg); |
|---|
| 94 | | - |
|---|
| 95 | 168 | const sent = wsSend(text); |
|---|
| 96 | 169 | updateMessageStatus(id, sent ? "sent" : "error"); |
|---|
| 97 | 170 | }, |
|---|
| .. | .. |
|---|
| 99 | 172 | ); |
|---|
| 100 | 173 | |
|---|
| 101 | 174 | const sendVoiceMessage = useCallback( |
|---|
| 102 | | - (audioUri: string, durationMs?: number) => { |
|---|
| 175 | + async (audioUri: string, durationMs?: number) => { |
|---|
| 103 | 176 | const id = generateId(); |
|---|
| 104 | 177 | const msg: Message = { |
|---|
| 105 | 178 | id, |
|---|
| .. | .. |
|---|
| 112 | 185 | duration: durationMs, |
|---|
| 113 | 186 | }; |
|---|
| 114 | 187 | addMessage(msg); |
|---|
| 115 | | - |
|---|
| 116 | | - // For now, send with empty base64 since we'd need expo-file-system to encode |
|---|
| 117 | | - const sent = wsVoice("", "Voice message"); |
|---|
| 118 | | - updateMessageStatus(id, sent ? "sent" : "error"); |
|---|
| 188 | + try { |
|---|
| 189 | + const base64 = await encodeAudioToBase64(audioUri); |
|---|
| 190 | + const sent = wsVoice(base64); |
|---|
| 191 | + updateMessageStatus(id, sent ? "sent" : "error"); |
|---|
| 192 | + } catch (err) { |
|---|
| 193 | + console.error("Failed to encode audio:", err); |
|---|
| 194 | + updateMessageStatus(id, "error"); |
|---|
| 195 | + } |
|---|
| 119 | 196 | }, |
|---|
| 120 | 197 | [wsVoice, addMessage, updateMessageStatus] |
|---|
| 121 | 198 | ); |
|---|
| .. | .. |
|---|
| 124 | 201 | setMessages([]); |
|---|
| 125 | 202 | }, []); |
|---|
| 126 | 203 | |
|---|
| 204 | + // --- Session management --- |
|---|
| 205 | + const requestSessions = useCallback(() => { |
|---|
| 206 | + sendCommand("sessions"); |
|---|
| 207 | + }, [sendCommand]); |
|---|
| 208 | + |
|---|
| 209 | + const switchSession = useCallback( |
|---|
| 210 | + (sessionId: string) => { |
|---|
| 211 | + sendCommand("switch", { sessionId }); |
|---|
| 212 | + }, |
|---|
| 213 | + [sendCommand] |
|---|
| 214 | + ); |
|---|
| 215 | + |
|---|
| 216 | + const renameSession = useCallback( |
|---|
| 217 | + (sessionId: string, name: string) => { |
|---|
| 218 | + sendCommand("rename", { sessionId, name }); |
|---|
| 219 | + }, |
|---|
| 220 | + [sendCommand] |
|---|
| 221 | + ); |
|---|
| 222 | + |
|---|
| 223 | + // --- Screenshot / navigation --- |
|---|
| 224 | + const requestScreenshot = useCallback(() => { |
|---|
| 225 | + sendCommand("screenshot"); |
|---|
| 226 | + }, [sendCommand]); |
|---|
| 227 | + |
|---|
| 228 | + const sendNavKey = useCallback( |
|---|
| 229 | + (key: string) => { |
|---|
| 230 | + sendCommand("nav", { key }); |
|---|
| 231 | + }, |
|---|
| 232 | + [sendCommand] |
|---|
| 233 | + ); |
|---|
| 234 | + |
|---|
| 127 | 235 | return ( |
|---|
| 128 | 236 | <ChatContext.Provider |
|---|
| 129 | | - value={{ messages, sendTextMessage, sendVoiceMessage, clearMessages }} |
|---|
| 237 | + value={{ |
|---|
| 238 | + messages, |
|---|
| 239 | + sendTextMessage, |
|---|
| 240 | + sendVoiceMessage, |
|---|
| 241 | + clearMessages, |
|---|
| 242 | + sessions, |
|---|
| 243 | + requestSessions, |
|---|
| 244 | + switchSession, |
|---|
| 245 | + renameSession, |
|---|
| 246 | + latestScreenshot, |
|---|
| 247 | + requestScreenshot, |
|---|
| 248 | + sendNavKey, |
|---|
| 249 | + }} |
|---|
| 130 | 250 | > |
|---|
| 131 | 251 | {children} |
|---|
| 132 | 252 | </ChatContext.Provider> |
|---|
| .. | .. |
|---|
| 7 | 7 | useState, |
|---|
| 8 | 8 | } from "react"; |
|---|
| 9 | 9 | import * as SecureStore from "expo-secure-store"; |
|---|
| 10 | | -import { ConnectionStatus, ServerConfig, WebSocketMessage } from "../types"; |
|---|
| 10 | +import { |
|---|
| 11 | + ConnectionStatus, |
|---|
| 12 | + ServerConfig, |
|---|
| 13 | + WsIncoming, |
|---|
| 14 | + WsOutgoing, |
|---|
| 15 | +} from "../types"; |
|---|
| 11 | 16 | import { wsClient } from "../services/websocket"; |
|---|
| 12 | 17 | |
|---|
| 13 | 18 | const SECURE_STORE_KEY = "pailot_server_config"; |
|---|
| .. | .. |
|---|
| 19 | 24 | disconnect: () => void; |
|---|
| 20 | 25 | sendTextMessage: (text: string) => boolean; |
|---|
| 21 | 26 | sendVoiceMessage: (audioBase64: string, transcript?: string) => boolean; |
|---|
| 27 | + sendCommand: (command: string, args?: Record<string, unknown>) => boolean; |
|---|
| 22 | 28 | saveServerConfig: (config: ServerConfig) => Promise<void>; |
|---|
| 23 | 29 | onMessageReceived: React.MutableRefObject< |
|---|
| 24 | | - ((data: WebSocketMessage) => void) | null |
|---|
| 30 | + ((data: WsIncoming) => void) | null |
|---|
| 25 | 31 | >; |
|---|
| 26 | 32 | } |
|---|
| 27 | 33 | |
|---|
| .. | .. |
|---|
| 34 | 40 | }) { |
|---|
| 35 | 41 | const [serverConfig, setServerConfig] = useState<ServerConfig | null>(null); |
|---|
| 36 | 42 | const [status, setStatus] = useState<ConnectionStatus>("disconnected"); |
|---|
| 37 | | - const onMessageReceived = useRef<((data: WebSocketMessage) => void) | null>( |
|---|
| 38 | | - null |
|---|
| 39 | | - ); |
|---|
| 43 | + const onMessageReceived = useRef<((data: WsIncoming) => void) | null>(null); |
|---|
| 40 | 44 | |
|---|
| 41 | 45 | useEffect(() => { |
|---|
| 42 | 46 | loadConfig(); |
|---|
| .. | .. |
|---|
| 48 | 52 | onClose: () => setStatus("disconnected"), |
|---|
| 49 | 53 | onError: () => setStatus("disconnected"), |
|---|
| 50 | 54 | onMessage: (data) => { |
|---|
| 51 | | - onMessageReceived.current?.(data); |
|---|
| 55 | + onMessageReceived.current?.(data as WsIncoming); |
|---|
| 52 | 56 | }, |
|---|
| 53 | 57 | }); |
|---|
| 54 | 58 | }, []); |
|---|
| .. | .. |
|---|
| 92 | 96 | }, []); |
|---|
| 93 | 97 | |
|---|
| 94 | 98 | const sendTextMessage = useCallback((text: string): boolean => { |
|---|
| 95 | | - const msg: WebSocketMessage = { type: "text", content: text }; |
|---|
| 96 | | - return wsClient.send(msg); |
|---|
| 99 | + return wsClient.send({ type: "text", content: text }); |
|---|
| 97 | 100 | }, []); |
|---|
| 98 | 101 | |
|---|
| 99 | 102 | const sendVoiceMessage = useCallback( |
|---|
| 100 | 103 | (audioBase64: string, transcript: string = ""): boolean => { |
|---|
| 101 | | - const msg: WebSocketMessage = { |
|---|
| 104 | + return wsClient.send({ |
|---|
| 102 | 105 | type: "voice", |
|---|
| 103 | 106 | content: transcript, |
|---|
| 104 | 107 | audioBase64, |
|---|
| 105 | | - }; |
|---|
| 106 | | - return wsClient.send(msg); |
|---|
| 108 | + }); |
|---|
| 109 | + }, |
|---|
| 110 | + [] |
|---|
| 111 | + ); |
|---|
| 112 | + |
|---|
| 113 | + const sendCommand = useCallback( |
|---|
| 114 | + (command: string, args?: Record<string, unknown>): boolean => { |
|---|
| 115 | + const msg: WsOutgoing = { type: "command", command, args }; |
|---|
| 116 | + return wsClient.send(msg as any); |
|---|
| 107 | 117 | }, |
|---|
| 108 | 118 | [] |
|---|
| 109 | 119 | ); |
|---|
| .. | .. |
|---|
| 117 | 127 | disconnect, |
|---|
| 118 | 128 | sendTextMessage, |
|---|
| 119 | 129 | sendVoiceMessage, |
|---|
| 130 | + sendCommand, |
|---|
| 120 | 131 | saveServerConfig, |
|---|
| 121 | 132 | onMessageReceived, |
|---|
| 122 | 133 | }} |
|---|
| .. | .. |
|---|
| 8 | 8 | "name": "pailot", |
|---|
| 9 | 9 | "version": "1.0.0", |
|---|
| 10 | 10 | "dependencies": { |
|---|
| 11 | + "@react-navigation/bottom-tabs": "^7.15.3", |
|---|
| 12 | + "@react-navigation/native": "^7.1.31", |
|---|
| 11 | 13 | "expo": "~55.0.4", |
|---|
| 12 | | - "expo-av": "^16.0.8", |
|---|
| 14 | + "expo-audio": "^55.0.8", |
|---|
| 15 | + "expo-constants": "~55.0.7", |
|---|
| 16 | + "expo-file-system": "~55.0.10", |
|---|
| 13 | 17 | "expo-haptics": "~55.0.8", |
|---|
| 18 | + "expo-linking": "~55.0.7", |
|---|
| 14 | 19 | "expo-router": "~55.0.3", |
|---|
| 15 | 20 | "expo-secure-store": "~55.0.8", |
|---|
| 21 | + "expo-speech-recognition": "^3.1.1", |
|---|
| 22 | + "expo-splash-screen": "~55.0.10", |
|---|
| 16 | 23 | "expo-status-bar": "~55.0.4", |
|---|
| 24 | + "expo-system-ui": "~55.0.9", |
|---|
| 25 | + "expo-web-browser": "~55.0.9", |
|---|
| 17 | 26 | "nativewind": "^4", |
|---|
| 18 | 27 | "react": "19.2.0", |
|---|
| 28 | + "react-dom": "^19.2.4", |
|---|
| 19 | 29 | "react-native": "0.83.2", |
|---|
| 20 | 30 | "react-native-gesture-handler": "~2.30.0", |
|---|
| 21 | 31 | "react-native-reanimated": "4.2.1", |
|---|
| 22 | 32 | "react-native-safe-area-context": "~5.6.2", |
|---|
| 23 | 33 | "react-native-screens": "~4.23.0", |
|---|
| 24 | | - "react-native-svg": "15.15.3" |
|---|
| 34 | + "react-native-svg": "15.15.3", |
|---|
| 35 | + "react-native-web": "^0.21.0", |
|---|
| 36 | + "react-native-worklets": "0.7.2" |
|---|
| 25 | 37 | }, |
|---|
| 26 | 38 | "devDependencies": { |
|---|
| 27 | 39 | "@types/react": "~19.2.2", |
|---|
| 28 | 40 | "babel-plugin-module-resolver": "^5.0.2", |
|---|
| 41 | + "babel-preset-expo": "^55.0.10", |
|---|
| 29 | 42 | "tailwindcss": "^3.4.19", |
|---|
| 30 | 43 | "typescript": "~5.9.2" |
|---|
| 31 | 44 | } |
|---|
| .. | .. |
|---|
| 1317 | 1330 | "@babel/core": "^7.0.0-0" |
|---|
| 1318 | 1331 | } |
|---|
| 1319 | 1332 | }, |
|---|
| 1333 | + "node_modules/@babel/plugin-transform-template-literals": { |
|---|
| 1334 | + "version": "7.27.1", |
|---|
| 1335 | + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", |
|---|
| 1336 | + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", |
|---|
| 1337 | + "license": "MIT", |
|---|
| 1338 | + "dependencies": { |
|---|
| 1339 | + "@babel/helper-plugin-utils": "^7.27.1" |
|---|
| 1340 | + }, |
|---|
| 1341 | + "engines": { |
|---|
| 1342 | + "node": ">=6.9.0" |
|---|
| 1343 | + }, |
|---|
| 1344 | + "peerDependencies": { |
|---|
| 1345 | + "@babel/core": "^7.0.0-0" |
|---|
| 1346 | + } |
|---|
| 1347 | + }, |
|---|
| 1320 | 1348 | "node_modules/@babel/plugin-transform-typescript": { |
|---|
| 1321 | 1349 | "version": "7.28.6", |
|---|
| 1322 | 1350 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", |
|---|
| .. | .. |
|---|
| 1733 | 1761 | "@xmldom/xmldom": "^0.8.8", |
|---|
| 1734 | 1762 | "base64-js": "^1.5.1", |
|---|
| 1735 | 1763 | "xmlbuilder": "^15.1.1" |
|---|
| 1764 | + } |
|---|
| 1765 | + }, |
|---|
| 1766 | + "node_modules/@expo/prebuild-config": { |
|---|
| 1767 | + "version": "55.0.8", |
|---|
| 1768 | + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.8.tgz", |
|---|
| 1769 | + "integrity": "sha512-VJNJiOmmZgyDnR7JMmc3B8Z0ZepZ17I8Wtw+wAH/2+UCUsFg588XU+bwgYcFGw+is28kwGjY46z43kfufpxOnA==", |
|---|
| 1770 | + "license": "MIT", |
|---|
| 1771 | + "dependencies": { |
|---|
| 1772 | + "@expo/config": "~55.0.8", |
|---|
| 1773 | + "@expo/config-plugins": "~55.0.6", |
|---|
| 1774 | + "@expo/config-types": "^55.0.5", |
|---|
| 1775 | + "@expo/image-utils": "^0.8.12", |
|---|
| 1776 | + "@expo/json-file": "^10.0.12", |
|---|
| 1777 | + "@react-native/normalize-colors": "0.83.2", |
|---|
| 1778 | + "debug": "^4.3.1", |
|---|
| 1779 | + "resolve-from": "^5.0.0", |
|---|
| 1780 | + "semver": "^7.6.0", |
|---|
| 1781 | + "xml2js": "0.6.0" |
|---|
| 1782 | + }, |
|---|
| 1783 | + "peerDependencies": { |
|---|
| 1784 | + "expo": "*" |
|---|
| 1736 | 1785 | } |
|---|
| 1737 | 1786 | }, |
|---|
| 1738 | 1787 | "node_modules/@expo/require-utils": { |
|---|
| .. | .. |
|---|
| 3466 | 3515 | "@babel/core": "^7.0.0 || ^8.0.0-0" |
|---|
| 3467 | 3516 | } |
|---|
| 3468 | 3517 | }, |
|---|
| 3518 | + "node_modules/babel-preset-expo": { |
|---|
| 3519 | + "version": "55.0.10", |
|---|
| 3520 | + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.10.tgz", |
|---|
| 3521 | + "integrity": "sha512-aRtW7qJKohGU2V0LUJ6IeP7py3+kVUo9zcc8+v1Kix8jGGuIvqvpo9S6W1Fmn9VFP2DBwkFDLiyzkCZS85urVA==", |
|---|
| 3522 | + "license": "MIT", |
|---|
| 3523 | + "dependencies": { |
|---|
| 3524 | + "@babel/generator": "^7.20.5", |
|---|
| 3525 | + "@babel/helper-module-imports": "^7.25.9", |
|---|
| 3526 | + "@babel/plugin-proposal-decorators": "^7.12.9", |
|---|
| 3527 | + "@babel/plugin-proposal-export-default-from": "^7.24.7", |
|---|
| 3528 | + "@babel/plugin-syntax-export-default-from": "^7.24.7", |
|---|
| 3529 | + "@babel/plugin-transform-class-static-block": "^7.27.1", |
|---|
| 3530 | + "@babel/plugin-transform-export-namespace-from": "^7.25.9", |
|---|
| 3531 | + "@babel/plugin-transform-flow-strip-types": "^7.25.2", |
|---|
| 3532 | + "@babel/plugin-transform-modules-commonjs": "^7.24.8", |
|---|
| 3533 | + "@babel/plugin-transform-object-rest-spread": "^7.24.7", |
|---|
| 3534 | + "@babel/plugin-transform-parameters": "^7.24.7", |
|---|
| 3535 | + "@babel/plugin-transform-private-methods": "^7.24.7", |
|---|
| 3536 | + "@babel/plugin-transform-private-property-in-object": "^7.24.7", |
|---|
| 3537 | + "@babel/plugin-transform-runtime": "^7.24.7", |
|---|
| 3538 | + "@babel/preset-react": "^7.22.15", |
|---|
| 3539 | + "@babel/preset-typescript": "^7.23.0", |
|---|
| 3540 | + "@react-native/babel-preset": "0.83.2", |
|---|
| 3541 | + "babel-plugin-react-compiler": "^1.0.0", |
|---|
| 3542 | + "babel-plugin-react-native-web": "~0.21.0", |
|---|
| 3543 | + "babel-plugin-syntax-hermes-parser": "^0.32.0", |
|---|
| 3544 | + "babel-plugin-transform-flow-enums": "^0.0.2", |
|---|
| 3545 | + "debug": "^4.3.4", |
|---|
| 3546 | + "resolve-from": "^5.0.0" |
|---|
| 3547 | + }, |
|---|
| 3548 | + "peerDependencies": { |
|---|
| 3549 | + "@babel/runtime": "^7.20.0", |
|---|
| 3550 | + "expo": "*", |
|---|
| 3551 | + "expo-widgets": "^55.0.2", |
|---|
| 3552 | + "react-refresh": ">=0.14.0 <1.0.0" |
|---|
| 3553 | + }, |
|---|
| 3554 | + "peerDependenciesMeta": { |
|---|
| 3555 | + "@babel/runtime": { |
|---|
| 3556 | + "optional": true |
|---|
| 3557 | + }, |
|---|
| 3558 | + "expo": { |
|---|
| 3559 | + "optional": true |
|---|
| 3560 | + }, |
|---|
| 3561 | + "expo-widgets": { |
|---|
| 3562 | + "optional": true |
|---|
| 3563 | + } |
|---|
| 3564 | + } |
|---|
| 3565 | + }, |
|---|
| 3469 | 3566 | "node_modules/babel-preset-jest": { |
|---|
| 3470 | 3567 | "version": "29.6.3", |
|---|
| 3471 | 3568 | "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", |
|---|
| .. | .. |
|---|
| 4048 | 4145 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", |
|---|
| 4049 | 4146 | "license": "MIT" |
|---|
| 4050 | 4147 | }, |
|---|
| 4148 | + "node_modules/cross-fetch": { |
|---|
| 4149 | + "version": "3.2.0", |
|---|
| 4150 | + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", |
|---|
| 4151 | + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", |
|---|
| 4152 | + "license": "MIT", |
|---|
| 4153 | + "dependencies": { |
|---|
| 4154 | + "node-fetch": "^2.7.0" |
|---|
| 4155 | + } |
|---|
| 4156 | + }, |
|---|
| 4051 | 4157 | "node_modules/cross-spawn": { |
|---|
| 4052 | 4158 | "version": "7.0.6", |
|---|
| 4053 | 4159 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", |
|---|
| .. | .. |
|---|
| 4060 | 4166 | }, |
|---|
| 4061 | 4167 | "engines": { |
|---|
| 4062 | 4168 | "node": ">= 8" |
|---|
| 4169 | + } |
|---|
| 4170 | + }, |
|---|
| 4171 | + "node_modules/css-in-js-utils": { |
|---|
| 4172 | + "version": "3.1.0", |
|---|
| 4173 | + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", |
|---|
| 4174 | + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", |
|---|
| 4175 | + "license": "MIT", |
|---|
| 4176 | + "dependencies": { |
|---|
| 4177 | + "hyphenate-style-name": "^1.0.3" |
|---|
| 4063 | 4178 | } |
|---|
| 4064 | 4179 | }, |
|---|
| 4065 | 4180 | "node_modules/css-select": { |
|---|
| .. | .. |
|---|
| 4457 | 4572 | } |
|---|
| 4458 | 4573 | } |
|---|
| 4459 | 4574 | }, |
|---|
| 4460 | | - "node_modules/expo-av": { |
|---|
| 4461 | | - "version": "16.0.8", |
|---|
| 4462 | | - "resolved": "https://registry.npmjs.org/expo-av/-/expo-av-16.0.8.tgz", |
|---|
| 4463 | | - "integrity": "sha512-cmVPftGR/ca7XBgs7R6ky36lF3OC0/MM/lpgX/yXqfv0jASTsh7AYX9JxHCwFmF+Z6JEB1vne9FDx4GiLcGreQ==", |
|---|
| 4575 | + "node_modules/expo-audio": { |
|---|
| 4576 | + "version": "55.0.8", |
|---|
| 4577 | + "resolved": "https://registry.npmjs.org/expo-audio/-/expo-audio-55.0.8.tgz", |
|---|
| 4578 | + "integrity": "sha512-X61pQSikE2rsP2ZTMFUMThOmgGyYEHcmZpGVMrKJgcYtRCFKuctB/z69dFQPoumL+zTz8qlBoGohjkHVvA9P8A==", |
|---|
| 4464 | 4579 | "license": "MIT", |
|---|
| 4465 | 4580 | "peerDependencies": { |
|---|
| 4466 | 4581 | "expo": "*", |
|---|
| 4582 | + "expo-asset": "*", |
|---|
| 4467 | 4583 | "react": "*", |
|---|
| 4468 | | - "react-native": "*", |
|---|
| 4469 | | - "react-native-web": "*" |
|---|
| 4470 | | - }, |
|---|
| 4471 | | - "peerDependenciesMeta": { |
|---|
| 4472 | | - "react-native-web": { |
|---|
| 4473 | | - "optional": true |
|---|
| 4474 | | - } |
|---|
| 4584 | + "react-native": "*" |
|---|
| 4475 | 4585 | } |
|---|
| 4476 | 4586 | }, |
|---|
| 4477 | 4587 | "node_modules/expo-constants": { |
|---|
| .. | .. |
|---|
| 4483 | 4593 | "@expo/config": "~55.0.8", |
|---|
| 4484 | 4594 | "@expo/env": "~2.1.1" |
|---|
| 4485 | 4595 | }, |
|---|
| 4596 | + "peerDependencies": { |
|---|
| 4597 | + "expo": "*", |
|---|
| 4598 | + "react-native": "*" |
|---|
| 4599 | + } |
|---|
| 4600 | + }, |
|---|
| 4601 | + "node_modules/expo-file-system": { |
|---|
| 4602 | + "version": "55.0.10", |
|---|
| 4603 | + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.10.tgz", |
|---|
| 4604 | + "integrity": "sha512-ysFdVdUgtfj2ApY0Cn+pBg+yK4xp+SNwcaH8j2B91JJQ4OXJmnyCSmrNZYz7J4mdYVuv2GzxIP+N/IGlHQG3Yw==", |
|---|
| 4605 | + "license": "MIT", |
|---|
| 4486 | 4606 | "peerDependencies": { |
|---|
| 4487 | 4607 | "expo": "*", |
|---|
| 4488 | 4608 | "react-native": "*" |
|---|
| .. | .. |
|---|
| 4540 | 4660 | "react-native-web": { |
|---|
| 4541 | 4661 | "optional": true |
|---|
| 4542 | 4662 | } |
|---|
| 4663 | + } |
|---|
| 4664 | + }, |
|---|
| 4665 | + "node_modules/expo-linking": { |
|---|
| 4666 | + "version": "55.0.7", |
|---|
| 4667 | + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-55.0.7.tgz", |
|---|
| 4668 | + "integrity": "sha512-MiGCedere1vzQTEi2aGrkzd7eh/rPSz4w6F3GMBuAJzYl+/0VhIuyhozpEGrueyDIXWfzaUVOcn3SfxVi+kwQQ==", |
|---|
| 4669 | + "license": "MIT", |
|---|
| 4670 | + "dependencies": { |
|---|
| 4671 | + "expo-constants": "~55.0.7", |
|---|
| 4672 | + "invariant": "^2.2.4" |
|---|
| 4673 | + }, |
|---|
| 4674 | + "peerDependencies": { |
|---|
| 4675 | + "react": "*", |
|---|
| 4676 | + "react-native": "*" |
|---|
| 4543 | 4677 | } |
|---|
| 4544 | 4678 | }, |
|---|
| 4545 | 4679 | "node_modules/expo-modules-autolinking": { |
|---|
| .. | .. |
|---|
| 4729 | 4863 | "node": ">=20.16.0" |
|---|
| 4730 | 4864 | } |
|---|
| 4731 | 4865 | }, |
|---|
| 4866 | + "node_modules/expo-speech-recognition": { |
|---|
| 4867 | + "version": "3.1.1", |
|---|
| 4868 | + "resolved": "https://registry.npmjs.org/expo-speech-recognition/-/expo-speech-recognition-3.1.1.tgz", |
|---|
| 4869 | + "integrity": "sha512-+1rviv+ZecAokY8PUfr3XJuhS4t0uKccewIPPUk5ooeEt5xKEWr6XYpKm3ggapPdJQbgMTjWbmSPT1ahTMyIqA==", |
|---|
| 4870 | + "license": "MIT", |
|---|
| 4871 | + "peerDependencies": { |
|---|
| 4872 | + "expo": "*", |
|---|
| 4873 | + "react": "*", |
|---|
| 4874 | + "react-native": "*" |
|---|
| 4875 | + } |
|---|
| 4876 | + }, |
|---|
| 4877 | + "node_modules/expo-splash-screen": { |
|---|
| 4878 | + "version": "55.0.10", |
|---|
| 4879 | + "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-55.0.10.tgz", |
|---|
| 4880 | + "integrity": "sha512-RN5qqrxudxFlRIjLFr/Ifmt+mUCLRc0gs66PekP6flzNS/JYEuoCbwJ+NmUwwJtPA+vyy60DYiky0QmS98ydmQ==", |
|---|
| 4881 | + "license": "MIT", |
|---|
| 4882 | + "dependencies": { |
|---|
| 4883 | + "@expo/prebuild-config": "^55.0.8" |
|---|
| 4884 | + }, |
|---|
| 4885 | + "peerDependencies": { |
|---|
| 4886 | + "expo": "*" |
|---|
| 4887 | + } |
|---|
| 4888 | + }, |
|---|
| 4732 | 4889 | "node_modules/expo-status-bar": { |
|---|
| 4733 | 4890 | "version": "55.0.4", |
|---|
| 4734 | 4891 | "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.4.tgz", |
|---|
| .. | .. |
|---|
| 4755 | 4912 | "expo": "*", |
|---|
| 4756 | 4913 | "expo-font": "*", |
|---|
| 4757 | 4914 | "react": "*", |
|---|
| 4915 | + "react-native": "*" |
|---|
| 4916 | + } |
|---|
| 4917 | + }, |
|---|
| 4918 | + "node_modules/expo-system-ui": { |
|---|
| 4919 | + "version": "55.0.9", |
|---|
| 4920 | + "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-55.0.9.tgz", |
|---|
| 4921 | + "integrity": "sha512-8ygP1B0uFAFI8s7eHY2IcGnE83GhFeZYwHBr/fQ4dSXnc7iVT9zp2PvyTyiDiibQ69dBG+fauMQ4KlPcOO51kQ==", |
|---|
| 4922 | + "license": "MIT", |
|---|
| 4923 | + "dependencies": { |
|---|
| 4924 | + "@react-native/normalize-colors": "0.83.2", |
|---|
| 4925 | + "debug": "^4.3.2" |
|---|
| 4926 | + }, |
|---|
| 4927 | + "peerDependencies": { |
|---|
| 4928 | + "expo": "*", |
|---|
| 4929 | + "react-native": "*", |
|---|
| 4930 | + "react-native-web": "*" |
|---|
| 4931 | + }, |
|---|
| 4932 | + "peerDependenciesMeta": { |
|---|
| 4933 | + "react-native-web": { |
|---|
| 4934 | + "optional": true |
|---|
| 4935 | + } |
|---|
| 4936 | + } |
|---|
| 4937 | + }, |
|---|
| 4938 | + "node_modules/expo-web-browser": { |
|---|
| 4939 | + "version": "55.0.9", |
|---|
| 4940 | + "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-55.0.9.tgz", |
|---|
| 4941 | + "integrity": "sha512-PvAVsG401QmZabtTsYh1cYcpPiqvBPs8oiOkSrp0jIXnneiM466HxmeNtvo+fNxqJ2nwOBz9qLPiWRO91VBfsQ==", |
|---|
| 4942 | + "license": "MIT", |
|---|
| 4943 | + "peerDependencies": { |
|---|
| 4944 | + "expo": "*", |
|---|
| 4758 | 4945 | "react-native": "*" |
|---|
| 4759 | 4946 | } |
|---|
| 4760 | 4947 | }, |
|---|
| .. | .. |
|---|
| 4839 | 5026 | } |
|---|
| 4840 | 5027 | } |
|---|
| 4841 | 5028 | }, |
|---|
| 4842 | | - "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/prebuild-config": { |
|---|
| 4843 | | - "version": "55.0.8", |
|---|
| 4844 | | - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.8.tgz", |
|---|
| 4845 | | - "integrity": "sha512-VJNJiOmmZgyDnR7JMmc3B8Z0ZepZ17I8Wtw+wAH/2+UCUsFg588XU+bwgYcFGw+is28kwGjY46z43kfufpxOnA==", |
|---|
| 4846 | | - "license": "MIT", |
|---|
| 4847 | | - "dependencies": { |
|---|
| 4848 | | - "@expo/config": "~55.0.8", |
|---|
| 4849 | | - "@expo/config-plugins": "~55.0.6", |
|---|
| 4850 | | - "@expo/config-types": "^55.0.5", |
|---|
| 4851 | | - "@expo/image-utils": "^0.8.12", |
|---|
| 4852 | | - "@expo/json-file": "^10.0.12", |
|---|
| 4853 | | - "@react-native/normalize-colors": "0.83.2", |
|---|
| 4854 | | - "debug": "^4.3.1", |
|---|
| 4855 | | - "resolve-from": "^5.0.0", |
|---|
| 4856 | | - "semver": "^7.6.0", |
|---|
| 4857 | | - "xml2js": "0.6.0" |
|---|
| 4858 | | - }, |
|---|
| 4859 | | - "peerDependencies": { |
|---|
| 4860 | | - "expo": "*" |
|---|
| 4861 | | - } |
|---|
| 4862 | | - }, |
|---|
| 4863 | 5029 | "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { |
|---|
| 4864 | 5030 | "version": "55.0.9", |
|---|
| 4865 | 5031 | "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.9.tgz", |
|---|
| .. | .. |
|---|
| 4940 | 5106 | "react-native": "*" |
|---|
| 4941 | 5107 | } |
|---|
| 4942 | 5108 | }, |
|---|
| 4943 | | - "node_modules/expo/node_modules/babel-preset-expo": { |
|---|
| 4944 | | - "version": "55.0.10", |
|---|
| 4945 | | - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.10.tgz", |
|---|
| 4946 | | - "integrity": "sha512-aRtW7qJKohGU2V0LUJ6IeP7py3+kVUo9zcc8+v1Kix8jGGuIvqvpo9S6W1Fmn9VFP2DBwkFDLiyzkCZS85urVA==", |
|---|
| 4947 | | - "license": "MIT", |
|---|
| 4948 | | - "dependencies": { |
|---|
| 4949 | | - "@babel/generator": "^7.20.5", |
|---|
| 4950 | | - "@babel/helper-module-imports": "^7.25.9", |
|---|
| 4951 | | - "@babel/plugin-proposal-decorators": "^7.12.9", |
|---|
| 4952 | | - "@babel/plugin-proposal-export-default-from": "^7.24.7", |
|---|
| 4953 | | - "@babel/plugin-syntax-export-default-from": "^7.24.7", |
|---|
| 4954 | | - "@babel/plugin-transform-class-static-block": "^7.27.1", |
|---|
| 4955 | | - "@babel/plugin-transform-export-namespace-from": "^7.25.9", |
|---|
| 4956 | | - "@babel/plugin-transform-flow-strip-types": "^7.25.2", |
|---|
| 4957 | | - "@babel/plugin-transform-modules-commonjs": "^7.24.8", |
|---|
| 4958 | | - "@babel/plugin-transform-object-rest-spread": "^7.24.7", |
|---|
| 4959 | | - "@babel/plugin-transform-parameters": "^7.24.7", |
|---|
| 4960 | | - "@babel/plugin-transform-private-methods": "^7.24.7", |
|---|
| 4961 | | - "@babel/plugin-transform-private-property-in-object": "^7.24.7", |
|---|
| 4962 | | - "@babel/plugin-transform-runtime": "^7.24.7", |
|---|
| 4963 | | - "@babel/preset-react": "^7.22.15", |
|---|
| 4964 | | - "@babel/preset-typescript": "^7.23.0", |
|---|
| 4965 | | - "@react-native/babel-preset": "0.83.2", |
|---|
| 4966 | | - "babel-plugin-react-compiler": "^1.0.0", |
|---|
| 4967 | | - "babel-plugin-react-native-web": "~0.21.0", |
|---|
| 4968 | | - "babel-plugin-syntax-hermes-parser": "^0.32.0", |
|---|
| 4969 | | - "babel-plugin-transform-flow-enums": "^0.0.2", |
|---|
| 4970 | | - "debug": "^4.3.4", |
|---|
| 4971 | | - "resolve-from": "^5.0.0" |
|---|
| 4972 | | - }, |
|---|
| 4973 | | - "peerDependencies": { |
|---|
| 4974 | | - "@babel/runtime": "^7.20.0", |
|---|
| 4975 | | - "expo": "*", |
|---|
| 4976 | | - "expo-widgets": "^55.0.2", |
|---|
| 4977 | | - "react-refresh": ">=0.14.0 <1.0.0" |
|---|
| 4978 | | - }, |
|---|
| 4979 | | - "peerDependenciesMeta": { |
|---|
| 4980 | | - "@babel/runtime": { |
|---|
| 4981 | | - "optional": true |
|---|
| 4982 | | - }, |
|---|
| 4983 | | - "expo": { |
|---|
| 4984 | | - "optional": true |
|---|
| 4985 | | - }, |
|---|
| 4986 | | - "expo-widgets": { |
|---|
| 4987 | | - "optional": true |
|---|
| 4988 | | - } |
|---|
| 4989 | | - } |
|---|
| 4990 | | - }, |
|---|
| 4991 | 5109 | "node_modules/expo/node_modules/ci-info": { |
|---|
| 4992 | 5110 | "version": "3.9.0", |
|---|
| 4993 | 5111 | "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", |
|---|
| .. | .. |
|---|
| 5015 | 5133 | "peerDependencies": { |
|---|
| 5016 | 5134 | "expo": "*", |
|---|
| 5017 | 5135 | "react": "*", |
|---|
| 5018 | | - "react-native": "*" |
|---|
| 5019 | | - } |
|---|
| 5020 | | - }, |
|---|
| 5021 | | - "node_modules/expo/node_modules/expo-file-system": { |
|---|
| 5022 | | - "version": "55.0.10", |
|---|
| 5023 | | - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.10.tgz", |
|---|
| 5024 | | - "integrity": "sha512-ysFdVdUgtfj2ApY0Cn+pBg+yK4xp+SNwcaH8j2B91JJQ4OXJmnyCSmrNZYz7J4mdYVuv2GzxIP+N/IGlHQG3Yw==", |
|---|
| 5025 | | - "license": "MIT", |
|---|
| 5026 | | - "peerDependencies": { |
|---|
| 5027 | | - "expo": "*", |
|---|
| 5028 | 5136 | "react-native": "*" |
|---|
| 5029 | 5137 | } |
|---|
| 5030 | 5138 | }, |
|---|
| .. | .. |
|---|
| 5148 | 5256 | "license": "Apache-2.0", |
|---|
| 5149 | 5257 | "dependencies": { |
|---|
| 5150 | 5258 | "bser": "2.1.1" |
|---|
| 5259 | + } |
|---|
| 5260 | + }, |
|---|
| 5261 | + "node_modules/fbjs": { |
|---|
| 5262 | + "version": "3.0.5", |
|---|
| 5263 | + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", |
|---|
| 5264 | + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", |
|---|
| 5265 | + "license": "MIT", |
|---|
| 5266 | + "dependencies": { |
|---|
| 5267 | + "cross-fetch": "^3.1.5", |
|---|
| 5268 | + "fbjs-css-vars": "^1.0.0", |
|---|
| 5269 | + "loose-envify": "^1.0.0", |
|---|
| 5270 | + "object-assign": "^4.1.0", |
|---|
| 5271 | + "promise": "^7.1.1", |
|---|
| 5272 | + "setimmediate": "^1.0.5", |
|---|
| 5273 | + "ua-parser-js": "^1.0.35" |
|---|
| 5274 | + } |
|---|
| 5275 | + }, |
|---|
| 5276 | + "node_modules/fbjs-css-vars": { |
|---|
| 5277 | + "version": "1.0.2", |
|---|
| 5278 | + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", |
|---|
| 5279 | + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", |
|---|
| 5280 | + "license": "MIT" |
|---|
| 5281 | + }, |
|---|
| 5282 | + "node_modules/fbjs/node_modules/promise": { |
|---|
| 5283 | + "version": "7.3.1", |
|---|
| 5284 | + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", |
|---|
| 5285 | + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", |
|---|
| 5286 | + "license": "MIT", |
|---|
| 5287 | + "dependencies": { |
|---|
| 5288 | + "asap": "~2.0.3" |
|---|
| 5151 | 5289 | } |
|---|
| 5152 | 5290 | }, |
|---|
| 5153 | 5291 | "node_modules/fetch-nodeshim": { |
|---|
| .. | .. |
|---|
| 5481 | 5619 | "node": ">= 14" |
|---|
| 5482 | 5620 | } |
|---|
| 5483 | 5621 | }, |
|---|
| 5622 | + "node_modules/hyphenate-style-name": { |
|---|
| 5623 | + "version": "1.1.0", |
|---|
| 5624 | + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", |
|---|
| 5625 | + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", |
|---|
| 5626 | + "license": "BSD-3-Clause" |
|---|
| 5627 | + }, |
|---|
| 5484 | 5628 | "node_modules/ignore": { |
|---|
| 5485 | 5629 | "version": "5.3.2", |
|---|
| 5486 | 5630 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", |
|---|
| .. | .. |
|---|
| 5530 | 5674 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", |
|---|
| 5531 | 5675 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", |
|---|
| 5532 | 5676 | "license": "ISC" |
|---|
| 5677 | + }, |
|---|
| 5678 | + "node_modules/inline-style-prefixer": { |
|---|
| 5679 | + "version": "7.0.1", |
|---|
| 5680 | + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", |
|---|
| 5681 | + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", |
|---|
| 5682 | + "license": "MIT", |
|---|
| 5683 | + "dependencies": { |
|---|
| 5684 | + "css-in-js-utils": "^3.1.0" |
|---|
| 5685 | + } |
|---|
| 5533 | 5686 | }, |
|---|
| 5534 | 5687 | "node_modules/invariant": { |
|---|
| 5535 | 5688 | "version": "2.2.4", |
|---|
| .. | .. |
|---|
| 6840 | 6993 | "node": ">= 0.6" |
|---|
| 6841 | 6994 | } |
|---|
| 6842 | 6995 | }, |
|---|
| 6996 | + "node_modules/node-fetch": { |
|---|
| 6997 | + "version": "2.7.0", |
|---|
| 6998 | + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", |
|---|
| 6999 | + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", |
|---|
| 7000 | + "license": "MIT", |
|---|
| 7001 | + "dependencies": { |
|---|
| 7002 | + "whatwg-url": "^5.0.0" |
|---|
| 7003 | + }, |
|---|
| 7004 | + "engines": { |
|---|
| 7005 | + "node": "4.x || >=6.0.0" |
|---|
| 7006 | + }, |
|---|
| 7007 | + "peerDependencies": { |
|---|
| 7008 | + "encoding": "^0.1.0" |
|---|
| 7009 | + }, |
|---|
| 7010 | + "peerDependenciesMeta": { |
|---|
| 7011 | + "encoding": { |
|---|
| 7012 | + "optional": true |
|---|
| 7013 | + } |
|---|
| 7014 | + } |
|---|
| 7015 | + }, |
|---|
| 6843 | 7016 | "node_modules/node-forge": { |
|---|
| 6844 | 7017 | "version": "1.3.3", |
|---|
| 6845 | 7018 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", |
|---|
| .. | .. |
|---|
| 6919 | 7092 | "version": "4.1.1", |
|---|
| 6920 | 7093 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", |
|---|
| 6921 | 7094 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", |
|---|
| 6922 | | - "dev": true, |
|---|
| 6923 | 7095 | "license": "MIT", |
|---|
| 6924 | 7096 | "engines": { |
|---|
| 6925 | 7097 | "node": ">=0.10.0" |
|---|
| .. | .. |
|---|
| 7499 | 7671 | "version": "4.2.0", |
|---|
| 7500 | 7672 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", |
|---|
| 7501 | 7673 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", |
|---|
| 7502 | | - "dev": true, |
|---|
| 7503 | 7674 | "license": "MIT" |
|---|
| 7504 | 7675 | }, |
|---|
| 7505 | 7676 | "node_modules/pretty-format": { |
|---|
| .. | .. |
|---|
| 7642 | 7813 | "dependencies": { |
|---|
| 7643 | 7814 | "shell-quote": "^1.6.1", |
|---|
| 7644 | 7815 | "ws": "^7" |
|---|
| 7816 | + } |
|---|
| 7817 | + }, |
|---|
| 7818 | + "node_modules/react-dom": { |
|---|
| 7819 | + "version": "19.2.4", |
|---|
| 7820 | + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", |
|---|
| 7821 | + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", |
|---|
| 7822 | + "license": "MIT", |
|---|
| 7823 | + "dependencies": { |
|---|
| 7824 | + "scheduler": "^0.27.0" |
|---|
| 7825 | + }, |
|---|
| 7826 | + "peerDependencies": { |
|---|
| 7827 | + "react": "^19.2.4" |
|---|
| 7645 | 7828 | } |
|---|
| 7646 | 7829 | }, |
|---|
| 7647 | 7830 | "node_modules/react-fast-compare": { |
|---|
| .. | .. |
|---|
| 8086 | 8269 | "peerDependencies": { |
|---|
| 8087 | 8270 | "react": "*", |
|---|
| 8088 | 8271 | "react-native": "*" |
|---|
| 8272 | + } |
|---|
| 8273 | + }, |
|---|
| 8274 | + "node_modules/react-native-web": { |
|---|
| 8275 | + "version": "0.21.2", |
|---|
| 8276 | + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", |
|---|
| 8277 | + "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", |
|---|
| 8278 | + "license": "MIT", |
|---|
| 8279 | + "dependencies": { |
|---|
| 8280 | + "@babel/runtime": "^7.18.6", |
|---|
| 8281 | + "@react-native/normalize-colors": "^0.74.1", |
|---|
| 8282 | + "fbjs": "^3.0.4", |
|---|
| 8283 | + "inline-style-prefixer": "^7.0.1", |
|---|
| 8284 | + "memoize-one": "^6.0.0", |
|---|
| 8285 | + "nullthrows": "^1.1.1", |
|---|
| 8286 | + "postcss-value-parser": "^4.2.0", |
|---|
| 8287 | + "styleq": "^0.1.3" |
|---|
| 8288 | + }, |
|---|
| 8289 | + "peerDependencies": { |
|---|
| 8290 | + "react": "^18.0.0 || ^19.0.0", |
|---|
| 8291 | + "react-dom": "^18.0.0 || ^19.0.0" |
|---|
| 8292 | + } |
|---|
| 8293 | + }, |
|---|
| 8294 | + "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { |
|---|
| 8295 | + "version": "0.74.89", |
|---|
| 8296 | + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz", |
|---|
| 8297 | + "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==", |
|---|
| 8298 | + "license": "MIT" |
|---|
| 8299 | + }, |
|---|
| 8300 | + "node_modules/react-native-web/node_modules/memoize-one": { |
|---|
| 8301 | + "version": "6.0.0", |
|---|
| 8302 | + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", |
|---|
| 8303 | + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", |
|---|
| 8304 | + "license": "MIT" |
|---|
| 8305 | + }, |
|---|
| 8306 | + "node_modules/react-native-worklets": { |
|---|
| 8307 | + "version": "0.7.2", |
|---|
| 8308 | + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.2.tgz", |
|---|
| 8309 | + "integrity": "sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog==", |
|---|
| 8310 | + "license": "MIT", |
|---|
| 8311 | + "dependencies": { |
|---|
| 8312 | + "@babel/plugin-transform-arrow-functions": "7.27.1", |
|---|
| 8313 | + "@babel/plugin-transform-class-properties": "7.27.1", |
|---|
| 8314 | + "@babel/plugin-transform-classes": "7.28.4", |
|---|
| 8315 | + "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", |
|---|
| 8316 | + "@babel/plugin-transform-optional-chaining": "7.27.1", |
|---|
| 8317 | + "@babel/plugin-transform-shorthand-properties": "7.27.1", |
|---|
| 8318 | + "@babel/plugin-transform-template-literals": "7.27.1", |
|---|
| 8319 | + "@babel/plugin-transform-unicode-regex": "7.27.1", |
|---|
| 8320 | + "@babel/preset-typescript": "7.27.1", |
|---|
| 8321 | + "convert-source-map": "2.0.0", |
|---|
| 8322 | + "semver": "7.7.3" |
|---|
| 8323 | + }, |
|---|
| 8324 | + "peerDependencies": { |
|---|
| 8325 | + "@babel/core": "*", |
|---|
| 8326 | + "react": "*", |
|---|
| 8327 | + "react-native": "*" |
|---|
| 8328 | + } |
|---|
| 8329 | + }, |
|---|
| 8330 | + "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-class-properties": { |
|---|
| 8331 | + "version": "7.27.1", |
|---|
| 8332 | + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", |
|---|
| 8333 | + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", |
|---|
| 8334 | + "license": "MIT", |
|---|
| 8335 | + "dependencies": { |
|---|
| 8336 | + "@babel/helper-create-class-features-plugin": "^7.27.1", |
|---|
| 8337 | + "@babel/helper-plugin-utils": "^7.27.1" |
|---|
| 8338 | + }, |
|---|
| 8339 | + "engines": { |
|---|
| 8340 | + "node": ">=6.9.0" |
|---|
| 8341 | + }, |
|---|
| 8342 | + "peerDependencies": { |
|---|
| 8343 | + "@babel/core": "^7.0.0-0" |
|---|
| 8344 | + } |
|---|
| 8345 | + }, |
|---|
| 8346 | + "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-classes": { |
|---|
| 8347 | + "version": "7.28.4", |
|---|
| 8348 | + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", |
|---|
| 8349 | + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", |
|---|
| 8350 | + "license": "MIT", |
|---|
| 8351 | + "dependencies": { |
|---|
| 8352 | + "@babel/helper-annotate-as-pure": "^7.27.3", |
|---|
| 8353 | + "@babel/helper-compilation-targets": "^7.27.2", |
|---|
| 8354 | + "@babel/helper-globals": "^7.28.0", |
|---|
| 8355 | + "@babel/helper-plugin-utils": "^7.27.1", |
|---|
| 8356 | + "@babel/helper-replace-supers": "^7.27.1", |
|---|
| 8357 | + "@babel/traverse": "^7.28.4" |
|---|
| 8358 | + }, |
|---|
| 8359 | + "engines": { |
|---|
| 8360 | + "node": ">=6.9.0" |
|---|
| 8361 | + }, |
|---|
| 8362 | + "peerDependencies": { |
|---|
| 8363 | + "@babel/core": "^7.0.0-0" |
|---|
| 8364 | + } |
|---|
| 8365 | + }, |
|---|
| 8366 | + "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-nullish-coalescing-operator": { |
|---|
| 8367 | + "version": "7.27.1", |
|---|
| 8368 | + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", |
|---|
| 8369 | + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", |
|---|
| 8370 | + "license": "MIT", |
|---|
| 8371 | + "dependencies": { |
|---|
| 8372 | + "@babel/helper-plugin-utils": "^7.27.1" |
|---|
| 8373 | + }, |
|---|
| 8374 | + "engines": { |
|---|
| 8375 | + "node": ">=6.9.0" |
|---|
| 8376 | + }, |
|---|
| 8377 | + "peerDependencies": { |
|---|
| 8378 | + "@babel/core": "^7.0.0-0" |
|---|
| 8379 | + } |
|---|
| 8380 | + }, |
|---|
| 8381 | + "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-optional-chaining": { |
|---|
| 8382 | + "version": "7.27.1", |
|---|
| 8383 | + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", |
|---|
| 8384 | + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", |
|---|
| 8385 | + "license": "MIT", |
|---|
| 8386 | + "dependencies": { |
|---|
| 8387 | + "@babel/helper-plugin-utils": "^7.27.1", |
|---|
| 8388 | + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" |
|---|
| 8389 | + }, |
|---|
| 8390 | + "engines": { |
|---|
| 8391 | + "node": ">=6.9.0" |
|---|
| 8392 | + }, |
|---|
| 8393 | + "peerDependencies": { |
|---|
| 8394 | + "@babel/core": "^7.0.0-0" |
|---|
| 8395 | + } |
|---|
| 8396 | + }, |
|---|
| 8397 | + "node_modules/react-native-worklets/node_modules/@babel/preset-typescript": { |
|---|
| 8398 | + "version": "7.27.1", |
|---|
| 8399 | + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", |
|---|
| 8400 | + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", |
|---|
| 8401 | + "license": "MIT", |
|---|
| 8402 | + "dependencies": { |
|---|
| 8403 | + "@babel/helper-plugin-utils": "^7.27.1", |
|---|
| 8404 | + "@babel/helper-validator-option": "^7.27.1", |
|---|
| 8405 | + "@babel/plugin-syntax-jsx": "^7.27.1", |
|---|
| 8406 | + "@babel/plugin-transform-modules-commonjs": "^7.27.1", |
|---|
| 8407 | + "@babel/plugin-transform-typescript": "^7.27.1" |
|---|
| 8408 | + }, |
|---|
| 8409 | + "engines": { |
|---|
| 8410 | + "node": ">=6.9.0" |
|---|
| 8411 | + }, |
|---|
| 8412 | + "peerDependencies": { |
|---|
| 8413 | + "@babel/core": "^7.0.0-0" |
|---|
| 8414 | + } |
|---|
| 8415 | + }, |
|---|
| 8416 | + "node_modules/react-native-worklets/node_modules/semver": { |
|---|
| 8417 | + "version": "7.7.3", |
|---|
| 8418 | + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", |
|---|
| 8419 | + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", |
|---|
| 8420 | + "license": "ISC", |
|---|
| 8421 | + "bin": { |
|---|
| 8422 | + "semver": "bin/semver.js" |
|---|
| 8423 | + }, |
|---|
| 8424 | + "engines": { |
|---|
| 8425 | + "node": ">=10" |
|---|
| 8089 | 8426 | } |
|---|
| 8090 | 8427 | }, |
|---|
| 8091 | 8428 | "node_modules/react-native/node_modules/@react-native/virtualized-lists": { |
|---|
| .. | .. |
|---|
| 8648 | 8985 | "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", |
|---|
| 8649 | 8986 | "license": "MIT" |
|---|
| 8650 | 8987 | }, |
|---|
| 8988 | + "node_modules/setimmediate": { |
|---|
| 8989 | + "version": "1.0.5", |
|---|
| 8990 | + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", |
|---|
| 8991 | + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", |
|---|
| 8992 | + "license": "MIT" |
|---|
| 8993 | + }, |
|---|
| 8651 | 8994 | "node_modules/setprototypeof": { |
|---|
| 8652 | 8995 | "version": "1.2.0", |
|---|
| 8653 | 8996 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", |
|---|
| .. | .. |
|---|
| 8900 | 9243 | "version": "0.4.1", |
|---|
| 8901 | 9244 | "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", |
|---|
| 8902 | 9245 | "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", |
|---|
| 9246 | + "license": "MIT" |
|---|
| 9247 | + }, |
|---|
| 9248 | + "node_modules/styleq": { |
|---|
| 9249 | + "version": "0.1.3", |
|---|
| 9250 | + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", |
|---|
| 9251 | + "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==", |
|---|
| 8903 | 9252 | "license": "MIT" |
|---|
| 8904 | 9253 | }, |
|---|
| 8905 | 9254 | "node_modules/sucrase": { |
|---|
| .. | .. |
|---|
| 9223 | 9572 | "integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==", |
|---|
| 9224 | 9573 | "license": "MIT" |
|---|
| 9225 | 9574 | }, |
|---|
| 9575 | + "node_modules/tr46": { |
|---|
| 9576 | + "version": "0.0.3", |
|---|
| 9577 | + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", |
|---|
| 9578 | + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", |
|---|
| 9579 | + "license": "MIT" |
|---|
| 9580 | + }, |
|---|
| 9226 | 9581 | "node_modules/ts-interface-checker": { |
|---|
| 9227 | 9582 | "version": "0.1.13", |
|---|
| 9228 | 9583 | "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", |
|---|
| .. | .. |
|---|
| 9266 | 9621 | }, |
|---|
| 9267 | 9622 | "engines": { |
|---|
| 9268 | 9623 | "node": ">=14.17" |
|---|
| 9624 | + } |
|---|
| 9625 | + }, |
|---|
| 9626 | + "node_modules/ua-parser-js": { |
|---|
| 9627 | + "version": "1.0.41", |
|---|
| 9628 | + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", |
|---|
| 9629 | + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", |
|---|
| 9630 | + "funding": [ |
|---|
| 9631 | + { |
|---|
| 9632 | + "type": "opencollective", |
|---|
| 9633 | + "url": "https://opencollective.com/ua-parser-js" |
|---|
| 9634 | + }, |
|---|
| 9635 | + { |
|---|
| 9636 | + "type": "paypal", |
|---|
| 9637 | + "url": "https://paypal.me/faisalman" |
|---|
| 9638 | + }, |
|---|
| 9639 | + { |
|---|
| 9640 | + "type": "github", |
|---|
| 9641 | + "url": "https://github.com/sponsors/faisalman" |
|---|
| 9642 | + } |
|---|
| 9643 | + ], |
|---|
| 9644 | + "license": "MIT", |
|---|
| 9645 | + "bin": { |
|---|
| 9646 | + "ua-parser-js": "script/cli.js" |
|---|
| 9647 | + }, |
|---|
| 9648 | + "engines": { |
|---|
| 9649 | + "node": "*" |
|---|
| 9269 | 9650 | } |
|---|
| 9270 | 9651 | }, |
|---|
| 9271 | 9652 | "node_modules/undici-types": { |
|---|
| .. | .. |
|---|
| 9500 | 9881 | "defaults": "^1.0.3" |
|---|
| 9501 | 9882 | } |
|---|
| 9502 | 9883 | }, |
|---|
| 9884 | + "node_modules/webidl-conversions": { |
|---|
| 9885 | + "version": "3.0.1", |
|---|
| 9886 | + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", |
|---|
| 9887 | + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", |
|---|
| 9888 | + "license": "BSD-2-Clause" |
|---|
| 9889 | + }, |
|---|
| 9503 | 9890 | "node_modules/whatwg-fetch": { |
|---|
| 9504 | 9891 | "version": "3.6.20", |
|---|
| 9505 | 9892 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", |
|---|
| 9506 | 9893 | "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", |
|---|
| 9507 | 9894 | "license": "MIT" |
|---|
| 9508 | 9895 | }, |
|---|
| 9896 | + "node_modules/whatwg-url": { |
|---|
| 9897 | + "version": "5.0.0", |
|---|
| 9898 | + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", |
|---|
| 9899 | + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", |
|---|
| 9900 | + "license": "MIT", |
|---|
| 9901 | + "dependencies": { |
|---|
| 9902 | + "tr46": "~0.0.3", |
|---|
| 9903 | + "webidl-conversions": "^3.0.0" |
|---|
| 9904 | + } |
|---|
| 9905 | + }, |
|---|
| 9509 | 9906 | "node_modules/whatwg-url-minimum": { |
|---|
| 9510 | 9907 | "version": "0.1.1", |
|---|
| 9511 | 9908 | "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.1.tgz", |
|---|
| .. | .. |
|---|
| 4 | 4 | "main": "expo-router/entry", |
|---|
| 5 | 5 | "scripts": { |
|---|
| 6 | 6 | "start": "expo start", |
|---|
| 7 | | - "android": "expo start --android", |
|---|
| 8 | | - "ios": "expo start --ios", |
|---|
| 7 | + "android": "expo run:android", |
|---|
| 8 | + "ios": "expo run:ios", |
|---|
| 9 | 9 | "web": "expo start --web" |
|---|
| 10 | 10 | }, |
|---|
| 11 | 11 | "dependencies": { |
|---|
| 12 | + "@react-navigation/bottom-tabs": "^7.15.3", |
|---|
| 13 | + "@react-navigation/native": "^7.1.31", |
|---|
| 12 | 14 | "expo": "~55.0.4", |
|---|
| 13 | | - "expo-av": "^16.0.8", |
|---|
| 15 | + "expo-audio": "^55.0.8", |
|---|
| 16 | + "expo-constants": "~55.0.7", |
|---|
| 17 | + "expo-file-system": "~55.0.10", |
|---|
| 14 | 18 | "expo-haptics": "~55.0.8", |
|---|
| 19 | + "expo-linking": "~55.0.7", |
|---|
| 15 | 20 | "expo-router": "~55.0.3", |
|---|
| 16 | 21 | "expo-secure-store": "~55.0.8", |
|---|
| 22 | + "expo-speech-recognition": "^3.1.1", |
|---|
| 23 | + "expo-splash-screen": "~55.0.10", |
|---|
| 17 | 24 | "expo-status-bar": "~55.0.4", |
|---|
| 25 | + "expo-system-ui": "~55.0.9", |
|---|
| 26 | + "expo-web-browser": "~55.0.9", |
|---|
| 18 | 27 | "nativewind": "^4", |
|---|
| 19 | 28 | "react": "19.2.0", |
|---|
| 29 | + "react-dom": "^19.2.4", |
|---|
| 20 | 30 | "react-native": "0.83.2", |
|---|
| 21 | 31 | "react-native-gesture-handler": "~2.30.0", |
|---|
| 22 | 32 | "react-native-reanimated": "4.2.1", |
|---|
| 23 | 33 | "react-native-safe-area-context": "~5.6.2", |
|---|
| 24 | 34 | "react-native-screens": "~4.23.0", |
|---|
| 25 | | - "react-native-svg": "15.15.3" |
|---|
| 35 | + "react-native-svg": "15.15.3", |
|---|
| 36 | + "react-native-web": "^0.21.0", |
|---|
| 37 | + "react-native-worklets": "0.7.2" |
|---|
| 26 | 38 | }, |
|---|
| 27 | 39 | "devDependencies": { |
|---|
| 28 | 40 | "@types/react": "~19.2.2", |
|---|
| 29 | 41 | "babel-plugin-module-resolver": "^5.0.2", |
|---|
| 42 | + "babel-preset-expo": "^55.0.10", |
|---|
| 30 | 43 | "tailwindcss": "^3.4.19", |
|---|
| 31 | 44 | "typescript": "~5.9.2" |
|---|
| 32 | 45 | }, |
|---|
| .. | .. |
|---|
| 1 | | -import { Audio, AVPlaybackStatus } from "expo-av"; |
|---|
| 1 | +import { |
|---|
| 2 | + createAudioPlayer, |
|---|
| 3 | + requestRecordingPermissionsAsync, |
|---|
| 4 | + setAudioModeAsync, |
|---|
| 5 | +} from "expo-audio"; |
|---|
| 2 | 6 | |
|---|
| 3 | 7 | export interface RecordingResult { |
|---|
| 4 | 8 | uri: string; |
|---|
| 5 | 9 | durationMs: number; |
|---|
| 6 | 10 | } |
|---|
| 7 | 11 | |
|---|
| 8 | | -let currentRecording: Audio.Recording | null = null; |
|---|
| 9 | | -let currentSound: Audio.Sound | null = null; |
|---|
| 12 | +let currentPlayer: ReturnType<typeof createAudioPlayer> | null = null; |
|---|
| 10 | 13 | |
|---|
| 11 | | -async function requestPermissions(): Promise<boolean> { |
|---|
| 12 | | - const { status } = await Audio.requestPermissionsAsync(); |
|---|
| 14 | +export async function requestPermissions(): Promise<boolean> { |
|---|
| 15 | + const { status } = await requestRecordingPermissionsAsync(); |
|---|
| 13 | 16 | return status === "granted"; |
|---|
| 14 | | -} |
|---|
| 15 | | - |
|---|
| 16 | | -export async function startRecording(): Promise<Audio.Recording | null> { |
|---|
| 17 | | - const granted = await requestPermissions(); |
|---|
| 18 | | - if (!granted) return null; |
|---|
| 19 | | - |
|---|
| 20 | | - try { |
|---|
| 21 | | - await Audio.setAudioModeAsync({ |
|---|
| 22 | | - allowsRecordingIOS: true, |
|---|
| 23 | | - playsInSilentModeIOS: true, |
|---|
| 24 | | - }); |
|---|
| 25 | | - |
|---|
| 26 | | - const { recording } = await Audio.Recording.createAsync( |
|---|
| 27 | | - Audio.RecordingOptionsPresets.HIGH_QUALITY |
|---|
| 28 | | - ); |
|---|
| 29 | | - |
|---|
| 30 | | - currentRecording = recording; |
|---|
| 31 | | - return recording; |
|---|
| 32 | | - } catch (error) { |
|---|
| 33 | | - console.error("Failed to start recording:", error); |
|---|
| 34 | | - return null; |
|---|
| 35 | | - } |
|---|
| 36 | | -} |
|---|
| 37 | | - |
|---|
| 38 | | -export async function stopRecording(): Promise<RecordingResult | null> { |
|---|
| 39 | | - if (!currentRecording) return null; |
|---|
| 40 | | - |
|---|
| 41 | | - try { |
|---|
| 42 | | - await currentRecording.stopAndUnloadAsync(); |
|---|
| 43 | | - const status = await currentRecording.getStatusAsync(); |
|---|
| 44 | | - const uri = currentRecording.getURI(); |
|---|
| 45 | | - currentRecording = null; |
|---|
| 46 | | - |
|---|
| 47 | | - await Audio.setAudioModeAsync({ |
|---|
| 48 | | - allowsRecordingIOS: false, |
|---|
| 49 | | - }); |
|---|
| 50 | | - |
|---|
| 51 | | - if (!uri) return null; |
|---|
| 52 | | - |
|---|
| 53 | | - const durationMs = (status as { durationMillis?: number }).durationMillis ?? 0; |
|---|
| 54 | | - return { uri, durationMs }; |
|---|
| 55 | | - } catch (error) { |
|---|
| 56 | | - console.error("Failed to stop recording:", error); |
|---|
| 57 | | - currentRecording = null; |
|---|
| 58 | | - return null; |
|---|
| 59 | | - } |
|---|
| 60 | 17 | } |
|---|
| 61 | 18 | |
|---|
| 62 | 19 | export async function playAudio( |
|---|
| 63 | 20 | uri: string, |
|---|
| 64 | 21 | onFinish?: () => void |
|---|
| 65 | | -): Promise<Audio.Sound | null> { |
|---|
| 22 | +): Promise<void> { |
|---|
| 66 | 23 | try { |
|---|
| 67 | 24 | await stopPlayback(); |
|---|
| 68 | 25 | |
|---|
| 69 | | - await Audio.setAudioModeAsync({ |
|---|
| 70 | | - allowsRecordingIOS: false, |
|---|
| 71 | | - playsInSilentModeIOS: true, |
|---|
| 26 | + await setAudioModeAsync({ |
|---|
| 27 | + playsInSilentMode: true, |
|---|
| 72 | 28 | }); |
|---|
| 73 | 29 | |
|---|
| 74 | | - const { sound } = await Audio.Sound.createAsync( |
|---|
| 75 | | - { uri }, |
|---|
| 76 | | - { shouldPlay: true } |
|---|
| 77 | | - ); |
|---|
| 30 | + const player = createAudioPlayer(uri); |
|---|
| 31 | + currentPlayer = player; |
|---|
| 78 | 32 | |
|---|
| 79 | | - currentSound = sound; |
|---|
| 80 | | - |
|---|
| 81 | | - sound.setOnPlaybackStatusUpdate((status: AVPlaybackStatus) => { |
|---|
| 82 | | - if (status.isLoaded && status.didJustFinish) { |
|---|
| 33 | + player.addListener("playbackStatusUpdate", (status) => { |
|---|
| 34 | + if (!status.playing && status.currentTime >= status.duration && status.duration > 0) { |
|---|
| 83 | 35 | onFinish?.(); |
|---|
| 84 | | - sound.unloadAsync().catch(() => {}); |
|---|
| 85 | | - currentSound = null; |
|---|
| 36 | + player.remove(); |
|---|
| 37 | + if (currentPlayer === player) currentPlayer = null; |
|---|
| 86 | 38 | } |
|---|
| 87 | 39 | }); |
|---|
| 88 | 40 | |
|---|
| 89 | | - return sound; |
|---|
| 41 | + player.play(); |
|---|
| 90 | 42 | } catch (error) { |
|---|
| 91 | 43 | console.error("Failed to play audio:", error); |
|---|
| 92 | | - return null; |
|---|
| 93 | 44 | } |
|---|
| 94 | 45 | } |
|---|
| 95 | 46 | |
|---|
| 96 | 47 | export async function stopPlayback(): Promise<void> { |
|---|
| 97 | | - if (currentSound) { |
|---|
| 48 | + if (currentPlayer) { |
|---|
| 98 | 49 | try { |
|---|
| 99 | | - await currentSound.stopAsync(); |
|---|
| 100 | | - await currentSound.unloadAsync(); |
|---|
| 50 | + currentPlayer.pause(); |
|---|
| 51 | + currentPlayer.remove(); |
|---|
| 101 | 52 | } catch { |
|---|
| 102 | | - // Ignore errors during cleanup |
|---|
| 53 | + // Ignore cleanup errors |
|---|
| 103 | 54 | } |
|---|
| 104 | | - currentSound = null; |
|---|
| 55 | + currentPlayer = null; |
|---|
| 105 | 56 | } |
|---|
| 106 | 57 | } |
|---|
| 107 | 58 | |
|---|
| 108 | | -export function encodeAudioToBase64(uri: string): Promise<string> { |
|---|
| 109 | | - // In React Native, we'd use FileSystem from expo-file-system |
|---|
| 110 | | - // For now, return the URI as-is since we may not have expo-file-system |
|---|
| 111 | | - return Promise.resolve(uri); |
|---|
| 59 | +export async function encodeAudioToBase64(uri: string): Promise<string> { |
|---|
| 60 | + const FileSystem = await import("expo-file-system"); |
|---|
| 61 | + const result = await FileSystem.readAsStringAsync(uri, { |
|---|
| 62 | + encoding: FileSystem.EncodingType.Base64, |
|---|
| 63 | + }); |
|---|
| 64 | + return result; |
|---|
| 112 | 65 | } |
|---|
| .. | .. |
|---|
| 1 | 1 | export type MessageRole = "user" | "assistant" | "system"; |
|---|
| 2 | | -export type MessageType = "text" | "voice"; |
|---|
| 2 | +export type MessageType = "text" | "voice" | "image"; |
|---|
| 3 | 3 | |
|---|
| 4 | 4 | export interface Message { |
|---|
| 5 | 5 | id: string; |
|---|
| .. | .. |
|---|
| 7 | 7 | type: MessageType; |
|---|
| 8 | 8 | content: string; |
|---|
| 9 | 9 | audioUri?: string; |
|---|
| 10 | + imageBase64?: string; |
|---|
| 10 | 11 | timestamp: number; |
|---|
| 11 | 12 | status?: "sending" | "sent" | "error"; |
|---|
| 12 | 13 | duration?: number; |
|---|
| .. | .. |
|---|
| 19 | 20 | |
|---|
| 20 | 21 | export type ConnectionStatus = "disconnected" | "connecting" | "connected"; |
|---|
| 21 | 22 | |
|---|
| 22 | | -export interface WebSocketMessage { |
|---|
| 23 | | - type: "text" | "voice"; |
|---|
| 23 | +// --- WebSocket protocol --- |
|---|
| 24 | + |
|---|
| 25 | +/** Outgoing from app to watcher */ |
|---|
| 26 | +export interface WsTextMessage { |
|---|
| 27 | + type: "text"; |
|---|
| 28 | + content: string; |
|---|
| 29 | +} |
|---|
| 30 | + |
|---|
| 31 | +export interface WsVoiceMessage { |
|---|
| 32 | + type: "voice"; |
|---|
| 33 | + audioBase64: string; |
|---|
| 34 | + content: string; |
|---|
| 35 | +} |
|---|
| 36 | + |
|---|
| 37 | +export interface WsCommandMessage { |
|---|
| 38 | + type: "command"; |
|---|
| 39 | + command: string; |
|---|
| 40 | + args?: Record<string, unknown>; |
|---|
| 41 | +} |
|---|
| 42 | + |
|---|
| 43 | +export type WsOutgoing = WsTextMessage | WsVoiceMessage | WsCommandMessage; |
|---|
| 44 | + |
|---|
| 45 | +/** Incoming from watcher to app */ |
|---|
| 46 | +export interface WsIncomingText { |
|---|
| 47 | + type: "text"; |
|---|
| 48 | + content: string; |
|---|
| 49 | +} |
|---|
| 50 | + |
|---|
| 51 | +export interface WsIncomingVoice { |
|---|
| 52 | + type: "voice"; |
|---|
| 24 | 53 | content: string; |
|---|
| 25 | 54 | audioBase64?: string; |
|---|
| 26 | 55 | } |
|---|
| 56 | + |
|---|
| 57 | +export interface WsIncomingImage { |
|---|
| 58 | + type: "image"; |
|---|
| 59 | + imageBase64: string; |
|---|
| 60 | + caption?: string; |
|---|
| 61 | +} |
|---|
| 62 | + |
|---|
| 63 | +export interface WsSession { |
|---|
| 64 | + index: number; |
|---|
| 65 | + name: string; |
|---|
| 66 | + type: "claude" | "terminal"; |
|---|
| 67 | + isActive: boolean; |
|---|
| 68 | + id: string; |
|---|
| 69 | +} |
|---|
| 70 | + |
|---|
| 71 | +export interface WsIncomingSessions { |
|---|
| 72 | + type: "sessions"; |
|---|
| 73 | + sessions: WsSession[]; |
|---|
| 74 | +} |
|---|
| 75 | + |
|---|
| 76 | +export interface WsIncomingSessionSwitched { |
|---|
| 77 | + type: "session_switched"; |
|---|
| 78 | + name: string; |
|---|
| 79 | + sessionId: string; |
|---|
| 80 | +} |
|---|
| 81 | + |
|---|
| 82 | +export interface WsIncomingSessionRenamed { |
|---|
| 83 | + type: "session_renamed"; |
|---|
| 84 | + sessionId: string; |
|---|
| 85 | + name: string; |
|---|
| 86 | +} |
|---|
| 87 | + |
|---|
| 88 | +export interface WsIncomingError { |
|---|
| 89 | + type: "error"; |
|---|
| 90 | + message: string; |
|---|
| 91 | +} |
|---|
| 92 | + |
|---|
| 93 | +export type WsIncoming = |
|---|
| 94 | + | WsIncomingText |
|---|
| 95 | + | WsIncomingVoice |
|---|
| 96 | + | WsIncomingImage |
|---|
| 97 | + | WsIncomingSessions |
|---|
| 98 | + | WsIncomingSessionSwitched |
|---|
| 99 | + | WsIncomingSessionRenamed |
|---|
| 100 | + | WsIncomingError; |
|---|