import { useCoreServices } from '@learnitive/context';
import { extractionMatrixToCsv, extractionMatrixToLatex, type PaperExtractionMatrix, type PaperIndexGeneration, type PaperMemorySearchResult, type PaperRecord, type PaperResearchArtifact, type PaperResearchEvidence } from '@learnitive/paper-memory';
import type { EmbeddingModelState } from '@learnitive/project-meaning';
import type {
  PaperMemoryCitationMetadata,
  PaperMemoryCitationPreparation,
  PaperRelatedCandidate,
} from '@learnitive/service-core';
import { Button, toast } from '@learnitive/ui-components';
import { useAtomValue } from 'jotai';
import {
  FileText,
  HardDrive,
  LoaderCircle,
  MoreVertical,
  Plus,
  Quote,
  Trash2,
  X,
} from 'lucide-react';
import React from 'react';
import { AiComposer, AiMessageBubble } from './ai-conversation-shell';
import { requestPageConfirm, requestPagePrompt } from './page-modal-service';
import type { PaperMemoryManuscriptActionRequest, PaperMemoryManuscriptSelection, PaperMemoryOpenPaperRequest } from '../paper-memory-manuscript';

function formatBytes(bytes = 0): string {
  if (bytes <= 0) return '0 B';
  const units = ['B', 'KB', 'MB', 'GB'];
  let value = bytes;
  let index = 0;
  while (value >= 1024 && index < units.length - 1) {
    value /= 1024;
    index += 1;
  }
  return `${value >= 100 || index === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[index]}`;
}

function paperLabel(paper: PaperRecord): string {
  return paper.title?.trim() || paper.fileName;
}

function statusText(paper: PaperRecord): string {
  switch (paper.indexingStatus) {
    case 'queued': return 'Queued';
    case 'extracting': return 'Extracting text';
    case 'chunking': return 'Chunking';
    case 'embedding': return 'Generating embeddings';
    case 'saving': return 'Saving';
    case 'ready': return 'Ready';
    case 'ocr-required': return 'OCR required';
    case 'cancelled': return 'Cancelled';
    case 'error': return 'Indexing failed';
    case 'reindex-required': return 'Re-index required';
  }
}

function VirtualPaperItem({ children, eager = false }: { children: React.ReactNode; eager?: boolean }) {
  const hostRef = React.useRef<HTMLDivElement>(null);
  const [visible, setVisible] = React.useState(eager);

  React.useEffect(() => {
    const host = hostRef.current;
    if (!host || typeof IntersectionObserver === 'undefined') {
      setVisible(true);
      return;
    }
    const observer = new IntersectionObserver(
      ([entry]) => setVisible(Boolean(entry?.isIntersecting)),
      { rootMargin: '700px 0px' },
    );
    observer.observe(host);
    return () => observer.disconnect();
  }, []);

  return (
    <div ref={hostRef} className="min-h-[76px]" data-paper-memory-virtual-item>
      {visible ? children : <div className="h-[76px] rounded-lg border bg-muted/10" aria-hidden="true" />}
    </div>
  );
}

function citationPaperIds(results: readonly PaperMemorySearchResult[]): string[] {
  const output: string[] = [];
  const seen = new Set<string>();
  for (const result of results) {
    if (seen.has(result.paper.id)) continue;
    seen.add(result.paper.id);
    output.push(result.paper.id);
  }
  return output;
}

function downloadResearchText(fileName: string, content: string, type: string) {
  const blob = new Blob([content], { type });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.download = fileName;
  anchor.click();
  URL.revokeObjectURL(url);
}

function MatrixExport({ matrix }: { matrix: PaperExtractionMatrix }) {
  return (
    <details className="relative">
      <summary className="cursor-pointer list-none rounded border px-2 py-1 text-[10px] hover:bg-muted">Export ▾</summary>
      <div className="absolute right-0 z-20 mt-1 min-w-28 rounded-md border bg-popover p-1 shadow-md">
        <button type="button" className="block w-full rounded px-2 py-1 text-left text-[10px] hover:bg-muted" onClick={() => downloadResearchText('papers-extraction.csv', extractionMatrixToCsv(matrix), 'text/csv')}>CSV</button>
        <button type="button" className="block w-full rounded px-2 py-1 text-left text-[10px] hover:bg-muted" onClick={() => downloadResearchText('papers-extraction.json', JSON.stringify(matrix, null, 2), 'application/json')}>JSON</button>
        <button type="button" className="block w-full rounded px-2 py-1 text-left text-[10px] hover:bg-muted" onClick={() => downloadResearchText('papers-extraction.tex', extractionMatrixToLatex(matrix), 'application/x-tex')}>LaTeX</button>
      </div>
    </details>
  );
}

function ResearchEvidenceLinks({ evidence, onOpen }: { evidence: readonly PaperResearchEvidence[]; onOpen: (evidence: PaperResearchEvidence) => void }) {
  if (!evidence.length) return <span className="text-[10px] text-muted-foreground">No supporting passage retrieved</span>;
  return (
    <span className="flex flex-wrap gap-1">
      {evidence.slice(0, 3).map((item) => (
        <button key={`${item.paperId}:${item.sourceId}:${item.pageStart}`} type="button" className="rounded bg-muted px-1.5 py-0.5 text-[10px] hover:underline" title={item.excerpt} onClick={() => onOpen(item)}>
          {item.paperTitle} · p. {item.pageStart}
        </button>
      ))}
    </span>
  );
}

function ResearchMatrix({ matrix, onOpen, collapsible = false }: { matrix: PaperExtractionMatrix; onOpen: (evidence: PaperResearchEvidence) => void; collapsible?: boolean }) {
  const table = (
    <div className="overflow-x-auto">
      <table className="min-w-full border-collapse text-[10px]">
        <thead><tr><th className="border p-1.5 text-left">Study</th>{matrix.fields.map((field) => <th key={field} className="border p-1.5 text-left">{field}</th>)}</tr></thead>
        <tbody>{matrix.rows.map((row) => <tr key={row.paperId}><td className="border p-1.5 align-top font-medium">{row.paperTitle}</td>{matrix.fields.map((field) => { const cell = row.cells[field]; return <td key={field} className="min-w-40 border p-1.5 align-top"><div>{cell?.value ?? 'Insufficient evidence'}</div><div className="mt-1"><ResearchEvidenceLinks evidence={cell?.evidence ?? []} onOpen={onOpen} /></div></td>; })}</tr>)}</tbody>
      </table>
    </div>
  );
  return collapsible ? <details className="rounded border p-2"><summary className="cursor-pointer text-xs font-medium">View comparison table</summary><div className="mt-2">{table}</div></details> : table;
}

function keyedTextValues(values: readonly string[]): Array<{ key: string; value: string }> {
  const occurrences = new Map<string, number>();
  return values.map((value) => {
    const occurrence = (occurrences.get(value) ?? 0) + 1;
    occurrences.set(value, occurrence);
    return { key: `${value}\u0000${occurrence}`, value };
  });
}

function keyedTableRows(rows: readonly (readonly string[])[]): Array<{ key: string; row: readonly string[] }> {
  const occurrences = new Map<string, number>();
  return rows.map((row) => {
    const signature = JSON.stringify(row);
    const occurrence = (occurrences.get(signature) ?? 0) + 1;
    occurrences.set(signature, occurrence);
    return { key: `${signature}\u0000${occurrence}`, row };
  });
}

