Initial commit

This commit is contained in:
2026-04-28 22:36:56 +08:00
commit 071ebf4b2a
26 changed files with 5853 additions and 0 deletions

32
src/App.tsx Normal file
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View 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>,
);