import { StrictMode, useEffect, useRef, useState } from "react";
import type { CSSProperties, FormEvent, PointerEvent as ReactPointerEvent } from "react";
import { createRoot } from "react-dom/client";
import type { InternalDocReference, RecommendedAction } from "@pi-chat/shared";
import {
  api,
  authorizedHeaders,
  jsonHeaders,
  AUTH_TOKEN_STORAGE_KEY,
  formatDuration,
  formatNumber,
  formatScore,
  parseMetadata,
  temporaryId,
} from "./api";
import { MarkdownView } from "./code-block";
import DocModal, { type KnowledgeDoc } from "./DocModal";
import WebhookFormTab, { getWebhookFormTabParams } from "./WebhookFormTab";
import { ErrorBoundary } from "./ErrorBoundary";
import "./styles.css";

type ChatSession = {
  id: string;
  title: string | null;
  system_prompt?: string | null;
  updated_at: string;
};

type AuthUser = {
  id: string;
  username?: string;
  email?: string;
  roles: string[];
};

type ChatMessage = {
  id: string;
  role: "user" | "assistant" | "system" | "tool";
  content: string;
  metadata: string | null;
  created_at?: string;
};

type SessionDetail = ChatSession & { messages: ChatMessage[] };

type SessionExportMessage = {
  id: string;
  session_id: string;
  user_id: string;
  role: "user" | "assistant" | "system" | "tool";
  content: string;
  metadata: string | null;
  created_at?: string;
};

type SessionExport = {
  version: number;
  exported_at: string;
  session: { id: string; title: string | null; created_at: string; updated_at: string };
  messages: SessionExportMessage[];
};

const escapeMarkdown = (value: string): string =>
  value
    // Preserve newlines as GFM breaks; everything else gets a light pass.
    .replace(/\r\n/g, "\n")
    .trim();

const renderMetadata = (raw: string | null): string => {
  if (!raw) return "";
  try {
    const parsed = JSON.parse(raw) as {
      usage?: {
        durationMs?: number;
        total_tokens?: number;
        cached_tokens?: number;
      };
    };
    const parts: string[] = [];
    if (typeof parsed.usage?.durationMs === "number") {
      parts.push(`${(parsed.usage.durationMs / 1000).toFixed(1)} s`);
    }
    if (typeof parsed.usage?.total_tokens === "number") {
      const total = parsed.usage.total_tokens;
      const cached = parsed.usage.cached_tokens;
      parts.push(cached ? `${total} tok (${cached} cached)` : `${total} tok`);
    }
    return parts.length > 0 ? ` · ${parts.join(" · ")}` : "";
  } catch {
    return "";
  }
};

const sessionToMarkdown = (payload: SessionExport): string => {
  const title = payload.session.title ?? "Untitled session";
  const lines: string[] = [
    `# ${title}`,
    "",
    `_Exported from SIC at ${payload.exported_at}._`,
    "",
    `Session id: \`${payload.session.id}\``,
    "",
    "---",
    "",
  ];

  for (const message of payload.messages) {
    const time = message.created_at
      ? new Date(message.created_at).toISOString().replace("T", " ").slice(0, 19)
      : "";
    const role = message.role.charAt(0).toUpperCase() + message.role.slice(1);
    const meta = message.role === "assistant" ? renderMetadata(message.metadata) : "";
    lines.push(`## ${role}${time ? ` · ${time}` : ""}${meta}`, "");
    lines.push(escapeMarkdown(message.content), "", "---", "");
  }

  return lines.join("\n");
};

type PublicWebhook = {
  id: string;
  label: string;
  description?: string;
  method: string;
  required_roles: string[];
  confirmation_required: boolean;
};

type WebhookRun = {
  id: string;
  webhook_id: string;
  session_id: string;
  status: "success" | "error";
  response_status: number | null;
  attempts: number;
  created_at: string;
};

type WebhookUsage = { runs: number; successes: number; success_rate: number };

type ChatModel = {
  id: string;
  label: string;
  provider: string;
  max_tokens?: number;
};

type StreamEvent =
  | { type: "docs"; docs: InternalDocReference[] }
  | { type: "token"; token: string }
  | { type: "actions"; actions: RecommendedAction[] }
  | { type: "error"; error: string }
  | { type: "done" };

const THEME_STORAGE_KEY = "sic.theme";
const LANGUAGE_STORAGE_KEY = "sic.language";
const AUDIT_POLL_INTERVAL_MS = 5_000;

type Theme = "dark" | "light";
type Language = "es" | "en";

const copy = {
  es: {
    newChat: "New chat",
    renameChat: "Rename chat",
    deleteChat: "Delete chat",
    exportChat: "Export",
    importChat: "Import",
    devToken: "Dev token",
    devTokenPlaceholder: "JWT for AUTH_MODE=keycloak",
    devTokenHelp:
      "Only for testing Keycloak before full login exists: paste a JWT and the UI sends it as Bearer. Leave empty in AUTH_MODE=local.",
    saveToken: "Save token",
    model: "Model",
    noUser: "no user",
    empty: "Ask something to start reliable history.",
    repeat: "Repeat query",
    askPlaceholder: "Ask something...",
    send: "Send",
    stop: "Stop",
    docs: "Documentation",
    noDocs: "No recommended docs.",
    relevance: "relevance",
    open: "Open",
    webhooks: "Webhooks",
    noWebhooks: "No webhooks available for your role.",
    suggested: "Suggested",
    mostUsed: "Most used",
    confidence: "confidence",
    execute: "Run",
    executeHelp: "Open this webhook's form in another tab",
    audit: "Audit",
    auditLive: "Live updates",
    noRuns: "No audited runs.",
    close: "Close",
    themeLabel: "Toggle theme",
    languageLabel: "Toggle language",
    showMore: "Show more",
    showLess: "Show less",
    showMoreCount: "({count} hidden)",
    openInTab: "Open in another tab",
    settings: "Settings",
    settingsOpen: "Open settings",
    contextSet: "Per-session context",
    contextEdit: "Edit per-session context",
    contextPlaceholder:
      "Add persistent context for this session (incident id, on-call name, runbook refs). Prepended to every chat turn until cleared.",
    contextHelp:
      "Sent to the model as a system message on every turn. Cleared by emptying the field. Max 8000 chars.",
    contextSave: "Save",
    contextClear: "Clear",
  },
  en: {
    newChat: "New chat",
    renameChat: "Rename chat",
    deleteChat: "Delete chat",
    exportChat: "Export",
    importChat: "Import",
    devToken: "Dev token",
    devTokenPlaceholder: "JWT for AUTH_MODE=keycloak",
    devTokenHelp:
      "Only for testing Keycloak before full login exists: paste a JWT and the UI sends it as Bearer. Leave empty in AUTH_MODE=local.",
    saveToken: "Save token",
    model: "Model",
    noUser: "no user",
    empty: "Ask something to start reliable history.",
    repeat: "Repeat query",
    askPlaceholder: "Ask something...",
    send: "Send",
    stop: "Stop",
    docs: "Documentation",
    noDocs: "No recommended docs.",
    relevance: "relevance",
    open: "Open",
    webhooks: "Webhooks",
    noWebhooks: "No webhooks available for your role.",
    suggested: "Suggested",
    mostUsed: "Most used",
    confidence: "confidence",
    execute: "Run",
    executeHelp: "Open this webhook's form in another tab",
    audit: "Audit",
    auditLive: "Live updates",
    noRuns: "No audited runs.",
    close: "Close",
    themeLabel: "Toggle theme",
    languageLabel: "Toggle language",
    showMore: "Show more",
    showLess: "Show less",
    showMoreCount: "({count} hidden)",
    openInTab: "Open in another tab",
    settings: "Settings",
    settingsOpen: "Open settings",
    contextSet: "Per-session context",
    contextEdit: "Edit per-session context",
    contextPlaceholder:
      "Add persistent context for this session (incident id, on-call name, runbook refs). Prepended to every chat turn until cleared.",
    contextHelp:
      "Sent to the model as a system message on every turn. Cleared by emptying the field. Max 8000 chars.",
    contextSave: "Save",
    contextClear: "Clear",
  },
} satisfies Record<Language, Record<string, string>>;

