import React, { createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"; import * as SecureStore from "expo-secure-store"; import { ConnectionStatus, ServerConfig, WsIncoming, WsOutgoing, } from "../types"; import { wsClient } from "../services/websocket"; import { sendWol, isValidMac } from "../services/wol"; const SECURE_STORE_KEY = "pailot_server_config"; interface ConnectionContextValue { serverConfig: ServerConfig | null; status: ConnectionStatus; connect: (config?: ServerConfig) => void; disconnect: () => void; sendTextMessage: (text: string, sessionId?: string) => boolean; sendVoiceMessage: (audioBase64: string, transcript?: string, messageId?: string, sessionId?: string) => boolean; sendImageMessage: (imageBase64: string, caption: string, mimeType: string, sessionId?: string) => boolean; sendCommand: (command: string, args?: Record, sessionId?: string) => boolean; saveServerConfig: (config: ServerConfig) => Promise; onMessageReceived: React.MutableRefObject< ((data: WsIncoming) => void) | null >; } const ConnectionContext = createContext(null); export function ConnectionProvider({ children, }: { children: React.ReactNode; }) { const [serverConfig, setServerConfig] = useState(null); const [status, setStatus] = useState("disconnected"); const onMessageReceived = useRef<((data: WsIncoming) => void) | null>(null); useEffect(() => { loadConfig(); }, []); useEffect(() => { wsClient.setCallbacks({ onOpen: () => setStatus("connected"), onClose: () => setStatus("disconnected"), onReconnecting: () => setStatus("reconnecting"), onError: () => setStatus("disconnected"), onMessage: (data) => { const msg = data as unknown as WsIncoming; // Handle server-side status changes (compaction indicator) if (msg.type === "status") { if (msg.status === "compacting") setStatus("compacting"); else if (msg.status === "online") setStatus("connected"); return; } onMessageReceived.current?.(msg); }, }); }, []); async function loadConfig() { try { const stored = await SecureStore.getItemAsync(SECURE_STORE_KEY); if (stored) { const config = JSON.parse(stored) as ServerConfig; setServerConfig(config); connectToServer(config); } } catch { // No stored config } } async function connectToServer(config: ServerConfig) { setStatus("connecting"); // Fire-and-forget WoL — never block the WebSocket connection if (config.macAddress && isValidMac(config.macAddress)) { sendWol(config.macAddress, config.host).catch(() => {}); } // Build URL list: local first (preferred), then remote const urls: string[] = []; if (config.localHost) { urls.push(`ws://${config.localHost}:${config.port}`); } urls.push(`ws://${config.host}:${config.port}`); wsClient.connect(urls); } const connect = useCallback( (config?: ServerConfig) => { const target = config ?? serverConfig; if (!target) return; connectToServer(target); }, [serverConfig] ); const disconnect = useCallback(() => { wsClient.disconnect(); setStatus("disconnected"); }, []); const saveServerConfig = useCallback(async (config: ServerConfig) => { await SecureStore.setItemAsync(SECURE_STORE_KEY, JSON.stringify(config)); setServerConfig(config); }, []); const sendTextMessage = useCallback((text: string, sessionId?: string): boolean => { return wsClient.send({ type: "text", content: text, sessionId }); }, []); const sendVoiceMessage = useCallback( (audioBase64: string, transcript: string = "", messageId?: string, sessionId?: string): boolean => { return wsClient.send({ type: "voice", content: transcript, audioBase64, messageId, sessionId, }); }, [] ); const sendImageMessage = useCallback( (imageBase64: string, caption: string = "", mimeType: string = "image/jpeg", sessionId?: string): boolean => { return wsClient.send({ type: "image", imageBase64, caption, mimeType, sessionId }); }, [] ); const sendCommand = useCallback( (command: string, args?: Record, sessionId?: string): boolean => { const msg: WsOutgoing = { type: "command", command, args, sessionId }; return wsClient.send(msg as any); }, [] ); return ( {children} ); } export function useConnection() { const ctx = useContext(ConnectionContext); if (!ctx) throw new Error("useConnection must be used within ConnectionProvider"); return ctx; }