import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Check, ChevronDown, Globe2, Menu, MessageSquare, Paperclip, Plus, Rabbit, Search, SendHorizontal, Trash2, X } from "lucide-preact";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import { AuthScreen } from "@/components/auth/auth-screen";
import { ChatAttachmentList } from "@/components/chat/chat-attachment-list";
import { ChatMessagesPanel } from "@/components/chat/chat-messages-panel";
import { SearchResultsPanel } from "@/components/search/search-results-panel";
import {
  createChat,
  createChatFromSearch,
  createSearch,
  deleteChat,
  deleteSearch,
  getChat,
  listModels,
  getSearch,
  listChats,
  listSearches,
  runCompletionStream,
  runSearchStream,
  suggestChatTitle,
  getMessageAttachments,
  type ChatAttachment,
  type ModelCatalogResponse,
  type Provider,
  type ChatDetail,
  type ChatSummary,
  type CompletionRequestMessage,
  type Message,
  type SearchDetail,
  type SearchSummary,
  type ToolCallEvent,
} from "@/lib/api";
import { useSessionAuth } from "@/hooks/use-session-auth";
import { cn } from "@/lib/utils";

type SidebarSelection = { kind: "chat" | "search"; id: string };
type DraftSelectionKind = "chat" | "search";
type SidebarItem = SidebarSelection & {
  title: string;
  updatedAt: string;
  createdAt: string;
  initiatedProvider: Provider | null;
  initiatedModel: string | null;
  lastUsedProvider: Provider | null;
  lastUsedModel: string | null;
};
type ContextMenuState = {
  item: SidebarSelection;
  x: number;
  y: number;
};

function readSidebarSelectionFromUrl(): SidebarSelection | null {
  if (typeof window === "undefined") return null;
  const params = new URLSearchParams(window.location.search);
  const chatId = params.get("chat")?.trim();
  if (chatId) {
    return { kind: "chat", id: chatId };
  }
  const searchId = params.get("search")?.trim();
  if (searchId) {
    return { kind: "search", id: searchId };
  }
  return null;
}

function buildWorkspaceUrl(selection: SidebarSelection | null) {
  if (typeof window === "undefined") return "/";
  const params = new URLSearchParams(window.location.search);
  params.delete("chat");
  params.delete("search");
  if (selection) {
    params.set(selection.kind === "chat" ? "chat" : "search", selection.id);
  }
  const query = params.toString();
  return `${window.location.pathname}${query ? `?${query}` : ""}`;
}

const PROVIDER_FALLBACK_MODELS: Record<Provider, string[]> = {
  openai: ["gpt-4.1-mini"],
  anthropic: ["claude-3-5-sonnet-latest"],
  xai: ["grok-3-mini"],
};

const EMPTY_MODEL_CATALOG: ModelCatalogResponse["providers"] = {
  openai: { models: [], loadedAt: null, error: null },
  anthropic: { models: [], loadedAt: null, error: null },
  xai: { models: [], loadedAt: null, error: null },
};

const MODEL_PREFERENCES_STORAGE_KEY = "sybil:modelPreferencesByProvider";
const QUICK_QUESTION_MODEL_SELECTION_STORAGE_KEY = "sybil:quickQuestionModelSelection";

type ProviderModelPreferences = Record<Provider, string | null>;

type QuickQuestionModelSelection = {
  provider: Provider;
  modelPreferences: ProviderModelPreferences;
};

const EMPTY_MODEL_PREFERENCES: ProviderModelPreferences = {
  openai: null,
  anthropic: null,
  xai: null,
};

const TRANSCRIPT_BOTTOM_GAP = 20;
const REPLY_SCROLL_BUFFER_MIN = 288;
const REPLY_SCROLL_BUFFER_MAX = 576;
const REPLY_SCROLL_BUFFER_VIEWPORT_RATIO = 0.52;
const MAX_CHAT_ATTACHMENTS = 8;
const MAX_IMAGE_ATTACHMENT_BYTES = 6 * 1024 * 1024;
const MAX_TEXT_ATTACHMENT_BYTES = 8 * 1024 * 1024;
const MAX_TEXT_ATTACHMENT_CHARS = 200_000;
const CHAT_FILE_ACCEPT =
  ".png,.jpg,.jpeg,.txt,.md,.markdown,.csv,.tsv,.json,.jsonl,.xml,.yaml,.yml,.html,.htm,.css,.js,.jsx,.ts,.tsx,.py,.rb,.java,.c,.cc,.cpp,.h,.hpp,.go,.rs,.sh,.sql,.log,.toml,.ini,.cfg,.conf,.swift,.kt,.m,.mm";
const TEXT_ATTACHMENT_EXTENSIONS = new Set([
  ".txt",
  ".md",
  ".markdown",
  ".csv",
  ".tsv",
  ".json",
  ".jsonl",
  ".xml",
  ".yaml",
  ".yml",
  ".html",
  ".htm",
  ".css",
  ".js",
  ".jsx",
  ".ts",
  ".tsx",
  ".py",
  ".rb",
  ".java",
  ".c",
  ".cc",
  ".cpp",
  ".h",
  ".hpp",
  ".go",
  ".rs",
  ".sh",
  ".sql",
  ".log",
  ".toml",
  ".ini",
  ".cfg",
  ".conf",
  ".swift",
  ".kt",
  ".m",
  ".mm",
]);
const TEXT_ATTACHMENT_MIME_TYPES = new Set([
  "application/json",
  "application/ld+json",
  "application/sql",
  "application/toml",
  "application/x-httpd-php",
  "application/x-javascript",
  "application/x-sh",
  "application/xml",
  "application/yaml",
  "application/x-yaml",
  "image/svg+xml",
]);

function getModelOptions(catalog: ModelCatalogResponse["providers"], provider: Provider) {
  const providerModels = catalog[provider]?.models ?? [];
  if (providerModels.length) return providerModels;
  return PROVIDER_FALLBACK_MODELS[provider];
}

function getReplyScrollBufferHeight() {
  if (typeof window === "undefined") return REPLY_SCROLL_BUFFER_MIN;
  return Math.min(
    REPLY_SCROLL_BUFFER_MAX,
    Math.max(REPLY_SCROLL_BUFFER_MIN, Math.round(window.innerHeight * REPLY_SCROLL_BUFFER_VIEWPORT_RATIO))
  );
}

function getFileExtension(filename: string) {
  const index = filename.lastIndexOf(".");
  return index >= 0 ? filename.slice(index).toLowerCase() : "";
}

