"use client" /** * ChatContext — chat state that persists across route changes AND across * hard refreshes (via the server-side /api/chat/history endpoint). * * THREE LAYERS OF STATE: * 1. React Context — survives client-side navigation between routes * 2. localStorage — remembers which sessionKey was active across reloads * 3. Server-side history — sqlite in bridge; survives device change, tab close * * SESSION MODEL (post WS-migration): * Each chat is keyed by a `sessionKey` like: * - "agent:main:main" → the default Tiger conversation * - "agent:main:webchat-<8hex>" → a fresh session created via "+ New" * The sessionKey is passed to /api/chat so the gateway routes to the right * conversation memory. It's also passed to /api/chat/history?sessionId=. * * FLOW: * Mount → load sessions list → load history for current sessionKey * New → POST /api/chat/sessions → set as current, clear messages * Switch → set current → load history * Send → optimistic UI → POST /api/chat with current sessionKey * Clear → DELETE /api/chat/history?sessionId= * Delete → DELETE /api/chat/sessions?key= → drop, switch to Main */ import * as React from "react" const DEFAULT_SESSION_KEY = "agent:main:main" const STORAGE_KEY = "tiger.currentSessionKey" export type ChatMessage = { id: string role: "user" | "agent" | "system" content: string streaming?: boolean timestamp: number } export type ChatSession = { key: string label: string updatedAt: number | null messageCount: number isDefault: boolean } const DEFAULT_WELCOME: ChatMessage = { id: "welcome", role: "agent", content: "Hey! I am Tiger, your AI assistant. Send me a message to get started.", timestamp: 0, } type ChatContextValue = { messages: ChatMessage[] setMessages: React.Dispatch> loading: boolean /** The sessionKey currently being chatted with — sent to /api/chat. */ currentSessionKey: string /** All webchat-visible sessions (from gateway sessions.list). */ sessions: ChatSession[] /** Switch to an existing session — loads its history. */ selectSession: (key: string) => Promise /** Mint and switch to a brand-new session — clears messages. */ newSession: () => Promise /** Delete a non-default session — switches to Main if the active one is removed. */ deleteSession: (key: string) => Promise /** Wipe the *current* session's history (keeps the session itself). */ clearChat: () => Promise /** Force-refresh the sessions list (e.g., after a send so updatedAt updates). */ refreshSessions: () => Promise } const ChatContext = React.createContext(null) /** Read the saved sessionKey from localStorage (SSR-safe). */ function readPersistedKey(): string { if (typeof window === "undefined") return DEFAULT_SESSION_KEY try { return localStorage.getItem(STORAGE_KEY) || DEFAULT_SESSION_KEY } catch { return DEFAULT_SESSION_KEY } } function writePersistedKey(key: string): void { if (typeof window === "undefined") return try { localStorage.setItem(STORAGE_KEY, key) } catch { /* quota / private mode */ } } /** Fetch history for a sessionKey, returning hydrated messages (welcome first). */ async function loadHistoryFor(sessionKey: string): Promise { try { const r = await fetch(`/api/chat/history?sessionId=${encodeURIComponent(sessionKey)}`, { cache: "no-store" }) if (!r.ok) throw new Error(`history ${r.status}`) const data = await r.json() if (!data?.ok || !Array.isArray(data.messages)) return [DEFAULT_WELCOME] return [ DEFAULT_WELCOME, ...data.messages.map((m: any) => ({ id: String(m.id), role: m.role, content: m.content, timestamp: m.timestamp, })), ] } catch (err) { console.warn("[chat] could not load history for", sessionKey, err) return [DEFAULT_WELCOME] } } export function ChatProvider({ children }: { children: React.ReactNode }) { const [messages, setMessages] = React.useState([DEFAULT_WELCOME]) const [loading, setLoading] = React.useState(true) const [currentSessionKey, setCurrentSessionKey] = React.useState(DEFAULT_SESSION_KEY) const [sessions, setSessions] = React.useState([]) const refreshSessions = React.useCallback(async () => { try { const r = await fetch("/api/chat/sessions", { cache: "no-store" }) if (!r.ok) return const data = await r.json() if (data?.ok && Array.isArray(data.sessions)) setSessions(data.sessions) } catch (err) { console.warn("[chat] sessions list failed:", err) } }, []) // Initial mount: pick stored sessionKey, load its history, fetch sessions list. React.useEffect(() => { let cancelled = false // Check URL for session param const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '') const sessionParam = urlParams.get('session') let initialKey = sessionParam ? `agent:main:${sessionParam}` : readPersistedKey() async function init() { if (cancelled) return setCurrentSessionKey(initialKey) const [hist] = await Promise.all([loadHistoryFor(initialKey), refreshSessions()]) if (!cancelled) { setMessages(hist) setLoading(false) } } init() return () => { cancelled = true } }, [refreshSessions]) const selectSession = React.useCallback(async (key: string) => { if (key === currentSessionKey) return setLoading(true) setCurrentSessionKey(key) writePersistedKey(key) const hist = await loadHistoryFor(key) setMessages(hist) setLoading(false) }, [currentSessionKey]) const newSession = React.useCallback(async () => { try { const r = await fetch("/api/chat/sessions", { method: "POST" }) const data = await r.json() if (!r.ok || !data?.ok || !data.session?.key) { console.warn("[chat] new session failed:", data) return } const key = data.session.key as string setCurrentSessionKey(key) writePersistedKey(key) setMessages([DEFAULT_WELCOME]) // Add to local list optimistically; full refresh happens after first send setSessions(prev => { if (prev.find(s => s.key === key)) return prev return [...prev, data.session as ChatSession] }) } catch (err) { console.warn("[chat] new session error:", err) } }, []) const deleteSession = React.useCallback(async (key: string) => { if (key === DEFAULT_SESSION_KEY) return try { await fetch(`/api/chat/sessions?key=${encodeURIComponent(key)}`, { method: "DELETE" }) } catch (err) { console.warn("[chat] delete session failed:", err) } setSessions(prev => prev.filter(s => s.key !== key)) // If the deleted session was active, fall back to Main. if (key === currentSessionKey) { setCurrentSessionKey(DEFAULT_SESSION_KEY) writePersistedKey(DEFAULT_SESSION_KEY) const hist = await loadHistoryFor(DEFAULT_SESSION_KEY) setMessages(hist) } }, [currentSessionKey]) const clearChat = React.useCallback(async () => { setMessages([DEFAULT_WELCOME]) try { await fetch(`/api/chat/history?sessionId=${encodeURIComponent(currentSessionKey)}`, { method: "DELETE" }) } catch (err) { console.warn("[chat] clear server failed (local cleared):", err) } }, [currentSessionKey]) return ( {children} ) } export function useChatContext(): ChatContextValue { const ctx = React.useContext(ChatContext) if (!ctx) { throw new Error( "useChatContext must be used inside . " + "Make sure app/layout.tsx wraps children with ." ) } return ctx }