feat: Initialize AgentBlock project structure

Sets up the foundational files and dependencies for the AgentBlock scientific modeling platform. Includes project metadata, React app structure, Vite configuration, and essential dependencies for AI integration, UI components, and styling.
This commit is contained in:
jerryW123
2026-05-06 00:40:25 +08:00
parent 2da7196efc
commit 2814ec412d
23 changed files with 7885 additions and 8 deletions
+924
View File
@@ -0,0 +1,924 @@
import React, { useState } from 'react';
import {
Play,
Settings,
Plus,
Minus,
Maximize,
Send,
Cpu,
Hexagon,
Book,
ChevronRight,
ChevronLeft,
Zap,
Terminal,
Users,
X,
MessageSquare,
PanelLeftClose,
PanelLeftOpen,
Upload,
Download,
CheckCircle,
LayoutGrid,
Share2,
Move,
Map,
Mic,
Trash2,
Menu,
ChevronDown,
Grid3X3,
Box,
Compass,
AlertCircle,
Link as LinkIcon,
ArrowRight
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { Link, useLocation } from 'react-router-dom';
import { useLanguage } from '../context/LanguageContext';
import ModuleSwitcher from '../components/ModuleSwitcher';
interface Node {
id: string;
type: 'environment' | 'agent' | 'parameter' | 'space';
x: number;
y: number;
data: any;
}
interface Edge {
id: string;
source: string;
target: string;
direction: 'none' | 'forward' | 'backward' | 'both';
isDashed: boolean;
}
const OmniEditor: React.FC = () => {
const { t, language } = useLanguage();
const location = useLocation();
const [isAssistantCollapsed, setIsAssistantCollapsed] = useState(false);
const [selectedNode, setSelectedNode] = useState<string | null>(null);
const [selectedEdge, setSelectedEdge] = useState<string | null>(null);
const [showInitModal, setShowInitModal] = useState(!!location.state?.fromLanding);
const [selectedSpace, setSelectedSpace] = useState<string | null>('grid');
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Connection state
const [linkingFrom, setLinkingFrom] = useState<string | null>(null);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
// Node dimension helper
const getNodeBounds = (node: Node) => {
const width = 256; // w-64
const height = node.type === 'environment' ? 200 : 100; // Estimated height for nodes
return {
left: node.x,
top: node.y,
width,
height,
centerX: node.x + width / 2,
centerY: node.y + height / 2
};
};
// Helper to calculate line between bounds
const getLineData = (sourceId: string, targetId: string) => {
const sourceNode = nodes.find(n => n.id === sourceId);
const targetNode = nodes.find(n => n.id === targetId);
if (!sourceNode || !targetNode) return null;
const s = getNodeBounds(sourceNode);
const t = getNodeBounds(targetNode);
// Simple connection between centers
// For a cleaner look, we can adjust these to be edge points
return { x1: s.centerX, y1: s.centerY, x2: t.centerX, y2: t.centerY };
};
// ABM Parameters State (from SciDashboard)
const [parameters, setParameters] = useState([
{ id: 1, name: 'agent_density', type: 'Float', dist: 'Constant', val: '0.75', min: 'N/A', max: 'N/A', desc: language === 'en' ? 'Initial population density' : '初始种群密度', error: false },
{ id: 2, name: 'interaction_radius', type: 'Integer', dist: 'Uniform', val: '--', min: '1', max: '10', desc: language === 'en' ? 'Missing initial value' : '缺少初始值', error: true },
{ id: 3, name: 'random_seed', type: 'Integer', dist: 'Constant', val: '42', min: 'N/A', max: 'N/A', desc: language === 'en' ? 'Reproducibility seed' : '可重复性种子', error: false },
]);
// New Node-Edge System State
const [nodes, setNodes] = useState<Node[]>(() => {
const initial: Node[] = [];
if (location.state?.fromLanding) {
initial.push({
id: 'env',
type: 'environment',
x: -250,
y: -100,
data: {
type: 'grid',
width: 40,
height: 40,
hWrap: true,
vWrap: true
}
});
}
return initial;
});
const [edges, setEdges] = useState<Edge[]>([]);
// Derived states for backward compatibility/quick access
const environment = nodes.find(n => n.type === 'environment')?.data || null;
const setEnvironment = (newData: any) => {
setNodes(prev => prev.map(n => n.id === 'env' ? { ...n, data: newData } : n));
};
const updateNodePos = (id: string, x: number, y: number) => {
setNodes(prev => prev.map(n => n.id === id ? { ...n, x, y } : n));
};
const handleCreateEdge = (targetId: string) => {
if (!linkingFrom || linkingFrom === targetId) return;
// Check if edge already exists
if (edges.some(e => (e.source === linkingFrom && e.target === targetId) || (e.source === targetId && e.target === linkingFrom))) {
setLinkingFrom(null);
return;
}
const newEdge: Edge = {
id: `edge-${Date.now()}`,
source: linkingFrom,
target: targetId,
direction: 'forward',
isDashed: true
};
setEdges(prev => [...prev, newEdge]);
setLinkingFrom(null);
};
const [messages, setMessages] = useState([
{
role: 'assistant',
content: language === 'en'
? "I've analyzed the Evolutionary Stable Strategy. Based on your inputs, I recommend adding a \"Cost of Sensing\" parameter to the Agent logic."
: "我分析了演化稳定策略。根据您的输入,我建议在代理逻辑中添加“感知成本”参数。"
}
]);
const [input, setInput] = useState('');
const [showToast, setShowToast] = useState<string | null>(null);
const handleAction = (name: string) => {
setShowToast(name);
setTimeout(() => setShowToast(null), 3000);
// Add logic for "Add Agent" to Node system
if (name === t('add_agent')) {
const id = `agent-${Date.now()}`;
setNodes(prev => [...prev, { id, type: 'agent', x: 200, y: 150, data: {} }]);
}
};
const handleSend = () => {
if (!input.trim()) return;
const userMsg = input.trim();
setMessages([...messages, { role: 'user', content: userMsg }]);
setInput('');
setTimeout(() => {
let aiContent = language === 'en' ? "Logic updated." : "逻辑已更新。";
// Basic pattern recognition for creating space
if (userMsg.toLowerCase().includes('grid') || userMsg.includes('网格')) {
const matches = userMsg.match(/(\d+)\D+(\d+)/);
const w = matches ? parseInt(matches[1]) : 40;
const h = matches ? parseInt(matches[2]) : 40;
setEnvironment({
type: 'grid',
width: w,
height: h,
hWrap: true,
vWrap: true
});
aiContent = language === 'en'
? `Created a ${w}x${h} grid space for your model.`
: `已为您创建了 ${w}x${h} 的网格空间。`;
}
setMessages(prev => [...prev, {
role: 'assistant',
content: aiContent
}]);
}, 800);
};
return (
<div className="bg-white text-slate-900 h-screen flex flex-col overflow-hidden font-sans relative">
<AnimatePresence>
{showToast && (
<motion.div
initial={{ opacity: 0, y: 50, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: 50, x: '-50%' }}
className="fixed bottom-8 left-1/2 z-[100] bg-white text-slate-900 px-6 py-3 rounded-xl shadow-2xl border border-slate-200 flex items-center gap-3"
>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
<span className="text-sm font-medium">"{showToast}" {t('coming_soon')}</span>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{showInitModal && (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-white/60 backdrop-blur-md"
onClick={() => setShowInitModal(false)}
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-2xl bg-white border border-slate-200 rounded-3xl shadow-2xl overflow-hidden"
>
<div className="p-8 border-b border-slate-100">
<h2 className="text-2xl font-bold text-slate-900 mb-2">{t('env_modal_title')}</h2>
<p className="text-slate-500 text-sm">{t('env_modal_subtitle')}</p>
</div>
<div className="p-8 grid grid-cols-2 gap-4">
{[
{ id: 'grid', icon: LayoutGrid, title: t('env_grid'), desc: t('env_grid_desc'), color: 'text-blue-600', bg: 'bg-blue-50' },
{ id: 'network', icon: Share2, title: t('env_network'), desc: t('env_network_desc'), color: 'text-purple-600', bg: 'bg-purple-50' },
{ id: 'continuous', icon: Move, title: t('env_continuous'), desc: t('env_continuous_desc'), color: 'text-green-600', bg: 'bg-green-50' },
{ id: 'gis', icon: Map, title: t('env_gis'), desc: t('env_gis_desc'), color: 'text-amber-600', bg: 'bg-amber-50' }
].map((item) => (
<button
key={item.id}
onClick={() => setSelectedSpace(item.id)}
className={`flex flex-col items-start p-4 rounded-2xl border-2 text-left transition-all ${
selectedSpace === item.id
? 'border-blue-500 bg-blue-50 ring-4 ring-blue-500/10'
: 'border-slate-100 hover:border-slate-200 bg-slate-50'
}`}
>
<div className={`p-2 rounded-xl mb-4 ${item.bg} ${item.color}`}>
<item.icon className="w-6 h-6" />
</div>
<h3 className="font-bold mb-1 text-slate-900">{item.title}</h3>
<p className="text-[11px] text-slate-500 leading-tight">{item.desc}</p>
</button>
))}
</div>
<div className="p-8 bg-slate-50 flex items-center justify-between">
<button onClick={() => setShowInitModal(false)} className="text-sm font-medium text-slate-500 hover:text-slate-900">{t('env_skip')}</button>
<button
onClick={() => {
setEnvironment({
type: selectedSpace || 'grid',
width: 40,
height: 40,
hWrap: true,
vWrap: true
});
setShowInitModal(false);
handleAction(language === 'en' ? `Initialized ${selectedSpace}` : `已初始化 ${selectedSpace}`);
}}
className="px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-bold transition-all shadow-lg shadow-blue-500/20"
>
{t('env_confirm')}
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<header className="border-b border-slate-200 bg-white flex flex-col z-50 shrink-0">
{/* Top Row: Context & System */}
<div className="h-10 flex items-center justify-between px-4 border-b border-slate-100 text-slate-500">
<div className="flex items-center gap-3">
<ModuleSwitcher />
<div className="h-4 w-px bg-slate-200 mx-1"></div>
<div className="flex items-center gap-2 px-2 py-1 bg-blue-50 text-blue-600 rounded-lg border border-blue-100 text-[10px] font-bold">
<Cpu className="w-3 h-3" />
</div>
<div className="flex items-center gap-2 px-2 py-1 bg-slate-50 rounded-md border border-slate-200 text-xs text-slate-700 font-bold cursor-pointer hover:bg-slate-100 transition-colors">
<span className="opacity-50 font-medium">Schema:</span>
<span>Evolutionary Stable Strategy</span>
<ChevronDown className="w-3 h-3 text-slate-400" />
</div>
</div>
<div className="flex items-center gap-4">
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-600 rounded border border-emerald-100 text-[9px] flex items-center gap-1 font-bold">
<span className="w-1 h-1 bg-emerald-500 rounded-full animate-pulse"></span> {t('visa_connected')}
</span>
</div>
</div>
{/* Bottom Row: ABM Workbench Menus */}
<div className="h-9 flex items-center px-4 gap-2 text-[11px] text-slate-500 font-medium uppercase tracking-wider bg-slate-50/50">
{[
{ label: language === 'en' ? 'Model(M)' : '模型(M)' },
{ label: language === 'en' ? 'Space(S)' : '空间(S)' },
{ label: language === 'en' ? 'Agents(A)' : '代理(A)' },
{ label: language === 'en' ? 'Logic(L)' : '交互(L)' },
{ label: language === 'en' ? 'Data(D)' : '数据(D)' }
].map((item, idx) => (
<button
key={idx}
className={`hover:text-blue-600 hover:bg-white px-3 py-1.5 rounded-lg transition-all ${idx === 0 ? 'text-blue-600 bg-white shadow-sm' : ''}`}
onClick={() => handleAction(item.label)}
>
{item.label}
</button>
))}
<div className="flex-1"></div>
<div className="flex items-center gap-2">
<button
onClick={() => handleAction('Setup Simulation')}
className="flex items-center gap-1.5 bg-white hover:bg-slate-50 text-slate-600 border border-slate-200 px-3 py-1 rounded-lg text-[10px] font-bold transition-all"
>
Setup
</button>
<button
onClick={() => handleAction(t('run_model'))}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-700 text-white px-4 py-1 rounded-lg text-[10px] font-bold shadow-lg shadow-blue-500/20 transition-all active:scale-95"
>
<Play className="w-3 h-3 fill-current" /> {t('run_model')}
</button>
</div>
</div>
</header>
<div className="flex flex-1 overflow-hidden">
<main className="flex-1 flex overflow-hidden">
{/* AI Assistant */}
<motion.section
animate={{ width: isAssistantCollapsed ? '48px' : '25%' }}
className="flex flex-col border-r border-slate-200 bg-slate-50 relative overflow-hidden"
>
<header className="p-3 border-b border-slate-200 bg-white flex justify-between items-center min-h-[56px]">
{!isAssistantCollapsed && <span className="text-[10px] font-bold uppercase tracking-[0.2em] text-slate-400">{t('ai_assistant')}</span>}
<button
onClick={() => setIsAssistantCollapsed(!isAssistantCollapsed)}
className="p-1.5 hover:bg-slate-100 rounded-md text-slate-400 transition-colors"
>
{isAssistantCollapsed ? <PanelLeftOpen className="w-4 h-4" /> : <PanelLeftClose className="w-4 h-4" />}
</button>
</header>
{!isAssistantCollapsed && (
<>
<div className="flex-1 overflow-y-auto p-4 space-y-6 text-sm">
{messages.map((msg, i) => (
<div key={i} className={`flex flex-col gap-1 ${msg.role === 'user' ? 'items-end' : ''}`}>
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">{msg.role === 'user' ? t('user') : t('ai_researcher')}</span>
<div className={`p-3 border shadow-sm ${msg.role === 'assistant' ? 'bg-white border-slate-200 text-slate-600 rounded-2xl rounded-tl-none' : 'bg-blue-600 text-white border-blue-500 rounded-2xl rounded-tr-none'}`}>
{msg.content}
</div>
</div>
))}
</div>
<div className="p-4 border-t border-slate-200 bg-white">
<div className="relative">
<textarea
className="w-full bg-slate-50 border border-slate-200 rounded-2xl p-4 pr-16 text-sm text-slate-700 focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none resize-none transition-all shadow-inner"
rows={2}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Simulate: 'Create agents'..."
/>
<div className="absolute bottom-3 right-3 flex gap-2">
<button onClick={handleSend} className="p-2.5 bg-blue-600 rounded-xl text-white shadow-lg shadow-blue-500/30 hover:bg-blue-700 transition-all active:scale-95">
<Send className="w-4 h-4" />
</button>
</div>
</div>
</div>
</>
)}
</motion.section>
{/* Visual Canvas Area */}
<section className="flex-1 flex flex-col relative bg-white overflow-hidden">
<header className="p-3 border-b border-slate-200 bg-white/80 backdrop-blur-md flex justify-between items-center z-10">
<div className="flex items-center gap-2">
<span className="p-1.5 bg-blue-50 text-blue-600 rounded-lg border border-blue-100"><Hexagon className="w-3.5 h-3.5" /></span>
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-slate-500">Schema Designer</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => { handleAction(t('add_agent')); }}
className="px-4 py-1.5 bg-white hover:bg-slate-50 text-blue-600 text-[10px] font-bold rounded-lg border border-slate-200 shadow-sm transition-all uppercase flex items-center gap-2 active:scale-95"
>
<Plus className="w-3.5 h-3.5" /> {t('add_agent')}
</button>
<button
onClick={() => { setShowInitModal(true); handleAction(t('add_space')); }}
className="px-4 py-1.5 bg-white hover:bg-slate-50 text-amber-600 text-[10px] font-bold rounded-lg border border-slate-200 shadow-sm transition-all uppercase flex items-center gap-2 active:scale-95"
>
<Plus className="w-3.5 h-3.5" /> {t('add_space')}
</button>
</div>
</header>
<div
className="flex-1 relative overflow-hidden flex items-center justify-center p-12 bg-white"
style={{
backgroundImage: 'radial-gradient(rgba(0,0,0,0.03) 1px, transparent 1.5px)',
backgroundSize: '32px 32px'
}}
onMouseMove={(e) => {
if (linkingFrom) {
const rect = e.currentTarget.getBoundingClientRect();
setMousePos({
x: e.clientX - rect.left - rect.width / 2,
y: e.clientY - rect.top - rect.height / 2
});
}
}}
onClick={() => { setSelectedNode(null); setSelectedEdge(null); setLinkingFrom(null); }}
>
{/* Connection Layer (SVG) - Orthogonal Paths */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none overflow-visible">
<svg className="w-full h-full overflow-visible z-0">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<marker id="arrowhead-blue" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#3B82F6" />
</marker>
</defs>
{/* Active Linking Preview (Straight for preview) */}
{linkingFrom && (
<line
x1={nodes.find(n => n.id === linkingFrom)?.x! + 128}
y1={nodes.find(n => n.id === linkingFrom)?.y!}
x2={mousePos.x}
y2={mousePos.y}
stroke="#3B82F6"
strokeWidth="2"
strokeDasharray="5,5"
className="opacity-50"
/>
)}
{edges.map(edge => {
const source = nodes.find(n => n.id === edge.source);
const target = nodes.find(n => n.id === edge.target);
if (!source || !target) return null;
const isSelected = selectedEdge === edge.id;
// Orthogonal Path Calculation
const x1 = source.x + 128;
const y1 = source.y;
const x2 = target.x - 128;
const y2 = target.y;
const midX = x1 + (x2 - x1) * 0.5;
const path = `M ${x1} ${y1} L ${midX} ${y2} L ${x2} ${y2}`;
return (
<g key={edge.id} className="pointer-events-auto cursor-pointer" onClick={(e) => { e.stopPropagation(); setSelectedEdge(edge.id); setSelectedNode(null); }}>
<path
d={path}
stroke="transparent"
strokeWidth="24"
fill="none"
/>
<motion.path
initial={false}
animate={{ d: path }}
stroke={isSelected ? "#3B82F6" : "#64748b"}
strokeWidth={isSelected ? "3" : "2"}
strokeDasharray={edge.isDashed ? "8,5" : "none"}
fill="none"
markerEnd={edge.direction === 'forward' || edge.direction === 'both' ? (isSelected ? "url(#arrowhead-blue)" : "url(#arrowhead)") : ""}
className="transition-colors duration-200"
/>
</g>
);
})}
</svg>
</div>
{nodes.map(node => (
<motion.div
key={node.id}
drag
dragMomentum={false}
dragElastic={0}
onDrag={(e, info) => {
updateNodePos(node.id, node.x + info.delta.x, node.y + info.delta.y);
}}
layout
initial={false}
animate={{
x: node.x,
y: node.y,
scale: selectedNode === node.id ? 1.02 : 1,
zIndex: selectedNode === node.id ? 40 : 10,
}}
whileDrag={{ cursor: 'grabbing', zIndex: 50 }}
transition={{ type: 'spring', damping: 25, stiffness: 300, mass: 0.1 }}
onClick={(e) => {
e.stopPropagation();
if (linkingFrom) {
handleCreateEdge(node.id);
} else {
setSelectedNode(node.id);
setSelectedEdge(null);
}
}}
className={`absolute w-64 bg-white border border-slate-200 rounded-xl z-20 cursor-grab active:cursor-grabbing shadow-xl ${selectedNode === node.id ? 'ring-2 ring-blue-500' : ''}`}
>
{/* Node Header */}
<div className={`px-4 py-2.5 rounded-t-xl border-b border-slate-100 flex items-center justify-between bg-slate-50`}>
<div className="flex items-center gap-2">
{node.type === 'environment' ? <LayoutGrid className="w-3.5 h-3.5 text-amber-600" /> : <Cpu className="w-3.5 h-3.5 text-blue-600" />}
<span className="text-[10px] font-black uppercase tracking-widest text-slate-700 whitespace-nowrap overflow-hidden text-ellipsis">
{node.id === 'env' ? 'Environment_Space' : (node.id === 'agent' ? 'Core_Agent' : node.id.split('-')[0].toUpperCase())}
</span>
</div>
<div className="flex items-center gap-1.5">
<button
onClick={(e) => {
e.stopPropagation();
setNodes(prev => prev.filter(n => n.id !== node.id));
setEdges(prev => prev.filter(edge => edge.source !== node.id && edge.target !== node.id));
if (selectedNode === node.id) setSelectedNode(null);
}}
className="p-1 hover:bg-red-50 text-slate-400 hover:text-red-500 rounded-md transition-all"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Node Content - List Style */}
<div className="p-3 space-y-1">
{node.type === 'environment' ? (
<>
<div className="flex items-center justify-between text-[9px] px-2 py-1.5 rounded hover:bg-slate-50 transition-colors group">
<div className="flex items-center gap-2 text-slate-500 uppercase font-bold">
<Grid3X3 className="w-3 h-3 text-slate-300 group-hover:text-amber-500 transition-colors" />
<span>Dimensions</span>
</div>
<span className="text-slate-600 font-mono italic">{node.data.width} × {node.data.height}</span>
</div>
<div className="flex items-center justify-between text-[9px] px-2 py-1.5 rounded hover:bg-slate-50 transition-colors group">
<div className="flex items-center gap-2 text-slate-500 uppercase font-bold">
<Move className="w-3 h-3 text-slate-300 group-hover:text-blue-500 transition-colors" />
<span>Boundary</span>
</div>
<span className="text-blue-600/80 font-mono font-bold">{node.data.hWrap ? 'Toroidal' : 'Hard'}</span>
</div>
</>
) : (
<>
<div className="flex items-center justify-between text-[9px] px-2 py-1.5 rounded hover:bg-slate-50 transition-colors group">
<div className="flex items-center gap-2 text-slate-500 uppercase font-bold">
<CheckCircle className="w-3 h-3 text-slate-300 group-hover:text-emerald-500 transition-colors" />
<span>Health_Initial</span>
</div>
<span className="text-slate-600 font-mono">100.0</span>
</div>
<div className="flex items-center justify-between text-[9px] px-2 py-1.5 rounded hover:bg-slate-50 transition-colors group">
<div className="flex items-center gap-2 text-slate-500 uppercase font-bold">
<Zap className="w-3 h-3 text-slate-300 group-hover:text-purple-500 transition-colors" />
<span>Energy_Sensing</span>
</div>
<span className="text-slate-600 font-mono">0.05</span>
</div>
<div className="flex items-center justify-between text-[9px] px-2 py-1.5 rounded hover:bg-slate-50 transition-colors group">
<div className="flex items-center gap-2 text-slate-500 uppercase font-bold">
<Users className="w-3 h-3 text-slate-300 group-hover:text-blue-500 transition-colors" />
<span>Generation</span>
</div>
<span className="text-slate-600 font-mono">v1.2.4</span>
</div>
</>
)}
</div>
{/* Ports / Connection Handles */}
<div className="absolute -left-1.5 top-1/2 -translate-y-1/2 w-3 h-3 bg-white border border-slate-300 rounded-full cursor-crosshair hover:bg-blue-500 transition-colors" />
<button
onClick={(e) => { e.stopPropagation(); setLinkingFrom(node.id); }}
className={`absolute -right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded border bg-white shadow-md flex items-center justify-center transition-all z-30 ${linkingFrom === node.id ? 'border-blue-500 text-blue-500 animate-pulse' : 'border-slate-200 text-slate-400 hover:border-blue-400 hover:text-blue-400'}`}
>
<LinkIcon className="w-3 h-3" />
</button>
</motion.div>
))}
{nodes.length === 0 && (
<div className="text-slate-700 pointer-events-none select-none">
<div className="text-center font-black text-[9px] tracking-[1em] uppercase opacity-20">
Scientific Ledger Empty // Define System Nodes
</div>
</div>
)}
{/* Canvas Controls */}
<div className="absolute bottom-8 right-8 flex flex-col items-center bg-white border border-slate-200 rounded-xl shadow-2xl p-1 z-50">
<button className="p-2.5 text-slate-400 hover:text-slate-900 transition-all active:scale-90" onClick={() => handleAction('Zoom In')}>
<Plus className="w-4 h-4" />
</button>
<button className="p-2.5 text-slate-400 hover:text-slate-900 transition-all active:scale-90" onClick={() => handleAction('Zoom Out')}>
<Minus className="w-4 h-4" />
</button>
</div>
</div>
</section>
{/* Properties Panel Overlay */}
<AnimatePresence>
{(selectedNode || selectedEdge) && (
<motion.section
initial={{ x: '100%', opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: '100%', opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="absolute right-0 top-0 bottom-0 w-80 md:w-96 flex flex-col bg-white border-l border-slate-200 shadow-2xl z-40"
>
<header className="p-3 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center shrink-0">
<div className="flex items-center gap-2 font-bold text-[10px] uppercase tracking-[0.2em] text-slate-400">
<Settings className="w-3.5 h-3.5" /> {t('properties_panel')}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => { setSelectedNode(null); setSelectedEdge(null); }}
className="p-1.5 hover:bg-slate-100 rounded-lg text-slate-400 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
</header>
<div className="flex-1 overflow-y-auto p-4 space-y-6">
{selectedEdge && (
<div className="space-y-6">
<div className="pb-4 border-b border-slate-100 flex items-center justify-between">
<h3 className="text-xl font-bold text-slate-900 uppercase tracking-tight">{language === 'en' ? 'Connection' : '连线设置'}</h3>
<button
onClick={() => { setEdges(prev => prev.filter(e => e.id !== selectedEdge)); setSelectedEdge(null); }}
className="p-2 bg-red-50 text-red-500 rounded-xl hover:bg-red-100 transition-colors"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
<h4 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{language === 'en' ? 'Flow Direction' : '流动方向'}</h4>
<div className="grid grid-cols-2 gap-2">
{[
{ id: 'forward', label: 'Forward' },
{ id: 'backward', label: 'Reverse' },
{ id: 'both', label: 'Bi-directional' },
{ id: 'none', label: 'Passive' }
].map(dir => (
<button
key={dir.id}
onClick={() => setEdges(prev => prev.map(e => e.id === selectedEdge ? { ...e, direction: dir.id as any } : e))}
className={`px-3 py-2 rounded-xl text-xs font-bold border transition-all ${edges.find(e => e.id === selectedEdge)?.direction === dir.id ? 'bg-blue-600 text-white border-blue-600 shadow-md' : 'bg-slate-50 text-slate-500 border-slate-200 hover:bg-white shadow-sm'}`}
>
{dir.label}
</button>
))}
</div>
</div>
<div className="space-y-4">
<h4 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Line Style</h4>
<label className="flex items-center justify-between p-3 bg-slate-50 border border-slate-200 rounded-xl cursor-pointer">
<span className="text-xs text-slate-600 font-medium">Dashed Line</span>
<input
type="checkbox"
checked={edges.find(e => e.id === selectedEdge)?.isDashed}
onChange={(e) => setEdges(prev => prev.map(edge => edge.id === selectedEdge ? { ...edge, isDashed: e.target.checked } : edge))}
className="w-4 h-4 rounded text-blue-600 bg-white border-slate-300"
/>
</label>
</div>
</div>
)}
{selectedNode === 'env' && environment && (
<div className="space-y-6">
<div className="pb-4 border-b border-slate-100 flex items-center justify-between">
<div>
<h3 className="text-xl font-bold text-slate-900 uppercase tracking-tight">{environment.type === 'grid' ? t('env_grid') : t('env_settings')}</h3>
<p className="text-[10px] text-slate-400 mt-1 font-mono uppercase tracking-widest">ID: SPACE_01</p>
</div>
<div className="p-2 bg-blue-50 text-blue-600 rounded-xl">
<Grid3X3 className="w-6 h-6" />
</div>
</div>
<div className="space-y-4">
<h4 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{t('env_dim')}</h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-[10px] text-slate-400 font-bold uppercase">{t('env_width')}</label>
<input
type="number"
value={environment.width}
onChange={(e) => setEnvironment({...environment, width: parseInt(e.target.value)||0})}
className="w-full bg-slate-50 border border-slate-200 p-2 text-sm text-blue-600 font-bold rounded-lg outline-none focus:ring-2 focus:ring-blue-500 transition-all font-mono"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] text-slate-400 font-bold uppercase">{t('env_height')}</label>
<input
type="number"
value={environment.height}
onChange={(e) => setEnvironment({...environment, height: parseInt(e.target.value)||0})}
className="w-full bg-slate-50 border border-slate-200 p-2 text-sm text-blue-600 font-bold rounded-lg outline-none focus:ring-2 focus:ring-blue-500 transition-all font-mono"
/>
</div>
</div>
</div>
<div className="space-y-4">
<h4 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{t('env_connections')}</h4>
<label className="flex items-center justify-between p-3 bg-slate-50 border border-slate-200 rounded-xl cursor-pointer hover:bg-white transition-colors shadow-sm">
<span className="text-xs text-slate-600 font-medium">{t('env_horizontal_wrap')}</span>
<input type="checkbox" checked={environment.hWrap} onChange={(e) => setEnvironment({...environment, hWrap: e.target.checked})} className="w-4 h-4 rounded text-blue-600 bg-white border-slate-300" />
</label>
<label className="flex items-center justify-between p-3 bg-slate-50 border border-slate-200 rounded-xl cursor-pointer hover:bg-white transition-colors shadow-sm">
<span className="text-xs text-slate-600 font-medium">{t('env_vertical_wrap')}</span>
<input type="checkbox" checked={environment.vWrap} onChange={(e) => setEnvironment({...environment, vWrap: e.target.checked})} className="w-4 h-4 rounded text-blue-600 bg-white border-slate-300" />
</label>
</div>
<div className="p-4 bg-blue-50 border border-blue-100 rounded-2xl">
<div className="flex gap-3">
<AlertCircle className="w-5 h-5 text-blue-500 shrink-0" />
<div>
<h5 className="text-[10px] font-bold text-blue-800 uppercase tracking-wide">Optimization Tip</h5>
<p className="text-[11px] text-slate-600 mt-1 leading-relaxed font-medium">
{language === 'en'
? 'Grid wrapping (toroidal) simulates infinite space, preventing edge-case biases in segregation models.'
: '开启环绕(环面拓扑)可以模拟无限空间,防止隔离模型中的边界偏差效应。'}
</p>
</div>
</div>
</div>
</div>
)}
{selectedNode === 'agent' && (
<div className="space-y-6">
<div className="pb-4 border-b border-slate-100">
<h3 className="text-xl font-bold text-slate-900 uppercase tracking-tight">Agent: Core_Model</h3>
</div>
{/* Detailed Parameters Table from SciDashboard */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{t('variable_params')}</h4>
<button onClick={() => handleAction(t('add_row'))} className="p-1 text-blue-600 hover:bg-blue-50 rounded transition-all"><Plus className="w-3.5 h-3.5" /></button>
</div>
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<table className="w-full text-[11px] border-collapse">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th className="p-2 text-left text-slate-400 font-bold uppercase tracking-wider">{t('variable_name')}</th>
<th className="p-2 text-left text-slate-400 font-bold uppercase tracking-wider">{t('initial_value')}</th>
<th className="p-2 text-center text-slate-400 font-bold uppercase tracking-wider"><Trash2 className="w-3 h-3 mx-auto" /></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{parameters.map((param) => (
<React.Fragment key={param.id}>
<tr className={`hover:bg-slate-50 transition-colors ${param.error ? 'bg-red-50/30' : ''}`}>
<td className="p-2 font-mono text-blue-600 font-bold">{param.name}</td>
<td className={`p-2 font-bold ${param.error ? 'text-red-500' : 'text-slate-700'}`}>{param.val}</td>
<td className="p-2 text-center">
<button className="text-slate-300 hover:text-red-500 transition-colors"><X className="w-3 h-3" /></button>
</td>
</tr>
{param.error && (
<tr>
<td colSpan={3} className="p-2 pt-0">
<div className="flex items-center gap-1 text-[9px] text-red-500 font-bold bg-red-100/30 p-1 rounded">
<AlertCircle className="w-2.5 h-2.5" />
{param.desc}
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Logic Validation Section from SciDashboard */}
<div className="space-y-4">
<h4 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{t('logic_validation')}</h4>
<div className="space-y-3">
<div className="p-3 bg-red-50 border border-red-100 rounded-xl">
<div className="flex items-start gap-2">
<AlertCircle className="w-3.5 h-3.5 text-red-600 mt-0.5" />
<div className="flex-1">
<p className="text-[10px] font-bold text-red-800 uppercase tracking-wide">{t('missing_value')}</p>
<p className="text-[11px] text-red-700 mt-0.5 leading-tight">Variable <span className="font-mono font-bold">interaction_radius</span> requires an initial value.</p>
</div>
</div>
</div>
<div className="p-3 bg-amber-50 border border-amber-100 rounded-xl">
<div className="flex items-start gap-2">
<AlertCircle className="w-3.5 h-3.5 text-amber-600 mt-0.5" />
<div className="flex-1">
<p className="text-[10px] font-bold text-amber-800 uppercase tracking-wide">{t('type_mismatch')}</p>
<p className="text-[11px] text-amber-700 mt-0.5 leading-tight">Potential float-to-int conversion detected in logic flow.</p>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</motion.section>
)}
</AnimatePresence>
</main>
</div>
{/* Bottom Drawer (Code Preview) - Merged from SciDashboard */}
<div
className={`absolute bottom-6 left-0 right-0 bg-white text-slate-800 z-40 transition-all duration-300 border-t border-slate-200 shadow-[0_-10px_40px_rgba(0,0,0,0.05)] ${isDrawerOpen ? 'translate-y-0 h-[280px]' : 'translate-y-[240px] h-[280px]'}`}
>
<div
className="flex items-center justify-between px-6 py-2 bg-slate-50 cursor-pointer border-b border-slate-100 group"
onClick={() => setIsDrawerOpen(!isDrawerOpen)}
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-xs font-bold text-slate-500 uppercase tracking-widest">
<Terminal className="w-3.5 h-3.5 text-blue-600" />
{t('agentpy_preview')}
</div>
<span className="px-2 py-0.5 rounded text-[9px] bg-green-50 text-green-600 border border-green-200 font-bold uppercase tracking-tighter">{t('auto_generated')}</span>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-4 opacity-0 group-hover:opacity-100 transition-opacity">
<button className="text-[10px] font-bold text-slate-400 hover:text-blue-600 transition-colors" onClick={(e) => { e.stopPropagation(); handleAction('Download Code'); }}>DOWNLOAD</button>
<button className="text-[10px] font-bold text-blue-600 hover:text-blue-700 transition-colors" onClick={(e) => { e.stopPropagation(); handleAction(t('copy_code')); }}>{t('copy_code')}</button>
</div>
<ChevronDown className={`h-4 w-4 text-slate-400 transition-transform ${isDrawerOpen ? '' : 'rotate-180'}`} />
</div>
</div>
<div className="p-6 font-mono text-xs overflow-auto h-full pb-12 bg-[#FAFBFC]">
<pre className="text-blue-700 leading-relaxed">
<span className="text-purple-600 italic"># AgentPy Model Implementation</span>{'\n'}
import <span className="text-slate-900 font-bold">agentpy</span> as ap{'\n\n'}
exp_parameters = {'{'}{'\n'}
{parameters.map(p => (
<React.Fragment key={p.id}>
{' '}<span className="text-amber-700">'{p.name}'</span>: <span className={p.error ? 'text-red-500 font-bold' : 'text-slate-900 font-bold'}>{p.val === '--' ? 'None' : p.val}</span>,{'\n'}
</React.Fragment>
))}
{'}'}{'\n\n'}
class <span className="text-slate-900 font-bold underline decoration-blue-500 underline-offset-4 decoration-2">ScientificModel</span>(ap.Model):{'\n'}
{' '}def setup(self):{'\n'}
{' '}self.agents = ap.AgentList(self, self.p.agent_density * 100){'\n'}
{' '}pass{'\n\n'}
{' '}def step(self):{'\n'}
{' '}pass
</pre>
</div>
</div>
<footer className="h-6 bg-white border-t border-slate-200 px-4 flex items-center justify-between text-[10px] text-slate-400 relative z-50">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1.5"><span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span> {language === 'en' ? 'System Stable' : '系统稳定'}</span>
</div>
<div className="flex items-center gap-3">
<span>OmniEditor v2.4</span>
</div>
</footer>
</div>
);
};
export default OmniEditor;