import React, { useEffect, useMemo, useRef, useState } from 'react'; import { AlertCircle, Bot, CheckCircle, ChevronDown, Cloud, Database, Download, Eye, Grid3X3, HelpCircle, History, Layers3, MessageSquare, PanelLeftClose, PanelLeftOpen, Play, Plus, Send, Settings, Share2, Sparkles, Table2, Terminal, Trash2, Upload, UserCircle, Workflow, X, Zap, } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { useLocation } from 'react-router-dom'; import ModuleSwitcher from '../components/ModuleSwitcher'; import { useLanguage } from '../context/LanguageContext'; import { buildRuntimeConfig, createDefaultWorkflow, ensureValidationChecklist, getBehaviorSectionsForNode, getInputOutputsForEdge, getInputOutputsForNode, getNodeConnectionAnchor, getNodeStatusFromValidation, getParametersForNode, getValidationsForNode, InitialSpaceType, WorkflowEdge, WorkflowModel, WorkflowNode, WorkflowNodeKind, WorkflowNodeStatus, WorkflowPortType, } from '../workflowModel'; const kindStyles: Record = { environment: { label: 'ENVIRONMENT', accent: 'text-slate-600', bg: 'bg-slate-50 border-slate-200', icon: Layers3 }, space: { label: 'SPACE', accent: 'text-emerald-600', bg: 'bg-emerald-50 border-emerald-100', icon: Grid3X3 }, agent: { label: 'AGENT', accent: 'text-blue-600', bg: 'bg-blue-50 border-blue-100', icon: Bot }, behavior: { label: 'BEHAVIOR', accent: 'text-amber-600', bg: 'bg-amber-50 border-amber-100', icon: Workflow }, output: { label: 'OUTPUT', accent: 'text-slate-600', bg: 'bg-slate-50 border-slate-200', icon: Database }, }; const portColors: Record = { ENV_CONTEXT: '#64748b', SPACE_CONTEXT: '#10b981', AGENT_CONTEXT: '#2563eb', OBSERVABLE_INFO: '#f59e0b', BEHAVIOR_OUTPUT: '#e11d48', REPORT: '#64748b', }; interface WorkflowCardProps { node: WorkflowNode; status: WorkflowNodeStatus; selected: boolean; onSelect: (id: string) => void; onDragStart: (id: string, event: React.MouseEvent) => void; onDelete: (id: string, event: React.MouseEvent) => void; } const WorkflowCard: React.FC = ({ node, status, selected, onSelect, onDragStart, onDelete, }) => { const style = kindStyles[node.kind]; const Icon = style.icon; const statusFrame = status === 'error' ? 'border-red-300 shadow-red-100' : status === 'warning' ? 'border-amber-300 shadow-amber-100' : 'border-slate-200'; const statusHeader = status === 'error' ? 'bg-red-50 border-red-100' : status === 'warning' ? 'bg-amber-50 border-amber-100' : style.bg; return (

{node.title}

{node.subtitle}

); }; const OmniEditor: React.FC = () => { const { t, language } = useLanguage(); const location = useLocation(); const initialWorkflow = useMemo(() => createDefaultWorkflow(), []); const canvasRef = useRef(null); const [workflow, setWorkflow] = useState(initialWorkflow); const [isAssistantCollapsed, setIsAssistantCollapsed] = useState(true); const [selectedNodeId, setSelectedNodeId] = useState('example-agent'); const [selectedEdgeId, setSelectedEdgeId] = useState(null); const [selectedVariableName, setSelectedVariableName] = useState(null); const [linkingFrom, setLinkingFrom] = useState(null); const [dragState, setDragState] = useState<{ id: string; startX: number; startY: number; nodeX: number; nodeY: number } | null>(null); const [bottomTab, setBottomTab] = useState<'agents' | 'variables' | 'sensing' | 'internalModels' | 'schedule' | 'data' | 'io' | 'validation'>('variables'); const [isDetailOpen, setIsDetailOpen] = useState(false); const [showInitModal, setShowInitModal] = useState(!!location.state?.fromLanding); const [selectedSpaceType, setSelectedSpaceType] = useState('grid'); const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); const [messages, setMessages] = useState([ { role: 'assistant', content: language === 'en' ? 'I can turn model ideas into a reproducible data-flow: source data, validation, parameter tables, ABM/game model, simulation, analysis and report output.' : '我可以辅助你创建环境、空间、Agent、属性和行为,但当前阶段先把右侧图形化建模功能做扎实。', }, ]); const [input, setInput] = useState(''); const [showToast, setShowToast] = useState(null); useEffect(() => { const element = canvasRef.current; if (!element) return; const updateSize = () => { setCanvasSize({ width: element.clientWidth, height: element.clientHeight }); }; updateSize(); const observer = new ResizeObserver(updateSize); observer.observe(element); return () => observer.disconnect(); }, []); const selectedNode = workflow.nodes.find((node) => node.id === selectedNodeId) ?? workflow.nodes[0]; const selectedNodeStatus = getNodeStatusFromValidation(workflow, selectedNode.id); const selectedNodeValidations = getValidationsForNode(workflow, selectedNode.id); const selectedNodeValidationIds = new Set(selectedNodeValidations.map((validation) => validation.id)); const selectedEdge = workflow.edges.find((edge) => edge.id === selectedEdgeId) ?? null; const selectedEdgeContracts = selectedEdge ? getInputOutputsForEdge(workflow, selectedEdge.id) : []; const selectedNodeContracts = getInputOutputsForNode(workflow, selectedNode.id); const selectedNodeInputContracts = selectedNodeContracts.filter((contract) => contract.direction === 'input'); const selectedNodeOutputContracts = selectedNodeContracts.filter((contract) => contract.direction === 'output'); const contextContracts = selectedEdge ? selectedEdgeContracts : selectedNodeContracts; const contextContractIds = new Set(contextContracts.map((contract) => contract.id)); const ioContextLabel = selectedEdge ? selectedEdge.label : selectedNode.title; const selectedParameters = getParametersForNode(workflow, selectedNode.id); const selectedBehaviorSections = getBehaviorSectionsForNode( workflow, selectedNode.kind === 'agent' ? 'on-sensing' : selectedNode.id, ); const sensingSections = selectedBehaviorSections.filter((section) => section.kind === 'observable_pool'); const internalModelSections = selectedBehaviorSections.filter((section) => section.kind !== 'observable_pool'); const scheduleRows = workflow.schedules; const associatedDataRows = workflow.associatedData; const ioRows = workflow.inputOutputs; const validationRows = workflow.validations; const getAssociatedDataForVariable = (variableName: string) => ( associatedDataRows.find((row) => row.relatedVariable === variableName) ); const selectedVariable = (selectedParameters.length ? selectedParameters : workflow.parameters).find( (parameter) => parameter.name === selectedVariableName, ); const selectedVariableData = selectedVariable ? getAssociatedDataForVariable(selectedVariable.name) : null; const edgeLines = workflow.edges .map((edge) => { const source = workflow.nodes.find((node) => node.id === edge.source); const target = workflow.nodes.find((node) => node.id === edge.target); if (!source || !target) return null; const sourceSide = source.x <= target.x ? 'right' : 'left'; const targetSide = source.x <= target.x ? 'left' : 'right'; const sourceAnchor = getNodeConnectionAnchor({ x: source.x, y: source.y, side: sourceSide }); const targetAnchor = getNodeConnectionAnchor({ x: target.x, y: target.y, side: targetSide }); return { ...edge, x1: canvasSize.width / 2 + sourceAnchor.x, y1: canvasSize.height / 2 + sourceAnchor.y, x2: canvasSize.width / 2 + targetAnchor.x, y2: canvasSize.height / 2 + targetAnchor.y, }; }) .filter(Boolean); const handleAction = (label: string) => { setShowToast(label); setTimeout(() => setShowToast(null), 2500); }; const handleExportRuntimeConfig = () => { const runtimeConfig = buildRuntimeConfig(workflow); const json = JSON.stringify(runtimeConfig, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = 'abm-runtime-config.json'; anchor.click(); URL.revokeObjectURL(url); setBottomTab('io'); handleAction('Export model runtime config JSON generated from I/O contracts'); }; const initializeSpace = (spaceType?: InitialSpaceType) => { const nextWorkflow = createDefaultWorkflow(spaceType); setWorkflow(nextWorkflow); setSelectedNodeId(spaceType ? `${spaceType}-space` : 'example-agent'); setSelectedEdgeId(null); setShowInitModal(false); }; const handleNodeMouseDown = (id: string, event: React.MouseEvent) => { if (event.button !== 0) return; const node = workflow.nodes.find((item) => item.id === id); if (!node) return; setDragState({ id, startX: event.clientX, startY: event.clientY, nodeX: node.x, nodeY: node.y }); }; const handleCanvasMouseMove = (event: React.MouseEvent) => { if (!dragState) return; const dx = event.clientX - dragState.startX; const dy = event.clientY - dragState.startY; setWorkflow((current) => ({ ...current, nodes: current.nodes.map((node) => ( node.id === dragState.id ? { ...node, x: dragState.nodeX + dx, y: dragState.nodeY + dy } : node )), })); }; const stopDragging = () => setDragState(null); const deleteSelectedNode = () => { if (!selectedNode) return; if (workflow.nodes.length <= 1) return; setWorkflow((current) => ({ ...current, nodes: current.nodes.filter((node) => node.id !== selectedNode.id), edges: current.edges.filter((edge) => edge.source !== selectedNode.id && edge.target !== selectedNode.id), parameters: current.parameters.filter((parameter) => parameter.nodeId !== selectedNode.id), behaviorSections: current.behaviorSections.filter((section) => section.nodeId !== selectedNode.id), })); const remaining = workflow.nodes.filter((node) => node.id !== selectedNode.id); setSelectedNodeId(remaining[0]?.id ?? ''); setSelectedEdgeId(null); }; const deleteNodeById = (id: string, event: React.MouseEvent) => { event.stopPropagation(); if (workflow.nodes.length <= 1) return; setWorkflow((current) => ({ ...current, nodes: current.nodes.filter((node) => node.id !== id), edges: current.edges.filter((edge) => edge.source !== id && edge.target !== id), parameters: current.parameters.filter((parameter) => parameter.nodeId !== id), behaviorSections: current.behaviorSections.filter((section) => section.nodeId !== id), })); if (selectedNodeId === id) { const remaining = workflow.nodes.filter((node) => node.id !== id); setSelectedNodeId(remaining[0]?.id ?? ''); } setSelectedEdgeId(null); }; const deleteSelectedEdge = () => { if (!selectedEdgeId) return; setWorkflow((current) => ({ ...current, edges: current.edges.filter((edge) => edge.id !== selectedEdgeId), })); setSelectedEdgeId(null); }; const handleNodeSelect = (id: string) => { if (linkingFrom && linkingFrom !== id) { const source = workflow.nodes.find((node) => node.id === linkingFrom); const target = workflow.nodes.find((node) => node.id === id); if (source && target) { const exists = workflow.edges.some((edge) => edge.source === source.id && edge.target === target.id); if (!exists) { const type = source.outputs[0]?.type ?? 'AGENT_CONTEXT'; const newEdge: WorkflowEdge = { id: `edge-${Date.now()}`, source: source.id, target: target.id, type, label: `${source.title} -> ${target.title}`, }; setWorkflow((current) => ({ ...current, edges: [...current.edges, newEdge] })); } } setLinkingFrom(null); } setSelectedNodeId(id); setSelectedEdgeId(null); }; const handleSend = () => { if (!input.trim()) return; const content = input.trim(); setMessages((prev) => [...prev, { role: 'user', content }]); setInput(''); setWorkflow((current) => ensureValidationChecklist(current)); setTimeout(() => { setMessages((prev) => [ ...prev, { role: 'assistant', content: language === 'en' ? 'I mapped that request onto the workflow structure and generated the matching validation checklist.' : '我会把这个需求映射到工作流结构,并同步生成对应的 validation checklist。', }, ]); }, 500); }; return (
{showToast && ( "{showToast}" {t('coming_soon')} )} {showInitModal && (
setShowInitModal(false)} />

{t('env_modal_title')}

{language === 'en' ? 'Choose the spatial subject you need now. You can skip this step and add space later.' : '选择当前需要的空间主体。你也可以先跳过,之后再添加。'}

{[ { id: 'Grid Space', icon: Grid3X3, desc: t('env_grid_desc') }, { id: 'Network Space', icon: Workflow, desc: t('env_network_desc') }, { id: 'Continuous Space', icon: Sparkles, desc: t('env_continuous_desc') }, { id: 'GIS Space', icon: Database, desc: t('env_gis_desc') }, ].map((item) => ( ))}
)}
{[ ['Share', Share2], ['History', History], ['Import', Upload], ['Export', Download], ].map(([label, Icon]) => { const actionLabel = label as string; return ( ); })}
{!isAssistantCollapsed && ( {t('ai_assistant')} )}
{!isAssistantCollapsed && ( <>
Design Assistant

{language === 'en' ? 'Collapsed by default for now: the visual model editor is the main workspace.' : '当前默认收起 AI:先把右侧图形化模型编辑器作为主工作区。'}

Pipeline Nodes
{[ ['Data Source', Database, 'Excel / raw data entry'], ['Parameter Table', Table2, 'Model-ready parameters'], ['Validation', CheckCircle, 'Rules and checks'], ['Model Output', Layers3, 'Simulation outputs'], ].map(([label, Icon, desc]) => ( ))}
{messages.map((msg, index) => (
{msg.role === 'user' ? t('user') : t('ai_researcher')}
{msg.content}
))}