type DynamicKey =
  | "loadingSessions"
  | "loadingModels"
  | "loadingWebhooks"
  | "loadingUser"
  | "cannotConnect"
  | "ready"
  | "openingDoc"
  | "cannotOpenDoc"
  | "noSessions"
  | "creatingSession"
  | "sendingQuery"
  | "persistingQuery"
  | "thinking"
  | "retrievingDocs"
  | "writing"
  | "preparingActions"
  | "errorPrefix"
  | "savedToken"
  | "clearedToken"
  | "savedTokenButRejected"
  | "renamed"
  | "cannotRename"
  | "cannotDelete"
  | "openingWebhook"
  | "openingWebhookFailed"
  | "queryStopped"
  | "queryStoppedDetail"
  | "queryFailed"
  | "queryFailedDetail"
  | "savedResponse"
  | "stopped"
  | "failed"
  | "resizeLeft"
  | "resizeRight"
  | "untitled"
  | "confirmDelete"
  | "renamePromptTitle"
  | "attempts"
  | "contextSaved"
  | "contextCleared"
  | "cannotSaveContext"
  | "cannotRename"
  | "cannotDelete"
  | "openingWebhook"
  | "openingWebhookFailed"
  | "queryStopped"
  | "deleteAllChats"
  | "deleteAllTitle"
  | "deleteAllPrompt"
  | "deleteAllPlaceholder"
  | "deleteAllConfirm"
  | "deleteAllCancel"
  | "deleteAllMustMatch"
  | "deleteAllSuccess"
  | "deleteAllFailed"
  | "queryStoppedDetail"
  | "queryFailed"
  | "queryFailedDetail"
  | "savedResponse"
  | "stopped"
  | "failed"
  | "resizeLeft"
  | "resizeRight"
  | "untitled"
  | "confirmDelete"
  | "renamePromptTitle"
  | "attempts"
  | "exported"
  | "cannotExport"
  | "imported"
  | "cannotImport";

const dynamic: Record<Language, Record<DynamicKey, string>> = {
  es: {
    loadingSessions: "Loading sessions...",
    loadingModels: "Loading models...",
    loadingWebhooks: "Loading webhooks...",
    loadingUser: "Loading user...",
    cannotConnect: "Cannot connect to the API",
    ready: "Ready",
    openingDoc: "Document opened: {title}",
    cannotOpenDoc: "Cannot open document",
    noSessions: "No sessions",
    creatingSession: "Creating session...",
    sendingQuery: "Sending query...",
    persistingQuery: "Persisting message and querying MiniMax...",
    thinking: "Querying model...",
    retrievingDocs: "Searching internal docs...",
    writing: "Writing response...",
    preparingActions: "Preparing suggested actions...",
    errorPrefix: "Handled error: {error}",
    savedToken: "Token saved. Reloading user...",
    clearedToken: "Token cleared. Using local auth or env token if available.",
    savedTokenButRejected: "Token saved but the API rejected authentication",
    renamed: "Session renamed",
    cannotRename: "Cannot rename session",
    cannotDelete: "Cannot delete session",
    exported: "Session exported",
    cannotExport: "Cannot export session",
    imported: "Imported {count} messages",
    cannotImport: "Cannot import file",
    openingWebhook: "Opening {id} in another tab...",
    openingWebhookFailed: "Could not open the webhook form",
    queryStopped: "Query stopped",
    queryStoppedDetail: "Query stopped by user.",
    queryFailed: "Stream failed",
    queryFailedDetail: "Could not complete the query. Check the API or model.",
    savedResponse: "Response saved",
    stopped: "Stopped",
    failed: "Query failed",
    resizeLeft: "Resize sessions panel",
    resizeRight: "Resize right panel",
    untitled: "Untitled",
    confirmDelete: "Delete chat \"{title}\"?",
    renamePromptTitle: "New title",
    deleteAllChats: "Delete all",
    deleteAllTitle: "Delete all chats",
    deleteAllPrompt: "This permanently deletes every chat for your user, including their messages and webhook audit. Type \"delete\" to confirm.",
    deleteAllPlaceholder: "Type delete to confirm",
    deleteAllConfirm: "Delete all chats",
    deleteAllCancel: "Cancel",
    deleteAllMustMatch: "Type \"delete\" exactly to enable the button.",
    deleteAllSuccess: "Deleted {count} chat(s).",
    deleteAllFailed: "Could not delete all chats.",
    attempts: "{count} attempts",
    contextSaved: "Session context saved",
    contextCleared: "Session context cleared",
    cannotSaveContext: "Could not save session context",
  },
  en: {
    loadingSessions: "Loading sessions...",
    loadingModels: "Loading models...",
    loadingWebhooks: "Loading webhooks...",
    loadingUser: "Loading user...",
    cannotConnect: "Cannot connect to the API",
    ready: "Ready",
    openingDoc: "Document opened: {title}",
    cannotOpenDoc: "Cannot open document",
    noSessions: "No sessions",
    creatingSession: "Creating session...",
    sendingQuery: "Sending query...",
    persistingQuery: "Persisting message and querying MiniMax...",
    thinking: "Querying model...",
    retrievingDocs: "Searching internal docs...",
    writing: "Writing response...",
    preparingActions: "Preparing suggested actions...",
    errorPrefix: "Handled error: {error}",
    savedToken: "Token saved. Reloading user...",
    clearedToken: "Token cleared. Using local auth or env token if available.",
    savedTokenButRejected: "Token saved but the API rejected authentication",
    renamed: "Session renamed",
    cannotRename: "Cannot rename session",
    cannotDelete: "Cannot delete session",
    exported: "Session exported",
    cannotExport: "Cannot export session",
    imported: "Imported {count} messages",
    cannotImport: "Cannot import file",
    openingWebhook: "Opening {id} in another tab...",
    openingWebhookFailed: "Could not open the webhook form",
    queryStopped: "Query stopped",
    queryStoppedDetail: "Query stopped by user.",
    queryFailed: "Stream failed",
    queryFailedDetail: "Could not complete the query. Check the API or model.",
    savedResponse: "Response saved",
    stopped: "Stopped",
    failed: "Query failed",
    resizeLeft: "Resize sessions panel",
    resizeRight: "Resize right panel",
    untitled: "Untitled",
    confirmDelete: "Delete chat \"{title}\"?",
    renamePromptTitle: "New title",
    deleteAllChats: "Delete all",
    deleteAllTitle: "Delete all chats",
    deleteAllPrompt: "This permanently deletes every chat for your user, including their messages and webhook audit. Type \"delete\" to confirm.",
    deleteAllPlaceholder: "Type delete to confirm",
    deleteAllConfirm: "Delete all chats",
    deleteAllCancel: "Cancel",
    deleteAllMustMatch: "Type \"delete\" exactly to enable the button.",
    deleteAllSuccess: "Deleted {count} chat(s).",
    deleteAllFailed: "Could not delete all chats.",
    attempts: "{count} attempts",
    contextSaved: "Session context saved",
    contextCleared: "Session context cleared",
    cannotSaveContext: "Could not save session context",
  },
};

