"use client"; import { useCallback, useMemo, useState } from "react"; import { searchDatasetsAPI, searchDatasetSourceAPI, importDatasetEntriesAPI, } from "@/lib/api"; import type { Artifact, DatasetSearchResult } from "@/lib/types"; type SourceFilter = "all" | "sabdab" | "oas" | "imgt"; const SOURCE_LABELS: Record = { sabdab: "SAbDab", oas: "OAS", imgt: "IMGT", fc: "Fc", }; const DATA_TYPE_COLORS: Record = { structure: "bg-blue-100 text-blue-700", sequence: "bg-emerald-100 text-emerald-700", gene: "bg-purple-100 text-purple-700", }; interface Props { open: boolean; onClose: () => void; onImport: (artifacts: Artifact[]) => void; } export default function DatasetSearchPanel({ open, onClose, onImport }: Props) { const [query, setQuery] = useState(""); const [source, setSource] = useState("all"); const [results, setResults] = useState>( {}, ); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selected, setSelected] = useState>(new Set()); const [importing, setImporting] = useState(false); const [collapsed, setCollapsed] = useState>(new Set()); const search = useCallback(async () => { if (!query.trim()) return; setLoading(true); setError(null); setResults({}); setSelected(new Set()); try { if (source === "all") { const res = await searchDatasetsAPI({ query: query.trim() }); setResults(res.results); } else { const res = await searchDatasetSourceAPI(source, { query: query.trim(), }); setResults({ [source]: res }); } } catch (e) { setError(e instanceof Error ? e.message : "Search failed"); } finally { setLoading(false); } }, [query, source]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); search(); } }, [search], ); const toggleSelect = useCallback((id: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const toggleCollapse = useCallback((src: string) => { setCollapsed((prev) => { const next = new Set(prev); if (next.has(src)) next.delete(src); else next.add(src); return next; }); }, []); const allResults = useMemo(() => { const flat: DatasetSearchResult[] = []; for (const items of Object.values(results)) { flat.push(...items); } return flat; }, [results]); const handleImport = useCallback(async () => { const entries = allResults .filter((r) => selected.has(r.id)) .map((r) => ({ source: r.source, entry_id: r.id })); if (entries.length === 0) return; setImporting(true); try { const res = await importDatasetEntriesAPI(entries); onImport(res.artifacts); } catch (e) { setError(e instanceof Error ? e.message : "Import failed"); } finally { setImporting(false); } }, [allResults, selected, onImport]); if (!open) return null; const sourceKeys = Object.keys(results).filter( (k) => results[k].length > 0, ); const hasResults = sourceKeys.length > 0; return (
e.stopPropagation()} > {/* Header */}

Search Databases

Query SAbDab, OAS, and IMGT for antibody data

{/* Search bar */}
setQuery(e.target.value)} onKeyDown={handleKeyDown} placeholder="Search antibody databases..." data-testid="dataset-search-input" className="flex-1 rounded-md border border-border bg-white px-3 py-1.5 text-xs focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20" />
{/* Results */}
{loading && (

Searching...

)} {error && (

{error}

)} {!loading && !error && !hasResults && query && (

No results found.

)} {!loading && !error && !query && !hasResults && (

Enter a query to search antibody databases.

)} {sourceKeys.map((src) => (
{!collapsed.has(src) && (
    {results[src].map((r) => (
  • toggleSelect(r.id)} className="mt-0.5 shrink-0 accent-accent" aria-label={`Select ${r.title}`} />
    {r.title} {r.data_type}

    {r.summary}

    {r.organism && (

    {r.organism}

    )}
  • ))}
)}
))}
{/* Footer with import */} {hasResults && (

{selected.size} of {allResults.length} selected

)}
); }