import { useEffect, useMemo, useRef, useState } from "preact/hooks"; import { LogOut, MessageSquare, Plus, SendHorizontal, ShieldCheck } from "lucide-preact"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Separator } from "@/components/ui/separator"; import { createChat, getChat, getConfiguredToken, listChats, runCompletion, setAuthToken, verifySession, type ChatDetail, type ChatSummary, type CompletionRequestMessage, } from "@/lib/api"; import { cn } from "@/lib/utils"; type Provider = "openai" | "anthropic" | "xai"; type AuthMode = "open" | "token"; const PROVIDER_DEFAULT_MODELS: Record = { openai: "gpt-4.1-mini", anthropic: "claude-3-5-sonnet-latest", xai: "grok-3-mini", }; const TOKEN_STORAGE_KEY = "sybil_admin_token"; function getChatTitle(chat: Pick, messages?: ChatDetail["messages"]) { if (chat.title?.trim()) return chat.title.trim(); const firstUserMessage = messages?.find((m) => m.role === "user")?.content.trim(); if (firstUserMessage) return firstUserMessage.slice(0, 48); return "New chat"; } function formatDate(value: string) { return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }).format(new Date(value)); } function readStoredToken() { return localStorage.getItem(TOKEN_STORAGE_KEY)?.trim() || null; } function persistToken(token: string | null) { if (token) { localStorage.setItem(TOKEN_STORAGE_KEY, token); return; } localStorage.removeItem(TOKEN_STORAGE_KEY); } function normalizeAuthError(message: string) { if (message.includes("missing bearer token") || message.includes("invalid bearer token")) { return "Authentication failed. Enter the ADMIN_TOKEN configured in server/.env."; } return message; } export default function App() { const initialToken = readStoredToken() ?? getConfiguredToken() ?? ""; const [authTokenInput, setAuthTokenInput] = useState(initialToken); const [isCheckingSession, setIsCheckingSession] = useState(true); const [isSigningIn, setIsSigningIn] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false); const [authMode, setAuthMode] = useState(null); const [authError, setAuthError] = useState(null); const [chats, setChats] = useState([]); const [selectedChatId, setSelectedChatId] = useState(null); const [selectedChat, setSelectedChat] = useState(null); const [isLoadingChats, setIsLoadingChats] = useState(false); const [isLoadingChat, setIsLoadingChat] = useState(false); const [isSending, setIsSending] = useState(false); const [composer, setComposer] = useState(""); const [provider, setProvider] = useState("openai"); const [model, setModel] = useState(PROVIDER_DEFAULT_MODELS.openai); const [error, setError] = useState(null); const transcriptEndRef = useRef(null); const completeSessionCheck = async (tokenCandidate: string | null) => { setAuthToken(tokenCandidate); const session = await verifySession(); setIsAuthenticated(true); setAuthMode(session.mode); setAuthError(null); persistToken(tokenCandidate); }; const handleAuthFailure = (message: string) => { setIsAuthenticated(false); setAuthMode(null); setAuthError(normalizeAuthError(message)); setAuthToken(null); persistToken(null); setChats([]); setSelectedChatId(null); setSelectedChat(null); }; const refreshChats = async (preferredChatId?: string) => { setIsLoadingChats(true); try { const nextChats = await listChats(); setChats(nextChats); setSelectedChatId((current) => { if (preferredChatId && nextChats.some((chat) => chat.id === preferredChatId)) { return preferredChatId; } if (current && nextChats.some((chat) => chat.id === current)) { return current; } return nextChats[0]?.id ?? null; }); } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("bearer token")) { handleAuthFailure(message); } else { setError(message); } } finally { setIsLoadingChats(false); } }; const refreshChat = async (chatId: string) => { setIsLoadingChat(true); try { const chat = await getChat(chatId); setSelectedChat(chat); } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("bearer token")) { handleAuthFailure(message); } else { setError(message); } } finally { setIsLoadingChat(false); } }; useEffect(() => { const token = readStoredToken() ?? getConfiguredToken(); void (async () => { try { await completeSessionCheck(token); } catch (err) { const message = err instanceof Error ? err.message : String(err); handleAuthFailure(message); } finally { setIsCheckingSession(false); } })(); }, []); useEffect(() => { if (!isAuthenticated) return; void refreshChats(); }, [isAuthenticated]); useEffect(() => { if (!isAuthenticated) { setSelectedChat(null); return; } if (!selectedChatId) { setSelectedChat(null); return; } void refreshChat(selectedChatId); }, [isAuthenticated, selectedChatId]); useEffect(() => { transcriptEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); }, [selectedChat?.messages.length, isSending]); const messages = selectedChat?.messages ?? []; const selectedChatTitle = useMemo(() => { if (!selectedChat) return "Sybil"; return getChatTitle(selectedChat, selectedChat.messages); }, [selectedChat]); const handleCreateChat = async () => { setError(null); try { const chat = await createChat(); setSelectedChatId(chat.id); setSelectedChat({ id: chat.id, title: chat.title, createdAt: chat.createdAt, updatedAt: chat.updatedAt, messages: [], }); await refreshChats(chat.id); } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("bearer token")) { handleAuthFailure(message); } else { setError(message); } } }; const handleSend = async () => { const content = composer.trim(); if (!content || isSending) return; setComposer(""); setError(null); setIsSending(true); let chatId = selectedChatId; try { if (!chatId) { const chat = await createChat(); chatId = chat.id; setSelectedChatId(chatId); } if (!chatId) { throw new Error("Unable to initialize chat"); } let baseChat = selectedChat; if (!baseChat || baseChat.id !== chatId) { baseChat = await getChat(chatId); } const optimisticUserMessage = { id: `temp-user-${Date.now()}`, createdAt: new Date().toISOString(), role: "user" as const, content, name: null, }; const optimisticAssistantMessage = { id: `temp-assistant-${Date.now()}`, createdAt: new Date().toISOString(), role: "assistant" as const, content: "", name: null, }; setSelectedChat((current) => { if (!current || current.id !== chatId) return current; return { ...current, messages: [...current.messages, optimisticUserMessage, optimisticAssistantMessage], }; }); const requestMessages: CompletionRequestMessage[] = [ ...baseChat.messages.map((message) => ({ role: message.role, content: message.content, ...(message.name ? { name: message.name } : {}), })), { role: "user", content, }, ]; await runCompletion({ chatId, provider, model: model.trim(), messages: requestMessages, }); await Promise.all([refreshChats(chatId), refreshChat(chatId)]); } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("bearer token")) { handleAuthFailure(message); } else { setError(message); } if (chatId) { await refreshChat(chatId); } } finally { setIsSending(false); } }; const handleSignIn = async (tokenCandidate: string | null) => { setIsSigningIn(true); setAuthError(null); try { await completeSessionCheck(tokenCandidate); await refreshChats(); } catch (err) { const message = err instanceof Error ? err.message : String(err); setAuthError(normalizeAuthError(message)); setIsAuthenticated(false); setAuthMode(null); } finally { setIsSigningIn(false); } }; const handleLogout = () => { setAuthToken(null); persistToken(null); setIsAuthenticated(false); setAuthMode(null); setAuthError(null); setChats([]); setSelectedChatId(null); setSelectedChat(null); setComposer(""); setError(null); }; if (isCheckingSession) { return (

Checking session...

); } if (!isAuthenticated) { return (

Sign in to Sybil

Use your backend admin token.

{ event.preventDefault(); void handleSignIn(authTokenInput.trim() || null); }} > setAuthTokenInput(event.currentTarget.value)} disabled={isSigningIn} />
{authError ?

{authError}

: null}

If `ADMIN_TOKEN` is set in `/server/.env`, token login is required.

); } return (

{selectedChatTitle}

Sybil Web{authMode ? ` (${authMode === "open" ? "open mode" : "token mode"})` : ""}

setModel(event.currentTarget.value)} placeholder="Model" disabled={isSending} />
{isLoadingChat && messages.length === 0 ?

Loading messages...

: null} {!isLoadingChat && messages.length === 0 ? (

How can I help today?

Ask a question to begin this conversation.

) : null}
{messages.map((message) => { const isUser = message.role === "user"; const isPendingAssistant = message.id.startsWith("temp-assistant-") && isSending; return (
{isPendingAssistant ? "Thinking..." : message.content}
); })}