const renderTemplate = (value: string, vars?: Record<string, string | number>) => {
  if (!vars) return value;
  return Object.entries(vars).reduce(
    (current, [key, val]) => current.replaceAll(`{${key}}`, String(val)),
    value,
  );
};

const clamp = (value: number, min: number, max: number) =>
  Math.max(min, Math.min(max, value));

const formatTime = (iso: string | null | undefined) => {
  if (!iso) return "";
  const parsed = new Date(iso).getTime();
  if (!Number.isFinite(parsed)) return "";
  return new Date(parsed).toLocaleTimeString([], {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });
};

const formatMessageStats = (message: ChatMessage): string => {
  const time = formatTime(message.created_at);
  const metadata = parseMetadata(message.metadata);
  const usage = metadata?.usage;
  const model = metadata?.model;

  if (message.role === "user") {
    return time && model ? `${time} · ${model}` : time;
  }

  if (message.role !== "assistant") {
    return time;
  }

  const parts: string[] = [];
  if (time) parts.push(time);
  if (typeof usage?.durationMs === "number") parts.push(formatDuration(usage.durationMs));
  if (typeof usage?.totalTokens === "number") parts.push(`${formatNumber(usage.totalTokens)} tok`);
  if (typeof usage?.cachedTokens === "number" && usage.cachedTokens > 0) {
    parts.push(`${formatNumber(usage.cachedTokens)} cached`);
  }
  if (model) parts.push(model);
  return parts.join(" · ");
};

