"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useStore } from "@/store/useStore"; import { Panel } from "./Panel"; import { useHyperScatter } from "./useHyperScatter"; import { useLabelLegend } from "./useLabelLegend"; import type { Geometry } from "@/types"; import { findLayoutByGeometry, listAvailableGeometries } from "@/lib/layouts"; import { fetchEmbeddings } from "@/lib/api"; interface ScatterPanelProps { className?: string; layoutKey?: string; geometry?: Geometry; } export function ScatterPanel({ className = "", layoutKey, geometry, }: ScatterPanelProps) { const { datasetInfo, embeddingsByLayoutKey, setEmbeddingsForLayout, selectedIds, setSelectedIds, beginLassoSelection, hoveredId, setHoveredId, setActiveLayoutKey, labelFilter, } = useStore(); const [localGeometry, setLocalGeometry] = useState("euclidean"); // Check which geometries are available const availableGeometries = useMemo(() => { return listAvailableGeometries(datasetInfo?.layouts ?? []); }, [datasetInfo?.layouts]); useEffect(() => { if (geometry) return; if (availableGeometries.length === 0) return; if (!availableGeometries.includes(localGeometry)) { setLocalGeometry(availableGeometries[0]); } }, [availableGeometries, geometry, localGeometry]); const resolvedGeometry = geometry ?? localGeometry; const resolvedLayoutKey = useMemo(() => { if (!datasetInfo) return layoutKey ?? null; if (layoutKey) { const exists = datasetInfo.layouts.some((layout) => layout.layout_key === layoutKey); if (exists) return layoutKey; } const layout = findLayoutByGeometry(datasetInfo.layouts, resolvedGeometry); return layout?.layout_key ?? datasetInfo.layouts[0]?.layout_key ?? null; }, [datasetInfo, layoutKey, resolvedGeometry]); const embeddings = resolvedLayoutKey ? embeddingsByLayoutKey[resolvedLayoutKey] ?? null : null; useEffect(() => { if (!resolvedLayoutKey) return; setActiveLayoutKey(resolvedLayoutKey); }, [resolvedLayoutKey, setActiveLayoutKey]); useEffect(() => { if (!resolvedLayoutKey) return; if (embeddingsByLayoutKey[resolvedLayoutKey]) return; let cancelled = false; fetchEmbeddings(resolvedLayoutKey) .then((data) => { if (cancelled) return; setEmbeddingsForLayout(resolvedLayoutKey, data); }) .catch((err) => { if (cancelled) return; console.error("Failed to load embeddings:", err); }) return () => { cancelled = true; }; }, [embeddingsByLayoutKey, resolvedLayoutKey, setEmbeddingsForLayout]); const { labelsInfo } = useLabelLegend({ datasetInfo, embeddings, labelFilter }); const { canvasRef, overlayCanvasRef, containerRef, handlePointerDown, handlePointerMove, handlePointerUp, handlePointerLeave, handleDoubleClick, rendererError, } = useHyperScatter({ embeddings, labelsInfo, selectedIds, hoveredId, setSelectedIds, beginLassoSelection, setHoveredId, hoverEnabled: !labelFilter, }); const focusLayout = useCallback(() => { if (!resolvedLayoutKey) return; setActiveLayoutKey(resolvedLayoutKey); }, [resolvedLayoutKey, setActiveLayoutKey]); const loadingLabel = resolvedLayoutKey ? "Loading embeddings..." : "No embeddings layout available"; return ( {/* Main content area - min-h-0 prevents flex overflow */}
{/* Canvas container */}
{ focusLayout(); handlePointerDown(e); }} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerCancel={handlePointerUp} onPointerLeave={handlePointerLeave} onDoubleClick={handleDoubleClick} onPointerEnter={focusLayout} /> {/* Lasso overlay (screen-space) */} {/* Loading overlay */} {rendererError ? (
Browser not supported
{rendererError}
) : ( !embeddings && (
{loadingLabel}
) )}
); }