import React, { useEffect, useMemo, useRef, useState } from 'react'; import { ChevronDown, Tag, Eye, Plus, X, Loader2 } from 'lucide-react'; import { useStore } from '../store/useStore'; import type { TemplateClass } from '../store/useStore'; import { cn } from '../lib/utils'; import { getActiveTemplate } from '../lib/templateSelection'; import { analyzeMask, updateTemplate, type MaskAnalysisResult } from '../lib/api'; export function OntologyInspector() { const templates = useStore((state) => state.templates); const activeTemplateId = useStore((state) => state.activeTemplateId); const activeClassId = useStore((state) => state.activeClassId); const activeClass = useStore((state) => state.activeClass); const frames = useStore((state) => state.frames); const currentFrameIndex = useStore((state) => state.currentFrameIndex); const masks = useStore((state) => state.masks); const selectedMaskIds = useStore((state) => state.selectedMaskIds); const maskPreviewOpacity = useStore((state) => state.maskPreviewOpacity); const setMasks = useStore((state) => state.setMasks); const updateTemplateStore = useStore((state) => state.updateTemplate); const setActiveTemplateId = useStore((state) => state.setActiveTemplateId); const setActiveClass = useStore((state) => state.setActiveClass); const setMaskPreviewOpacity = useStore((state) => state.setMaskPreviewOpacity); const [showAddForm, setShowAddForm] = useState(false); const [newClassName, setNewClassName] = useState(''); const [newClassColor, setNewClassColor] = useState('#06b6d4'); const [isSavingClass, setIsSavingClass] = useState(false); const [classSaveMessage, setClassSaveMessage] = useState(''); const [maskAnalysis, setMaskAnalysis] = useState(null); const [isAnalyzingMask, setIsAnalyzingMask] = useState(false); const [analysisMessage, setAnalysisMessage] = useState(''); const activeTemplate = getActiveTemplate(templates, activeTemplateId); const templateClasses = activeTemplate?.classes || []; const allClasses = [...templateClasses].sort((a, b) => b.zIndex - a.zIndex); const selectedMask = masks.find((mask) => selectedMaskIds.includes(mask.id)) || null; const currentFrame = frames[currentFrameIndex] || null; const classButtonRefs = useRef(new Map()); const selectedMaskClass = useMemo(() => { if (!selectedMask) return null; const allTemplateClasses = templates.flatMap((template) => ( template.classes.map((templateClass) => ({ template, templateClass })) )); const selectedName = selectedMask.className || selectedMask.label; return allTemplateClasses.find(({ templateClass }) => selectedMask.classId && templateClass.id === selectedMask.classId) || allTemplateClasses.find(({ templateClass }) => templateClass.name === selectedName && templateClass.color === selectedMask.color) || allTemplateClasses.find(({ templateClass }) => templateClass.name === selectedName) || null; }, [selectedMask?.classId, selectedMask?.className, selectedMask?.color, selectedMask?.id, selectedMask?.label, templates]); useEffect(() => { if (!selectedMaskClass) return; if (activeTemplateId !== selectedMaskClass.template.id) { setActiveTemplateId(selectedMaskClass.template.id); } setActiveClass(selectedMaskClass.templateClass); const timer = window.setTimeout(() => { const node = classButtonRefs.current.get(selectedMaskClass.templateClass.id); node?.scrollIntoView?.({ block: 'nearest' }); node?.focus?.({ preventScroll: true }); }, 0); return () => window.clearTimeout(timer); }, [activeTemplateId, selectedMaskClass, setActiveClass, setActiveTemplateId]); const handleSelectClass = (templateClass: TemplateClass) => { if (activeTemplate && !activeTemplateId) { setActiveTemplateId(activeTemplate.id); } setActiveClass(templateClass); const selectedIdSet = new Set(selectedMaskIds); const hasSelectedMasks = masks.some((mask) => selectedIdSet.has(mask.id)); if (!hasSelectedMasks) return; const templateId = activeTemplate?.id || activeTemplateId || undefined; const updatedMasks = masks.map((mask) => { if (!selectedIdSet.has(mask.id)) return mask; return { ...mask, templateId: templateId || mask.templateId, classId: templateClass.id, className: templateClass.name, classZIndex: templateClass.zIndex, label: templateClass.name, color: templateClass.color, saveStatus: mask.annotationId ? 'dirty' as const : 'draft' as const, saved: mask.annotationId ? false : mask.saved, }; }); const selectedMasksOnTop = selectedMaskIds .map((id) => updatedMasks.find((mask) => mask.id === id)) .filter((mask): mask is (typeof updatedMasks)[number] => Boolean(mask)); setMasks([ ...updatedMasks.filter((mask) => !selectedIdSet.has(mask.id)), ...selectedMasksOnTop, ]); }; const refreshMaskAnalysis = async (extractSkeleton = false) => { if (!selectedMask || !currentFrame) { setMaskAnalysis(null); setAnalysisMessage(selectedMask ? '当前帧信息不可用,无法读取后端属性' : '请选择一个 mask 查看后端属性'); return; } setIsAnalyzingMask(true); setAnalysisMessage(''); try { const result = await analyzeMask(selectedMask, currentFrame, { extractSkeleton }); setMaskAnalysis(result); setAnalysisMessage(result.message); } catch (err) { console.error('Mask analysis failed:', err); setMaskAnalysis(null); setAnalysisMessage('后端属性读取失败'); } finally { setIsAnalyzingMask(false); } }; React.useEffect(() => { void refreshMaskAnalysis(false); // selectedMask is intentionally tracked by id and geometry fields to avoid // re-running analysis for unrelated store changes. }, [selectedMask?.id, selectedMask?.segmentation, selectedMask?.points, currentFrame?.id]); const handleAddCustom = async () => { if (!newClassName.trim()) return; if (!activeTemplate) { setClassSaveMessage('请先选择一个模板'); return; } const maxZ = templateClasses.length > 0 ? Math.max(...templateClasses.map((c) => c.zIndex)) : 0; const newClass: TemplateClass = { id: `custom-${Date.now()}`, name: newClassName.trim(), color: newClassColor, zIndex: maxZ + 10, category: '自定义', }; setIsSavingClass(true); setClassSaveMessage(''); try { const updated = await updateTemplate(activeTemplate.id, { name: activeTemplate.name, description: activeTemplate.description, classes: [...templateClasses, newClass], rules: activeTemplate.rules || [], }); updateTemplateStore(updated); setActiveTemplateId(updated.id); handleSelectClass(newClass); setNewClassName(''); setShowAddForm(false); setClassSaveMessage('自定义分类已保存到后端模板'); } catch (err) { console.error('Save custom class failed:', err); setClassSaveMessage('自定义分类保存失败'); } finally { setIsSavingClass(false); } }; return (
{/* Template Selector */}

当前激活模板

{activeTemplate && (
{activeTemplate.classes?.length ?? 0} 个分类来自模板
)}
{/* Workspace Mask Opacity */}
{maskPreviewOpacity}%
setMaskPreviewOpacity(Number(event.target.value))} className="w-full accent-cyan-500" />
{/* Semantic Classification Tree */}

语义分类树 (高度/Z-Index)

{allClasses.map(cls => (
))} {allClasses.length === 0 && (
请先选择一个模板
)}
{/* Add Custom Class */}

自定义分类

{showAddForm && (
setNewClassColor(e.target.value)} className="w-8 h-8 rounded bg-transparent border-0 cursor-pointer" /> setNewClassName(e.target.value)} placeholder="分类名称" className="flex-1 bg-[#111] border border-white/10 rounded px-2 py-1 text-xs text-white" onKeyDown={(e) => e.key === 'Enter' && handleAddCustom()} />
)} {classSaveMessage && (
{classSaveMessage}
)}
{/* Current Active Object Properties */}

特定目标实例属性追踪

{activeClass?.name || activeTemplate?.name || '未选择'}
当前选中区域: {selectedMaskIds.length}
{maskAnalysis?.confidence != null ? maskAnalysis.confidence.toFixed(4) : '无模型分数'}
后端拓扑锚点: {maskAnalysis?.topology_anchor_count ?? 0} 节点
{analysisMessage && (
{analysisMessage}
)}
); }