function PaperResearchArtifactView({ artifact, onOpen, onCitePaper }: { artifact: PaperResearchArtifact; onOpen: (evidence: PaperResearchEvidence) => void; onCitePaper?: (paperId: string) => void }) {
  if (artifact.kind === 'study-snapshot') {
    return <div className="mt-2 rounded-lg border p-2 text-xs" data-testid="paper-study-snapshot"><div className="mb-2 font-medium">Study Snapshot · {artifact.paperTitle}</div><div className="space-y-2">{Object.entries(artifact.fields).map(([field, cell]) => <div key={field}><div className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">{field}</div><div>{cell.value}</div><ResearchEvidenceLinks evidence={cell.evidence} onOpen={onOpen} /></div>)}</div></div>;
  }
  if (artifact.kind === 'extraction-matrix') {
    return <div className="mt-2 rounded-lg border p-2" data-testid="paper-extraction-matrix"><div className="mb-2 flex items-center justify-between"><span className="text-xs font-medium">Extraction Matrix</span><MatrixExport matrix={artifact} /></div><ResearchMatrix matrix={artifact} onOpen={onOpen} /></div>;
  }
  if (artifact.kind === 'comparison') {
    return <div className="mt-2 space-y-2" data-testid="paper-comparison"><ResearchMatrix matrix={artifact.matrix} onOpen={onOpen} collapsible /><div className="flex justify-end"><MatrixExport matrix={artifact.matrix} /></div></div>;
  }
  if (artifact.kind === 'contradictions') {
    return <div className="mt-2 space-y-2 rounded-lg border p-2 text-xs" data-testid="paper-contradictions"><div className="font-medium">Evidence differs</div>{artifact.groups.map((group) => <div key={`${group.label}:${group.paperIds.join(',')}`} className="rounded bg-muted/40 p-2"><div className="font-medium">{group.label} · {group.paperIds.length} stud{group.paperIds.length === 1 ? 'y' : 'ies'}</div><p className="mt-1 text-muted-foreground">{group.explanation}</p></div>)}{artifact.interpretation ? <div><div className="font-medium">AI interpretation</div><p className="text-muted-foreground">{artifact.interpretation}</p></div> : null}<ResearchMatrix matrix={artifact.matrix} onOpen={onOpen} collapsible /></div>;
  }
  if (artifact.kind === 'verification') {
    return <div className="mt-2 rounded-lg border p-2 text-xs" data-testid="paper-verification"><div className="font-medium">Independent verification · {artifact.classification}</div><p className="mt-1 text-muted-foreground">{artifact.rationale}</p><div className="mt-2"><ResearchEvidenceLinks evidence={artifact.evidence} onOpen={onOpen} /></div><div className="mt-1 text-[10px] text-muted-foreground">Fresh retrieval pass; not self-approval of the original answer.</div></div>;
  }
  if (artifact.kind === 'deep-research') {
    return <div className="mt-2 space-y-2 rounded-lg border p-2 text-xs" data-testid="paper-deep-research"><div className="font-medium">Deep Research</div><div className="text-[10px] text-muted-foreground">{artifact.stages.join(' → ')}</div><ResearchMatrix matrix={artifact.matrix} onOpen={onOpen} collapsible /><div className="rounded bg-muted/30 p-2"><div className="font-medium">Independent verification · {artifact.verification.classification}</div><p className="mt-1 text-muted-foreground">{artifact.verification.rationale}</p><ResearchEvidenceLinks evidence={artifact.verification.evidence} onOpen={onOpen} /></div></div>;
  }
  if (artifact.kind === 'research-insights') {
    const group = (title: string, items: readonly string[]) => items.length ? <div><div className="font-medium">{title}</div><ul className="mt-1 list-disc space-y-1 pl-4 text-muted-foreground">{Array.from(new Set(items)).map((item) => <li key={`${title}:${item}`}>{item}</li>)}</ul></div> : null;
    return <div className="mt-2 space-y-2 rounded-lg border p-2 text-xs" data-testid="paper-research-insights"><div className="font-medium">Research insights</div><div className="text-[10px] text-muted-foreground">Coverage: {artifact.coverage.paperCount} paper{artifact.coverage.paperCount === 1 ? '' : 's'} · {artifact.coverage.evidencePassages} retrieved passages · {artifact.coverage.scopeLabel}</div>{group('Current approaches', artifact.currentApproaches)}{group('Potential gaps in this corpus', artifact.potentialGaps)}{group('Novelty signals', artifact.noveltySignals)}{group('Hypotheses to investigate', artifact.hypotheses)}<ResearchEvidenceLinks evidence={artifact.evidence} onOpen={onOpen} /></div>;
  }
  if (artifact.kind === 'paper-table') {
    const csv = [artifact.columns, ...artifact.rows].map((row) => row.map((cell) => `"${cell.replace(/"/gu, '""')}"`).join(',')).join('\n');
    const latex = [`\\begin{tabular}{${'l'.repeat(Math.max(1, artifact.columns.length))}}`, `${artifact.columns.join(' & ')} \\\\`, ...artifact.rows.map((row) => `${row.join(' & ')} \\\\`), '\\end{tabular}'].join('\n');
    return <div className="mt-2 rounded-lg border p-2 text-xs" data-testid="paper-table-extraction"><div className="mb-2 flex items-center justify-between"><span className="font-medium">{artifact.title}</span><details className="relative"><summary className="cursor-pointer list-none rounded border px-2 py-1 text-[10px]">Export ▾</summary><div className="absolute right-0 z-20 mt-1 min-w-28 rounded border bg-popover p-1 shadow"><button type="button" className="block w-full px-2 py-1 text-left text-[10px]" onClick={() => downloadResearchText('paper-table.csv', csv, 'text/csv')}>CSV</button><button type="button" className="block w-full px-2 py-1 text-left text-[10px]" onClick={() => downloadResearchText('paper-table.json', JSON.stringify(artifact, null, 2), 'application/json')}>JSON</button><button type="button" className="block w-full px-2 py-1 text-left text-[10px]" onClick={() => downloadResearchText('paper-table.tex', latex, 'application/x-tex')}>LaTeX</button></div></details></div><div className="overflow-x-auto"><table className="min-w-full border-collapse text-[10px]"><thead><tr>{artifact.columns.map((column) => <th key={column} className="border p-1 text-left">{column}</th>)}</tr></thead><tbody>{keyedTableRows(artifact.rows).map(({ key, row }) => <tr key={key}>{keyedTextValues(row).map(({ key: cellKey, value }) => <td key={cellKey} className="border p-1">{value}</td>)}</tr>)}</tbody></table></div><div className="mt-2"><ResearchEvidenceLinks evidence={artifact.evidence} onOpen={onOpen} /></div></div>;
  }
  return <div className="mt-2 rounded-lg border p-2 text-xs" data-testid="paper-evidence-balance"><div className="font-medium">Evidence balance</div><div className="mt-2 grid grid-cols-2 gap-1 text-[11px]"><span>Supports</span><strong>{artifact.counts.supports}</strong><span>Mixed</span><strong>{artifact.counts.mixed}</strong><span>Does not support</span><strong>{artifact.counts['does-not-support']}</strong><span>Insufficient</span><strong>{artifact.counts.insufficient}</strong></div><details className="mt-2"><summary className="cursor-pointer">View study evidence</summary><div className="mt-2 space-y-2">{artifact.studies.map((study) => <div key={study.paperId} className="rounded border p-2"><div className="flex items-center gap-2"><div className="font-medium">{study.paperTitle} · {study.classification}</div><span className="text-[10px] text-muted-foreground">{study.strength} evidence</span>{onCitePaper && study.classification !== 'insufficient' ? <button type="button" className="ml-auto rounded border px-1.5 py-0.5 text-[10px] hover:bg-muted" onClick={() => onCitePaper(study.paperId)}>Cite</button> : null}</div><p className="mt-1 text-muted-foreground">{study.rationale}</p><ResearchEvidenceLinks evidence={study.evidence} onOpen={onOpen} /></div>)}</div></details></div>;
}

export function PaperMemoryPanel({ wsName, manuscriptSelection, actionRequest, openPaperRequest }: {
  wsName: string;
  manuscriptSelection?: PaperMemoryManuscriptSelection;
  actionRequest?: PaperMemoryManuscriptActionRequest;
  openPaperRequest?: PaperMemoryOpenPaperRequest;
}) {
  const { projectPaperMemory } = useCoreServices();
  const serviceVersion = useAtomValue(projectPaperMemory.$version);
  const state = projectPaperMemory.getWorkspaceState(wsName);
  const manifest = state.manifest;
  const papers = manifest?.papers ?? [];
  const collections = manifest?.collections ?? [];
  const notes = manifest?.notes ?? [];
  const systematicReviews = manifest?.systematicReviews ?? [];
  const livingLiteratureSearches = manifest?.livingLiteratureSearches ?? [];
  const fileInputRef = React.useRef<HTMLInputElement>(null);
  const downloadController = React.useRef<AbortController | undefined>(undefined);
  const [adding, setAdding] = React.useState(false);
  const [menuPaper, setMenuPaper] = React.useState<string>();
  const [selectedPapers, setSelectedPapers] = React.useState<Set<string>>(() => new Set());
  const [scope, setScope] = React.useState<string>('all');
  const [composerMode, setComposerMode] = React.useState<'ask' | 'search'>('ask');
  const [libraryOpen, setLibraryOpen] = React.useState(false);
  const [manageLibrary, setManageLibrary] = React.useState(false);
  const [readerPaper, setReaderPaper] = React.useState<PaperRecord>();
  const [readerIndex, setReaderIndex] = React.useState<PaperIndexGeneration>();
  const [readerPage, setReaderPage] = React.useState(1);
  const [readerSelection, setReaderSelection] = React.useState<{ text: string; page: number }>();
  const [query, setQuery] = React.useState('');
  const [question, setQuestion] = React.useState('');
  const [searching, setSearching] = React.useState(false);
  const [results, setResults] = React.useState<PaperMemorySearchResult[]>([]);
  const [embeddingState, setEmbeddingState] = React.useState<Awaited<ReturnType<typeof projectPaperMemory.embeddingStatus>>>();
  const [modelProgress, setModelProgress] = React.useState<EmbeddingModelState>();
  const [downloading, setDownloading] = React.useState(false);
  const [embeddingPapers, setEmbeddingPapers] = React.useState(false);
  const [manuscriptActionBusy, setManuscriptActionBusy] = React.useState(false);
  const [citationPreparation, setCitationPreparation] = React.useState<PaperMemoryCitationPreparation>();
  const [citationSelected, setCitationSelected] = React.useState<Set<string>>(() => new Set());
  const [citationMetadata, setCitationMetadata] = React.useState<Map<string, PaperMemoryCitationMetadata>>(() => new Map());
  const [citationTarget, setCitationTarget] = React.useState('references.bib');
  const [citationBusy, setCitationBusy] = React.useState(false);
  const [relatedResults, setRelatedResults] = React.useState<PaperRelatedCandidate[]>([]);
  const [relatedTitle, setRelatedTitle] = React.useState('');
  const [relatedBusy, setRelatedBusy] = React.useState(false);
  const [activeSystematicReviewId, setActiveSystematicReviewId] = React.useState<string>();
  const handledActionRequest = React.useRef(0);
  const handledOpenPaperRequest = React.useRef(0);
  const debugEnabled = React.useMemo(() => {
    const dev = Boolean((import.meta as ImportMeta & { env?: { DEV?: boolean } }).env?.DEV);
    return dev && typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('paperMemoryDebug') === '1';
  }, []);

  React.useEffect(() => {
    void projectPaperMemory.ensureLoaded(wsName).catch((reason) => {
      toast.error(reason instanceof Error ? reason.message : 'Unable to load Papers.');
    });
  }, [projectPaperMemory, wsName]);

  React.useEffect(() => {
    if (!openPaperRequest || handledOpenPaperRequest.current === openPaperRequest.id) return;
    handledOpenPaperRequest.current = openPaperRequest.id;
    void (async () => {
      const loaded = await projectPaperMemory.ensureLoaded(wsName);
      const paper = loaded.manifest?.papers.find((candidate) => candidate.id === openPaperRequest.paperId);
      if (!paper) throw new Error('The requested local paper is no longer in Papers.');
      const index = await projectPaperMemory.getActiveIndex(wsName, paper.id);
      if (!index) throw new Error('The requested paper is not ready to open yet.');
      setReaderPaper(paper);
      setReaderIndex(index);
      setReaderPage(Math.max(1, Math.min(openPaperRequest.page ?? 1, paper.pageCount || 1)));
      setReaderSelection(undefined);
      setLibraryOpen(false);
    })().catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to open the requested paper.'));
  }, [openPaperRequest?.id, projectPaperMemory, wsName]);

  React.useEffect(() => {
    let cancelled = false;
    void projectPaperMemory.embeddingStatus(wsName).then((value) => {
      if (!cancelled) setEmbeddingState(value);
    }).catch(() => undefined);
    return () => { cancelled = true; };
  }, [projectPaperMemory, serviceVersion, wsName]);

  React.useEffect(() => () => downloadController.current?.abort(), []);

  React.useEffect(() => {
    const validIds = new Set(papers.map((paper) => paper.id));
    setSelectedPapers((current) => new Set([...current].filter((id) => validIds.has(id))));
  }, [papers.map((paper) => paper.id).join('|')]);


  const scopedPaperIds = React.useMemo(() => {
    if (scope === 'selected') return [...selectedPapers];
    if (scope.startsWith('collection:')) {
      const collection = collections.find((item) => item.id === scope.slice('collection:'.length));
      return collection?.paperIds ?? [];
    }
    return undefined;
  }, [collections, scope, selectedPapers]);

  const addFiles = async (files: FileList | null) => {
    if (!files?.length) return;
    setAdding(true);
    try {
      const inputs = [];
      for (const file of Array.from(files)) {
        if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
          toast.error(`${file.name} is not a PDF.`);
          continue;
        }
        inputs.push({ fileName: file.name, bytes: await file.arrayBuffer() });
      }
      if (!inputs.length) return;
      const result = await projectPaperMemory.addPapers(wsName, inputs);
      if (result.added.length) toast.success(`${result.added.length} paper${result.added.length === 1 ? '' : 's'} added to Papers.`);
      for (const duplicate of result.duplicates) toast.info(`${duplicate}: This paper is already in Papers.`);
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to add papers.');
    } finally {
      if (fileInputRef.current) fileInputRef.current.value = '';
      setAdding(false);
    }
  };

  const addLivingLiteraturePaper = async (result: { title: string; openAccessPdfUrl?: string }) => {
    if (!result.openAccessPdfUrl) {
      toast.info('No open-access PDF is available for this result. Open the paper and add the PDF manually.');
      return;
    }
    const controller = new AbortController();
    downloadController.current?.abort();
    downloadController.current = controller;
    setAdding(true);
    try {
      const response = await fetch(result.openAccessPdfUrl, { signal: controller.signal });
      if (!response.ok) throw new Error(`Unable to download the open-access PDF (${response.status}).`);
      const bytes = await response.arrayBuffer();
      // The existing PDF extractor is the authoritative validation step. Some
      // repositories return inaccurate content-type headers, so do not reject
      // a user-requested open-access download on headers alone.
      const safeName = `${result.title.replace(/[^A-Za-z0-9._ -]+/gu, '').trim().slice(0, 90) || 'paper'}.pdf`;
      const added = await projectPaperMemory.addPapers(wsName, [{ fileName: safeName, bytes }]);
      if (added.added.length) toast.success('Paper added to Papers.');
      else if (added.duplicates.length) toast.info('This paper is already in Papers.');
    } catch (reason) {
      if (!controller.signal.aborted) toast.error(reason instanceof Error ? reason.message : 'Unable to add the paper.');
    } finally {
      if (downloadController.current === controller) downloadController.current = undefined;
      setAdding(false);
    }
  };

  const openPaperReader = async (paper: PaperRecord, page = 1) => {
    try {
      const index = await projectPaperMemory.getActiveIndex(wsName, paper.id);
      if (!index) throw new Error('This paper is not ready for local reading yet.');
      setReaderPaper(paper);
      setReaderIndex(index);
      setReaderPage(Math.max(1, Math.min(paper.pageCount || 1, page)));
      setReaderSelection(undefined);
      setLibraryOpen(false);
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to open the local paper reader.');
    }
  };

  const openPaper = async (paper: PaperRecord, page?: number) => {
    try {
      const bytes = await projectPaperMemory.readPaperPdf(wsName, paper.id);
      if (!bytes) throw new Error('Stored PDF is unavailable.');
      const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
      const target = page && page > 0 ? `${url}#page=${page}` : url;
      const opened = window.open(target, '_blank', 'noopener,noreferrer');
      if (!opened) toast.info('Allow pop-ups to open this local PDF.');
      window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to open this PDF.');
    }
  };

  const prepareEmbeddingModel = async () => {
    if (downloading) {
      downloadController.current?.abort();
      return;
    }
    const controller = new AbortController();
    downloadController.current = controller;
    setDownloading(true);
    try {
      await projectPaperMemory.prepareEmbeddingModel((next) => setModelProgress(next), controller.signal);
      toast.success('EmbeddingGemma downloaded for local semantic search.');
      setEmbeddingState(await projectPaperMemory.embeddingStatus(wsName));
    } catch (reason) {
      if (!(reason instanceof DOMException && reason.name === 'AbortError')) toast.error(reason instanceof Error ? reason.message : 'Unable to download EmbeddingGemma.');
    } finally {
      if (downloadController.current === controller) downloadController.current = undefined;
      setDownloading(false);
    }
  };

  const generateEmbeddings = async () => {
    setEmbeddingPapers(true);
    try {
      await projectPaperMemory.generateEmbeddings(wsName);
      toast.success('Papers semantic index is ready.');
      setEmbeddingState(await projectPaperMemory.embeddingStatus(wsName));
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to generate paper embeddings.');
    } finally {
      setEmbeddingPapers(false);
    }
  };

  const askPapers = async () => {
    const normalized = question.trim();
    if (!normalized) return;
    if (scope !== 'all' && (!scopedPaperIds || scopedPaperIds.length === 0)) {
      toast.info(scope === 'selected' ? 'Select at least one paper first.' : 'This collection has no papers yet.');
      return;
    }
    setQuestion('');
    try {
      await projectPaperMemory.ask(wsName, normalized, {
        ...(scopedPaperIds ? { paperIds: scopedPaperIds } : {}),
        limit: 8,
        diversify: 'auto',
      });
    } catch (reason) {
      if (!(reason instanceof DOMException && reason.name === 'AbortError')) {
        toast.error(reason instanceof Error ? reason.message : 'Unable to answer from Papers.');
      }
    }
  };

  const runStudySnapshot = async (paper: PaperRecord) => {
    setLibraryOpen(false);
    setScope('selected');
    setSelectedPapers(new Set([paper.id]));
    try {
      await projectPaperMemory.ask(wsName, 'Create a Study Snapshot for this paper.', { paperIds: [paper.id], limit: 8, diversify: false });
    } catch (reason) {
      if (!(reason instanceof DOMException && reason.name === 'AbortError')) {
        toast.error(reason instanceof Error ? reason.message : 'Unable to create the Study Snapshot.');
      }
    }
  };

  const runRelatedPapers = async (paper: PaperRecord) => {
    setMenuPaper(undefined);
    setLibraryOpen(false);
    setRelatedBusy(true);
    setRelatedTitle(paperLabel(paper));
    try {
      setRelatedResults(await projectPaperMemory.findRelatedPapers(wsName, paper.id));
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to find related papers.');
    } finally {
      setRelatedBusy(false);
    }
  };

  const runSearch = async () => {
    const normalized = query.trim();
    if (!normalized) return;
    if (scope !== 'all' && (!scopedPaperIds || scopedPaperIds.length === 0)) {
      toast.info(scope === 'selected' ? 'Select at least one paper first.' : 'This collection has no papers yet.');
      return;
    }
    setSearching(true);
    try {
      const next = await projectPaperMemory.search(wsName, normalized, {
        ...(scopedPaperIds ? { paperIds: scopedPaperIds } : {}),
        limit: 12,
        diversify: 'auto',
      });
      setResults(next);
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to search Papers.');
    } finally {
      setSearching(false);
    }
  };

  const runManuscriptAction = React.useCallback(async (action: PaperMemoryManuscriptActionRequest['action']) => {
    if (!manuscriptSelection) {
      toast.info('Select manuscript text in Source or Visual mode first.');
      return;
    }
    if (scope !== 'all' && (!scopedPaperIds || scopedPaperIds.length === 0)) {
      toast.info(scope === 'selected' ? 'Select at least one paper first.' : 'This collection has no papers yet.');
      return;
    }
    setManuscriptActionBusy(true);
    try {
      const result = await projectPaperMemory.runManuscriptAction(
        wsName,
        action,
        { anchor: manuscriptSelection.anchor, text: manuscriptSelection.text },
        {
          ...(scopedPaperIds ? { paperIds: scopedPaperIds } : {}),
          limit: action === 'find-citation' ? 12 : 8,
          diversify: 'auto',
        },
      );
      if (result.kind === 'search') {
        setQuery(result.query);
        setResults(result.results);
        if (!result.results.length) {
          setCitationPreparation(undefined);
          setCitationSelected(new Set());
          toast.info('No strong citation candidates were found in the indexed passages.');
        } else if (action === 'find-citation') {
          const paperIds = citationPaperIds(result.results);
          const preparation = await projectPaperMemory.prepareCitations(wsName, paperIds);
          setCitationPreparation(preparation);
          setCitationTarget(preparation.defaultBibliographyFile);
          setCitationSelected(new Set(paperIds.slice(0, Math.min(3, paperIds.length))));
          setCitationMetadata(new Map(preparation.candidates.map((candidate) => [candidate.paperId, candidate.metadata])));
        }
      } else if (action === 'evidence' && result.message.researchArtifact?.kind === 'evidence-balance') {
        const studies = result.message.researchArtifact.studies.filter((study) => study.classification !== 'insufficient');
        const paperIds = studies.map((study) => study.paperId);
        if (paperIds.length) {
          const preparation = await projectPaperMemory.prepareCitations(wsName, paperIds);
          setCitationPreparation(preparation);
          setCitationTarget(preparation.defaultBibliographyFile);
          const preferred = studies.filter((study) => study.classification === 'supports' || study.classification === 'mixed').slice(0, 3).map((study) => study.paperId);
          setCitationSelected(new Set(preferred.length ? preferred : paperIds.slice(0, 3)));
          setCitationMetadata(new Map(preparation.candidates.map((candidate) => [candidate.paperId, candidate.metadata])));
        }
      }
    } catch (reason) {
      if (!(reason instanceof DOMException && reason.name === 'AbortError')) {
        toast.error(reason instanceof Error ? reason.message : 'Unable to use Papers with the manuscript selection.');
      }
    } finally {
      setManuscriptActionBusy(false);
    }
  }, [manuscriptSelection, projectPaperMemory, scope, scopedPaperIds, wsName]);

  React.useEffect(() => {
    if (!actionRequest || actionRequest.id <= handledActionRequest.current) return;
    handledActionRequest.current = actionRequest.id;
    void runManuscriptAction(actionRequest.action);
  }, [actionRequest?.id, runManuscriptAction]);

  const refreshCitationPreparation = React.useCallback(async () => {
    if (!citationPreparation) return undefined;
    const paperIds = citationPreparation.candidates.map((candidate) => candidate.paperId);
    const next = await projectPaperMemory.prepareCitations(wsName, paperIds);
    setCitationPreparation(next);
    setCitationTarget((current) => current || next.defaultBibliographyFile);
    setCitationMetadata((current) => {
      const updated = new Map(current);
      for (const candidate of next.candidates) if (!updated.has(candidate.paperId)) updated.set(candidate.paperId, candidate.metadata);
      return updated;
    });
    return next;
  }, [citationPreparation, projectPaperMemory, wsName]);

  const citationSelections = React.useCallback(() => {
    if (!citationPreparation) return [];
    return citationPreparation.candidates
      .filter((candidate) => citationSelected.has(candidate.paperId))
      .map((candidate) => ({
        paperId: candidate.paperId,
        metadata: citationMetadata.get(candidate.paperId) ?? candidate.metadata,
      }));
  }, [citationMetadata, citationPreparation, citationSelected]);


  const addSelectedToBibliography = React.useCallback(async () => {
    const selections = citationSelections();
    if (!selections.length) {
      toast.info('Select at least one citation candidate.');
      return;
    }
    setCitationBusy(true);
    try {
      const result = await projectPaperMemory.addCitationsToBibliography(wsName, selections, citationTarget);
      await refreshCitationPreparation();
      toast.success(result.addedKeys.length
        ? `${result.addedKeys.length} reference${result.addedKeys.length === 1 ? '' : 's'} added to the bibliography.`
        : 'Selected papers already exist in the bibliography.');
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to add Papers references.');
    } finally {
      setCitationBusy(false);
    }
  }, [citationSelections, citationTarget, projectPaperMemory, refreshCitationPreparation, wsName]);

  const insertSelectedCitations = React.useCallback(async () => {
    if (!manuscriptSelection) {
      toast.info('Select manuscript text in Source or Visual mode first.');
      return;
    }
    const selections = citationSelections();
    if (!selections.length) {
      toast.info('Select at least one citation candidate.');
      return;
    }
    setCitationBusy(true);
    try {
      const result = await projectPaperMemory.insertCitations(
        wsName,
        { anchor: manuscriptSelection.anchor, text: manuscriptSelection.text },
        selections,
        citationTarget,
      );
      await refreshCitationPreparation();
      toast.success(`Inserted \\cite{${result.keys.join(',')}} into the manuscript.`);
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to insert Papers citations.');
    } finally {
      setCitationBusy(false);
    }
  }, [citationSelections, citationTarget, manuscriptSelection, projectPaperMemory, refreshCitationPreparation, wsName]);

  const releaseAiResources = async () => {
    try {
      await projectPaperMemory.releaseAiResources(wsName);
      toast.success('Released Papers local AI memory. Cached models remain installed.');
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to release Papers AI memory.');
    }
  };

  const requestPersistentStorage = async () => {
    try {
      const granted = await projectPaperMemory.requestPersistentStorage(wsName);
      if (granted === true) toast.success('Browser persistent storage is enabled for Papers.');
      else if (granted === false) toast.info('The browser did not grant persistent storage. Papers remain local but may be evicted under storage pressure.');
      else toast.info('Persistent-storage requests are not supported by this browser.');
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : 'Unable to request persistent browser storage.');
    }
  };

  const activeIndexing = papers.some((paper) => ['queued', 'extracting', 'chunking', 'embedding', 'saving'].includes(paper.indexingStatus));
  const ready = papers.filter((paper) => paper.indexingStatus === 'ready').length;
  const totalStorage = (state.usage?.sourceBytes ?? 0) + (state.usage?.metadataBytes ?? 0) + (state.usage?.vectorBytes ?? 0);
  const modelState = modelProgress ?? embeddingState?.model;
  const modelInstalled = modelState?.status === 'installed' || modelState?.status === 'ready';
  const semanticComplete = state.passageCount > 0 && state.embeddedPassageCount === state.passageCount;
  const ragBusy = state.ragStatus === 'retrieving' || state.ragStatus === 'generating' || state.ragStatus === 'validating';

  return (
    <div className="relative flex min-h-0 flex-1 flex-col overflow-hidden" data-testid="interactive-paper-memory">
      <input
        ref={fileInputRef}
        className="hidden"
        type="file"
        accept="application/pdf,.pdf"
        multiple
        aria-label="Add research PDFs to Papers"
        onChange={(event) => void addFiles(event.currentTarget.files)}
      />

      {state.loading && !manifest ? (
        <div className="flex min-h-0 flex-1 items-center justify-center gap-2 text-xs text-muted-foreground">
          <LoaderCircle className="size-4 animate-spin" /> Loading papers…
        </div>
      ) : papers.length === 0 ? (
        <div className="flex min-h-0 flex-1 items-center justify-center p-6">
          <div className="flex max-w-sm flex-col items-center gap-3 text-center">
            <FileText className="size-8 text-muted-foreground" />
            <div>
              <div className="text-sm font-medium">Upload papers to ask questions about your research library.</div>
              <p className="mt-1 text-xs text-muted-foreground">Your papers remain on this device.</p>
            </div>
            <Button onClick={() => fileInputRef.current?.click()} disabled={adding}>
              {adding ? <LoaderCircle className="mr-1 size-4 animate-spin" /> : <Plus className="mr-1 size-4" />}
              Add papers
            </Button>
          </div>
        </div>
      ) : (
        <>
          <div className="min-h-0 flex-1 overflow-y-auto p-3" data-testid="papers-conversation">
            <div className="mb-3 flex items-center justify-between gap-2 text-xs text-muted-foreground">
              <button type="button" className="font-medium text-foreground hover:underline" onClick={() => { setManageLibrary(false); setLibraryOpen(true); }}>
                {papers.length} paper{papers.length === 1 ? '' : 's'}
              </button>
              <span>{ready} ready{activeIndexing ? ' · indexing…' : ''}</span>
            </div>

            {manuscriptSelection && (
              <div className="mb-3 rounded-lg border border-primary/30 bg-primary/5 p-3 text-xs" data-testid="paper-memory-manuscript-context">
                <div className="font-medium">Manuscript selection</div>
                <p className="mt-1 line-clamp-3 text-muted-foreground">{manuscriptSelection.text}</p>
                <div className="mt-2 flex flex-wrap gap-1.5">
                  <Button size="sm" variant="outline" disabled={manuscriptActionBusy || ragBusy} onClick={() => void runManuscriptAction('explain')}>Explain</Button>
                  <Button size="sm" variant="outline" disabled={manuscriptActionBusy} onClick={() => void runManuscriptAction('find-supporting')}>Supporting evidence</Button>
                  <Button size="sm" variant="outline" disabled={manuscriptActionBusy} onClick={() => void runManuscriptAction('find-contradicting')}>Contradicting evidence</Button>
                  <Button size="sm" disabled={manuscriptActionBusy} onClick={() => void runManuscriptAction('find-citation')}>Find citation</Button>
                </div>
              </div>
            )}

            {citationPreparation && (
              <div className="mb-3 rounded-lg border p-3 text-xs" data-testid="paper-memory-citation-preparation">
                <div className="font-medium">Citation candidates</div>
                <div className="mt-2 space-y-1.5">
                  {citationPreparation.candidates.slice(0, 8).map((candidate) => (
                    <label key={candidate.paperId} className="flex items-start gap-2 rounded border bg-background p-2">
                      <input
                        className="mt-0.5"
                        type="checkbox"
                        checked={citationSelected.has(candidate.paperId)}
                        onChange={(event) => {
                          const checked = event.currentTarget.checked;
                          setCitationSelected((current) => {
                            const next = new Set(current);
                            if (checked) next.add(candidate.paperId); else next.delete(candidate.paperId);
                            return next;
                          });
                        }}
                      />
                      <span className="min-w-0 flex-1">
                        <span className="block truncate font-medium">{candidate.metadata.title || candidate.fileName}</span>
                        <span className="text-[10px] text-muted-foreground">{candidate.existing ? `Already in ${candidate.existing.file}` : candidate.missingFields.length ? 'Metadata review may be needed' : 'Ready to cite'}</span>
                      </span>
                    </label>
                  ))}
                </div>
                <div className="mt-2 flex flex-wrap justify-end gap-1.5">
                  <Button size="sm" variant="outline" disabled={citationBusy || citationSelected.size === 0} onClick={() => void addSelectedToBibliography()}>Add to bibliography</Button>
                  <Button size="sm" disabled={citationBusy || citationSelected.size === 0 || !manuscriptSelection} onClick={() => void insertSelectedCitations()}>
                    {citationBusy ? <LoaderCircle className="mr-1 size-3.5 animate-spin" /> : <Quote className="mr-1 size-3.5" />} Insert citations
                  </Button>
                </div>
              </div>
            )}

            {(relatedBusy || relatedResults.length > 0) && (
              <div className="mb-3 rounded-lg border p-2 text-xs" data-testid="papers-related-results">
                <div className="flex items-center gap-2"><span className="font-medium">Related papers{relatedTitle ? ` · ${relatedTitle}` : ''}</span>{relatedBusy ? <LoaderCircle className="size-3.5 animate-spin" /> : null}<Button size="sm" variant="ghost" className="ml-auto h-7" onClick={() => { setRelatedResults([]); setRelatedTitle(''); }}>Clear</Button></div>
                {!relatedBusy && <div className="mt-2 space-y-1.5">{relatedResults.map((candidate) => <div key={candidate.id} className="rounded border p-2"><div className="font-medium">{candidate.title}</div><div className="text-[10px] text-muted-foreground">{[candidate.authors.slice(0, 3).join(', '), candidate.year, candidate.relation, candidate.source === 'local' ? 'In Papers' : candidate.sourceProviders?.join(' + ')].filter(Boolean).join(' · ')}</div>{candidate.source === 'local' && candidate.paperId ? <Button size="sm" variant="ghost" className="mt-1 h-7" onClick={() => { const paper = papers.find((item) => item.id === candidate.paperId); if (paper) void openPaperReader(paper); }}>Read</Button> : candidate.url ? <a className="mt-1 inline-block text-[10px] underline" href={candidate.url} target="_blank" rel="noreferrer">Open</a> : null}</div>)}</div>}
              </div>
            )}

            <div className="space-y-3" role="group" aria-label="Papers grounded conversation">
              {state.messages.map((message) => (
                <AiMessageBubble key={message.id} messageRole={message.role}>
                  <p className="whitespace-pre-wrap">{message.content}</p>
                  {message.sources?.length ? (
                    <div className="mt-2 space-y-1.5" role="group" aria-label="Papers answer sources">
                      {message.sources.map((source) => {
                        const paper = papers.find((item) => item.id === source.paperId);
                        return (
                          <button
                            key={`${message.id}:${source.sourceId}:${source.chunkId}`}
                            type="button"
                            className="block w-full rounded border bg-background/70 p-2 text-left text-xs text-foreground hover:bg-background disabled:opacity-60"
                            disabled={!paper}
                            onClick={() => paper && void openPaper(paper, source.pageStart)}
                          >
                            <span className="font-medium">[{source.sourceId}] {source.paperTitle}</span>
                            <span className="ml-1 text-muted-foreground">· {source.pageStart === source.pageEnd ? `p. ${source.pageStart}` : `pp. ${source.pageStart}–${source.pageEnd}`}</span>
                            <span className="mt-1 block line-clamp-2 text-muted-foreground">{source.excerpt}</span>
                          </button>
                        );
                      })}
                    </div>
                  ) : null}
                  {message.researchArtifact ? (
                    <PaperResearchArtifactView
                      artifact={message.researchArtifact}
                      onOpen={(evidence) => {
                        const paper = papers.find((item) => item.id === evidence.paperId);
                        if (paper) void openPaper(paper, evidence.pageStart);
                      }}
                      onCitePaper={(paperId) => {
                        void projectPaperMemory.prepareCitations(wsName, [paperId]).then((preparation) => {
                          setCitationPreparation(preparation);
                          setCitationTarget(preparation.defaultBibliographyFile);
                          setCitationSelected(new Set([paperId]));
                          setCitationMetadata(new Map(preparation.candidates.map((candidate) => [candidate.paperId, candidate.metadata])));
                        }).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to prepare citation.'));
                      }}
                    />
                  ) : null}
                </AiMessageBubble>
              ))}

              {state.draftAnswer !== undefined && (
                <AiMessageBubble messageRole="assistant">
                  <div data-testid="paper-memory-streaming-answer">
                    <div className="mb-1 flex items-center gap-1 text-[10px] text-muted-foreground"><LoaderCircle className="size-3 animate-spin" />{state.ragStatus === 'retrieving' ? 'Finding evidence…' : state.ragStatus === 'validating' ? 'Checking sources…' : 'Writing…'}</div>
                    <p className="whitespace-pre-wrap">{state.draftAnswer || '…'}</p>
                  </div>
                </AiMessageBubble>
              )}
            </div>

            {composerMode === 'search' && results.length > 0 && (
              <div className="mt-4 space-y-2" role="group" aria-label="Papers search results">
                <div className="text-xs font-medium">Search results</div>
                {results.map((result) => (
                  <button
                    key={`${result.paper.id}:${result.chunk.id}`}
                    type="button"
                    className="block w-full rounded-lg border bg-background p-2.5 text-left hover:bg-muted/40"
                    onClick={() => void openPaper(result.paper, result.chunk.pageStart)}
                  >
                    <div className="truncate text-xs font-medium">{paperLabel(result.paper)} · p. {result.chunk.pageStart}{result.chunk.pageEnd !== result.chunk.pageStart ? `–${result.chunk.pageEnd}` : ''}</div>
                    <div className="truncate text-[10px] text-muted-foreground">{result.chunk.section ?? result.chunk.heading ?? result.chunk.kind}</div>
                    <p className="mt-1 line-clamp-3 text-[10px] leading-relaxed text-muted-foreground">{result.chunk.text}</p>
                  </button>
                ))}
              </div>
            )}

            {state.ragError && <div className="mt-3 rounded-md border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive">{state.ragError}</div>}
            {composerMode === 'search' && !searching && query.trim() && results.length === 0 && <p className="mt-3 text-xs text-muted-foreground">No matching passages found.</p>}
            {state.messages.length === 0 && results.length === 0 && !state.draftAnswer && (
              <div className="py-10 text-center text-xs text-muted-foreground">Ask a question, compare papers, extract study details, or search your library.</div>
            )}
          </div>

          <div className="border-t p-2">
            <AiComposer
              value={composerMode === 'ask' ? question : query}
              onChange={composerMode === 'ask' ? setQuestion : setQuery}
              onSend={() => { if (composerMode === 'ask') void askPapers(); else void runSearch(); }}
              onStop={composerMode === 'ask' ? () => projectPaperMemory.cancelAnswer(wsName) : undefined}
              busy={composerMode === 'ask' ? ragBusy : searching}
              placeholder={composerMode === 'ask' ? 'Ask your papers…' : 'Search your papers…'}
              ariaLabel={composerMode === 'ask' ? 'Ask Papers' : 'Search Papers'}
              prefix={(
                <div className="flex shrink-0 items-center gap-1">
                  <Button size="icon" variant="outline" aria-label="Open paper library" onClick={() => { setManageLibrary(false); setLibraryOpen(true); }}><Plus className="size-4" /></Button>
                  <select className="h-10 rounded-md border bg-background px-2 text-xs" value={composerMode} onChange={(event) => setComposerMode(event.currentTarget.value as 'ask' | 'search')} aria-label="Papers action">
                    <option value="ask">Ask</option>
                    <option value="search">Search</option>
                  </select>
                  <select className="h-10 max-w-44 rounded-md border bg-background px-2 text-xs" value={scope} onChange={(event) => setScope(event.currentTarget.value)} aria-label="Paper scope">
                    <option value="all">All papers</option>
                    <option value="selected" disabled={selectedPapers.size === 0}>Selected ({selectedPapers.size})</option>
                    {collections.map((collection) => <option key={collection.id} value={`collection:${collection.id}`}>{collection.name} ({collection.paperIds.length})</option>)}
                  </select>
                </div>
              )}
            />
          </div>
        </>
      )}

      {readerPaper && readerIndex && (
        <div className="absolute inset-0 z-40 flex flex-col bg-background" data-testid="papers-local-reader">
          <div className="flex items-center gap-2 border-b p-2">
            <div className="min-w-0 flex-1 truncate text-xs font-medium">{paperLabel(readerPaper)} · p. {readerPage}</div>
            <Button size="sm" variant="ghost" onClick={() => void openPaper(readerPaper, readerPage)}>Open PDF</Button>
            <Button size="icon" variant="ghost" aria-label="Close local paper reader" onClick={() => { setReaderPaper(undefined); setReaderIndex(undefined); setReaderSelection(undefined); }}><X className="size-4" /></Button>
          </div>
          {readerSelection && (
            <div className="flex flex-wrap items-center gap-1 border-b bg-muted/30 p-2" data-testid="paper-selection-actions">
              <Button size="sm" onClick={() => void projectPaperMemory.runPaperSelectionAction(wsName, readerPaper.id, readerSelection.page, readerSelection.text, 'explain')}>Explain</Button>
              <Button size="sm" variant="outline" onClick={() => { setQuestion(`Regarding this passage from ${paperLabel(readerPaper)} (p. ${readerSelection.page}):\\n\"${readerSelection.text}\"\\n\\n`); setReaderPaper(undefined); setReaderIndex(undefined); }}>Ask</Button>
              <Button size="sm" variant="outline" onClick={() => void projectPaperMemory.prepareCitations(wsName, [readerPaper.id]).then((preparation) => { setCitationPreparation(preparation); setCitationTarget(preparation.defaultBibliographyFile); setCitationSelected(new Set([readerPaper.id])); setCitationMetadata(new Map(preparation.candidates.map((candidate) => [candidate.paperId, candidate.metadata]))); setReaderPaper(undefined); setReaderIndex(undefined); })}>Cite</Button>
              <details className="relative"><summary className="cursor-pointer list-none rounded border px-3 py-1.5 text-xs">More</summary><div className="absolute left-0 z-50 mt-1 min-w-40 rounded-md border bg-popover p-1 text-xs shadow-lg">
                <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => void projectPaperMemory.runPaperSelectionAction(wsName, readerPaper.id, readerSelection.page, readerSelection.text, 'summarise')}>Summarise</button>
                <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => void projectPaperMemory.runPaperSelectionAction(wsName, readerPaper.id, readerSelection.page, readerSelection.text, 'equation')}>Explain equation</button>
                <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => void projectPaperMemory.runPaperSelectionAction(wsName, readerPaper.id, readerSelection.page, readerSelection.text, 'table-extract')}>Extract table</button>
                <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => {
                  void requestPagePrompt({ title: 'Research note', description: 'Save a browser-local research note linked to this paper selection.', label: 'Note', confirmText: 'Save note' }).then((body) => {
                  if (body?.trim()) return projectPaperMemory.saveResearchNote(wsName, { body, quote: readerSelection.text, paperId: readerPaper.id, pageStart: readerSelection.page, pageEnd: readerSelection.page }).then(() => toast.success('Research note saved locally.')).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to save note.'));
                  return undefined;
                  });
                }}>Save note</button>
                <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => { setQuestion(`Find related evidence for this passage:\\n${readerSelection.text}`); setScope('all'); setReaderPaper(undefined); setReaderIndex(undefined); }}>Find related evidence</button>
                <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => { setQuestion(`Which papers contradict or qualify this passage?\\n${readerSelection.text}`); setScope('all'); setReaderPaper(undefined); setReaderIndex(undefined); }}>Find contradictory evidence</button>
              </div></details>
              <span className="ml-auto text-[10px] text-muted-foreground">Selected from p. {readerSelection.page}</span>
            </div>
          )}
          <div className="min-h-0 flex-1 overflow-y-auto bg-muted/20 p-3" role="region" aria-label="Local paper text" onMouseUp={(event) => {
            const selection = window.getSelection();
            const text = selection?.toString().replace(/\s+/gu, ' ').trim() ?? '';
            if (!text) { setReaderSelection(undefined); return; }
            const pageHost = (event.target as HTMLElement | null)?.closest?.('[data-paper-page]') as HTMLElement | null;
            const page = Number(pageHost?.dataset.paperPage ?? readerPage);
            setReaderPage(page);
            setReaderSelection({ text: text.slice(0, 7_500), page });
          }}>
            <div className="mx-auto max-w-3xl space-y-3">
              {readerIndex.pages.map((page) => <article key={page.pageNumber} data-paper-page={page.pageNumber} className="min-h-64 select-text rounded border bg-background p-5 text-sm leading-6 shadow-sm"><div className="mb-3 text-[10px] text-muted-foreground">Page {page.pageNumber}</div><div className="whitespace-pre-wrap">{page.text || 'No extractable text on this page.'}</div></article>)}
            </div>
          </div>
        </div>
      )}

      {libraryOpen && (
        <div className="absolute inset-0 z-30 flex bg-background/50 backdrop-blur-[1px]" data-testid="papers-library-drawer">
          <div className="flex h-full w-full max-w-sm flex-col border-r bg-background shadow-xl">
            <div className="flex items-center gap-2 border-b p-3">
              <div className="text-sm font-medium">{manageLibrary ? 'Manage library' : 'Library'}</div>
              <Button className="ml-auto" size="icon" variant="ghost" aria-label="Close paper library" onClick={() => setLibraryOpen(false)}><X className="size-4" /></Button>
            </div>

            {manageLibrary ? (
              <div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-3 text-xs">
                <Button size="sm" variant="ghost" onClick={() => setManageLibrary(false)}>← Library</Button>

                <details className="rounded-lg border p-3" data-testid="papers-collections">
                  <summary className="cursor-pointer font-medium">Collections ({collections.length})</summary>
                  <div className="mt-2 space-y-2">
                    <Button size="sm" variant="outline" onClick={() => {
                      void requestPagePrompt({ title: 'New collection', description: 'Group papers into a browser-local research collection.', label: 'Collection name', confirmText: 'Create collection' }).then((name) => {
                      if (name?.trim()) return projectPaperMemory.createCollection(wsName, name, [...selectedPapers]).then((collection) => { setScope(`collection:${collection.id}`); toast.success('Collection created locally.'); }).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to create collection.'));
                      return undefined;
                      });
                    }}>New collection</Button>
                    {collections.length === 0 ? <p className="text-[10px] text-muted-foreground">Group papers into local project collections such as Thesis Sources or Scaling Laws.</p> : collections.map((collection) => <div key={collection.id} className="flex items-center gap-2 rounded border p-2"><button type="button" className="min-w-0 flex-1 truncate text-left" onClick={() => { setScope(`collection:${collection.id}`); setManageLibrary(false); setLibraryOpen(false); }}>{collection.name} <span className="text-muted-foreground">({collection.paperIds.length})</span></button><Button size="sm" variant="ghost" className="h-7" onClick={() => void requestPagePrompt({ title: 'Rename collection', label: 'Collection name', initialValue: collection.name, confirmText: 'Rename' }).then((name) => name?.trim() ? projectPaperMemory.renameCollection(wsName, collection.id, name).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to rename collection.')) : undefined)}>Rename</Button><Button size="sm" variant="ghost" className="h-7 text-destructive" onClick={() => void projectPaperMemory.deleteCollection(wsName, collection.id)}>Delete</Button></div>)}
                  </div>
                </details>

                <details className="rounded-lg border p-3" data-testid="papers-research-notes">
                  <summary className="cursor-pointer font-medium">Research notes ({notes.length})</summary>
                  <div className="mt-2 space-y-2">
                    <p className="text-[10px] text-muted-foreground">Notes stay browser-local and retain paper/page provenance when saved from the Paper Reader. Ask “Draft Related Work using my notes” to synthesise them locally.</p>
                    {notes.slice().reverse().map((note) => <div key={note.id} className="rounded border p-2"><div>{note.title || note.body}</div>{note.paperTitle ? <div className="mt-1 text-[10px] text-muted-foreground">{note.paperTitle}{note.pageStart ? ` · p. ${note.pageStart}${note.pageEnd && note.pageEnd !== note.pageStart ? `–${note.pageEnd}` : ''}` : ''}</div> : null}{note.quote ? <div className="mt-1 line-clamp-2 border-l pl-2 text-[10px] text-muted-foreground">{note.quote}</div> : null}<Button size="sm" variant="ghost" className="mt-1 h-7 text-destructive" onClick={() => void projectPaperMemory.deleteResearchNote(wsName, note.id)}>Delete</Button></div>)}
                  </div>
                </details>

                <details className="rounded-lg border p-3" data-testid="papers-living-literature">
                  <summary className="cursor-pointer font-medium">Saved literature searches ({livingLiteratureSearches.length})</summary>
                  <div className="mt-2 space-y-3">
                    <p className="text-[10px] text-muted-foreground">Opt-in living searches check Research Search only when you ask. Learnitive never downloads newly found PDFs automatically.</p>
                    <Button size="sm" variant="outline" onClick={() => {
                      void requestPagePrompt({ title: 'Save literature search', description: 'Save a browser-local living literature query. Learnitive checks it only when you ask.', label: 'Search query', confirmText: 'Next' }).then(async (query) => {
                      if (!query?.trim()) return;
                      const name = await requestPagePrompt({ title: 'Search name', label: 'Name', initialValue: query.trim().slice(0, 80), confirmText: 'Save search' });
                      return projectPaperMemory.createLivingLiteratureSearch(wsName, query, name ?? undefined).then(() => toast.success('Saved search created. Current results were recorded as the baseline.')).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to save this search.'));
                      });
                    }}>New saved search</Button>
                    {livingLiteratureSearches.map((saved) => <div key={saved.id} className="rounded border p-2" data-testid="living-literature-search">
                      <div className="flex items-start gap-2"><div className="min-w-0 flex-1"><div className="font-medium">{saved.name}</div><div className="mt-0.5 line-clamp-2 text-[10px] text-muted-foreground">{saved.query}</div></div>{saved.newResultCount > 0 ? <span className="rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium">{saved.newResultCount} new paper{saved.newResultCount === 1 ? '' : 's'} found</span> : null}</div>
                      <div className="mt-2 flex flex-wrap gap-1.5"><Button size="sm" variant="outline" className="h-7" onClick={() => void projectPaperMemory.checkLivingLiteratureSearch(wsName, saved.id).then((updated) => toast.success(updated.newResultCount ? `${updated.newResultCount} new paper${updated.newResultCount === 1 ? '' : 's'} found.` : 'No new papers found.')).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to check this search.'))}>Check now</Button>{saved.newResultCount > 0 ? <Button size="sm" className="h-7" onClick={() => void projectPaperMemory.markLivingLiteratureReviewed(wsName, saved.id)}>Review new papers</Button> : null}<Button size="sm" variant="ghost" className="h-7 text-destructive" onClick={() => void projectPaperMemory.deleteLivingLiteratureSearch(wsName, saved.id)}>Delete</Button></div>
                      {saved.latestResults.length > 0 ? <details className="mt-2"><summary className="cursor-pointer text-[10px]">Latest results</summary><div className="mt-2 max-h-64 space-y-1.5 overflow-y-auto">{saved.latestResults.slice(0, 20).map((result) => <div key={result.id} className="rounded border p-2"><div className="font-medium">{result.title}</div><div className="mt-0.5 text-[10px] text-muted-foreground">{result.authors.slice(0, 3).join(', ')}{result.year ? ` · ${result.year}` : ''}</div><div className="mt-1 flex gap-1.5">{result.url ? <a className="rounded border px-2 py-1 text-[10px] hover:bg-muted" href={result.url} target="_blank" rel="noreferrer">Open</a> : null}<Button size="sm" variant="outline" className="h-6 px-2 text-[10px]" disabled={!result.openAccessPdfUrl || adding} onClick={() => void addLivingLiteraturePaper(result)}>Add to Papers</Button></div></div>)}</div></details> : null}
                    </div>)}
                  </div>
                </details>

                <details className="rounded-lg border p-3" data-testid="papers-systematic-reviews">
                  <summary className="cursor-pointer font-medium">Systematic reviews ({systematicReviews.length})</summary>
                  <div className="mt-2 space-y-3">
                    <Button size="sm" variant="outline" onClick={() => {
                      void requestPagePrompt({ title: 'New systematic review', description: 'Create a browser-local systematic review workspace around a research question.', label: 'Review question', confirmText: 'Create review' }).then((question) => {
                      if (question?.trim()) return projectPaperMemory.createSystematicReview(wsName, question).then((review) => setActiveSystematicReviewId(review.id)).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to create systematic review.'));
                      return undefined;
                      });
                    }}>New systematic review</Button>
                    {systematicReviews.map((review) => {
                      const counts = projectPaperMemory.systematicReviewCounts(review);
                      const expanded = activeSystematicReviewId === review.id;
                      const includedIds = review.studies.filter((study) => study.decision === 'include').map((study) => study.paperId);
                      return <div key={review.id} className="rounded border p-2"><button type="button" className="w-full text-left font-medium" onClick={() => setActiveSystematicReviewId(expanded ? undefined : review.id)}>{review.title}</button><div className="mt-1 text-[10px] text-muted-foreground">Identified {counts.total} · Included {counts.include} · Maybe {counts.maybe} · Excluded {counts.exclude} · Awaiting {counts.unreviewed}</div>{expanded ? <div className="mt-3 space-y-3">
                        <div><div className="font-medium">1. Question</div><p className="text-[10px] text-muted-foreground">{review.question}</p></div>
                        <div><div className="font-medium">2. Search</div><p className="text-[10px] text-muted-foreground">Candidate studies are the PDFs currently added to Papers. Use the unified Research Search to discover more, then add the PDFs you choose; Learnitive never downloads them automatically.</p></div>
                        <div><div className="flex items-center gap-2"><span className="font-medium">3. Screen</span><Button size="sm" variant="outline" className="h-7" onClick={() => void projectPaperMemory.suggestSystematicScreening(wsName, review.id).then(() => toast.success('Screening suggestions ready. Human decisions were not changed.')).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to suggest screening.'))}>AI suggestions</Button></div><div className="mt-2 max-h-52 space-y-1 overflow-y-auto">{review.studies.map((study) => { const paper = papers.find((item) => item.id === study.paperId); if (!paper) return null; return <div key={study.paperId} className="rounded border p-2"><div className="truncate font-medium">{paperLabel(paper)}</div><div className="mt-1 flex items-center gap-2"><select className="rounded border bg-background px-1 py-0.5 text-[10px]" value={study.decision} onChange={(event) => void projectPaperMemory.updateSystematicReviewDecision(wsName, review.id, study.paperId, event.currentTarget.value as 'include' | 'maybe' | 'exclude' | 'unreviewed')}><option value="unreviewed">Unreviewed</option><option value="include">Include</option><option value="maybe">Maybe</option><option value="exclude">Exclude</option></select>{study.aiSuggestion ? <span className="text-[10px] text-muted-foreground">AI suggests {study.aiSuggestion}: {study.aiRationale}</span> : null}</div></div>; })}</div></div>
                        <div><div className="flex items-center gap-2"><span className="font-medium">4. Extract</span><Button size="sm" variant="outline" className="h-7" disabled={!includedIds.length} onClick={() => { setSelectedPapers(new Set(includedIds)); setScope('selected'); setQuestion('Extract the study design, population, intervention, comparator, outcomes, main result and limitations from the selected papers.'); setManageLibrary(false); setLibraryOpen(false); }}>Prepare extraction</Button><Button size="sm" variant="outline" className="h-7" disabled={!includedIds.length} onClick={() => void projectPaperMemory.assistSystematicRiskOfBias(wsName, review.id).then(() => toast.success('Risk-of-bias assistance ready. Review it manually.')).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to assess risk of bias.'))}>Risk of bias</Button></div><p className="mt-1 text-[10px] text-muted-foreground">AI assistance is evidence-backed and does not claim standards compliance.</p></div>
                        <div><div className="font-medium">5. Synthesise</div><Button size="sm" variant="outline" className="mt-1 h-7" disabled={!includedIds.length} onClick={() => { setSelectedPapers(new Set(includedIds)); setScope('selected'); setQuestion(`Compare the selected studies for this systematic review question: ${review.question}`); setManageLibrary(false); setLibraryOpen(false); }}>Synthesise included studies</Button></div>
                        <div className="rounded bg-muted/30 p-2 text-[10px] text-muted-foreground">PRISMA-style flow counts are descriptive only: identified {counts.total}, screened {counts.total - counts.unreviewed}, excluded {counts.exclude}, included {counts.include}. Learnitive does not claim PRISMA compliance.</div>
                      </div> : null}</div>;
                    })}
                  </div>
                </details>

                <div className="rounded-lg border p-3">
                  <div className="font-medium">Local indexing</div>
                  <div className="mt-2 grid grid-cols-2 gap-1 text-[10px] text-muted-foreground">
                    <span>Papers</span><span>{papers.length}</span>
                    <span>Ready</span><span>{ready}</span>
                    <span>Passages</span><span>{state.passageCount.toLocaleString()}</span>
                    <span>Semantic passages</span><span>{state.embeddedPassageCount.toLocaleString()}</span>
                    <span>Local storage</span><span>{formatBytes(totalStorage)}</span>
                  </div>
                  <div className="mt-3 flex flex-wrap gap-1.5">
                    {!modelInstalled && (
                      <Button size="sm" variant="outline" onClick={() => void prepareEmbeddingModel()} disabled={downloading}>
                        {downloading ? <LoaderCircle className="mr-1 size-3.5 animate-spin" /> : <HardDrive className="mr-1 size-3.5" />} Prepare semantic search
                      </Button>
                    )}
                    {modelInstalled && !semanticComplete && (
                      <Button size="sm" variant="outline" onClick={() => void generateEmbeddings()} disabled={embeddingPapers || activeIndexing}>
                        {embeddingPapers ? <LoaderCircle className="mr-1 size-3.5 animate-spin" /> : null} Build semantic index
                      </Button>
                    )}
                    {(modelInstalled || state.embeddedPassageCount > 0) && <Button size="sm" variant="ghost" onClick={() => void releaseAiResources()} disabled={ragBusy || activeIndexing}>Unload AI memory</Button>}
                  </div>
                </div>

                {embeddingState && (
                  <details className="rounded-lg border p-3">
                    <summary className="cursor-pointer font-medium">Advanced local model details</summary>
                    <div className="mt-2 space-y-1 text-[10px] text-muted-foreground">
                      <div>Embedding model: {embeddingState.model.status}</div>
                      <div>{embeddingState.embeddedPapers}/{embeddingState.readyPapers} ready papers have semantic indexes</div>
                      <div>{embeddingState.embeddedPassages}/{embeddingState.totalPassages} passages embedded</div>
                    </div>
                  </details>
                )}

                {state.capabilities?.persistentStorage && state.capabilities.persisted === false && <Button size="sm" variant="outline" onClick={() => void requestPersistentStorage()}>Keep library storage persistent</Button>}
                {activeIndexing && <Button size="sm" variant="outline" onClick={() => void projectPaperMemory.cancelIndexing(wsName)}>Cancel indexing</Button>}
                <Button size="sm" variant="destructive" onClick={() => void requestPageConfirm({ title: 'Clear Paper Memory library?', description: 'Clear all browser-local Papers PDFs and indexes for this project? TeX project files will not be deleted.', confirmText: 'Clear library', tone: 'destructive' }).then((confirmed) => confirmed ? projectPaperMemory.clear(wsName).then(() => setLibraryOpen(false)).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to clear Papers.')) : undefined)}><Trash2 className="mr-1 size-3.5" />Clear library</Button>

                {debugEnabled && state.lastRetrievalDebug && (
                  <details className="rounded-lg border border-dashed p-3 text-[10px]">
                    <summary className="cursor-pointer font-medium">Retrieval diagnostics</summary>
                    <div className="mt-2 text-muted-foreground">{state.lastRetrievalDebug.consideredPaperIds.length} papers considered · {state.lastRetrievalDebug.timing.totalMs.toFixed(1)} ms</div>
                  </details>
                )}
              </div>
            ) : (
              <>
                <div className="flex items-center gap-2 border-b p-2">
                  <Button size="sm" onClick={() => fileInputRef.current?.click()} disabled={adding}><Plus className="mr-1 size-3.5" />Add papers</Button>
                  <Button size="sm" variant="ghost" onClick={() => setManageLibrary(true)}>Manage library</Button>
                  <details className="relative ml-auto"><summary className="cursor-pointer list-none rounded p-1.5 text-sm" aria-label="More Papers actions">⋯</summary><div className="absolute right-0 z-30 mt-1 min-w-48 rounded-md border bg-popover p-1 text-xs shadow-lg">
                    <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => void requestPagePrompt({ title: 'New collection', label: 'Collection name', confirmText: 'Create collection' }).then((name) => name?.trim() ? projectPaperMemory.createCollection(wsName, name, [...selectedPapers]).then((collection) => { setScope(`collection:${collection.id}`); toast.success('Collection created locally.'); }) : undefined)}>New collection</button>
                    <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => void requestPagePrompt({ title: 'New systematic review', label: 'Review question', confirmText: 'Create review' }).then((question) => question?.trim() ? projectPaperMemory.createSystematicReview(wsName, question).then((review) => { setActiveSystematicReviewId(review.id); setManageLibrary(true); }) : undefined)}>New systematic review</button>
                    <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => setManageLibrary(true)}>Research notes & collections</button>
                  </div></details>
                </div>
                <div className="min-h-0 flex-1 space-y-2 overflow-y-auto p-2">
                  {papers.map((paper, paperIndex) => {
                    const selected = selectedPapers.has(paper.id);
                    const progress = state.progress.get(paper.id);
                    const progressPercent = progress?.total ? Math.min(100, Math.round(progress.completed / progress.total * 100)) : undefined;
                    return (
                      <VirtualPaperItem key={paper.id} eager={paperIndex < 12}>
                        <div className="relative rounded-lg border bg-background p-2.5" data-testid={`paper-memory-paper-${paper.id}`}>
                          <div className="flex items-start gap-2">
                            <input
                              type="checkbox"
                              className="mt-1"
                              checked={selected}
                              aria-label={`Select ${paperLabel(paper)} for Papers`}
                              onChange={(event) => {
                                const checked = event.currentTarget.checked;
                                setSelectedPapers((current) => {
                                  const next = new Set(current);
                                  if (checked) next.add(paper.id); else next.delete(paper.id);
                                  return next;
                                });
                              }}
                            />
                            <FileText className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
                            <div className="min-w-0 flex-1">
                              <button type="button" className="block max-w-full truncate text-left text-xs font-medium hover:underline" onClick={() => void openPaperReader(paper)}>{paperLabel(paper)}</button>
                              <div className="mt-0.5 truncate text-[10px] text-muted-foreground">{paper.authors?.length ? `${paper.authors.join(', ')}${paper.year ? ` · ${paper.year}` : ''}` : paper.fileName}</div>
                              <div className="mt-1 text-[10px] text-muted-foreground">{progress ? `${statusText(paper)}${progressPercent === undefined ? '' : ` ${progressPercent}%`}` : statusText(paper)}</div>
                            </div>
                            <Button size="icon" variant="ghost" className="size-7" aria-label={`Paper actions for ${paperLabel(paper)}`} onClick={() => setMenuPaper((value) => value === paper.id ? undefined : paper.id)}><MoreVertical className="size-4" /></Button>
                          </div>
                          {menuPaper === paper.id && (
                            <div className="absolute right-2 top-9 z-20 min-w-32 rounded-md border bg-popover p-1 text-xs shadow-md">
                              <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => { setMenuPaper(undefined); void runStudySnapshot(paper); }}>Study Snapshot</button>
                              <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => { setMenuPaper(undefined); void openPaperReader(paper); }}>Read</button>
                              <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => { setMenuPaper(undefined); void openPaper(paper); }}>Open PDF</button>
                              <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => void runRelatedPapers(paper)}>Related papers</button>
                              {collections.map((collection) => {
                                const included = collection.paperIds.includes(paper.id);
                                return <button key={collection.id} type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" onClick={() => { const ids = included ? collection.paperIds.filter((id) => id !== paper.id) : [...collection.paperIds, paper.id]; void projectPaperMemory.setCollectionPapers(wsName, collection.id, ids).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to update collection.')); setMenuPaper(undefined); }}>{included ? 'Remove from' : 'Add to'} {collection.name}</button>;
                              })}
                              <button type="button" className="block w-full rounded px-2 py-1.5 text-left hover:bg-muted" disabled={state.progress.has(paper.id)} onClick={() => { setMenuPaper(undefined); projectPaperMemory.reindexPaper(wsName, paper.id); }}>Re-index</button>
                              <button type="button" className="block w-full rounded px-2 py-1.5 text-left text-destructive hover:bg-muted" onClick={() => { setMenuPaper(undefined); void projectPaperMemory.removePaper(wsName, paper.id).catch((reason) => toast.error(reason instanceof Error ? reason.message : 'Unable to remove paper.')); }}>Remove</button>
                            </div>
                          )}
                        </div>
                      </VirtualPaperItem>
                    );
                  })}
                </div>
              </>
            )}
          </div>
          <button type="button" className="min-w-0 flex-1" aria-label="Close paper library backdrop" onClick={() => setLibraryOpen(false)} />
        </div>
      )}
    </div>
  );
}