function App() {
  // If the URL is a webhook form tab, render only that and skip the chat UI.
  const tabParams = getWebhookFormTabParams();
  if (tabParams) {
    return (
      <WebhookFormTab
        sessionId={tabParams.sessionId}
        webhookId={tabParams.webhookId}
        onBack={() => {
          window.close();
        }}
      />
    );
  }

  const initialized = useRef(false);
  const abortControllerRef = useRef<AbortController | null>(null);
  const messagesEndRef = useRef<HTMLDivElement | null>(null);
  const auditPollRef = useRef<number | null>(null);
  const [sessions, setSessions] = useState<ChatSession[]>([]);
  const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
  const [activeSession, setActiveSession] = useState<ChatSession | null>(null);
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [input, setInput] = useState("");
  const [docs, setDocs] = useState<InternalDocReference[]>([]);
  const [configuredDocs, setConfiguredDocs] = useState<InternalDocReference[]>([]);
  const [actions, setActions] = useState<RecommendedAction[]>([]);
  const [webhooks, setWebhooks] = useState<PublicWebhook[]>([]);
  const [webhookUsage, setWebhookUsage] = useState<Record<string, WebhookUsage>>({});
  const [webhookRuns, setWebhookRuns] = useState<WebhookRun[]>([]);
  const [models, setModels] = useState<ChatModel[]>([]);
  const [selectedModel, setSelectedModel] = useState("fast");
  const [selectedDoc, setSelectedDoc] = useState<KnowledgeDoc | null>(null);
  const [user, setUser] = useState<AuthUser | null>(null);
  const [authTokenInput, setAuthTokenInput] = useState(
    () => window.localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) ?? "",
  );
  const [status, setStatus] = useState("Loading sessions...");
  const [activity, setActivity] = useState("Initializing...");
  const [isSending, setIsSending] = useState(false);
  const [leftColumnWidth, setLeftColumnWidth] = useState(20);
  const [rightColumnWidth, setRightColumnWidth] = useState(35);
  const [theme, setTheme] = useState<Theme>(() =>
    window.localStorage.getItem(THEME_STORAGE_KEY) === "light" ? "light" : "dark",
  );
  const [language, setLanguage] = useState<Language>("en");
  const [showAllSessions, setShowAllSessions] = useState(false);
  const [contextEditor, setContextEditor] = useState<{ session: ChatSession; draft: string } | null>(null);
  const [showSettings, setShowSettings] = useState(false);
  const [showDeleteAll, setShowDeleteAll] = useState(false);
  const [deleteAllInput, setDeleteAllInput] = useState("");
  const [isDeletingAll, setIsDeletingAll] = useState(false);

  const MAX_VISIBLE_SESSIONS = 10;

  const t = copy[language];
  const dyn = (key: DynamicKey, vars?: Record<string, string | number>) =>
    renderTemplate(dynamic[language][key], vars);
  const untitled = dyn("untitled");

  const loadAudit = async (sessionId: string) => {
    try {
      const runs = await api<{ items: WebhookRun[] }>(
        `/api/webhook-runs?sessionId=${encodeURIComponent(sessionId)}`,
      );
      setWebhookRuns(runs.items);
    } catch (error) {
      console.error(error);
    }
  };

  const loadSessions = async () => {
    const result = await api<{ items: ChatSession[] }>("/api/sessions");
    setSessions(result.items);
    return result.items;
  };

  const loadWebhooks = async () => {
    const result = await api<{ items: PublicWebhook[] }>("/api/webhooks");
    setWebhooks(result.items);
    try {
      const usage = await api<{ window_days: number; items: Array<{ webhook_id: string; runs: number; successes: number; success_rate: number; window_days: number }> }>(
        "/api/webhooks/usage?days=7",
      );
      const map: Record<string, WebhookUsage> = {};
      for (const row of usage.items) {
        map[row.webhook_id] = {
          runs: row.runs,
          successes: row.successes,
          success_rate: row.success_rate,
        };
      }
      setWebhookUsage(map);
    } catch {
      // Non-fatal: the panel works without usage stats.
      setWebhookUsage({});
    }
  };

  const loadDocsCatalog = async () => {
    try {
      const result = await api<{ items: InternalDocReference[]; total: number }>("/api/docs");
      setConfiguredDocs(result.items);
    } catch {
      // Non-fatal: the panel works without the docs count badge.
      setConfiguredDocs([]);
    }
  };

  const loadModels = async () => {
    const result = await api<{ default_model?: string; items: ChatModel[] }>("/api/models");
    setModels(result.items);
    setSelectedModel(result.default_model ?? result.items[0]?.id ?? "fast");
  };

  const loadMe = async () => {
    const result = await api<{ user: AuthUser }>("/api/me");
    setUser(result.user);
  };

  const bootstrap = async () => {
    setStatus(dyn("loadingUser"));
    await loadMe();
    setStatus(dyn("loadingModels"));
    await loadModels();
    setStatus(dyn("loadingWebhooks"));
    await Promise.all([loadWebhooks(), loadDocsCatalog()]);
    setStatus(dyn("loadingSessions"));
    const items = await loadSessions();
    if (items[0]) {
      await loadSession(items[0].id);
    } else {
      await createSession();
    }
  };

  const loadSession = async (sessionId: string) => {
    const detail = await api<SessionDetail>(`/api/sessions/${sessionId}`);
    setActiveSessionId(detail.id);
    setActiveSession(detail);
    setMessages(detail.messages);

    const lastAssistantMetadata = [...detail.messages]
      .reverse()
      .find((message) => message.role === "assistant")?.metadata;
    const metadata = parseMetadata(lastAssistantMetadata ?? null);
    const recommended = Array.isArray(metadata?.actions)
      ? (metadata?.actions as RecommendedAction[])
      : [];
    setDocs(Array.isArray(metadata?.docs) ? (metadata?.docs as InternalDocReference[]) : []);
    setActions(recommended);
    setSelectedDoc(null);
    setStatus(dyn("ready"));
    setActivity(dyn("ready"));
    await loadAudit(sessionId);
  };

  const openDoc = async (docId: string) => {
    try {
      const doc = await api<KnowledgeDoc>(`/api/docs/${encodeURIComponent(docId)}`);
      setSelectedDoc(doc);
      setStatus(dyn("openingDoc", { title: doc.title }));
    } catch (error) {
      console.error(error);
      setStatus(dyn("cannotOpenDoc"));
    }
  };

  const createSession = async () => {
    const session = await api<ChatSession>("/api/sessions", {
      method: "POST",
      body: JSON.stringify({}),
    });
    await loadSessions();
    setShowAllSessions(false);
    await loadSession(session.id);
    return session;
  };

  const renameSession = async (session: ChatSession) => {
    const title = window.prompt(dyn("renamePromptTitle"), session.title ?? "")?.trim();
    if (!title) return;

    try {
      const updated = await api<ChatSession>(`/api/sessions/${session.id}`, {
        method: "PATCH",
        body: JSON.stringify({ title }),
      });
      setSessions((current) => current.map((item) => (item.id === updated.id ? updated : item)));
      if (activeSessionId === updated.id) {
        setStatus(dyn("renamed"));
      }
    } catch (error) {
      console.error(error);
      setStatus(dyn("cannotRename"));
    }
  };

  const updateSessionSystemPrompt = async (session: ChatSession, prompt: string | null) => {
    try {
      const updated = await api<ChatSession>(`/api/sessions/${session.id}/system-prompt`, {
        method: "PATCH",
        body: JSON.stringify({ system_prompt: prompt }),
      });
      setSessions((current) => current.map((item) => (item.id === updated.id ? updated : item)));
      if (activeSessionId === updated.id) {
        setActiveSession(updated);
      }
      setStatus(prompt ? dyn("contextSaved") : dyn("contextCleared"));
    } catch (error) {
      console.error(error);
      setStatus(dyn("cannotSaveContext"));
    }
  };

  const openContextEditor = (session: ChatSession) => {
    setContextEditor({ session, draft: session.system_prompt ?? "" });
  };

  const closeContextEditor = () => setContextEditor(null);

  const saveContextEditor = async () => {
    if (!contextEditor) return;
    const trimmed = contextEditor.draft.trim();
    const next = trimmed.length === 0 ? null : contextEditor.draft;
    setContextEditor(null);
    await updateSessionSystemPrompt(contextEditor.session, next);
  };

  const exportSession = async (session: ChatSession) => {
    try {
      const response = await fetch(`/api/sessions/${session.id}/export`, {
        headers: authorizedHeaders(),
      });
      if (!response.ok) {
        throw new Error(`export_error:${response.status}`);
      }
      const payload = (await response.json()) as SessionExport;
      const markdown = sessionToMarkdown(payload);
      const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      const safeTitle = (session.title ?? untitled).replace(/[^a-z0-9-_]+/gi, "-").slice(0, 60);
      link.href = url;
      link.download = `sic-session-${safeTitle || session.id.slice(0, 8)}.md`;
      link.rel = "noreferrer";
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      window.setTimeout(() => URL.revokeObjectURL(url), 0);
      setStatus(dyn("exported"));
    } catch (error) {
      console.error(error);
      setStatus(dyn("cannotExport"));
    }
  };

  const importSessionFromFile = async (file: File) => {
    try {
      const raw = await file.text();
      const parsed = JSON.parse(raw);
      const result = await api<{ session: ChatSession; imported_messages: number }>(
        "/api/sessions/import",
        {
          method: "POST",
          body: JSON.stringify(parsed),
        },
      );
      await loadSessions();
      await loadSession(result.session.id);
      setStatus(dyn("imported", { count: result.imported_messages }));
    } catch (error) {
      console.error(error);
      setStatus(dyn("cannotImport"));
    }
  };

  const deleteSession = async (session: ChatSession) => {
    const title = session.title ?? untitled;
    const confirmed = window.confirm(dyn("confirmDelete", { title }));
    if (!confirmed) return;

    try {
      const response = await fetch(`/api/sessions/${session.id}`, {
        headers: authorizedHeaders(),
        method: "DELETE",
      });
      if (!response.ok) {
        throw new Error(`delete_session_error:${response.status}`);
      }

      const items = await loadSessions();

      if (activeSessionId === session.id) {
        if (items[0]) {
          await loadSession(items[0].id);
        } else {
          setActiveSessionId(null);
          setMessages([]);
          setDocs([]);
          setActions([]);
          setWebhookRuns([]);
          setSelectedDoc(null);
          setStatus(dyn("noSessions"));
        }
      }
    } catch (error) {
      console.error(error);
      setStatus(dyn("cannotDelete"));
    }
  };

  const DELETE_ALL_CONFIRM_TOKEN = "delete";
  const deleteAllMatches = deleteAllInput.trim() === DELETE_ALL_CONFIRM_TOKEN;

  const handleDeleteAllChats = async () => {
    if (!deleteAllMatches || isDeletingAll) return;
    setIsDeletingAll(true);
    try {
      const response = await fetch(`/api/sessions`, {
        headers: authorizedHeaders(),
        method: "DELETE",
      });
      if (!response.ok) {
        throw new Error(`delete_all_sessions_error:${response.status}`);
      }
      const body = (await response.json()) as { deleted?: number };
      // Tear down local state: every session we held is gone.
      setActiveSessionId(null);
      setMessages([]);
      setDocs([]);
      setActions([]);
      setWebhookRuns([]);
      setSelectedDoc(null);
      setSelectedDoc(null);
      setShowDeleteAll(false);
      setDeleteAllInput("");
      await loadSessions();
      setStatus(dyn("deleteAllSuccess", { count: body.deleted ?? 0 }));
    } catch (error) {
      console.error(error);
      setStatus(dyn("deleteAllFailed"));
    } finally {
      setIsDeletingAll(false);
    }
  };

  // Open a webhook's form in a new tab. Execution happens in the new tab;
  // the parent chat tab polls the audit list to pick up the new run.
  const openWebhookForm = (actionId: string) => {
    if (!activeSessionId) return;
    const url = `${window.location.pathname}?webhook=${encodeURIComponent(actionId)}&session=${encodeURIComponent(activeSessionId)}`;
    const opened = window.open(url, "_blank", "noopener,noreferrer");
    setStatus(dyn("openingWebhook", { id: actionId }));
    if (!opened) {
      setStatus(dyn("openingWebhookFailed"));
    }
  };

  useEffect(() => {
    if (initialized.current) return;
    initialized.current = true;

    void (async () => {
      try {
        await bootstrap();
      } catch (error) {
        console.error(error);
        setStatus(dyn("cannotConnect"));
      }
    })();
  }, []);

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    window.localStorage.setItem(THEME_STORAGE_KEY, theme);
    const favicon = document.getElementById("favicon") as HTMLLinkElement | null;
    if (favicon) {
      favicon.href = theme === "light" ? "/favicon-light.png" : "/favicon-dark.png";
    }
  }, [theme]);

  useEffect(() => {
    window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language);
  }, [language]);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ block: "end" });
  }, [messages, activity]);

  // Audit list polling: when the active session changes, start a 5s interval
  // that refreshes webhook-runs. This makes the audit list feel "live" — the
  // user can switch to the webhook form tab in another window, execute, and
  // come back to find the new run already in the right panel.
  useEffect(() => {
    if (auditPollRef.current !== null) {
      window.clearInterval(auditPollRef.current);
      auditPollRef.current = null;
    }
    if (!activeSessionId) return;
    void loadAudit(activeSessionId);
    auditPollRef.current = window.setInterval(() => {
      void loadAudit(activeSessionId);
    }, AUDIT_POLL_INTERVAL_MS);
    return () => {
      if (auditPollRef.current !== null) {
        window.clearInterval(auditPollRef.current);
        auditPollRef.current = null;
      }
    };
  }, [activeSessionId]);

  // Also refresh on window focus (when the user comes back from the webhook tab)
  useEffect(() => {
    const onFocus = () => {
      if (activeSessionId) void loadAudit(activeSessionId);
    };
    window.addEventListener("focus", onFocus);
    return () => window.removeEventListener("focus", onFocus);
  }, [activeSessionId]);

  const startColumnResize =
    (side: "left" | "right") => (event: ReactPointerEvent<HTMLDivElement>) => {
      event.preventDefault();
      const startX = event.clientX;
      const startLeft = leftColumnWidth;
      const startRight = rightColumnWidth;

      const onMove = (moveEvent: PointerEvent) => {
        const deltaPercent = ((moveEvent.clientX - startX) / window.innerWidth) * 100;
        if (side === "left") {
          setLeftColumnWidth(clamp(startLeft + deltaPercent, 18, 50));
        } else {
          setRightColumnWidth(clamp(startRight - deltaPercent, 18, 50));
        }
      };

      const onUp = () => {
        window.removeEventListener("pointermove", onMove);
        window.removeEventListener("pointerup", onUp);
      };

      window.addEventListener("pointermove", onMove);
      window.addEventListener("pointerup", onUp);
    };

  const saveAuthToken = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const token = authTokenInput.trim();
    if (token) {
      window.localStorage.setItem(AUTH_TOKEN_STORAGE_KEY, token);
      setStatus(dyn("savedToken"));
    } else {
      window.localStorage.removeItem(AUTH_TOKEN_STORAGE_KEY);
      setUser(null);
      setStatus(dyn("clearedToken"));
    }

    try {
      await bootstrap();
    } catch (error) {
      console.error(error);
      setStatus(dyn("savedTokenButRejected"));
    }
  };

  const appendAssistantToken = (messageId: string, token: string) => {
    setMessages((current) =>
      current.map((message) =>
        message.id === messageId ? { ...message, content: `${message.content}${token}` } : message,
      ),
    );
  };

  const handleStreamEvent = (event: StreamEvent, assistantMessageId: string) => {
    if (event.type === "docs") {
      setDocs(event.docs);
      setActivity(dyn("retrievingDocs"));
      return;
    }

    if (event.type === "token") {
      appendAssistantToken(assistantMessageId, event.token);
      setActivity(dyn("writing"));
      return;
    }

    if (event.type === "actions") {
      setActions(event.actions);
      setActivity(dyn("preparingActions"));
      return;
    }

    if (event.type === "error") {
      setStatus(dyn("errorPrefix", { error: event.error }));
    }
  };

  const stopCurrentRequest = () => {
    abortControllerRef.current?.abort();
  };

  const sendMessage = async (event?: FormEvent<HTMLFormElement>, repeatedMessage?: string) => {
    event?.preventDefault();
    const message = (repeatedMessage ?? input).trim();
    if (!message || isSending) return;

    const userMessageId = temporaryId();
    const assistantMessageId = temporaryId();
    const abortController = new AbortController();
    abortControllerRef.current = abortController;

    setInput("");
    setIsSending(true);
    setStatus(activeSessionId ? dyn("thinking") : dyn("creatingSession"));
    setActivity(activeSessionId ? dyn("sendingQuery") : dyn("creatingSession"));
    setDocs([]);
    setActions([]);
    setSelectedDoc(null);

    try {
      const sessionId = activeSessionId ?? (await createSession()).id;

      setStatus(dyn("thinking"));
      setActivity(dyn("persistingQuery"));
      const optimisticTimestamp = new Date().toISOString();
      setMessages((current) => [
        ...current,
        {
          id: userMessageId,
          role: "user",
          content: message,
          metadata: null,
          created_at: optimisticTimestamp,
        },
        {
          id: assistantMessageId,
          role: "assistant",
          content: "",
          metadata: null,
          created_at: optimisticTimestamp,
        },
      ]);

      const response = await fetch("/api/chat/stream", {
        method: "POST",
        headers: jsonHeaders(),
        signal: abortController.signal,
        body: JSON.stringify({ sessionId, message, model: selectedModel }),
      });

      if (!response.ok || !response.body) {
        throw new Error(`stream_error:${response.status}`);
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";

      while (true) {
        const { done, value } = await reader.read();
        buffer += decoder.decode(value, { stream: !done });

        const chunks = buffer.split("\n\n");
        buffer = chunks.pop() ?? "";

        for (const chunk of chunks) {
          const line = chunk.split("\n").find((part) => part.startsWith("data: "));
          if (!line) continue;
          handleStreamEvent(JSON.parse(line.slice(6)) as StreamEvent, assistantMessageId);
        }

        if (done) break;
      }

      await loadSessions();
      setStatus(dyn("savedResponse"));
      setActivity(dyn("ready"));
    } catch (error) {
      console.error(error);
      if (error instanceof DOMException && error.name === "AbortError") {
        appendAssistantToken(assistantMessageId, dyn("queryStoppedDetail"));
        setStatus(dyn("queryStopped"));
        setActivity(dyn("stopped"));
      } else {
        appendAssistantToken(assistantMessageId, dyn("queryFailedDetail"));
        setStatus(dyn("queryFailed"));
        setActivity(dyn("failed"));
      }
    } finally {
      abortControllerRef.current = null;
      setIsSending(false);
    }
  };

  const webhookById = new Map(webhooks.map((webhook) => [webhook.id, webhook]));

  const visibleSessions = showAllSessions ? sessions : sessions.slice(0, MAX_VISIBLE_SESSIONS);
  const hiddenSessionsCount = Math.max(0, sessions.length - visibleSessions.length);

  return (
    <main
      className="layout"
      style={{
        "--left-column": `${leftColumnWidth}%`,
        "--chat-column": `${Math.max(20, 100 - leftColumnWidth - rightColumnWidth)}%`,
        "--right-column": `${rightColumnWidth}%`,
      } as CSSProperties}
    >
      <aside className="sidebar">
        <div className="sidebarHeader">
          <div className="brand">
            <img
              alt="SIC logo"
              className="brandLogo"
              src={theme === "light" ? "/favicon-light.png" : "/favicon-dark.png"}
            />
            <div>
              <strong>SIC</strong>
              <span>Super Incident Commander</span>
            </div>
          </div>
          <div className="sidebarHeaderActions">
            <label
              aria-label={t.importChat}
              className="iconToggle newChatToggle"
              title={t.importChat}
            >
              <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
                <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 7V3.5L18.5 9H13z" />
                <path d="M12 18l-4-4h2.5v-3h3v3H16l-4 4z" />
              </svg>
              <input
                accept="application/json,.json"
                onChange={(event) => {
                  const input = event.currentTarget;
                  const file = input.files?.[0];
                  if (file) void importSessionFromFile(file);
                  input.value = "";
                }}
                type="file"
              />
            </label>
            <button
              aria-label={t.newChat}
              className="iconToggle newChatToggle"
              title={t.newChat}
              type="button"
              onClick={createSession}
            >
              +
            </button>
            <button
              aria-label={dyn("deleteAllChats")}
              className="iconToggle newChatToggle"
              title={dyn("deleteAllChats")}
              type="button"
              onClick={() => {
                setDeleteAllInput("");
                setShowDeleteAll(true);
              }}
            >
              <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M3 6h18" />
                <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
                <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
                <path d="M10 11v6" />
                <path d="M14 11v6" />
              </svg>
            </button>
          </div>
        </div>
        <div className="sessionList">
          {visibleSessions.map((session) => (
            <article
              className={session.id === activeSessionId ? "session active" : "session"}
              key={session.id}
            >
              <button
                className="sessionOpen"
                title={session.title ?? untitled}
                type="button"
                onClick={() => void loadSession(session.id)}
              >
                <strong>{session.title ?? untitled}</strong>
              </button>
              <div className="sessionActions">
                <button
                  aria-label={t.renameChat}
                  title={t.renameChat}
                  type="button"
                  onClick={() => void renameSession(session)}
                >
                  ✎
                </button>
                <button
                  aria-label={t.exportChat}
                  title={`${t.exportChat} (.md)`}
                  type="button"
                  onClick={() => void exportSession(session)}
                >
                  <svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
                    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 7V3.5L18.5 9H13z" />
                    <path d="M12 11l-4 4h2.5v3h3v-3H16l-4-4z" />
                  </svg>
                </button>
                <button
                  aria-label={session.system_prompt ? t.contextEdit : t.contextSet}
                  title={session.system_prompt ? t.contextEdit : t.contextSet}
                  type="button"
                  className={session.system_prompt ? "hasContext" : ""}
                  onClick={() => void openContextEditor(session)}
                >
                  {session.system_prompt ? "●" : "○"}
                </button>
                <button
                  aria-label={t.deleteChat}
                  title={t.deleteChat}
                  type="button"
                  onClick={() => void deleteSession(session)}
                >
                  🗑
                </button>
              </div>
            </article>
          ))}
          {sessions.length > MAX_VISIBLE_SESSIONS ? (
            <button
              className="showMore"
              type="button"
              onClick={() => setShowAllSessions((current) => !current)}
            >
              {showAllSessions
                ? t.showLess
                : `${t.showMore} ${renderTemplate(t.showMoreCount, { count: hiddenSessionsCount })}`}
            </button>
          ) : null}
        </div>

        <div className="sidebarFooter">
          <span className="userBadge">{user?.username ?? user?.id ?? t.noUser}</span>
          <button
            aria-label={t.settingsOpen}
            className="iconToggle"
            title={t.settingsOpen}
            type="button"
            onClick={() => setShowSettings(true)}
          >
            <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="3" />
              <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
            </svg>
          </button>
        </div>
      </aside>

      <div
        aria-label={dyn("resizeLeft")}
        className="resizeHandle leftHandle"
        onPointerDown={startColumnResize("left")}
        role="separator"
      />

      <section className="chat">
        <header className="header">
          <label className="modelPicker">
            {t.model}
            <select
              disabled={isSending}
              onChange={(event) => setSelectedModel(event.target.value)}
              value={selectedModel}
            >
              {models.map((model) => (
                <option key={model.id} value={model.id}>
                  {model.label}
                </option>
              ))}
            </select>
          </label>
          <div className="headerControls">
            <button
              aria-label={t.themeLabel}
              className="iconToggle"
              title={t.themeLabel}
              type="button"
              onClick={() => setTheme((current) => (current === "dark" ? "light" : "dark"))}
            >
              {theme === "dark" ? "☀" : "☾"}
            </button>
            <button
              aria-label={t.languageLabel}
              className="languageToggle"
              title={t.languageLabel}
              type="button"
              onClick={() => setLanguage((current) => (current === "es" ? "en" : "es"))}
            >
              {language.toUpperCase()}
            </button>
          </div>
        </header>

        <div className="messages">
          {messages.length === 0 ? (
            <p className="empty">{t.empty}</p>
          ) : (
            messages.map((message) => (
              <article className={`message ${message.role}`} key={message.id}>
                <div className="messageMeta">
                  <span>{message.role}</span>
                  {message.role === "user" ? (
                    <button
                      aria-label={t.repeat}
                      className="repeatButton"
                      disabled={isSending}
                      title={t.repeat}
                      type="button"
                      onClick={() => void sendMessage(undefined, message.content)}
                    >
                      ↻
                    </button>
                  ) : null}
                </div>
                <div className="markdownBody">
                  <MarkdownView source={message.content} />
                </div>
                <small className="messageStats">
                  {formatMessageStats(message)}
                </small>
              </article>
            ))
          )}
          <div ref={messagesEndRef} />
        </div>

        <form className="inputBar" onSubmit={sendMessage}>
          <input
            disabled={isSending}
            onChange={(event) => setInput(event.target.value)}
            placeholder={t.askPlaceholder}
            value={input}
          />
          <button
            aria-label={isSending ? t.stop : t.send}
            className={`sendButton${isSending ? " sending" : ""}`}
            disabled={!isSending && !input.trim()}
            onClick={isSending ? stopCurrentRequest : undefined}
            title={isSending ? t.stop : t.send}
            type={isSending ? "button" : "submit"}
          >
            {isSending ? (
              <svg aria-hidden="true" viewBox="0 0 24 24">
                <rect x="6" y="6" width="12" height="12" rx="2" />
              </svg>
            ) : (
              <svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor">
                <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2 .01 7z" />
              </svg>
            )}
          </button>
        </form>
        <small className="inputStatus">
          {status} · {activity}
        </small>
      </section>

      <div
        aria-label={dyn("resizeRight")}
        className="resizeHandle rightHandle"
        onPointerDown={startColumnResize("right")}
        role="separator"
      />

      <aside className="rightPanel">
        <div className="panelSection">
          <div className="panelHeading panelHeadingRow">
            <strong className="panelHeading">{t.docs}</strong>
            <span
              className="usageBadge"
              title={`${configuredDocs.length} configured doc${configuredDocs.length === 1 ? "" : "s"}`}
            >
              {configuredDocs.length}
            </span>
          </div>
          <div className="panelList">
            {docs.length === 0 ? <p className="muted">{t.noDocs}</p> : null}
            {docs.map((doc) => (
              <article className="panelCard" key={doc.id}>
                <div className="panelCardBody">
                  <strong>{doc.title}</strong>
                  <span>{doc.source}</span>
                  <small>
                    {t.relevance} {formatScore(doc.relevance)}
                  </small>
                </div>
                <button
                  aria-label={t.open}
                  className="iconToggle panelCardAction"
                  title={t.open}
                  type="button"
                  onClick={() => void openDoc(doc.id)}
                >
                  ↗
                </button>
              </article>
            ))}
          </div>
        </div>

        <div className="panelSection">
          <div className="panelHeading panelHeadingRow">
            <strong className="panelHeading">{t.webhooks}</strong>
            {(() => {
              const totalRuns = Object.values(webhookUsage).reduce((sum, u) => sum + u.runs, 0);
              return (
                <span
                  className="usageBadge"
                  title={
                    totalRuns === 0
                      ? "No runs in the last 7 days"
                      : `${totalRuns} runs in the last 7 days`
                  }
                >
                  {totalRuns}
                </span>
              );
            })()}
          </div>
          <div className="panelList">
            {actions.length === 0 ? (
              <p className="muted">{t.noWebhooks}</p>
            ) : null}
            {actions.map((action) => {
              const webhook = webhookById.get(action.id);
              const title = webhook?.label ?? action.id;
              const description = webhook?.description ?? action.reason;
              return (
                <article className="panelCard suggested" key={action.id}>
                  <div className="panelCardBody">
                    <div className="panelCardTitle">
                      <strong>{title}</strong>
                      {action.usageHint ? (
                        <span
                          className="usageBadge"
                          title={`${t.mostUsed}: ${action.usageHint}`}
                        >
                          ★ {t.mostUsed}
                        </span>
                      ) : (
                        <span className="suggestedBadge" title={t.suggested}>
                          ★ {t.suggested}
                        </span>
                      )}
                    </div>
                    {description ? <p>{description}</p> : null}
                    <small>
                      {action.usageHint ?? (action.confidence ? `${t.confidence} ${formatScore(action.confidence)}` : null)}
                    </small>
                  </div>
                  <button
                    aria-label={`${t.openInTab}: ${title}`}
                    className="iconToggle panelCardAction actionExecute"
                    title={t.executeHelp}
                    type="button"
                    onClick={() => openWebhookForm(action.id)}
                  >
                    ⚡
                  </button>
                </article>
              );
            })}
          </div>
        </div>

        <div className="panelSection">
          <div className="auditHeader">
            <strong className="panelHeading">{t.audit}</strong>
            <span className="auditLive" title={t.auditLive}>
              <span className="auditLiveDot" /> {t.auditLive}
            </span>
          </div>
        <div className="panelList">
          {webhookRuns.length === 0 ? <p className="muted">{t.noRuns}</p> : null}
          {webhookRuns.map((run) => (
            <article className={`panelCard run ${run.status}`} key={run.id}>
              <div className="panelCardBody">
                <div className="panelCardTitle">
                  <strong>
                    {webhookById.get(run.webhook_id)?.label ?? run.webhook_id}
                  </strong>
                  <span
                    aria-hidden="true"
                    className={`runStatus ${run.status}`}
                    title={run.status}
                  >
                    {run.status === "success" ? "✓" : "✗"}
                  </span>
                </div>
                <span>{new Date(run.created_at).toLocaleString()}</span>
                <small>
                  {run.status}
                  {run.response_status ? ` · HTTP ${run.response_status}` : ""}
                  {run.attempts && run.attempts > 1 ? ` · ${dyn("attempts", { count: run.attempts })}` : ""}
                </small>
              </div>
            </article>
          ))}
        </div>
        </div>
      </aside>

      {selectedDoc ? (
        <DocModal
          doc={selectedDoc}
          labels={{
            close: t.close,
            tags: "tags",
            owner: "owner",
            updated: "updated",
          }}
          onClose={() => setSelectedDoc(null)}
        />
      ) : null}

      {showSettings ? (
        <div
          aria-label={t.settings}
          aria-modal="true"
          className="docModalBackdrop"
          onClick={() => setShowSettings(false)}
          role="dialog"
        >
          <div
            className="docModal settingsModal"
            onClick={(event) => event.stopPropagation()}
          >
            <header className="docModalHeader">
              <div>
                <small>SIC</small>
                <h1>{t.settings}</h1>
              </div>
              <button
                aria-label={t.close}
                className="iconToggle"
                title={t.close}
                type="button"
                onClick={() => setShowSettings(false)}
              >
                ×
              </button>
            </header>
            <form className="authTokenBox" onSubmit={saveAuthToken}>
              <strong>{t.devToken}</strong>
              <textarea
                onChange={(event) => setAuthTokenInput(event.target.value)}
                placeholder={t.devTokenPlaceholder}
                rows={4}
                value={authTokenInput}
              />
              <button type="submit">{t.saveToken}</button>
              <small>{t.devTokenHelp}</small>
            </form>
          </div>
        </div>
      ) : null}

      {contextEditor ? (
        <div
          aria-label={t.contextSet}
          aria-modal="true"
          className="docModalBackdrop"
          onClick={closeContextEditor}
          role="dialog"
        >
          <div
            className="docModal settingsModal"
            onClick={(event) => event.stopPropagation()}
          >
            <header className="docModalHeader">
              <div>
                <small>SIC</small>
                <h1>{t.contextSet}</h1>
                <small className="muted">
                  {contextEditor.session.title ?? untitled}
                </small>
              </div>
              <button
                aria-label={t.close}
                className="iconToggle"
                title={t.close}
                type="button"
                onClick={closeContextEditor}
              >
                ×
              </button>
            </header>
            <form
              className="authTokenBox"
              onSubmit={(event) => {
                event.preventDefault();
                void saveContextEditor();
              }}
            >
              <strong>{t.contextSet}</strong>
              <textarea
                autoFocus
                onChange={(event) =>
                  setContextEditor({ ...contextEditor, draft: event.target.value })
                }
                placeholder={t.contextPlaceholder}
                rows={10}
                value={contextEditor.draft}
              />
              <div className="formActions">
                <button type="submit">{t.contextSave}</button>
                <button
                  type="button"
                  onClick={() => void updateSessionSystemPrompt(contextEditor.session, null).then(closeContextEditor)}
                >
                  {t.contextClear}
                </button>
              </div>
              <small className="muted">{t.contextHelp}</small>
            </form>
          </div>
        </div>
      ) : null}

      {showDeleteAll ? (
        <div
          aria-label={dyn("deleteAllTitle")}
          aria-modal="true"
          className="docModalBackdrop"
          onClick={() => {
            if (!isDeletingAll) setShowDeleteAll(false);
          }}
          role="dialog"
        >
          <div
            className="docModal deleteAllModal"
            onClick={(event) => event.stopPropagation()}
          >
            <header className="docModalHeader">
              <div>
                <small>SIC</small>
                <h1>{dyn("deleteAllTitle")}</h1>
              </div>
              <button
                aria-label={dyn("deleteAllCancel")}
                className="iconToggle"
                title={dyn("deleteAllCancel")}
                type="button"
                onClick={() => {
                  if (!isDeletingAll) setShowDeleteAll(false);
                }}
              >
                ×
              </button>
            </header>
            <form
              className="authTokenBox"
              onSubmit={(event) => {
                event.preventDefault();
                void handleDeleteAllChats();
              }}
            >
              <p className="muted">{dyn("deleteAllPrompt")}</p>
              <input
                aria-label={dyn("deleteAllPlaceholder")}
                autoComplete="off"
                autoFocus
                className="deleteAllInput"
                onChange={(event) => setDeleteAllInput(event.target.value)}
                placeholder={dyn("deleteAllPlaceholder")}
                spellCheck={false}
                type="text"
                value={deleteAllInput}
              />
              {!deleteAllMatches ? (
                <small className="muted">{dyn("deleteAllMustMatch")}</small>
              ) : null}
              <div className="formActions">
                <button
                  className="danger"
                  disabled={!deleteAllMatches || isDeletingAll}
                  type="submit"
                >
                  {isDeletingAll ? "…" : dyn("deleteAllConfirm")}
                </button>
                <button
                  disabled={isDeletingAll}
                  type="button"
                  onClick={() => setShowDeleteAll(false)}
                >
                  {dyn("deleteAllCancel")}
                </button>
              </div>
            </form>
          </div>
        </div>
      ) : null}
    </main>
  );
}

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ErrorBoundary>
      <App />
    </ErrorBoundary>
  </StrictMode>,
);
