Initial commit
This commit is contained in:
32
src/App.tsx
Normal file
32
src/App.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Sidebar } from './components/layout/Sidebar';
|
||||
import { Dashboard } from './components/dashboard/Dashboard';
|
||||
import { ProjectLibrary } from './components/projects/ProjectLibrary';
|
||||
import { VideoWorkspace } from './components/workspace/VideoWorkspace';
|
||||
import { TemplateRegistry } from './components/templates/TemplateRegistry';
|
||||
import { AISegmentation } from './components/ai/AISegmentation';
|
||||
import { Login } from './components/auth/Login';
|
||||
|
||||
export type ActiveModule = 'dashboard' | 'projects' | 'ai' | 'workspace' | 'templates';
|
||||
|
||||
export default function App() {
|
||||
const [activeModule, setActiveModule] = useState<ActiveModule>('workspace');
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Login onLoginSuccess={() => setIsAuthenticated(true)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full bg-[#0a0a0a] text-gray-200 overflow-hidden font-sans">
|
||||
<Sidebar activeModule={activeModule} setActiveModule={setActiveModule} />
|
||||
<main className="flex-1 flex flex-col min-w-0 h-full relative">
|
||||
{activeModule === 'dashboard' && <Dashboard />}
|
||||
{activeModule === 'projects' && <ProjectLibrary onProjectSelect={() => setActiveModule('workspace')} />}
|
||||
{activeModule === 'ai' && <AISegmentation onSendToWorkspace={() => setActiveModule('workspace')} />}
|
||||
{activeModule === 'workspace' && <VideoWorkspace onNavigateToAI={() => setActiveModule('ai')} />}
|
||||
{activeModule === 'templates' && <TemplateRegistry />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
269
src/components/ai/AISegmentation.tsx
Normal file
269
src/components/ai/AISegmentation.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Target, PlusCircle, MinusCircle, SquareDashed, Sparkles, Settings2, Cpu, Image as ImageIcon, SendToBack, Tags, Undo, Redo } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Stage, Layer, Image as KonvaImage, Circle, Path, Group } from 'react-konva';
|
||||
import useImage from 'use-image';
|
||||
import { OntologyInspector } from '../workspace/OntologyInspector';
|
||||
|
||||
interface AISegmentationProps {
|
||||
onSendToWorkspace: () => void;
|
||||
}
|
||||
|
||||
export function AISegmentation({ onSendToWorkspace }: AISegmentationProps) {
|
||||
const [activeTool, setActiveTool] = useState('point_pos');
|
||||
const [modelSize, setModelSize] = useState('vit_l');
|
||||
const [semanticText, setSemanticText] = useState('');
|
||||
const [autoDeleteBg, setAutoDeleteBg] = useState(true);
|
||||
const [cropMode, setCropMode] = useState(false);
|
||||
|
||||
// Canvas state
|
||||
const [scale, setScale] = useState(1);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [points, setPoints] = useState<{ x: number, y: number, type: 'pos'|'neg' }[]>([]);
|
||||
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
|
||||
const [image] = useImage('https://images.unsplash.com/photo-1549317661-bd32c8ce0be2?q=80&w=2070&auto=format&fit=crop');
|
||||
|
||||
const handleWheel = (e: any) => {
|
||||
e.evt.preventDefault();
|
||||
const scaleBy = 1.1;
|
||||
const stage = e.target.getStage();
|
||||
const oldScale = stage.scaleX();
|
||||
const mousePointTo = {
|
||||
x: stage.getPointerPosition().x / oldScale - stage.x() / oldScale,
|
||||
y: stage.getPointerPosition().y / oldScale - stage.y() / oldScale,
|
||||
};
|
||||
const newScale = e.evt.deltaY < 0 ? oldScale * scaleBy : oldScale / scaleBy;
|
||||
setScale(newScale);
|
||||
setPosition({
|
||||
x: -(mousePointTo.x - stage.getPointerPosition().x / newScale) * newScale,
|
||||
y: -(mousePointTo.y - stage.getPointerPosition().y / newScale) * newScale,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: any) => {
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) return;
|
||||
const pos = stage.getPointerPosition();
|
||||
if (pos) {
|
||||
const imageX = (pos.x - position.x) / scale;
|
||||
const imageY = (pos.y - position.y) / scale;
|
||||
setCursorPos({ x: imageX, y: imageY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleStageClick = (e: any) => {
|
||||
if (activeTool === 'move') return;
|
||||
if (activeTool === 'point_pos' || activeTool === 'point_neg') {
|
||||
const stage = e.target.getStage();
|
||||
const pos = stage.getRelativePointerPosition();
|
||||
if (pos) {
|
||||
setPoints([...points, { x: pos.x, y: pos.y, type: activeTool === 'point_pos' ? 'pos' : 'neg' }]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex bg-[#0a0a0a]">
|
||||
{/* Left AI Controller Panel */}
|
||||
<aside className="w-80 bg-[#0d0d0d] flex flex-col border-r border-white/5 shrink-0 z-10 overflow-hidden">
|
||||
<div className="h-16 border-b border-white/5 flex items-center px-6 shrink-0 justify-between">
|
||||
<div className="flex items-center font-medium text-[11px] uppercase tracking-widest text-cyan-400">
|
||||
<Cpu size={16} className="mr-3 text-cyan-400" />
|
||||
AI智能分割引擎
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 flex flex-col gap-8">
|
||||
{/* Model Select */}
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-3">视觉基础模型选型</h3>
|
||||
<div className="bg-[#111] border border-white/5 flex p-1 rounded-lg">
|
||||
{['vit_b', 'vit_l', 'vit_h'].map(m => (
|
||||
<button
|
||||
key={m}
|
||||
className={cn("flex-1 text-xs py-2 rounded-md transition-colors text-center uppercase tracking-wider font-mono", modelSize === m ? "bg-white/10 text-white font-medium shadow-sm" : "text-gray-500 hover:text-gray-300 hover:bg-white/5")}
|
||||
onClick={() => setModelSize(m)}
|
||||
>
|
||||
{m.split('_')[1]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prompt Tools */}
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-3">交互式提示工具</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => setActiveTool('point_pos')}
|
||||
className={cn("flex flex-col items-center justify-center p-4 rounded-lg border transition-all", activeTool === 'point_pos' ? "bg-green-500/10 border-green-500/30 text-green-400 shadow-[0_0_15px_rgba(34,197,94,0.1)]" : "bg-[#111] border-white/5 text-gray-400 hover:bg-white/5 hover:text-gray-200")}
|
||||
>
|
||||
<PlusCircle size={22} className="mb-3" />
|
||||
<span className="text-[10px] uppercase tracking-wider font-semibold">正向选点</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTool('point_neg')}
|
||||
className={cn("flex flex-col items-center justify-center p-4 rounded-lg border transition-all", activeTool === 'point_neg' ? "bg-red-500/10 border-red-500/30 text-red-500 shadow-[0_0_15px_rgba(239,68,68,0.1)]" : "bg-[#111] border-white/5 text-gray-400 hover:bg-white/5 hover:text-gray-200")}
|
||||
>
|
||||
<MinusCircle size={22} className="mb-3" />
|
||||
<span className="text-[10px] uppercase tracking-wider font-semibold">反向选点</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTool('box')}
|
||||
className={cn("flex flex-col items-center justify-center p-4 rounded-lg border transition-all", activeTool === 'box' ? "bg-blue-500/10 border-blue-500/30 text-blue-400 shadow-[0_0_15px_rgba(59,130,246,0.1)]" : "bg-[#111] border-white/5 text-gray-400 hover:bg-white/5 hover:text-gray-200")}
|
||||
>
|
||||
<SquareDashed size={22} className="mb-3" />
|
||||
<span className="text-[10px] uppercase tracking-wider font-semibold">边界框选</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTool('move')}
|
||||
className={cn("flex flex-col items-center justify-center p-4 rounded-lg border transition-all", activeTool === 'move' ? "bg-white/10 border-white/20 text-white shadow-[0_0_15px_rgba(255,255,255,0.05)]" : "bg-[#111] border-white/5 text-gray-400 hover:bg-white/5 hover:text-gray-200")}
|
||||
>
|
||||
<Target size={22} className="mb-3" />
|
||||
<span className="text-[10px] uppercase tracking-wider font-semibold">视口控制</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Semantic Description */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">语义引导描述</h3>
|
||||
<span className="text-[9px] bg-cyan-500/10 text-cyan-400 px-1.5 py-0.5 rounded border border-cyan-500/20 font-mono">零样本推理</span>
|
||||
</div>
|
||||
<textarea
|
||||
value={semanticText}
|
||||
onChange={e => setSemanticText(e.target.value)}
|
||||
placeholder="例如:'分割出左侧车道上行驶的所有红色汽车'..."
|
||||
className="w-full bg-[#111] border border-white/5 rounded-lg p-3 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/50 transition-all font-sans min-h-[100px] resize-none hover:border-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Parameters */}
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-3 flex items-center gap-2">核心参数设定</h3>
|
||||
<div className="space-y-4 bg-[#111] rounded-lg p-5 border border-white/5">
|
||||
<div className="flex items-center justify-between cursor-pointer group" onClick={() => setCropMode(!cropMode)}>
|
||||
<span className="text-[11px] text-gray-400 uppercase tracking-wider font-medium group-hover:text-gray-200 transition-colors">自动裁剪无锚区域</span>
|
||||
<button className={cn("w-8 h-4 rounded-full transition-colors relative", cropMode ? "bg-cyan-500" : "bg-white/20")}>
|
||||
<div className={cn("absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full transition-transform shadow-sm", cropMode ? "translate-x-4" : "")} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between cursor-pointer group" onClick={() => setAutoDeleteBg(!autoDeleteBg)}>
|
||||
<span className="text-[11px] text-gray-400 uppercase tracking-wider font-medium group-hover:text-gray-200 transition-colors">自动清理干涉点</span>
|
||||
<button className={cn("w-8 h-4 rounded-full transition-colors relative", autoDeleteBg ? "bg-cyan-500" : "bg-white/20")}>
|
||||
<div className={cn("absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full transition-transform shadow-sm", autoDeleteBg ? "translate-x-4" : "")} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-[#0a0a0a] border-t border-white/5 shrink-0 flex flex-col gap-3">
|
||||
<button
|
||||
className="w-full py-3.5 rounded-lg flex items-center justify-center gap-2 transition-all shadow-lg font-medium tracking-wide text-xs uppercase bg-cyan-500 hover:bg-cyan-400 text-black shadow-cyan-500/20 hover:shadow-cyan-500/40"
|
||||
>
|
||||
<Sparkles size={16} /> 执行高精度语义分割
|
||||
</button>
|
||||
<button
|
||||
onClick={onSendToWorkspace}
|
||||
className="w-full py-3.5 rounded-lg flex items-center justify-center gap-2 transition-all font-medium tracking-wide text-xs uppercase bg-white/5 hover:bg-white/10 text-gray-300 border border-white/5 hover:border-white/10"
|
||||
>
|
||||
<SendToBack size={16} /> 退档推送至工作区重组
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Right Canvas Area */}
|
||||
<main className="flex-1 bg-[#151515] relative overflow-hidden flex flex-col">
|
||||
<header className="h-16 border-b border-white/5 bg-[#111] flex items-center justify-between px-6 shrink-0">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-sm font-semibold tracking-wide text-white">模型端推理侧可视化 (Visualizer)</h2>
|
||||
<span className="text-[10px] text-gray-500 uppercase tracking-widest font-mono">SAM 3 内核级动态即时渲染</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button className="w-8 h-8 rounded text-gray-400 hover:bg-white/5 hover:text-white flex items-center justify-center transition-colors" title="撤销操作 (Ctrl+Z)">
|
||||
<Undo size={14} />
|
||||
</button>
|
||||
<button className="w-8 h-8 rounded text-gray-400 hover:bg-white/5 hover:text-white flex items-center justify-center transition-colors" title="重做操作 (Ctrl+Shift+Z)">
|
||||
<Redo size={14} />
|
||||
</button>
|
||||
<div className="w-px h-4 bg-white/10 mx-1"></div>
|
||||
<button className="flex items-center gap-2 text-xs text-gray-400 hover:text-white transition-colors bg-white/5 hover:bg-white/10 px-3 py-1.5 rounded-md border border-white/5">
|
||||
<ImageIcon size={14} /> 上传替换底图
|
||||
</button>
|
||||
<button className="text-xs text-gray-400 hover:text-white transition-colors px-3 py-1.5" onClick={() => setPoints([])}>
|
||||
清空全体锚点
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 relative p-8">
|
||||
<div className="w-full h-full relative border border-white/5 rounded shadow-2xl bg-[#1e1e1e] overflow-hidden cursor-crosshair">
|
||||
<Stage
|
||||
width={window.innerWidth - 320 - 64} // approx sizing, uses window to avoid ResizeObserver for simplicity here
|
||||
height={window.innerHeight - 64 - 64}
|
||||
onWheel={handleWheel}
|
||||
onMouseMove={handleMouseMove}
|
||||
onClick={handleStageClick}
|
||||
scaleX={scale}
|
||||
scaleY={scale}
|
||||
x={position.x}
|
||||
y={position.y}
|
||||
draggable={activeTool === 'move'}
|
||||
>
|
||||
<Layer>
|
||||
{/* Background Image */}
|
||||
{image && (
|
||||
<KonvaImage
|
||||
image={image}
|
||||
x={0}
|
||||
y={0}
|
||||
opacity={0.8}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mock Instance Mask from SAM3 */}
|
||||
<Group opacity={0.4}>
|
||||
<Path
|
||||
data="M 300 200 Q 400 150 450 250 T 400 350 Q 250 350 280 250 Z"
|
||||
fill="#06b6d4" // cyan-500
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Points */}
|
||||
{points.map((p, i) => (
|
||||
<Group key={i} x={p.x} y={p.y}>
|
||||
<Circle
|
||||
radius={6 / scale}
|
||||
fill={p.type === 'pos' ? '#22c55e' : '#ef4444'}
|
||||
stroke="#ffffff"
|
||||
strokeWidth={2 / scale}
|
||||
shadowColor="black"
|
||||
shadowBlur={4}
|
||||
/>
|
||||
<Circle
|
||||
radius={1.5 / scale}
|
||||
fill="#ffffff"
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</Layer>
|
||||
</Stage>
|
||||
<div className="absolute bottom-4 left-4 flex gap-4 text-[10px] font-mono text-gray-500 pointer-events-none">
|
||||
<span>光标坐标: {cursorPos.x.toFixed(2)}, {cursorPos.y.toFixed(2)}</span>
|
||||
<span>缩放比率: {(scale * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Right Ontology / Label Assignment Panel */}
|
||||
<OntologyInspector />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
src/components/auth/Login.tsx
Normal file
97
src/components/auth/Login.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BrainCircuit } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface LoginProps {
|
||||
onLoginSuccess: (token: string) => void;
|
||||
}
|
||||
|
||||
export function Login({ onLoginSuccess }: LoginProps) {
|
||||
const [username, setUsername] = useState('admin');
|
||||
const [password, setPassword] = useState('123456');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
onLoginSuccess(data.token);
|
||||
} else {
|
||||
const errData = await response.json();
|
||||
setError(errData.error || '登录失败');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('网络异常,无法连接到后端验证');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center bg-[#0a0a0a] text-gray-200">
|
||||
<div className="absolute inset-0 z-0 opacity-10 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-cyan-500 via-[#0a0a0a] to-transparent pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-md p-8 bg-[#111] border border-white/5 rounded-2xl shadow-2xl scale-in shadow-black/50">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-16 h-16 bg-white rounded-2xl flex items-center justify-center text-cyan-500 shadow-lg shadow-cyan-500/20 mb-4 overflow-hidden border border-white/10">
|
||||
<img src="/Logo.png" alt="Logo" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white tracking-wider mb-2">欢迎登录协同工作站</h1>
|
||||
<p className="text-sm text-gray-500">AI 智能切分与多模态数据标注系统</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-400 uppercase tracking-widest mb-2">账号</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full bg-[#1a1a1a] border border-white/10 rounded-lg px-4 py-3 text-sm focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/50 transition-all font-mono"
|
||||
placeholder="输入账号"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-400 uppercase tracking-widest mb-2">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-[#1a1a1a] border border-white/10 rounded-lg px-4 py-3 text-sm focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/50 transition-all font-mono"
|
||||
placeholder="输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-red-400 text-sm font-medium p-3 bg-red-400/10 rounded-lg border border-red-500/20 text-center">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
"w-full py-3.5 rounded-lg flex items-center justify-center gap-2 transition-all shadow-lg font-bold tracking-wider text-sm",
|
||||
isLoading ? "bg-cyan-500/50 cursor-not-allowed" : "bg-cyan-500 hover:bg-cyan-400 text-black shadow-cyan-500/20 hover:shadow-cyan-500/40"
|
||||
)}
|
||||
>
|
||||
{isLoading ? '验证中...' : '安全登录'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-white/5 text-center px-4">
|
||||
<p className="text-[10px] text-gray-600">系统受准入受控环境保护。所有流转数据与标注资产将进行端到端加密与访问溯源审计。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
78
src/components/dashboard/Dashboard.tsx
Normal file
78
src/components/dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { Activity, Clock, Folders, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
export function Dashboard() {
|
||||
const stats = [
|
||||
{ label: '运行中项目', value: '14', icon: Folders, color: 'text-blue-400', bg: 'bg-blue-400/10' },
|
||||
{ label: '排队处理任务', value: '3,291', icon: Clock, color: 'text-orange-400', bg: 'bg-orange-400/10' },
|
||||
{ label: '已归档批次', value: '128', icon: CheckCircle2, color: 'text-emerald-400', bg: 'bg-emerald-400/10' },
|
||||
{ label: '系统负载', value: '78%', icon: Activity, color: 'text-cyan-400', bg: 'bg-cyan-400/10' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-8 w-full h-full overflow-y-auto bg-[#0a0a0a]">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-medium tracking-tight text-white">系统整体概况</h1>
|
||||
<p className="text-gray-400 text-sm mt-1">系统全局数据吞吐状态与所有接入项目进度实时洞察驾驶舱。</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
{stats.map((stat, i) => {
|
||||
const Icon = stat.icon;
|
||||
return (
|
||||
<div key={i} className="bg-[#111] border border-white/5 p-5 rounded-xl block transition-all hover:border-white/20">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${stat.bg} ${stat.color}`}>
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
<div className="text-xl font-mono text-gray-100">{stat.value}</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-500 uppercase tracking-widest">{stat.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 bg-[#111] border border-white/5 rounded-xl p-6 min-h-[400px]">
|
||||
<h2 className="text-sm font-medium text-gray-400 uppercase tracking-widest mb-6">解析队列 (FFmpeg 挂起任务)</h2>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ name: 'City_Driving_Dataset_004.mp4', progress: 85, status: '正在截取帧 (30fps)' },
|
||||
{ name: 'Pedestrian_Night_Vision_02.mkv', progress: 32, status: '正在截取帧 (60fps)' },
|
||||
{ name: 'Drone_Mapping_Sector_7.avi', progress: 0, status: '队列排队等待中' }
|
||||
].map((task, i) => (
|
||||
<div key={i} className="bg-[#0d0d0d] border border-white/5 p-4 rounded-lg">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="font-mono text-sm text-gray-200">{task.name}</span>
|
||||
<span className="text-xs text-cyan-400 font-mono">{task.progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-white/5 rounded-full overflow-hidden mb-2">
|
||||
<div className="h-full bg-gradient-to-r from-cyan-600 to-cyan-400 rounded-full" style={{ width: `${task.progress}%` }} />
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{task.status}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#111] border border-white/5 rounded-xl p-6 min-h-[400px]">
|
||||
<h2 className="text-sm font-medium text-gray-400 uppercase tracking-widest mb-6">近期实时流转记录</h2>
|
||||
<div className="space-y-6 relative before:absolute before:inset-0 before:ml-[11px] before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-white/10 before:to-transparent">
|
||||
{/* Activity log mockup */}
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group is-active">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full border border-white/10 bg-[#111] group-[.is-active]:bg-cyan-500 group-[.is-active]:border-cyan-400 text-slate-500 group-[.is-active]:text-black shadow shrink-0 md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 z-10" />
|
||||
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-2.5rem)] bg-[#0d0d0d] p-3 rounded border border-white/5">
|
||||
<div className="text-xs text-gray-400 mb-1">10 分钟前</div>
|
||||
<div className="text-sm font-medium text-gray-200">语义归档完成 54 帧</div>
|
||||
<div className="text-xs text-gray-500">归属项目: Highway_Data</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/components/layout/Sidebar.tsx
Normal file
56
src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { Home, FolderOpen, Edit3, LayoutTemplate, BrainCircuit } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import type { ActiveModule } from '../../App';
|
||||
|
||||
interface SidebarProps {
|
||||
activeModule: ActiveModule;
|
||||
setActiveModule: (m: ActiveModule) => void;
|
||||
}
|
||||
|
||||
export function Sidebar({ activeModule, setActiveModule }: SidebarProps) {
|
||||
const navItems = [
|
||||
{ id: 'dashboard', icon: Home, label: '总体概况' },
|
||||
{ id: 'projects', icon: FolderOpen, label: '项目库' },
|
||||
{ id: 'workspace', icon: Edit3, label: '分割工作区' },
|
||||
{ id: 'ai', icon: BrainCircuit, label: 'AI智能分割' },
|
||||
{ id: 'templates', icon: LayoutTemplate, label: '模板库' },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<aside className="w-16 flex flex-col items-center py-6 bg-[#0d0d0d] border-r border-white/10 z-50 gap-8">
|
||||
<div className="w-10 h-10 rounded-lg overflow-hidden flex items-center justify-center bg-white">
|
||||
<img src="/Logo.png" alt="Logo" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<nav className="flex flex-col gap-6 w-full px-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeModule === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveModule(item.id as ActiveModule)}
|
||||
className={cn(
|
||||
"relative group flex items-center justify-center w-full aspect-square rounded-xl transition-all duration-200",
|
||||
isActive ? "text-cyan-400 border-l-2 border-cyan-400 bg-cyan-400/5 rounded-none aspect-auto py-2" : "text-gray-500 hover:text-white"
|
||||
)}
|
||||
title={item.label}
|
||||
>
|
||||
{/* Indicator removed in favor of border-l-2 added above */}
|
||||
<Icon size={22} strokeWidth={isActive ? 2.5 : 2} />
|
||||
|
||||
<div className="absolute left-full ml-2 px-2 py-1 bg-[#222] border border-[#333] text-xs rounded opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 shadow-xl">
|
||||
{item.label}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="mt-auto mb-4 flex flex-col gap-4">
|
||||
<div className="w-8 h-8 rounded-full border border-cyan-500/50 flex items-center justify-center text-[10px] text-cyan-400 font-bold cursor-pointer transition-all hover:bg-cyan-500/10">
|
||||
GPU
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
68
src/components/projects/ProjectLibrary.tsx
Normal file
68
src/components/projects/ProjectLibrary.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { UploadCloud, Film, Settings2, MoreHorizontal } from 'lucide-react';
|
||||
|
||||
interface ProjectLibraryProps {
|
||||
onProjectSelect: () => void;
|
||||
}
|
||||
|
||||
export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
|
||||
const [projects, setProjects] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/projects')
|
||||
.then(res => res.json())
|
||||
.then(data => setProjects(data))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-8 w-full h-full overflow-y-auto bg-[#0a0a0a]">
|
||||
<div className="flex justify-between items-end mb-8 border-b border-white/5 pb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-medium tracking-tight text-white mb-2">视频与连续帧项目库</h1>
|
||||
<p className="text-gray-400 text-sm">上传源文件、按帧解析配置,并结构化管理多媒体资产实体。</p>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 bg-cyan-600 hover:bg-cyan-500 text-white px-5 py-2.5 rounded-lg font-medium text-sm transition-colors border border-cyan-500 shadow-lg shadow-cyan-900/20">
|
||||
<UploadCloud size={18} />
|
||||
<span>导入多媒体资源</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{projects.map((proj) => (
|
||||
<div
|
||||
key={proj.id}
|
||||
className="group flex flex-col bg-[#111] border border-white/5 rounded-xl overflow-hidden cursor-pointer hover:border-cyan-500/50 transition-all hover:shadow-[0_0_20px_rgba(6,182,212,0.15)]"
|
||||
onClick={onProjectSelect}
|
||||
>
|
||||
<div className={`w-full aspect-[16/9] ${proj.thumbnail} relative flex items-center justify-center overflow-hidden`}>
|
||||
{/* Stand-in for actual video frame thumbnail */}
|
||||
<Film className="w-12 h-12 text-[#2a2a2a] group-hover:text-[#333] transition-colors" />
|
||||
<div className="absolute top-2 right-2 flex gap-2">
|
||||
<span className="backdrop-blur-md bg-black/40 text-gray-200 text-[10px] font-mono px-2 py-1 rounded border border-white/10 uppercase tracking-widest">
|
||||
{proj.fps}
|
||||
</span>
|
||||
<span className="backdrop-blur-md bg-black/40 text-gray-200 text-[10px] px-2 py-1 rounded border border-white/10 flex items-center gap-1 uppercase tracking-widest">
|
||||
{proj.status === 'Ready' ? (
|
||||
<><div className="w-1.5 h-1.5 bg-emerald-500 rounded-full" /> 已就绪</>
|
||||
) : (
|
||||
<><div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" /> 解析拆帧中</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col gap-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<h3 className="text-sm font-medium text-gray-200 truncate pr-4" title={proj.name}>{proj.name}</h3>
|
||||
<button className="text-gray-500 hover:text-gray-300"><MoreHorizontal size={16} /></button>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 font-mono mt-2">
|
||||
<span className="flex items-center gap-1.5"><Settings2 size={12} /> {proj.frames} 帧节点</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
111
src/components/templates/TemplateRegistry.tsx
Normal file
111
src/components/templates/TemplateRegistry.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Settings, FileJson, ArrowRightLeft, Database } from 'lucide-react';
|
||||
|
||||
export function TemplateRegistry() {
|
||||
const [templates, setTemplates] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/templates')
|
||||
.then(res => res.json())
|
||||
.then(data => setTemplates(data))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-8 w-full h-full overflow-y-auto bg-[#0a0a0a]">
|
||||
<div className="mb-8 border-b border-white/5 pb-6">
|
||||
<h1 className="text-3xl font-medium tracking-tight text-white mb-2">分割模板与分类优先级管理库</h1>
|
||||
<p className="text-gray-400 text-sm">定义业务语义本体树架构、约束覆盖遮罩优先级(Z-Index裁决权重),以及真实标签数据的向下兼容转换映射(Dict Translation)原则。</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
<div className="xl:col-span-1 border-r border-white/5 pr-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-sm font-bold text-gray-500 uppercase tracking-widest">生效中模板架构清单</h2>
|
||||
<button className="text-cyan-400 hover:text-cyan-300 text-sm transition-colors">+ 新建方案</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{templates.map(t => (
|
||||
<div key={t.id} className="bg-[#111] border border-white/5 hover:border-cyan-500/50 p-4 rounded-xl cursor-pointer transition-all hover:shadow-lg hover:shadow-cyan-900/10">
|
||||
<h3 className="font-medium text-gray-200 mb-1">{t.name}</h3>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 font-mono">
|
||||
<span>涵盖 {t.classes} 个字典大类</span>
|
||||
<span>挂载 {t.rules} 项解析规则</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="xl:col-span-2 space-y-6">
|
||||
<div className="bg-[#111] border border-white/5 rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-6 border-b border-white/5 pb-4">
|
||||
<h2 className="text-lg font-medium text-gray-200 flex items-center gap-2"><Database size={18} /> Cityscapes_v2_Mapping</h2>
|
||||
<button className="bg-white/5 hover:bg-white/10 border border-white/10 px-4 py-1.5 rounded text-sm text-gray-300 transition-colors">修改库视图结构 (Schema)</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-4">特定领域分类渲染级重叠裁决权重阵列 (Painter's Algorithm Weight)</h3>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ l: 'pedestrian', z: 90, c: '#ec4899', t: '运动中物理特型 (Dynamic Entity)' },
|
||||
{ l: 'bicycle', z: 85, c: '#f59e0b', t: '运动中物理特型 (Dynamic Entity)' },
|
||||
{ l: 'vehicle_car', z: 80, c: '#6366f1', t: '运动中物理特型 (Dynamic Entity)' },
|
||||
{ l: 'traffic_sign', z: 60, c: '#eab308', t: '交通属性静态特型 (Static Entity)' },
|
||||
{ l: 'road_surface', z: 10, c: '#71717a', t: '全局视野底板 (Background / Floor)' },
|
||||
].map(cls => (
|
||||
<div key={cls.l} className="flex grid grid-cols-4 gap-4 p-3 bg-[#0d0d0d] border border-white/5 rounded items-center">
|
||||
<div className="col-span-1 flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded" style={{ backgroundColor: cls.c }}></div>
|
||||
<span className="font-medium text-sm text-gray-300">{cls.l}</span>
|
||||
</div>
|
||||
<div className="col-span-1 font-mono text-xs text-gray-500">优先级 Z-Level: {cls.z}</div>
|
||||
<div className="col-span-2 flex justify-end">
|
||||
<span className="bg-white/5 text-gray-400 text-xs px-2 py-1 rounded border border-white/10">{cls.t}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-4 flex items-center gap-2">
|
||||
<ArrowRightLeft size={14} /> 强兼容真实标签 (GT Source) 闭环降维转置拓扑结构约束表
|
||||
</h3>
|
||||
<div className="bg-[#0d0d0d] border border-white/5 rounded-lg overflow-hidden">
|
||||
<table className="w-full text-left text-sm text-gray-400">
|
||||
<thead className="bg-[#111] border-b border-white/5 text-xs uppercase text-gray-500 font-mono">
|
||||
<tr>
|
||||
<th className="px-4 py-3">原始 JSON 键 (Legacy Key)</th>
|
||||
<th className="px-4 py-3">映射降维引挚解析路径</th>
|
||||
<th className="px-4 py-3">并轨至标准分类</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
<tr>
|
||||
<td className="px-4 py-3 font-mono text-gray-300">"car_sedan"</td>
|
||||
<td className="px-4 py-3 font-mono text-cyan-400">布尔合并聚类覆盖 (Logical OR)</td>
|
||||
<td className="px-4 py-3 font-medium text-gray-300">vehicle_car</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-3 font-mono text-gray-300">"car_suv"</td>
|
||||
<td className="px-4 py-3 font-mono text-cyan-400">布尔合并聚类覆盖 (Logical OR)</td>
|
||||
<td className="px-4 py-3 font-medium text-gray-300">vehicle_car</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-3 font-mono text-gray-300">"sidewalk_curb"</td>
|
||||
<td className="px-4 py-3 font-mono text-cyan-400">形态学极限分离-内切骨架法 (Skeletonization)</td>
|
||||
<td className="px-4 py-3 font-medium text-gray-300">road_curb</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
src/components/workspace/CanvasArea.tsx
Normal file
140
src/components/workspace/CanvasArea.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Stage, Layer, Image as KonvaImage, Circle, Rect, Path, Group } from 'react-konva';
|
||||
import useImage from 'use-image';
|
||||
|
||||
interface CanvasAreaProps {
|
||||
activeTool: string;
|
||||
}
|
||||
|
||||
export function CanvasArea({ activeTool }: CanvasAreaProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [stageSize, setStageSize] = useState({ width: 800, height: 600 });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [points, setPoints] = useState<{ x: number, y: number, type: 'pos'|'neg' }[]>([]);
|
||||
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
|
||||
|
||||
// We load a mock image representing a frame
|
||||
const [image] = useImage('https://images.unsplash.com/photo-1549317661-bd32c8ce0be2?q=80&w=2070&auto=format&fit=crop');
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (containerRef.current) {
|
||||
setStageSize({
|
||||
width: containerRef.current.clientWidth,
|
||||
height: containerRef.current.clientHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const handleWheel = (e: any) => {
|
||||
e.evt.preventDefault();
|
||||
const scaleBy = 1.1;
|
||||
const stage = e.target.getStage();
|
||||
const oldScale = stage.scaleX();
|
||||
|
||||
const mousePointTo = {
|
||||
x: stage.getPointerPosition().x / oldScale - stage.x() / oldScale,
|
||||
y: stage.getPointerPosition().y / oldScale - stage.y() / oldScale,
|
||||
};
|
||||
|
||||
const newScale = e.evt.deltaY < 0 ? oldScale * scaleBy : oldScale / scaleBy;
|
||||
setScale(newScale);
|
||||
setPosition({
|
||||
x: -(mousePointTo.x - stage.getPointerPosition().x / newScale) * newScale,
|
||||
y: -(mousePointTo.y - stage.getPointerPosition().y / newScale) * newScale,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: any) => {
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) return;
|
||||
const pos = stage.getPointerPosition();
|
||||
if (pos) {
|
||||
// Convert to image coordinates
|
||||
const imageX = (pos.x - position.x) / scale;
|
||||
const imageY = (pos.y - position.y) / scale;
|
||||
setCursorPos({ x: imageX, y: imageY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleStageClick = (e: any) => {
|
||||
if (activeTool === 'move') return;
|
||||
|
||||
if (activeTool === 'point_pos' || activeTool === 'point_neg') {
|
||||
const stage = e.target.getStage();
|
||||
const pos = stage.getRelativePointerPosition();
|
||||
setPoints([...points, { x: pos.x, y: pos.y, type: activeTool === 'point_pos' ? 'pos' : 'neg' }]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-full relative cursor-crosshair overflow-hidden rounded-sm">
|
||||
<Stage
|
||||
width={stageSize.width}
|
||||
height={stageSize.height}
|
||||
onWheel={handleWheel}
|
||||
onMouseMove={handleMouseMove}
|
||||
scaleX={scale}
|
||||
scaleY={scale}
|
||||
x={position.x}
|
||||
y={position.y}
|
||||
draggable={activeTool === 'move'}
|
||||
onClick={handleStageClick}
|
||||
>
|
||||
<Layer>
|
||||
{/* Background Image Layer */}
|
||||
{image && (
|
||||
<KonvaImage
|
||||
image={image}
|
||||
x={0}
|
||||
y={0}
|
||||
opacity={0.8}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mock Instance Mask overlapping */}
|
||||
<Group opacity={0.4}>
|
||||
<Path
|
||||
data="M 300 200 Q 400 150 450 250 T 400 350 Q 250 350 280 250 Z"
|
||||
fill="#06b6d4" // cyan-500
|
||||
/>
|
||||
<Path
|
||||
data="M 600 400 Q 700 350 750 450 T 650 550 Q 550 550 580 450 Z"
|
||||
fill="#a855f7" // purple-500
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* AI Prompts Point Regions */}
|
||||
{points.map((p, i) => (
|
||||
<Group key={i} x={p.x} y={p.y}>
|
||||
<Circle
|
||||
radius={6 / scale}
|
||||
fill={p.type === 'pos' ? '#22c55e' : '#ef4444'} // green or red
|
||||
stroke="#ffffff"
|
||||
strokeWidth={2 / scale}
|
||||
shadowColor="black"
|
||||
shadowBlur={4}
|
||||
/>
|
||||
<Circle
|
||||
radius={1.5 / scale}
|
||||
fill="#ffffff"
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</Layer>
|
||||
</Stage>
|
||||
|
||||
<div className="absolute bottom-4 left-4 flex gap-4 text-[10px] font-mono text-gray-500 pointer-events-none">
|
||||
<span>光标: {cursorPos.x.toFixed(2)}, {cursorPos.y.toFixed(2)}</span>
|
||||
<span>当前图层树: OBJECT_VEHICLE_01</span>
|
||||
<span>缩放比: {(scale * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
src/components/workspace/FrameTimeline.tsx
Normal file
82
src/components/workspace/FrameTimeline.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Play, Pause } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export function FrameTimeline() {
|
||||
const [currentFrame, setCurrentFrame] = useState(142);
|
||||
const totalFrames = 2400;
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
// show frames around current frame
|
||||
const frameWindow = 20;
|
||||
const frames = Array.from({ length: 41 }, (_, i) => currentFrame - frameWindow + i);
|
||||
|
||||
return (
|
||||
<div className="h-32 bg-[#111] border-t border-white/5 flex flex-col shrink-0 z-20">
|
||||
<div className="h-4 bg-[#0d0d0d] flex items-center group relative">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max={totalFrames}
|
||||
value={currentFrame}
|
||||
onChange={(e) => setCurrentFrame(parseInt(e.target.value))}
|
||||
className="w-full absolute inset-0 opacity-0 cursor-ew-resize z-20"
|
||||
/>
|
||||
<div className="h-1 bg-white/10 w-full relative group-hover:h-2 transition-all">
|
||||
<div
|
||||
className="h-full bg-cyan-500 absolute left-0"
|
||||
style={{ width: `${(currentFrame / totalFrames) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 bg-white rounded-full absolute top-1/2 -translate-y-1/2 -ml-1.5 shadow-sm transform scale-0 group-hover:scale-100 transition-transform shadow-cyan-500/50"
|
||||
style={{ left: `${(currentFrame / totalFrames) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex items-center px-4 gap-6">
|
||||
<div className="flex flex-col items-center gap-2 px-4 border-r border-white/10 shrink-0">
|
||||
<button
|
||||
className="p-2 rounded-full bg-white/5 text-white hover:bg-white/10"
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
>
|
||||
{isPlaying ? <Pause size={20} fill="currentColor" /> : <Play size={20} fill="currentColor" />}
|
||||
</button>
|
||||
<span className="text-[10px] font-mono text-gray-500">{isPlaying ? '暂停 (SPACE)' : '播放序列 (F5)'}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex-1 flex gap-px flex-nowrap items-center overflow-hidden justify-center"
|
||||
style={{ WebkitMaskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)' }}
|
||||
>
|
||||
{frames.map((frameIdx) => {
|
||||
if (frameIdx < 1 || frameIdx > totalFrames) {
|
||||
return <div key={`empty-${frameIdx}`} className="w-16 h-12 opacity-0 shrink-0" />
|
||||
}
|
||||
const isCurrent = frameIdx === currentFrame;
|
||||
return (
|
||||
<div
|
||||
key={frameIdx}
|
||||
onClick={() => setCurrentFrame(frameIdx)}
|
||||
className={cn(
|
||||
"relative shrink-0 rounded-sm transition-all cursor-pointer flex items-center justify-center overflow-hidden group mx-0.5",
|
||||
isCurrent ? "w-28 h-16 border-2 border-cyan-500 bg-gray-700 shadow-[0_0_15px_rgba(6,182,212,0.3)] z-10" : "w-16 h-12 border border-white/5 bg-gray-800/50 opacity-40 hover:opacity-100"
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-gray-800 to-gray-900 opacity-20" />
|
||||
<span className={cn("text-[9px] font-mono text-white text-center z-10", isCurrent ? "text-[10px] font-bold" : "")}>
|
||||
{frameIdx.toString().padStart(4, '0')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="w-48 text-right shrink-0">
|
||||
<div className="text-2xl font-mono text-white">{currentFrame}<span className="text-xs text-gray-500"> / {totalFrames}</span></div>
|
||||
<div className="text-[10px] text-gray-500 uppercase tracking-widest mt-1">底层时序视频图层截帧导航轴</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
89
src/components/workspace/OntologyInspector.tsx
Normal file
89
src/components/workspace/OntologyInspector.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Layers, ChevronDown, Tag, Eye } from 'lucide-react';
|
||||
|
||||
export function OntologyInspector() {
|
||||
const ontology = [
|
||||
{ id: '1', label: 'vehicle_four_wheels', color: 'bg-cyan-500', count: 4, zIndex: 60 },
|
||||
{ id: '2', label: 'pedestrian', color: 'bg-purple-500', count: 2, zIndex: 70 },
|
||||
{ id: '3', label: 'road_surface', color: 'bg-gray-500', count: 1, zIndex: 10 },
|
||||
{ id: '4', label: 'traffic_sign', color: 'bg-green-500', count: 3, zIndex: 50 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-60 bg-[#0d0d0d] flex flex-col border-l border-white/5 shrink-0 z-10 overflow-hidden">
|
||||
<div className="h-14 border-b border-white/5 flex items-center px-4 shrink-0 font-medium text-[10px] uppercase tracking-widest text-gray-500">
|
||||
<Layers size={14} className="mr-2 text-gray-400" />
|
||||
本体论与属性分类管理树
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
|
||||
{/* Frame Metadata */}
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-3">局部帧元数据</h3>
|
||||
<div className="bg-white/5 rounded p-2 text-[11px] space-y-2 font-mono text-gray-300">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">物理分辨率:</span> <span>1920x1080</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">绝对时间码:</span> <span>00:01:24.16</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">涵盖实体:</span> <span>10 个已实例化</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Priority Classes */}
|
||||
<div>
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-3 flex justify-between items-center">
|
||||
<span>语义分类树 (高度/Z-Index)</span>
|
||||
<button className="text-cyan-400 hover:text-cyan-300"><ChevronDown size={14} /></button>
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{ontology.sort((a,b) => b.zIndex - a.zIndex).map(cls => (
|
||||
<div key={cls.id} className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between p-2 rounded bg-white/5 hover:bg-white/10 cursor-pointer group transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2.5 h-2.5 rounded-sm ${cls.color}`} />
|
||||
<span className="text-xs font-medium text-gray-200">{cls.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[10px] text-gray-500 font-mono">z:{cls.zIndex}</span>
|
||||
<Eye size={14} className="text-gray-500 group-hover:text-gray-300" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Active Object Properties */}
|
||||
<div className="mt-4 pt-4 border-t border-[#222]">
|
||||
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-3">特定目标实例属性追踪</h3>
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Tag size={12} className="text-cyan-400" />
|
||||
<span className="text-xs font-semibold text-gray-200">vehicle_four_wheels</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] text-gray-500 uppercase">感知算法置信度</label>
|
||||
<div className="h-1.5 w-full bg-white/10 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-green-500 w-[94%]" />
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-green-500 text-right">0.9412</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-gray-500 uppercase">降维点拓扑锚点:</span>
|
||||
<span className="text-xs font-mono text-gray-300">12 节点</span>
|
||||
</div>
|
||||
<button className="w-full mt-2 bg-white/5 hover:bg-white/10 border border-white/10 text-xs text-gray-300 py-1.5 rounded transition-colors">
|
||||
重新提取内侧中轴树骨架
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
78
src/components/workspace/ToolsPalette.tsx
Normal file
78
src/components/workspace/ToolsPalette.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { MousePointer2, Hexagon, Square, Circle, Minus, Combine, Scissors, Wand2, Undo, Redo, Crosshair } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface ToolsPaletteProps {
|
||||
activeTool: string;
|
||||
setActiveTool: (tool: string) => void;
|
||||
onTriggerAI?: () => void;
|
||||
}
|
||||
|
||||
export function ToolsPalette({ activeTool, setActiveTool, onTriggerAI }: ToolsPaletteProps) {
|
||||
const tools = [
|
||||
{ id: 'move', icon: MousePointer2, label: '拖拽 / 选择 (V)' },
|
||||
{ id: 'create_polygon', icon: Hexagon, label: '创建多边形 (P)' },
|
||||
{ id: 'create_rectangle', icon: Square, label: '创建矩形 (R)' },
|
||||
{ id: 'create_circle', icon: Circle, label: '创建圆 (O)' },
|
||||
{ id: 'create_point', icon: Crosshair, label: '创建点 (C)' },
|
||||
{ id: 'create_line', icon: Minus, label: '创建线段 (L)' },
|
||||
{ id: 'area_merge', icon: Combine, label: '区域合并 (+)' },
|
||||
{ id: 'area_remove', icon: Scissors, label: '重叠区域去除 (-)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-12 bg-[#0d0d0d] border-r border-white/5 flex flex-col items-center py-4 shrink-0 z-10">
|
||||
<div className="flex flex-col gap-4 w-full px-2">
|
||||
{tools.map(tool => {
|
||||
const Icon = tool.icon;
|
||||
const isActive = activeTool === tool.id;
|
||||
return (
|
||||
<button
|
||||
key={tool.id}
|
||||
onClick={() => setActiveTool(tool.id)}
|
||||
title={tool.label}
|
||||
className={cn(
|
||||
"w-10 h-10 rounded-lg flex items-center justify-center transition-all p-2",
|
||||
isActive
|
||||
? (tool.id.includes('remove') ? "bg-red-500/10 text-red-500"
|
||||
: tool.id.includes('merge') ? "bg-green-500/10 text-green-500"
|
||||
: "bg-white/10 text-white")
|
||||
: "text-gray-500 hover:bg-white/5 hover:text-white"
|
||||
)}
|
||||
>
|
||||
<Icon size={18} strokeWidth={isActive ? 2.5 : 2} />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
<div className="w-full h-px bg-white/10 my-1" />
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTool('sam_trigger');
|
||||
if (onTriggerAI) onTriggerAI();
|
||||
}}
|
||||
title="触发 SAM 3 推理 (Enter)"
|
||||
className={cn(
|
||||
"w-10 h-10 rounded-lg flex items-center justify-center transition-all",
|
||||
activeTool === 'sam_trigger'
|
||||
? "bg-cyan-600 text-white shadow-lg shadow-cyan-900/20"
|
||||
: "text-gray-500 hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
<Wand2 size={18} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
<div className="w-full h-px bg-white/10 my-1" />
|
||||
|
||||
<button className="w-10 h-10 rounded text-gray-500 hover:bg-white/5 hover:text-white flex items-center justify-center transition-colors" title="撤销操作 (Ctrl+Z)">
|
||||
<Undo size={18} />
|
||||
</button>
|
||||
<button className="w-10 h-10 rounded text-gray-500 hover:bg-white/5 hover:text-white flex items-center justify-center transition-colors" title="重做操作 (Ctrl+Shift+Z)">
|
||||
<Redo size={18} />
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
src/components/workspace/VideoWorkspace.tsx
Normal file
49
src/components/workspace/VideoWorkspace.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CanvasArea } from './CanvasArea';
|
||||
import { ToolsPalette } from './ToolsPalette';
|
||||
import { OntologyInspector } from './OntologyInspector';
|
||||
import { FrameTimeline } from './FrameTimeline';
|
||||
|
||||
export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void }) {
|
||||
const [activeTool, setActiveTool] = useState<string>('move');
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col bg-[#0a0a0a]">
|
||||
{/* Top Header / Status bar */}
|
||||
<div className="h-14 border-b border-white/5 bg-[#111] flex items-center justify-between px-6 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-widest text-gray-400">核心分割工作区</h2>
|
||||
<div className="h-4 w-px bg-white/10"></div>
|
||||
<span className="text-sm text-white font-mono">Autonomous_Nav_Cam_Left.mp4</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1.5 text-[10px] uppercase font-medium">
|
||||
<span className="px-2 py-0.5 rounded bg-green-500/10 text-green-400 border border-green-500/20">SAM 3 部署就绪</span>
|
||||
</div>
|
||||
<button className="px-4 py-1.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-md text-xs transition-colors text-white">
|
||||
导出 JSON 标注集
|
||||
</button>
|
||||
<button className="px-4 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white text-xs font-medium rounded-md transition-shadow shadow-lg shadow-cyan-900/20">
|
||||
结构化归档保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Workspace Area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<ToolsPalette activeTool={activeTool} setActiveTool={setActiveTool} onTriggerAI={onNavigateToAI} />
|
||||
|
||||
<div className="flex-1 relative flex items-center justify-center p-8 bg-[#151515] overflow-hidden">
|
||||
<div className="relative w-full h-full bg-[#1e1e1e] border border-white/5 shadow-2xl rounded-sm">
|
||||
<CanvasArea activeTool={activeTool} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OntologyInspector />
|
||||
</div>
|
||||
|
||||
{/* Bottom Timeline */}
|
||||
<FrameTimeline />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/index.css
Normal file
11
src/index.css
Normal file
@@ -0,0 +1,11 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer utilities {
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
Reference in New Issue
Block a user