function createAttachmentId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    return crypto.randomUUID();
  }
  return `att-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}

function inferImageMimeType(file: File) {
  if (file.type === "image/png" || file.type === "image/jpeg") return file.type;
  const extension = getFileExtension(file.name);
  if (extension === ".png") return "image/png";
  if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
  return null;
}

function isTextLikeFile(file: File) {
  const mimeType = file.type.toLowerCase();
  if (mimeType.startsWith("text/")) return true;
  if (TEXT_ATTACHMENT_MIME_TYPES.has(mimeType)) return true;
  return TEXT_ATTACHMENT_EXTENSIONS.has(getFileExtension(file.name));
}

function arrayBufferToBase64(buffer: ArrayBuffer) {
  const bytes = new Uint8Array(buffer);
  const chunkSize = 0x8000;
  let binary = "";
  for (let index = 0; index < bytes.length; index += chunkSize) {
    const chunk = bytes.subarray(index, index + chunkSize);
    binary += String.fromCharCode(...chunk);
  }
  return btoa(binary);
}

async function buildChatAttachment(file: File): Promise<ChatAttachment> {
  const imageMimeType = inferImageMimeType(file);
  if (imageMimeType) {
    if (file.size > MAX_IMAGE_ATTACHMENT_BYTES) {
      throw new Error(`Image '${file.name}' exceeds the 6 MB upload limit.`);
    }
    const base64 = arrayBufferToBase64(await file.arrayBuffer());
    return {
      kind: "image",
      id: createAttachmentId(),
      filename: file.name,
      mimeType: imageMimeType,
      sizeBytes: file.size,
      dataUrl: `data:${imageMimeType};base64,${base64}`,
    };
  }

  if (!isTextLikeFile(file)) {
    throw new Error(`Unsupported file type for '${file.name}'. Use PNG/JPEG images or text-based files.`);
  }

  if (file.size > MAX_TEXT_ATTACHMENT_BYTES) {
    throw new Error(`Text file '${file.name}' exceeds the 8 MB upload limit.`);
  }

  const normalizedText = (await file.text()).replace(/\r\n/g, "\n").replace(/\u0000/g, "");
  const truncated = normalizedText.length > MAX_TEXT_ATTACHMENT_CHARS;
  return {
    kind: "text",
    id: createAttachmentId(),
    filename: file.name,
    mimeType: file.type || "text/plain",
    sizeBytes: file.size,
    text: truncated ? normalizedText.slice(0, MAX_TEXT_ATTACHMENT_CHARS) : normalizedText,
    truncated,
  };
}

function buildAttachmentSummary(attachments: ChatAttachment[]) {
  if (!attachments.length) return "";
  const filenames = attachments.map((attachment) => attachment.filename).join(", ");
  return attachments.length === 1 ? filenames : `Attached: ${filenames}`;
}

function getFilesFromDataTransfer(dataTransfer: DataTransfer | null) {
  if (!dataTransfer) return [];
  const fromItems = Array.from(dataTransfer.items ?? [])
    .filter((item) => item.kind === "file")
    .map((item) => item.getAsFile())
    .filter((file): file is File => file instanceof File);
  if (fromItems.length) return fromItems;
  return Array.from(dataTransfer.files ?? []);
}

function hasFileTransfer(dataTransfer: DataTransfer | null) {
  if (!dataTransfer) return false;
  return Array.from(dataTransfer.types ?? []).includes("Files") || getFilesFromDataTransfer(dataTransfer).length > 0;
}

function loadStoredModelPreferences() {
  if (typeof window === "undefined") return EMPTY_MODEL_PREFERENCES;
  try {
    const raw = window.localStorage.getItem(MODEL_PREFERENCES_STORAGE_KEY);
    if (!raw) return EMPTY_MODEL_PREFERENCES;
    const parsed = JSON.parse(raw) as Partial<Record<Provider, string>>;
    return {
      openai: typeof parsed.openai === "string" && parsed.openai.trim() ? parsed.openai.trim() : null,
      anthropic: typeof parsed.anthropic === "string" && parsed.anthropic.trim() ? parsed.anthropic.trim() : null,
      xai: typeof parsed.xai === "string" && parsed.xai.trim() ? parsed.xai.trim() : null,
    };
  } catch {
    return EMPTY_MODEL_PREFERENCES;
  }
}

function normalizeStoredProvider(value: unknown): Provider {
  return value === "anthropic" || value === "xai" || value === "openai" ? value : "openai";
}

function normalizeStoredModelPreferences(value: unknown): ProviderModelPreferences {
  if (!value || typeof value !== "object" || Array.isArray(value)) return EMPTY_MODEL_PREFERENCES;
  const parsed = value as Partial<Record<Provider, unknown>>;
  return {
    openai: typeof parsed.openai === "string" && parsed.openai.trim() ? parsed.openai.trim() : null,
    anthropic: typeof parsed.anthropic === "string" && parsed.anthropic.trim() ? parsed.anthropic.trim() : null,
    xai: typeof parsed.xai === "string" && parsed.xai.trim() ? parsed.xai.trim() : null,
  };
}

function loadStoredQuickQuestionModelSelection(): QuickQuestionModelSelection {
  if (typeof window === "undefined") {
    return { provider: "openai", modelPreferences: EMPTY_MODEL_PREFERENCES };
  }
  try {
    const raw = window.localStorage.getItem(QUICK_QUESTION_MODEL_SELECTION_STORAGE_KEY);
    if (!raw) return { provider: "openai", modelPreferences: EMPTY_MODEL_PREFERENCES };
    const parsed = JSON.parse(raw) as { provider?: unknown; modelPreferences?: unknown };
    return {
      provider: normalizeStoredProvider(parsed.provider),
      modelPreferences: normalizeStoredModelPreferences(parsed.modelPreferences),
    };
  } catch {
    return { provider: "openai", modelPreferences: EMPTY_MODEL_PREFERENCES };
  }
}

function pickProviderModel(options: string[], preferred: string | null) {
  if (preferred?.trim()) return preferred.trim();
  return options[0] ?? "";
}

function getProviderLabel(provider: Provider | null | undefined) {
  if (provider === "openai") return "OpenAI";
  if (provider === "anthropic") return "Anthropic";
  if (provider === "xai") return "xAI";
  return "";
}

function getChatModelSelection(chat: Pick<ChatSummary, "lastUsedProvider" | "lastUsedModel"> | Pick<ChatDetail, "lastUsedProvider" | "lastUsedModel"> | null) {
  if (!chat?.lastUsedProvider || !chat.lastUsedModel?.trim()) return null;
  return {
    provider: chat.lastUsedProvider,
    model: chat.lastUsedModel.trim(),
  };
}

type ToolLogMetadata = {
  kind: "tool_call";
  toolCallId?: string;
  toolName?: string;
  status?: "completed" | "failed";
  summary?: string;
  args?: Record<string, unknown>;
  startedAt?: string;
  completedAt?: string;
  durationMs?: number;
  error?: string | null;
  resultPreview?: string | null;
};

function asToolLogMetadata(value: unknown): ToolLogMetadata | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  const record = value as Record<string, unknown>;
  if (record.kind !== "tool_call") return null;
  return record as ToolLogMetadata;
}

function isToolCallLogMessage(message: Message) {
  return asToolLogMetadata(message.metadata) !== null;
}

function isDisplayableMessage(message: Message) {
  return message.role !== "system";
}

function buildOptimisticToolMessage(event: ToolCallEvent): Message {
  return {
    id: `temp-tool-${event.toolCallId}`,
    createdAt: event.completedAt ?? new Date().toISOString(),
    role: "tool",
    content: event.summary,
    name: event.name,
    metadata: {
      kind: "tool_call",
      toolCallId: event.toolCallId,
      toolName: event.name,
      status: event.status,
      summary: event.summary,
      args: event.args,
      startedAt: event.startedAt,
      completedAt: event.completedAt,
      durationMs: event.durationMs,
      error: event.error ?? null,
      resultPreview: event.resultPreview ?? null,
    } satisfies ToolLogMetadata,
  };
}

type ModelComboboxProps = {
  options: string[];
  value: string;
  onChange: (value: string) => void;
  disabled?: boolean;
};

function ModelCombobox({ options, value, onChange, disabled = false }: ModelComboboxProps) {
  const [open, setOpen] = useState(false);
  const [draftValue, setDraftValue] = useState(value);
  const rootRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  const normalizedDraftValue = draftValue.trim();
  const filteredOptions = useMemo(() => {
    const needle = normalizedDraftValue.toLowerCase();
    if (!needle) return options;
    return options.filter((option) => option.toLowerCase().includes(needle));
  }, [normalizedDraftValue, options]);
  const hasExactOption = options.includes(normalizedDraftValue);

  useEffect(() => {
    if (open) return;
    setDraftValue(value);
  }, [open, value]);

  useEffect(() => {
    if (!open) return;
    inputRef.current?.focus();
  }, [open]);

  const commitDraftValue = () => {
    onChange(normalizedDraftValue);
    setDraftValue(normalizedDraftValue);
    setOpen(false);
  };

  useEffect(() => {
    if (!open) return;

    const handlePointerDown = (event: PointerEvent) => {
      if (rootRef.current?.contains(event.target as Node)) return;
      commitDraftValue();
    };

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "Escape") return;
      setOpen(false);
      setDraftValue(value);
    };

    window.addEventListener("pointerdown", handlePointerDown);
    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("pointerdown", handlePointerDown);
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [commitDraftValue, open, value]);

  return (
    <div className="relative" ref={rootRef}>
      <div className="flex h-10 min-w-56 items-center rounded-lg border border-violet-300/22 bg-background/72 px-3 text-sm shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.06)]">
        <input
          ref={inputRef}
          value={draftValue}
          onFocus={() => {
            if (disabled) return;
            setDraftValue(value);
            setOpen(true);
          }}
          onInput={(event) => {
            setDraftValue(event.currentTarget.value);
            setOpen(true);
          }}
          onKeyDown={(event) => {
            if (event.key !== "Enter") return;
            event.preventDefault();
            commitDraftValue();
          }}
          className="h-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
          placeholder="Select or type model"
          disabled={disabled}
        />
        <button
          type="button"
          className="ml-2 shrink-0 text-muted-foreground disabled:opacity-50"
          onClick={() => {
            if (disabled) return;
            if (open) {
              commitDraftValue();
              return;
            }
            setDraftValue(value);
            setOpen(true);
          }}
          disabled={disabled}
          aria-label="Toggle model options"
        >
          <ChevronDown className="h-4 w-4" />
        </button>
      </div>
      {open ? (
        <div className="absolute right-0 z-50 mt-2 w-full rounded-lg border border-violet-300/20 bg-[hsl(238_48%_7%)] p-1 shadow-2xl shadow-black/45">
          <div className="max-h-64 overflow-y-auto">
            {normalizedDraftValue && !hasExactOption ? (
              <button
                type="button"
                className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-violet-400/12"
                onClick={commitDraftValue}
              >
                <Check className={cn("h-4 w-4", normalizedDraftValue === value ? "opacity-100" : "opacity-0")} />
                <span className="truncate">Use "{normalizedDraftValue}"</span>
              </button>
            ) : null}
            {filteredOptions.length ? (
              filteredOptions.map((option) => (
                <button
                  key={option}
                  type="button"
                  className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-violet-400/12"
                  onClick={() => {
                    onChange(option);
                    setOpen(false);
                    setDraftValue(option);
                  }}
                >
                  <Check className={cn("h-4 w-4", option === value ? "opacity-100" : "opacity-0")} />
                  <span className="truncate">{option}</span>
                </button>
              ))
            ) : !normalizedDraftValue ? (
              <p className="px-2 py-2 text-sm text-muted-foreground">No models found</p>
            ) : null}
          </div>
        </div>
      ) : null}
    </div>
  );
}

function getChatTitle(chat: Pick<ChatSummary, "title">, messages?: ChatDetail["messages"]) {
  if (chat.title?.trim()) return chat.title.trim();
  const firstUserMessage = messages?.find((message) => message.role === "user");
  const firstUserText = firstUserMessage?.content.trim();
  if (firstUserText) return firstUserText.slice(0, 48);
  const firstUserAttachments = firstUserMessage ? getMessageAttachments(firstUserMessage.metadata) : [];
  const attachmentSummary = buildAttachmentSummary(firstUserAttachments);
  if (attachmentSummary) return attachmentSummary.slice(0, 48);
  return "New chat";
}

function getSearchTitle(search: Pick<SearchSummary, "title" | "query">) {
  if (search.title?.trim()) return search.title.trim();
  if (search.query?.trim()) return search.query.trim().slice(0, 64);
  return "New search";
}

function buildSidebarItems(chats: ChatSummary[], searches: SearchSummary[]): SidebarItem[] {
  const items: SidebarItem[] = [
    ...chats.map((chat) => ({
      kind: "chat" as const,
      id: chat.id,
      title: getChatTitle(chat),
      updatedAt: chat.updatedAt,
      createdAt: chat.createdAt,
      initiatedProvider: chat.initiatedProvider,
      initiatedModel: chat.initiatedModel,
      lastUsedProvider: chat.lastUsedProvider,
      lastUsedModel: chat.lastUsedModel,
    })),
    ...searches.map((search) => ({
      kind: "search" as const,
      id: search.id,
      title: getSearchTitle(search),
      updatedAt: search.updatedAt,
      createdAt: search.createdAt,
      initiatedProvider: null,
      initiatedModel: null,
      lastUsedProvider: null,
      lastUsedModel: null,
    })),
  ];

  return items.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
}

function formatDate(value: string) {
  return new Intl.DateTimeFormat(undefined, {
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  }).format(new Date(value));
}

function getSidebarSectionLabel(value: string) {
  const date = new Date(value);
  const now = new Date();
  const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
  const startOfItemDay = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
  const dayMs = 24 * 60 * 60 * 1000;
  const dayDelta = Math.floor((startOfToday - startOfItemDay) / dayMs);

  if (dayDelta <= 0) return "TODAY";
  if (dayDelta < 7) return "LAST 7 DAYS";
  return "EARLIER";
}

function buildSidebarSections(items: SidebarItem[]) {
  return items.reduce<Array<{ label: string; items: SidebarItem[] }>>((sections, item) => {
    const label = getSidebarSectionLabel(item.updatedAt);
    const section = sections.find((candidate) => candidate.label === label);
    if (section) {
      section.items.push(item);
    } else {
      sections.push({ label, items: [item] });
    }
    return sections;
  }, []);
}

export default function App() {
  const {
    authTokenInput,
    setAuthTokenInput,
    isCheckingSession,
    isSigningIn,
    isAuthenticated,
    authMode,
    authError,
    handleAuthFailure: baseHandleAuthFailure,
    handleSignIn,
    logout,
  } = useSessionAuth();

  const [chats, setChats] = useState<ChatSummary[]>([]);
  const [searches, setSearches] = useState<SearchSummary[]>([]);
  const [selectedItem, setSelectedItem] = useState<SidebarSelection | null>(null);
  const [selectedChat, setSelectedChat] = useState<ChatDetail | null>(null);
  const [selectedSearch, setSelectedSearch] = useState<SearchDetail | null>(null);
  const [draftKind, setDraftKind] = useState<DraftSelectionKind | null>(null);
  const [isLoadingCollections, setIsLoadingCollections] = useState(false);
  const [isLoadingSelection, setIsLoadingSelection] = useState(false);
  const [isSending, setIsSending] = useState(false);
  const [isStartingSearchChat, setIsStartingSearchChat] = useState(false);
  const [pendingChatState, setPendingChatState] = useState<{ chatId: string | null; messages: Message[] } | null>(null);
  const [composer, setComposer] = useState("");
  const [pendingAttachments, setPendingAttachments] = useState<ChatAttachment[]>([]);
  const [isComposerDropActive, setIsComposerDropActive] = useState(false);
  const [provider, setProvider] = useState<Provider>("openai");
  const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse["providers"]>(EMPTY_MODEL_CATALOG);
  const [providerModelPreferences, setProviderModelPreferences] = useState<ProviderModelPreferences>(() => loadStoredModelPreferences());
  const [model, setModel] = useState(() => {
    const stored = loadStoredModelPreferences();
    return stored.openai ?? PROVIDER_FALLBACK_MODELS.openai[0];
  });
  const [quickProvider, setQuickProvider] = useState<Provider>(() => loadStoredQuickQuestionModelSelection().provider);
  const [quickProviderModelPreferences, setQuickProviderModelPreferences] = useState<ProviderModelPreferences>(
    () => loadStoredQuickQuestionModelSelection().modelPreferences
  );
  const [quickModel, setQuickModel] = useState(() => {
    const stored = loadStoredQuickQuestionModelSelection();
    return stored.modelPreferences[stored.provider] ?? PROVIDER_FALLBACK_MODELS[stored.provider][0];
  });
  const [isQuickQuestionOpen, setIsQuickQuestionOpen] = useState(false);
  const [quickPrompt, setQuickPrompt] = useState("");
  const [quickSubmittedPrompt, setQuickSubmittedPrompt] = useState<string | null>(null);
  const [quickSubmittedModelSelection, setQuickSubmittedModelSelection] = useState<{ provider: Provider; model: string } | null>(null);
  const [quickQuestionMessages, setQuickQuestionMessages] = useState<Message[]>([]);
  const [isQuickQuestionSending, setIsQuickQuestionSending] = useState(false);
  const [isConvertingQuickQuestion, setIsConvertingQuickQuestion] = useState(false);
  const [quickQuestionError, setQuickQuestionError] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP);
  const transcriptContainerRef = useRef<HTMLDivElement>(null);
  const transcriptEndRef = useRef<HTMLDivElement>(null);
  const contextMenuRef = useRef<HTMLDivElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const dragDepthRef = useRef(0);
  const pendingAttachmentsRef = useRef<ChatAttachment[]>([]);
  const selectedItemRef = useRef<SidebarSelection | null>(null);
  const pendingTitleGenerationRef = useRef<Set<string>>(new Set());
  const searchRunAbortRef = useRef<AbortController | null>(null);
  const quickQuestionAbortRef = useRef<AbortController | null>(null);
  const searchRunCounterRef = useRef(0);
  const shouldAutoScrollRef = useRef(true);
  const wasSendingRef = useRef(false);
  const pendingReplyScrollRef = useRef(false);
  const transcriptTailSpacerHeightRef = useRef(TRANSCRIPT_BOTTOM_GAP);
  const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
  const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
  const [sidebarQuery, setSidebarQuery] = useState("");
  const initialRouteSelectionRef = useRef<SidebarSelection | null>(readSidebarSelectionFromUrl());
  const hasSyncedSelectionHistoryRef = useRef(false);

  const setTranscriptTailSpacer = (height: number) => {
    const nextHeight = Math.max(TRANSCRIPT_BOTTOM_GAP, Math.ceil(height));
    transcriptTailSpacerHeightRef.current = nextHeight;
    setTranscriptTailSpacerHeight(nextHeight);
  };

  const expandTranscriptTailSpacer = (height: number) => {
    const targetHeight = Math.max(TRANSCRIPT_BOTTOM_GAP, Math.ceil(height));
    setTranscriptTailSpacerHeight((currentHeight) => {
      const nextHeight = Math.max(currentHeight, targetHeight);
      transcriptTailSpacerHeightRef.current = nextHeight;
      return nextHeight;
    });
  };

  const settleTranscriptTailSpacer = () => {
    const container = transcriptContainerRef.current;
    const currentSpacerHeight = transcriptTailSpacerHeightRef.current;
    if (!container) {
      setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
      return;
    }

    const scrollHeightWithoutSpacer = container.scrollHeight - currentSpacerHeight;
    const requiredSpacerHeight = container.scrollTop + container.clientHeight - scrollHeightWithoutSpacer;
    setTranscriptTailSpacer(requiredSpacerHeight);
  };

  const focusComposer = () => {
    if (typeof window === "undefined") return;
    window.requestAnimationFrame(() => {
      const textarea = document.getElementById("composer-input") as HTMLTextAreaElement | null;
      textarea?.focus();
    });
  };

  useEffect(() => {
    if (typeof document === "undefined") return;
    const textarea = document.getElementById("composer-input") as HTMLTextAreaElement | null;
    if (!textarea) return;
    textarea.style.height = "0px";
    textarea.style.height = `${textarea.scrollHeight}px`;
  }, [composer]);

  useEffect(() => {
    pendingAttachmentsRef.current = pendingAttachments;
  }, [pendingAttachments]);

  const sidebarItems = useMemo(() => buildSidebarItems(chats, searches), [chats, searches]);
  const filteredSidebarItems = useMemo(() => {
    const query = sidebarQuery.trim().toLowerCase();
    if (!query) return sidebarItems;
    return sidebarItems.filter((item) => {
      const providerLabel = getProviderLabel(item.lastUsedProvider || item.initiatedProvider).toLowerCase();
      return [item.title, item.initiatedModel, item.lastUsedModel, providerLabel]
        .filter(Boolean)
        .some((value) => String(value).toLowerCase().includes(query));
    });
  }, [sidebarItems, sidebarQuery]);
  const sidebarSections = useMemo(() => buildSidebarSections(filteredSidebarItems), [filteredSidebarItems]);

  const resetWorkspaceState = () => {
    setChats([]);
    setSearches([]);
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
    setDraftKind(null);
    setPendingChatState(null);
    setComposer("");
    setPendingAttachments([]);
    setIsQuickQuestionOpen(false);
    setQuickPrompt("");
    setQuickSubmittedPrompt(null);
    setQuickSubmittedModelSelection(null);
    setQuickQuestionMessages([]);
    setQuickQuestionError(null);
    setError(null);
  };

  const handleAuthFailure = (message: string) => {
    baseHandleAuthFailure(message);
    resetWorkspaceState();
  };

  const refreshCollections = async (preferredSelection?: SidebarSelection) => {
    setIsLoadingCollections(true);
    try {
      const [nextChats, nextSearches] = await Promise.all([listChats(), listSearches()]);
      const nextItems = buildSidebarItems(nextChats, nextSearches);
      setChats(nextChats);
      setSearches(nextSearches);

      setSelectedItem((current) => {
        const hasItem = (candidate: SidebarSelection | null) => {
          if (!candidate) return false;
          return nextItems.some((item) => item.kind === candidate.kind && item.id === candidate.id);
        };

        if (preferredSelection && hasItem(preferredSelection)) {
          return preferredSelection;
        }
        if (hasItem(current)) {
          return current;
        }
        const first = nextItems[0];
        return first ? { kind: first.kind, id: first.id } : null;
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      setIsLoadingCollections(false);
    }
  };

  const refreshModels = async () => {
    try {
      const data = await listModels();
      setModelCatalog(data.providers);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  const refreshChat = async (chatId: string) => {
    setIsLoadingSelection(true);
    try {
      const chat = await getChat(chatId);
      setSelectedChat(chat);
      setSelectedSearch(null);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      setIsLoadingSelection(false);
    }
  };

  const refreshSearch = async (searchId: string) => {
    setIsLoadingSelection(true);
    try {
      const search = await getSearch(searchId);
      setSelectedSearch(search);
      setSelectedChat(null);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      setIsLoadingSelection(false);
    }
  };

  useEffect(() => {
    if (!isAuthenticated) return;
    const preferredSelection = initialRouteSelectionRef.current;
    initialRouteSelectionRef.current = null;
    void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels()]);
  }, [isAuthenticated]);

  useEffect(() => {
    const onPopState = () => {
      setContextMenu(null);
      setDraftKind(null);
      setSelectedItem(readSidebarSelectionFromUrl());
      setIsMobileSidebarOpen(false);
    };
    window.addEventListener("popstate", onPopState);
    return () => window.removeEventListener("popstate", onPopState);
  }, []);

  useEffect(() => {
    if (!isAuthenticated) {
      hasSyncedSelectionHistoryRef.current = false;
      return;
    }
    const current = `${window.location.pathname}${window.location.search}`;
    const next = buildWorkspaceUrl(selectedItem);

    if (!hasSyncedSelectionHistoryRef.current) {
      hasSyncedSelectionHistoryRef.current = true;
      if (current !== next) {
        window.history.replaceState({}, "", next);
      }
      return;
    }

    if (current !== next) {
      window.history.pushState({}, "", next);
    }
  }, [isAuthenticated, selectedItem]);

  const providerModelOptions = useMemo(() => getModelOptions(modelCatalog, provider), [modelCatalog, provider]);
  const quickProviderModelOptions = useMemo(() => getModelOptions(modelCatalog, quickProvider), [modelCatalog, quickProvider]);

  useEffect(() => {
    if (model.trim()) return;
    setModel((current) => {
      return current.trim() || pickProviderModel(providerModelOptions, providerModelPreferences[provider]);
    });
  }, [model, provider, providerModelOptions, providerModelPreferences]);

  useEffect(() => {
    if (typeof window === "undefined") return;
    window.localStorage.setItem(MODEL_PREFERENCES_STORAGE_KEY, JSON.stringify(providerModelPreferences));
  }, [providerModelPreferences]);

  useEffect(() => {
    if (quickModel.trim()) return;
    setQuickModel((current) => {
      return current.trim() || pickProviderModel(quickProviderModelOptions, quickProviderModelPreferences[quickProvider]);
    });
  }, [quickModel, quickProvider, quickProviderModelOptions, quickProviderModelPreferences]);

  useEffect(() => {
    if (typeof window === "undefined") return;
    window.localStorage.setItem(
      QUICK_QUESTION_MODEL_SELECTION_STORAGE_KEY,
      JSON.stringify({
        provider: quickProvider,
        modelPreferences: quickProviderModelPreferences,
      } satisfies QuickQuestionModelSelection)
    );
  }, [quickProvider, quickProviderModelPreferences]);

  useEffect(() => {
    if (!isQuickQuestionOpen || typeof window === "undefined") return;
    window.requestAnimationFrame(() => {
      const textarea = document.getElementById("quick-question-input") as HTMLTextAreaElement | null;
      if (!textarea) return;
      textarea.focus();
      textarea.style.height = "0px";
      textarea.style.height = `${textarea.scrollHeight}px`;
      if (textarea.value.length > 0) {
        textarea.select();
      }
    });
  }, [isQuickQuestionOpen]);

  useEffect(() => {
    if (typeof document === "undefined") return;
    const textarea = document.getElementById("quick-question-input") as HTMLTextAreaElement | null;
    if (!textarea) return;
    textarea.style.height = "0px";
    textarea.style.height = `${textarea.scrollHeight}px`;
  }, [quickPrompt, isQuickQuestionOpen]);

  const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null;
  const isChatReplyStreamingInView =
    isSending &&
    draftKind !== "search" &&
    selectedItem?.kind !== "search" &&
    !!pendingChatState &&
    (!pendingChatState.chatId || (selectedItem?.kind === "chat" && selectedItem.id === pendingChatState.chatId));

  useEffect(() => {
    shouldAutoScrollRef.current = true;
    if (!isSending || !isChatReplyStreamingInView) {
      setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
    }
  }, [draftKind, selectedItem?.kind, selectedKey]);

  useEffect(() => {
    selectedItemRef.current = selectedItem;
  }, [selectedItem]);

  useEffect(() => {
    if (!isAuthenticated) {
      setSelectedChat(null);
      setSelectedSearch(null);
      return;
    }
    if (!selectedItem) {
      setSelectedChat(null);
      setSelectedSearch(null);
      return;
    }

    if (selectedItem.kind === "chat") {
      void refreshChat(selectedItem.id);
      return;
    }
    void refreshSearch(selectedItem.id);
  }, [isAuthenticated, selectedKey]);

  useEffect(() => {
    if (draftKind === "search" || selectedItem?.kind === "search") return;
    const wasSending = wasSendingRef.current;
    wasSendingRef.current = isSending;
    if (isSending) return;
    if (wasSending) {
      shouldAutoScrollRef.current = false;
      return;
    }
    if (!shouldAutoScrollRef.current) return;
    transcriptEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
  }, [draftKind, selectedChat?.messages.length, isSending, selectedItem?.kind, selectedKey]);

  useEffect(() => {
    if (!isChatReplyStreamingInView || !pendingReplyScrollRef.current) return;
    pendingReplyScrollRef.current = false;
    shouldAutoScrollRef.current = true;

    window.requestAnimationFrame(() => {
      const container = transcriptContainerRef.current;
      if (!container) return;
      container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
    });
  }, [isChatReplyStreamingInView, pendingChatState?.chatId]);

  useEffect(() => {
    if (isSending) return;
    const hasWorkspaceSelection = Boolean(selectedItem) || draftKind !== null;
    if (!hasWorkspaceSelection) return;
    focusComposer();
  }, [draftKind, isSending, selectedKey]);

  useEffect(() => {
    return () => {
      searchRunAbortRef.current?.abort();
      searchRunAbortRef.current = null;
      quickQuestionAbortRef.current?.abort();
      quickQuestionAbortRef.current = null;
    };
  }, []);

  const messages = selectedChat?.messages ?? [];
  const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search";
  const isSearchRunning = isSending && isSearchMode;
  const isSendingActiveChat = isChatReplyStreamingInView;

  useEffect(() => {
    if (isSearchMode && pendingAttachments.length) {
      setPendingAttachments([]);
    }
    if (isSearchMode) {
      dragDepthRef.current = 0;
      setIsComposerDropActive(false);
    }
  }, [isSearchMode, pendingAttachments.length]);
  const displayMessages = useMemo(() => {
    if (!pendingChatState) return messages.filter(isDisplayableMessage);
    if (pendingChatState.chatId) {
      if (selectedItem?.kind === "chat" && selectedItem.id === pendingChatState.chatId) {
        return pendingChatState.messages.filter(isDisplayableMessage);
      }
      return messages.filter(isDisplayableMessage);
    }
    return (isSearchMode ? messages : pendingChatState.messages).filter(isDisplayableMessage);
  }, [isSearchMode, messages, pendingChatState, selectedItem]);
  const quickAnswerText = useMemo(() => {
    for (let index = quickQuestionMessages.length - 1; index >= 0; index -= 1) {
      const message = quickQuestionMessages[index];
      if (message.role === "assistant") return message.content;
    }
    return "";
  }, [quickQuestionMessages]);
  const canConvertQuickQuestion =
    Boolean(quickSubmittedPrompt?.trim()) &&
    Boolean(quickSubmittedModelSelection?.model.trim()) &&
    Boolean(quickAnswerText.trim()) &&
    !isQuickQuestionSending;

  const selectedChatSummary = useMemo(() => {
    if (!selectedItem || selectedItem.kind !== "chat") return null;
    return chats.find((chat) => chat.id === selectedItem.id) ?? null;
  }, [chats, selectedItem]);

  const selectedSearchSummary = useMemo(() => {
    if (!selectedItem || selectedItem.kind !== "search") return null;
    return searches.find((search) => search.id === selectedItem.id) ?? null;
  }, [searches, selectedItem]);

  useEffect(() => {
    if (draftKind || selectedItem?.kind !== "chat") return;
    const detailSelection = selectedChat?.id === selectedItem.id ? getChatModelSelection(selectedChat) : null;
    const summarySelection = getChatModelSelection(selectedChatSummary);
    const nextSelection = detailSelection ?? summarySelection;
    if (!nextSelection) return;
    setProvider(nextSelection.provider);
    setModel(nextSelection.model);
  }, [draftKind, selectedChat, selectedChatSummary, selectedItem]);

  const selectedTitle = useMemo(() => {
    if (draftKind === "chat") return "New chat";
    if (draftKind === "search") return "New search";
    if (!selectedItem) return "Sybil";
    if (selectedItem.kind === "chat") {
      if (selectedChat) return getChatTitle(selectedChat, selectedChat.messages);
      if (selectedChatSummary) return getChatTitle(selectedChatSummary);
      return "New chat";
    }
    if (selectedSearch) return getSearchTitle(selectedSearch);
    if (selectedSearchSummary) return getSearchTitle(selectedSearchSummary);
    return "New search";
  }, [draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearch, selectedSearchSummary]);

  const pageTitle = useMemo(() => {
    if (draftKind || !selectedItem) return "Sybil";
    if (selectedItem.kind === "chat") {
      if (selectedChat) return `${getChatTitle(selectedChat, selectedChat.messages)} — Sybil`;
      if (selectedChatSummary) return `${getChatTitle(selectedChatSummary)} — Sybil`;
      return "Sybil";
    }
    const searchQuery = selectedSearch?.query?.trim() || selectedSearchSummary?.query?.trim();
    if (searchQuery) return `${searchQuery} — Sybil`;
    if (selectedSearch) return `${getSearchTitle(selectedSearch)} — Sybil`;
    if (selectedSearchSummary) return `${getSearchTitle(selectedSearchSummary)} — Sybil`;
    return "Sybil";
  }, [draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearch, selectedSearchSummary]);
  const primaryShortcutModifier = useMemo(() => {
    if (typeof navigator === "undefined") return "Ctrl";
    return /Mac|iPhone|iPad|iPod/i.test(navigator.platform) ? "Cmd" : "Ctrl";
  }, []);

  useEffect(() => {
    document.title = pageTitle;
  }, [pageTitle]);

  const handleCreateChat = () => {
    setError(null);
    setContextMenu(null);
    setDraftKind("chat");
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
    setPendingAttachments([]);
    setIsMobileSidebarOpen(false);
  };

  const handleOpenQuickQuestion = () => {
    setQuickQuestionError(null);
    setIsQuickQuestionOpen(true);
    setIsMobileSidebarOpen(false);
  };

  const handleCreateSearch = () => {
    setError(null);
    setContextMenu(null);
    setDraftKind("search");
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
    setPendingAttachments([]);
    setIsMobileSidebarOpen(false);
  };

  const selectAdjacentSidebarItem = (direction: -1 | 1) => {
    if (!filteredSidebarItems.length) return;

    setError(null);
    setContextMenu(null);
    setDraftKind(null);
    setIsMobileSidebarOpen(false);
    setSelectedItem((current) => {
      const currentIndex = current
        ? filteredSidebarItems.findIndex((item) => item.kind === current.kind && item.id === current.id)
        : -1;
      const fallbackIndex = direction > 0 ? 0 : filteredSidebarItems.length - 1;
      const nextIndex =
        currentIndex < 0
          ? fallbackIndex
          : Math.min(filteredSidebarItems.length - 1, Math.max(0, currentIndex + direction));
      const nextItem = filteredSidebarItems[nextIndex];
      return { kind: nextItem.kind, id: nextItem.id };
    });
  };

  useEffect(() => {
    if (!isAuthenticated) return;

    const handleKeyDown = (event: KeyboardEvent) => {
      const hasPrimaryModifier = event.metaKey || event.ctrlKey;
      if (!hasPrimaryModifier || event.altKey) return;

      const key = event.key.toLowerCase();
      if (key === "i" && !event.shiftKey) {
        event.preventDefault();
        setQuickQuestionError(null);
        setIsQuickQuestionOpen((current) => !current);
        return;
      }

      if (isQuickQuestionOpen) return;

      if (key === "j") {
        event.preventDefault();
        if (event.shiftKey) {
          handleCreateSearch();
        } else {
          handleCreateChat();
        }
        focusComposer();
        return;
      }

      if (event.key === "ArrowUp" || event.key === "ArrowDown") {
        event.preventDefault();
        selectAdjacentSidebarItem(event.key === "ArrowUp" ? -1 : 1);
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [filteredSidebarItems, isAuthenticated, isQuickQuestionOpen]);

  const openContextMenu = (event: MouseEvent, item: SidebarSelection) => {
    event.preventDefault();
    const menuWidth = 160;
    const menuHeight = 40;
    const padding = 8;
    const x = Math.min(event.clientX, window.innerWidth - menuWidth - padding);
    const y = Math.min(event.clientY, window.innerHeight - menuHeight - padding);
    setContextMenu({ item, x: Math.max(padding, x), y: Math.max(padding, y) });
  };

  const handleDeleteFromContextMenu = async () => {
    if (!contextMenu || isSending) return;
    const target = contextMenu.item;
    setContextMenu(null);
    setError(null);
    try {
      if (target.kind === "chat") {
        await deleteChat(target.id);
      } else {
        await deleteSearch(target.id);
      }
      await refreshCollections();
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  useEffect(() => {
    if (!contextMenu) return;
    const handlePointerDown = (event: PointerEvent) => {
      if (contextMenuRef.current?.contains(event.target as Node)) return;
      setContextMenu(null);
    };
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") setContextMenu(null);
    };
    window.addEventListener("pointerdown", handlePointerDown);
    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("pointerdown", handlePointerDown);
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [contextMenu]);

  useEffect(() => {
    if (!isQuickQuestionOpen) return;
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "Escape") return;
      event.preventDefault();
      setIsQuickQuestionOpen(false);
    };
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [isQuickQuestionOpen]);

  const handleOpenAttachmentPicker = () => {
    fileInputRef.current?.click();
  };

  const handleRemovePendingAttachment = (attachmentId: string) => {
    setPendingAttachments((current) => current.filter((attachment) => attachment.id !== attachmentId));
  };

  const appendPendingAttachments = async (files: File[]) => {
    if (!files.length) return;
    if (isSearchMode) {
      setError("Attachments are only available in chat mode.");
      return;
    }

    setError(null);

    try {
      const attachments = await Promise.all(files.map((file) => buildChatAttachment(file)));
      if (pendingAttachmentsRef.current.length + attachments.length > MAX_CHAT_ATTACHMENTS) {
        throw new Error(`You can attach up to ${MAX_CHAT_ATTACHMENTS} files per message.`);
      }
      setPendingAttachments((current) => current.concat(attachments));
      focusComposer();
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      setError(message);
    }
  };

  const handleFileSelection = async (event: Event) => {
    const input = event.currentTarget as HTMLInputElement;
    const files = Array.from(input.files ?? []);
    input.value = "";
    await appendPendingAttachments(files);
  };

  const handleComposerPaste = async (event: ClipboardEvent) => {
    const files = getFilesFromDataTransfer(event.clipboardData);
    if (!files.length) return;
    event.preventDefault();
    await appendPendingAttachments(files);
  };

  const handleComposerDragEnter = (event: DragEvent) => {
    if (!hasFileTransfer(event.dataTransfer)) return;
    event.preventDefault();
    if (isSearchMode) return;
    dragDepthRef.current += 1;
    setIsComposerDropActive(true);
  };

  const handleComposerDragOver = (event: DragEvent) => {
    if (!hasFileTransfer(event.dataTransfer)) return;
    event.preventDefault();
    if (event.dataTransfer) {
      event.dataTransfer.dropEffect = isSearchMode ? "none" : "copy";
    }
    if (!isSearchMode) {
      setIsComposerDropActive(true);
    }
  };

  const handleComposerDragLeave = (event: DragEvent) => {
    if (!hasFileTransfer(event.dataTransfer)) return;
    event.preventDefault();
    if (isSearchMode) return;
    dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
    if (dragDepthRef.current === 0) {
      setIsComposerDropActive(false);
    }
  };

  const handleComposerDrop = async (event: DragEvent) => {
    if (!hasFileTransfer(event.dataTransfer)) return;
    event.preventDefault();
    dragDepthRef.current = 0;
    setIsComposerDropActive(false);
    await appendPendingAttachments(getFilesFromDataTransfer(event.dataTransfer));
  };

  const handleSendChat = async (content: string, attachments: ChatAttachment[]) => {
    pendingReplyScrollRef.current = true;
    expandTranscriptTailSpacer(getReplyScrollBufferHeight());

    const optimisticUserMessage: Message = {
      id: `temp-user-${Date.now()}`,
      createdAt: new Date().toISOString(),
      role: "user",
      content,
      name: null,
      metadata: attachments.length ? { attachments } : null,
    };

    const optimisticAssistantMessage: Message = {
      id: `temp-assistant-${Date.now()}`,
      createdAt: new Date().toISOString(),
      role: "assistant",
      content: "",
      name: null,
      metadata: null,
    };

    setPendingChatState({
      chatId: selectedItem?.kind === "chat" ? selectedItem.id : null,
      messages: (selectedChat?.messages ?? []).concat(optimisticUserMessage, optimisticAssistantMessage),
    });

    let chatId = draftKind === "chat" ? null : selectedItem?.kind === "chat" ? selectedItem.id : null;

    if (!chatId) {
      const chat = await createChat();
      chatId = chat.id;
      setDraftKind(null);
      setChats((current) => {
        const withoutExisting = current.filter((existing) => existing.id !== chat.id);
        return [chat, ...withoutExisting];
      });
      setSelectedItem({ kind: "chat", id: chatId });
      setPendingChatState((current) => (current ? { ...current, chatId } : current));
      setSelectedChat({
        id: chat.id,
        title: chat.title,
        createdAt: chat.createdAt,
        updatedAt: chat.updatedAt,
        initiatedProvider: chat.initiatedProvider,
        initiatedModel: chat.initiatedModel,
        lastUsedProvider: chat.lastUsedProvider,
        lastUsedModel: chat.lastUsedModel,
        messages: [],
      });
      setSelectedSearch(null);
    }

    if (!chatId) {
      throw new Error("Unable to initialize chat");
    }

    let baseChat = selectedChat;
    if (!baseChat || baseChat.id !== chatId) {
      baseChat = await getChat(chatId);
    }

    const requestMessages: CompletionRequestMessage[] = [
      ...baseChat.messages
        .filter((message) => !isToolCallLogMessage(message))
        .map((message) => ({
          role: message.role,
          content: message.content,
          ...(message.name ? { name: message.name } : {}),
          ...(getMessageAttachments(message.metadata).length ? { attachments: getMessageAttachments(message.metadata) } : {}),
        })),
      {
        role: "user",
        content,
        ...(attachments.length ? { attachments } : {}),
      },
    ];

    const selectedModel = model.trim();
    if (!selectedModel) {
      throw new Error("No model available for selected provider");
    }

    const chatSummary = chats.find((chat) => chat.id === chatId);
    const hasExistingTitle = Boolean(selectedChat?.id === chatId ? selectedChat.title?.trim() : chatSummary?.title?.trim());
    if (!hasExistingTitle && !pendingTitleGenerationRef.current.has(chatId)) {
      pendingTitleGenerationRef.current.add(chatId);
      const titleSeed = content || buildAttachmentSummary(attachments) || "Uploaded files";
      void suggestChatTitle({ chatId, content: titleSeed })
        .then((updatedChat) => {
          setChats((current) =>
            current.map((chat) => {
              if (chat.id !== updatedChat.id) return chat;
              return { ...chat, title: updatedChat.title, updatedAt: updatedChat.updatedAt };
            })
          );
          setSelectedChat((current) => {
            if (!current || current.id !== updatedChat.id) return current;
            return { ...current, title: updatedChat.title, updatedAt: updatedChat.updatedAt };
          });
        })
        .catch(() => {
          // ignore title suggestion errors so chat flow is not interrupted
        })
        .finally(() => {
          pendingTitleGenerationRef.current.delete(chatId);
        });
    }

    let streamErrorMessage: string | null = null;

    await runCompletionStream(
      {
        chatId,
        provider,
        model: selectedModel,
        messages: requestMessages,
      },
      {
        onMeta: (payload) => {
          if (payload.chatId !== chatId) return;
          setPendingChatState((current) => (current ? { ...current, chatId: payload.chatId } : current));
        },
        onToolCall: (payload) => {
          setPendingChatState((current) => {
            if (!current) return current;
            if (
              current.messages.some(
                (message) =>
                  asToolLogMetadata(message.metadata)?.toolCallId === payload.toolCallId || message.id === `temp-tool-${payload.toolCallId}`
              )
            ) {
              return current;
            }

            const toolMessage = buildOptimisticToolMessage(payload);
            const assistantIndex = current.messages.findIndex(
              (message, index, all) => index === all.length - 1 && message.id.startsWith("temp-assistant-")
            );
            if (assistantIndex < 0) {
              return { ...current, messages: current.messages.concat(toolMessage) };
            }
            return {
              ...current,
              messages: [
                ...current.messages.slice(0, assistantIndex),
                toolMessage,
                ...current.messages.slice(assistantIndex),
              ],
            };
          });
        },
        onDelta: (payload) => {
          if (!payload.text) return;
          setPendingChatState((current) => {
            if (!current) return current;
            let updated = false;
            const nextMessages = current.messages.map((message, index, all) => {
              const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-");
              if (!isTarget) return message;
              updated = true;
              return { ...message, content: message.content + payload.text };
            });
            return updated ? { ...current, messages: nextMessages } : current;
          });
        },
        onDone: (payload) => {
          setPendingChatState((current) => {
            if (!current) return current;
            let updated = false;
            const nextMessages = current.messages.map((message, index, all) => {
              const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-");
              if (!isTarget) return message;
              updated = true;
              return { ...message, content: payload.text };
            });
            return updated ? { ...current, messages: nextMessages } : current;
          });
        },
        onError: (payload) => {
          streamErrorMessage = payload.message;
        },
      }
    );

    if (streamErrorMessage) {
      throw new Error(streamErrorMessage);
    }

    await refreshCollections();
    const currentSelection = selectedItemRef.current;
    if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
      await refreshChat(chatId);
    }
    settleTranscriptTailSpacer();
    setPendingChatState(null);
  };

  const handleSendSearch = async (query: string) => {
    const runId = ++searchRunCounterRef.current;
    searchRunAbortRef.current?.abort();
    const abortController = new AbortController();
    searchRunAbortRef.current = abortController;

    let searchId = draftKind === "search" ? null : selectedItem?.kind === "search" ? selectedItem.id : null;

    if (!searchId) {
      const search = await createSearch({
        query,
        title: query.slice(0, 80),
      });
      searchId = search.id;
      setDraftKind(null);
      setSelectedItem({ kind: "search", id: searchId });
    }

    if (!searchId) {
      throw new Error("Unable to initialize search");
    }

    const nowIso = new Date().toISOString();
    setSelectedSearch((current) => {
      if (!current || current.id !== searchId) {
        return {
          id: searchId,
          title: query.slice(0, 80),
          query,
          createdAt: nowIso,
          updatedAt: nowIso,
          requestId: null,
          latencyMs: null,
          error: null,
          answerText: null,
          answerRequestId: null,
          answerCitations: null,
          answerError: null,
          results: [],
        };
      }
      return {
        ...current,
        title: query.slice(0, 80),
        query,
        error: null,
        latencyMs: null,
        answerText: null,
        answerRequestId: null,
        answerCitations: null,
        answerError: null,
        results: [],
      };
    });

    try {
      await runSearchStream(
        searchId,
        {
          query,
          title: query.slice(0, 80),
          type: "auto",
          numResults: 10,
        },
        {
          onSearchResults: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch((current) => {
              if (!current || current.id !== searchId) return current;
              return {
                ...current,
                requestId: payload.requestId ?? current.requestId,
                error: null,
                results: payload.results,
              };
            });
          },
          onSearchError: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch((current) => {
              if (!current || current.id !== searchId) return current;
              return { ...current, error: payload.error };
            });
          },
          onAnswer: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch((current) => {
              if (!current || current.id !== searchId) return current;
              return {
                ...current,
                answerText: payload.answerText,
                answerRequestId: payload.answerRequestId,
                answerCitations: payload.answerCitations,
                answerError: null,
              };
            });
          },
          onAnswerError: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch((current) => {
              if (!current || current.id !== searchId) return current;
              return { ...current, answerError: payload.error };
            });
          },
          onDone: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setSelectedSearch(payload.search);
            setSelectedChat(null);
          },
          onError: (payload) => {
            if (runId !== searchRunCounterRef.current) return;
            setError(payload.message);
          },
        },
        { signal: abortController.signal }
      );
    } catch (err) {
      if (abortController.signal.aborted) return;
      throw err;
    } finally {
      if (runId === searchRunCounterRef.current) {
        searchRunAbortRef.current = null;
      }
    }

    await refreshCollections({ kind: "search", id: searchId });
  };

  const handleStartChatFromSearch = async () => {
    if (!selectedSearch || isStartingSearchChat || isSending) return;

    setError(null);
    setIsStartingSearchChat(true);
    try {
      const chat = await createChatFromSearch(selectedSearch.id);
      setDraftKind(null);
      setPendingChatState(null);
      setComposer("");
      setPendingAttachments([]);
      setChats((current) => {
        const withoutExisting = current.filter((existing) => existing.id !== chat.id);
        return [chat, ...withoutExisting];
      });
      setSelectedItem({ kind: "chat", id: chat.id });
      setSelectedChat({
        id: chat.id,
        title: chat.title,
        createdAt: chat.createdAt,
        updatedAt: chat.updatedAt,
        initiatedProvider: chat.initiatedProvider,
        initiatedModel: chat.initiatedModel,
        lastUsedProvider: chat.lastUsedProvider,
        lastUsedModel: chat.lastUsedModel,
        messages: [],
      });
      setSelectedSearch(null);
      await refreshCollections({ kind: "chat", id: chat.id });
      await refreshChat(chat.id);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      setIsStartingSearchChat(false);
    }
  };

  const handleSendQuickQuestion = async () => {
    const content = quickPrompt.trim();
    if (!content || isQuickQuestionSending || isConvertingQuickQuestion) return;

    const selectedModel = quickModel.trim();
    if (!selectedModel) {
      setQuickQuestionError("No model available for selected provider");
      return;
    }

    const now = new Date().toISOString();
    const optimisticAssistantMessage: Message = {
      id: `temp-assistant-quick-${Date.now()}`,
      createdAt: now,
      role: "assistant",
      content: "",
      name: null,
      metadata: null,
    };

    quickQuestionAbortRef.current?.abort();
    const abortController = new AbortController();
    quickQuestionAbortRef.current = abortController;

    setQuickQuestionError(null);
    setQuickSubmittedPrompt(content);
    setQuickSubmittedModelSelection({ provider: quickProvider, model: selectedModel });
    setQuickQuestionMessages([optimisticAssistantMessage]);
    setIsQuickQuestionSending(true);

    let streamErrorMessage: string | null = null;

    try {
      await runCompletionStream(
        {
          persist: false,
          provider: quickProvider,
          model: selectedModel,
          messages: [{ role: "user", content }],
        },
        {
          onToolCall: (payload) => {
            setQuickQuestionMessages((current) => {
              if (
                current.some(
                  (message) =>
                    asToolLogMetadata(message.metadata)?.toolCallId === payload.toolCallId || message.id === `temp-tool-${payload.toolCallId}`
                )
              ) {
                return current;
              }

              const toolMessage = buildOptimisticToolMessage(payload);
              const assistantIndex = current.findIndex(
                (message, index, all) => index === all.length - 1 && message.id.startsWith("temp-assistant-quick-")
              );
              if (assistantIndex < 0) return current.concat(toolMessage);
              return [
                ...current.slice(0, assistantIndex),
                toolMessage,
                ...current.slice(assistantIndex),
              ];
            });
          },
          onDelta: (payload) => {
            if (!payload.text) return;
            setQuickQuestionMessages((current) => {
              let updated = false;
              const nextMessages = current.map((message, index, all) => {
                const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-quick-");
                if (!isTarget) return message;
                updated = true;
                return { ...message, content: message.content + payload.text };
              });
              return updated ? nextMessages : current;
            });
          },
          onDone: (payload) => {
            setQuickQuestionMessages((current) => {
              let updated = false;
              const nextMessages = current.map((message, index, all) => {
                const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-quick-");
                if (!isTarget) return message;
                updated = true;
                return { ...message, content: payload.text };
              });
              return updated ? nextMessages : current;
            });
          },
          onError: (payload) => {
            streamErrorMessage = payload.message;
          },
        },
        { signal: abortController.signal }
      );

      if (streamErrorMessage) {
        throw new Error(streamErrorMessage);
      }
    } catch (err) {
      if (abortController.signal.aborted) return;
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setQuickQuestionError(message);
      }
    } finally {
      if (quickQuestionAbortRef.current === abortController) {
        quickQuestionAbortRef.current = null;
      }
      if (!abortController.signal.aborted) {
        setIsQuickQuestionSending(false);
      }
    }
  };

  const handleConvertQuickQuestionToChat = async () => {
    const question = quickSubmittedPrompt?.trim();
    const answer = quickAnswerText.trim();
    const selection = quickSubmittedModelSelection;
    if (!question || !answer || !selection || isQuickQuestionSending || isConvertingQuickQuestion) return;

    setQuickQuestionError(null);
    setIsConvertingQuickQuestion(true);

    try {
      const title = question.split(/\r?\n/)[0]?.trim().slice(0, 48) || "Quick question";
      const chat = await createChat({
        title,
        provider: selection.provider,
        model: selection.model,
        messages: [
          { role: "user", content: question },
          { role: "assistant", content: answer },
        ],
      });

      setDraftKind(null);
      setPendingChatState(null);
      setComposer("");
      setPendingAttachments([]);
      setIsQuickQuestionOpen(false);
      setProvider(selection.provider);
      setModel(selection.model);
      setChats((current) => {
        const withoutExisting = current.filter((existing) => existing.id !== chat.id);
        return [chat, ...withoutExisting];
      });
      setSelectedItem({ kind: "chat", id: chat.id });
      setSelectedChat({
        id: chat.id,
        title: chat.title,
        createdAt: chat.createdAt,
        updatedAt: chat.updatedAt,
        initiatedProvider: chat.initiatedProvider,
        initiatedModel: chat.initiatedModel,
        lastUsedProvider: chat.lastUsedProvider,
        lastUsedModel: chat.lastUsedModel,
        messages: [],
      });
      setSelectedSearch(null);
      await refreshCollections({ kind: "chat", id: chat.id });
      await refreshChat(chat.id);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setQuickQuestionError(message);
      }
    } finally {
      setIsConvertingQuickQuestion(false);
    }
  };

  const handleSend = async () => {
    const content = composer.trim();
    const attachments = pendingAttachments;
    if ((!content && !attachments.length) || isSending) return;
    if (isSearchMode && attachments.length) {
      setError("Attachments are only available in chat mode.");
      return;
    }

    setComposer("");
    setPendingAttachments([]);
    setError(null);
    setIsSending(true);

    try {
      if (isSearchMode) {
        await handleSendSearch(content);
      } else {
        await handleSendChat(content, attachments);
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }

      if (!isSearchMode) {
        setComposer(content);
        setPendingAttachments(attachments);
        setPendingChatState(null);
      }

      if (selectedItem?.kind === "chat") {
        await refreshChat(selectedItem.id);
      }
      if (selectedItem?.kind === "search") {
        await refreshSearch(selectedItem.id);
      }
    } finally {
      setIsSending(false);
      focusComposer();
    }
  };


  if (isCheckingSession) {
    return (
      <div className="flex h-full items-center justify-center">
        <p className="text-sm text-muted-foreground">Checking session...</p>
      </div>
    );
  }

  if (!isAuthenticated) {
    return (
      <AuthScreen
        authTokenInput={authTokenInput}
        setAuthTokenInput={setAuthTokenInput}
        isSigningIn={isSigningIn}
        authError={authError}
        onSignIn={handleSignIn}
      />
    );
  }

  return (
    <div className="app-grid-surface h-full p-0 md:p-2">
      <div className="flex h-full w-full overflow-hidden bg-transparent md:gap-2">
        {isMobileSidebarOpen ? (
          <button
            type="button"
            className="fixed inset-0 z-30 bg-black/70 backdrop-blur-sm md:hidden"
            onClick={() => setIsMobileSidebarOpen(false)}
            aria-label="Close sidebar"
          />
        ) : null}

        <aside
          className={cn(
            "glass-panel fixed inset-y-0 left-0 z-40 flex w-[86vw] max-w-80 shrink-0 flex-col border-r border-violet-300/18 transition-transform md:static md:z-auto md:w-80 md:max-w-none md:rounded-2xl md:border",
            isMobileSidebarOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
          )}
        >
          <div className="px-4 pb-4 pt-5">
            <div className="sybil-wordmark bg-[linear-gradient(90deg,#ff8df8,#9a6dff_54%,#67dfff)] bg-clip-text text-3xl text-transparent">
              SYBIL
            </div>
            <p className="mt-2 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
              <span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
              Sybil Web{authMode ? ` (${authMode === "open" ? "open mode" : "token mode"})` : ""}
            </p>
          </div>

          <div className="space-y-3 px-3 pb-3">
            <div className="flex gap-2">
              <Button className="h-11 min-w-0 flex-1 justify-start gap-3 text-[15px]" onClick={handleCreateChat}>
                <Plus className="h-4 w-4" />
                New chat
                <span className="ml-auto rounded-md border border-violet-100/12 bg-white/5 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-100/52">
                  {primaryShortcutModifier} J
                </span>
              </Button>
              <div className="group relative">
                <Button
                  className="h-11 w-11 rounded-lg"
                  onClick={handleOpenQuickQuestion}
                  size="icon"
                  variant="secondary"
                  title={`${primaryShortcutModifier}+i`}
                  aria-label="Quick question"
                >
                  <Rabbit className="h-4 w-4" />
                </Button>
                <span className="pointer-events-none absolute left-1/2 top-full z-50 mt-2 -translate-x-1/2 whitespace-nowrap rounded-md border border-violet-300/22 bg-[hsl(238_48%_7%)] px-2 py-1 text-xs font-semibold text-violet-100/90 opacity-0 shadow-xl shadow-black/35 transition group-hover:opacity-100 group-focus-within:opacity-100">
                  {primaryShortcutModifier}+i
                </span>
              </div>
            </div>
            <Button className="h-10 w-full justify-start gap-3" variant="secondary" onClick={handleCreateSearch}>
              <Search className="h-4 w-4" />
              New search
              <span className="ml-auto rounded-md border border-violet-100/10 bg-white/[0.035] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-100/44">
                Shift {primaryShortcutModifier} J
              </span>
            </Button>
            <div className="relative">
              <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-violet-200/58" />
              <input
                value={sidebarQuery}
                onInput={(event) => setSidebarQuery(event.currentTarget.value)}
                placeholder="Search chats"
                className="h-10 w-full rounded-lg border border-violet-300/18 bg-background/66 pl-9 pr-3 text-sm text-violet-50 outline-none shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.05)] placeholder:text-muted-foreground focus:border-violet-300/45 focus:ring-1 focus:ring-ring/70"
              />
            </div>
          </div>
          <Separator className="bg-violet-300/10" />
          <div className="flex-1 overflow-y-auto px-2 py-3">
            {isLoadingCollections && sidebarItems.length === 0 ? <p className="px-2 py-3 text-sm text-muted-foreground">Loading conversations...</p> : null}
            {!isLoadingCollections && sidebarItems.length === 0 ? (
              <div className="flex h-full flex-col items-center justify-center gap-2 p-5 text-center text-sm text-muted-foreground">
                <MessageSquare className="h-5 w-5" />
                Start a chat or run your first search.
              </div>
            ) : null}
            {!isLoadingCollections && sidebarItems.length > 0 && filteredSidebarItems.length === 0 ? (
              <p className="px-2 py-3 text-sm text-muted-foreground">No chats found.</p>
            ) : null}
            {sidebarSections.map((section) => (
              <div key={section.label} className="mb-4">
                <p className="px-3 pb-2 text-[11px] font-semibold text-violet-200/48">{section.label}</p>
                {section.items.map((item) => {
                  const active = selectedItem?.kind === item.kind && selectedItem.id === item.id;
                  const initiatedLabel = item.kind === "chat" && item.initiatedModel
                    ? `${getProviderLabel(item.initiatedProvider)}${item.initiatedProvider ? " · " : ""}${item.initiatedModel}`
                    : null;
                  return (
                    <button
                      key={`${item.kind}-${item.id}`}
                      className={cn(
                        "mb-1 w-full rounded-lg border px-3 py-2.5 text-left transition",
                        active
                          ? "border-violet-300/45 bg-[linear-gradient(135deg,hsl(258_86%_52%_/_0.58),hsl(277_78%_28%_/_0.55))] text-violet-50"
                          : "border-transparent text-violet-100/78 hover:border-violet-300/18 hover:bg-violet-400/10"
                      )}
                      onClick={() => {
                        setContextMenu(null);
                        setDraftKind(null);
                        setSelectedItem({ kind: item.kind, id: item.id });
                        setIsMobileSidebarOpen(false);
                      }}
                      onContextMenu={(event) => openContextMenu(event, { kind: item.kind, id: item.id })}
                      type="button"
                    >
                      <div className="flex items-center gap-2">
                        <span
                          className={cn(
                            "flex h-5 w-5 shrink-0 items-center justify-center rounded-md border",
                            active ? "border-cyan-200/35 bg-cyan-300/12 text-cyan-100" : "border-violet-300/18 text-violet-200/70"
                          )}
                        >
                          {item.kind === "chat" ? <MessageSquare className="h-3.5 w-3.5" /> : <Search className="h-3.5 w-3.5" />}
                        </span>
                        <p className="truncate text-sm font-semibold">{item.title}</p>
                        <p className={cn("ml-auto shrink-0 text-xs", active ? "text-violet-100/86" : "text-violet-200/50")}>{formatDate(item.updatedAt)}</p>
                      </div>
                      {initiatedLabel ? (
                        <p className={cn("mt-1 truncate text-right text-xs", active ? "text-violet-100/62" : "text-violet-200/42")}>{initiatedLabel}</p>
                      ) : null}
                    </button>
                  );
                })}
              </div>
            ))}
          </div>
        </aside>

        <main className="glass-panel relative flex min-w-0 flex-1 flex-col overflow-hidden border-violet-300/18 md:rounded-2xl md:border">
          <header className="flex flex-wrap items-center justify-between gap-3 border-b border-violet-300/12 bg-[linear-gradient(180deg,hsl(243_48%_10%_/_0.86),hsl(236_48%_6%_/_0.66))] px-4 py-3 md:px-7">
            <div className="flex items-start gap-2">
              <Button
                type="button"
                size="icon"
                variant="ghost"
                className="-ml-1 h-8 w-8 md:hidden"
                onClick={() => setIsMobileSidebarOpen(true)}
                aria-label="Open sidebar"
              >
                <Menu className="h-4 w-4" />
              </Button>

              <div>
                <h1 className="text-sm font-semibold text-violet-50 md:text-base">{selectedTitle}</h1>
              </div>
            </div>
            <div className="flex w-full max-w-xl items-center gap-2 md:w-auto">
              {!isSearchMode ? (
                <>
                  <select
                    className="h-10 min-w-32 rounded-lg border border-violet-300/22 bg-background/72 px-3 text-sm text-violet-50 outline-none shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.06)] focus:border-violet-300/45 focus:ring-1 focus:ring-ring/70"
                    value={provider}
                    onChange={(event) => {
                      const nextProvider = event.currentTarget.value as Provider;
                      setProvider(nextProvider);
                      const options = getModelOptions(modelCatalog, nextProvider);
                      setModel(pickProviderModel(options, providerModelPreferences[nextProvider]));
                    }}
                    disabled={isSending}
                  >
                    <option value="openai">OpenAI</option>
                    <option value="anthropic">Anthropic</option>
                    <option value="xai">xAI</option>
                  </select>
                  <ModelCombobox
                    options={providerModelOptions}
                    value={model}
                    disabled={isSending}
                    onChange={(nextModel) => {
                      const normalizedModel = nextModel.trim();
                      setModel(normalizedModel);
                      setProviderModelPreferences((current) => ({
                        ...current,
                        [provider]: normalizedModel || null,
                      }));
                    }}
                  />
                </>
              ) : (
                <div className="flex h-10 items-center rounded-lg border border-cyan-300/22 bg-cyan-300/8 px-3 text-sm text-cyan-100">
                  <Globe2 className="mr-2 h-4 w-4" />
                  Search mode
                </div>
              )}
            </div>
          </header>

          <div
            ref={transcriptContainerRef}
            className="flex-1 overflow-y-auto px-4 pt-8 md:px-10 lg:px-14 pb-36 md:pb-44 [overflow-anchor:none]"
            onScroll={() => {
              const container = transcriptContainerRef.current;
              if (!container) return;
              const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
              shouldAutoScrollRef.current = distanceFromBottom < 96;
            }}
          >
            {!isSearchMode ? (
              <ChatMessagesPanel messages={displayMessages} isLoading={isLoadingSelection} isSending={isSendingActiveChat} />
            ) : (
              <SearchResultsPanel
                search={selectedSearch}
                isLoading={isLoadingSelection}
                isRunning={isSearchRunning}
                isStartingChat={isStartingSearchChat}
                onStartChat={selectedSearch ? handleStartChatFromSearch : undefined}
              />
            )}
            <div
              className="mx-auto max-w-4xl"
              style={{ height: `${transcriptTailSpacerHeight}px` }}
              aria-hidden="true"
            />
            <div ref={transcriptEndRef} />
          </div>

          <footer className="pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-[linear-gradient(to_top,hsl(235_50%_4%)_0%,hsl(235_50%_4%_/_0.92)_58%,transparent)] p-3 pt-14 md:p-6 md:pt-20">
            <div
              className={cn(
                "pointer-events-auto mx-auto max-w-4xl rounded-2xl border bg-[linear-gradient(135deg,hsl(235_48%_7%_/_0.96),hsl(258_48%_11%_/_0.94))] p-2 shadow-lg shadow-black/20 transition",
                isComposerDropActive
                  ? "border-cyan-300/70 shadow-cyan-500/20"
                  : "border-violet-300/30"
              )}
              onDragEnter={handleComposerDragEnter}
              onDragOver={handleComposerDragOver}
              onDragLeave={handleComposerDragLeave}
              onDrop={(event) => {
                void handleComposerDrop(event);
              }}
            >
              <input
                ref={fileInputRef}
                type="file"
                multiple
                accept={CHAT_FILE_ACCEPT}
                className="hidden"
                onChange={(event) => {
                  void handleFileSelection(event);
                }}
              />
              {!isSearchMode && pendingAttachments.length ? (
                <div className="px-2 pb-2 pt-1">
                  <ChatAttachmentList attachments={pendingAttachments} onRemove={handleRemovePendingAttachment} />
                </div>
              ) : null}
              {!isSearchMode && isComposerDropActive ? (
                <div className="px-3 pb-2">
                  <div className="rounded-xl border border-dashed border-cyan-300/55 bg-cyan-300/8 px-4 py-3 text-sm text-cyan-100">
                    Drop files to attach them
                  </div>
                </div>
              ) : null}
              <Textarea
                id="composer-input"
                rows={1}
                value={composer}
                onInput={(event) => {
                  const textarea = event.currentTarget;
                  textarea.style.height = "0px";
                  textarea.style.height = `${textarea.scrollHeight}px`;
                  setComposer(textarea.value);
                }}
                onPaste={(event) => {
                  void handleComposerPaste(event);
                }}
                onKeyDown={(event) => {
                  if (event.key === "Enter" && !event.shiftKey) {
                    event.preventDefault();
                    void handleSend();
                  }
                }}
                placeholder={isSearchMode ? "Search the web" : "Message Sybil..."}
                className="max-h-40 min-h-0 resize-none overflow-y-auto border-0 bg-transparent px-3 py-3 text-base text-violet-50 shadow-none placeholder:text-violet-200/45 focus-visible:ring-0"
                disabled={isSending}
              />
              <div className={cn("flex items-center gap-3 px-2 pb-1", error ? "justify-between" : "justify-end")}>
                {error ? <p className="min-w-0 truncate text-xs text-rose-300">{error}</p> : null}
                {!isSearchMode ? (
                  <Button
                    className="h-10 w-10 rounded-lg"
                    onClick={handleOpenAttachmentPicker}
                    size="icon"
                    variant="secondary"
                    disabled={isSending || pendingAttachments.length >= MAX_CHAT_ATTACHMENTS}
                    aria-label="Attach files"
                  >
                    <Paperclip className="h-4 w-4" />
                  </Button>
                ) : null}
                <Button
                  className="h-10 w-10 rounded-lg"
                  onClick={() => void handleSend()}
                  size="icon"
                  disabled={isSending || (!composer.trim() && !pendingAttachments.length)}
                >
                  {isSearchMode ? <Search className="h-4 w-4" /> : <SendHorizontal className="h-4 w-4" />}
                </Button>
              </div>
            </div>
          </footer>
        </main>
      </div>
      {contextMenu ? (
        <div
          ref={contextMenuRef}
          className="fixed z-50 min-w-40 rounded-lg border border-violet-300/20 bg-[hsl(238_48%_7%)] p-1 shadow-2xl shadow-black/45"
          style={{ left: contextMenu.x, top: contextMenu.y }}
          onContextMenu={(event) => event.preventDefault()}
        >
          <button
            type="button"
            className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-rose-300 transition hover:bg-rose-500/12 disabled:text-muted-foreground"
            onClick={() => void handleDeleteFromContextMenu()}
            disabled={isSending}
          >
            <Trash2 className="h-3.5 w-3.5" />
            Delete
          </button>
        </div>
      ) : null}
      {isQuickQuestionOpen ? (
        <div
          className="fixed inset-0 z-[60] flex items-center justify-center bg-black/72 p-3 backdrop-blur-md md:p-6"
          onMouseDown={(event) => {
            if (event.target === event.currentTarget) setIsQuickQuestionOpen(false);
          }}
        >
          <section
            role="dialog"
            aria-modal="true"
            aria-labelledby="quick-question-title"
            className="glass-panel flex max-h-[88vh] w-full max-w-3xl flex-col rounded-2xl border border-violet-300/24 p-4 shadow-2xl shadow-black/45 md:p-5"
          >
            <div className="mb-3 flex items-center justify-between gap-3">
              <h2 id="quick-question-title" className="text-sm font-semibold text-violet-50">
                Quick question
              </h2>
              <Button
                type="button"
                size="icon"
                variant="ghost"
                className="h-8 w-8"
                onClick={() => setIsQuickQuestionOpen(false)}
                aria-label="Close quick question"
              >
                <X className="h-4 w-4" />
              </Button>
            </div>

            <div className="min-h-0 flex-1 space-y-3">
              <Textarea
                id="quick-question-input"
                rows={2}
                value={quickPrompt}
                onInput={(event) => {
                  const textarea = event.currentTarget;
                  textarea.style.height = "0px";
                  textarea.style.height = `${textarea.scrollHeight}px`;
                  const nextPrompt = textarea.value;
                  if (nextPrompt !== quickPrompt) {
                    quickQuestionAbortRef.current?.abort();
                    quickQuestionAbortRef.current = null;
                    setIsQuickQuestionSending(false);
                    setQuickSubmittedPrompt(null);
                    setQuickSubmittedModelSelection(null);
                    setQuickQuestionMessages([]);
                    setQuickQuestionError(null);
                  }
                  setQuickPrompt(nextPrompt);
                }}
                onKeyDown={(event) => {
                  if (event.key === "Enter" && !event.shiftKey) {
                    event.preventDefault();
                    void handleSendQuickQuestion();
                  }
                }}
                placeholder="Ask Sybil..."
                className="max-h-36 min-h-[4.75rem] resize-none overflow-y-auto border-violet-300/24 bg-background/72 text-base text-violet-50 placeholder:text-violet-200/45"
                disabled={isQuickQuestionSending || isConvertingQuickQuestion}
              />

              <div className="h-[min(34vh,22rem)] overflow-y-auto rounded-xl border border-violet-300/16 bg-background/38 px-3 py-4">
                {quickQuestionMessages.length ? (
                  <ChatMessagesPanel messages={quickQuestionMessages} isLoading={false} isSending={isQuickQuestionSending} />
                ) : null}
                {quickQuestionError ? (
                  <p className="text-sm text-rose-300">{quickQuestionError}</p>
                ) : null}
              </div>
            </div>

            <div className="mt-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
              <div className="flex min-w-0 flex-1 flex-col gap-2 sm:flex-row sm:items-center">
                <select
                  className="h-10 min-w-32 rounded-lg border border-violet-300/22 bg-background/72 px-3 text-sm text-violet-50 outline-none shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.06)] focus:border-violet-300/45 focus:ring-1 focus:ring-ring/70"
                  value={quickProvider}
                  onChange={(event) => {
                    const nextProvider = event.currentTarget.value as Provider;
                    setQuickProvider(nextProvider);
                    const options = getModelOptions(modelCatalog, nextProvider);
                    setQuickModel(pickProviderModel(options, quickProviderModelPreferences[nextProvider]));
                  }}
                  disabled={isQuickQuestionSending || isConvertingQuickQuestion}
                  aria-label="Quick question provider"
                >
                  <option value="openai">OpenAI</option>
                  <option value="anthropic">Anthropic</option>
                  <option value="xai">xAI</option>
                </select>
                <ModelCombobox
                  options={quickProviderModelOptions}
                  value={quickModel}
                  disabled={isQuickQuestionSending || isConvertingQuickQuestion}
                  onChange={(nextModel) => {
                    const normalizedModel = nextModel.trim();
                    setQuickModel(normalizedModel);
                    setQuickProviderModelPreferences((current) => ({
                      ...current,
                      [quickProvider]: normalizedModel || null,
                    }));
                  }}
                />
              </div>

              <div className="flex items-center justify-end gap-2">
                <Button
                  type="button"
                  variant="secondary"
                  className="gap-2"
                  onClick={() => void handleConvertQuickQuestionToChat()}
                  disabled={!canConvertQuickQuestion || isConvertingQuickQuestion}
                >
                  <MessageSquare className="h-4 w-4" />
                  Convert to chat
                </Button>
                <Button
                  type="button"
                  className="h-10 w-10 rounded-lg"
                  onClick={() => void handleSendQuickQuestion()}
                  size="icon"
                  disabled={isQuickQuestionSending || isConvertingQuickQuestion || !quickPrompt.trim()}
                  aria-label="Ask quick question"
                >
                  <SendHorizontal className="h-4 w-4" />
                </Button>
              </div>
            </div>
          </section>
        </div>
      ) : null}
    </div>
  );
}
