Files
AgentBlock/src/pages/OmniEditor.tsx
T
2026-05-11 19:55:38 +08:00

1802 lines
99 KiB
TypeScript

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<WorkflowNodeKind, { label: string; accent: string; bg: string; icon: React.ElementType }> = {
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<WorkflowPortType, string> = {
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<HTMLButtonElement>) => void;
onDelete: (id: string, event: React.MouseEvent<HTMLButtonElement>) => void;
}
const WorkflowCard: React.FC<WorkflowCardProps> = ({
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 (
<button
onClick={(event) => {
event.stopPropagation();
onSelect(node.id);
}}
onMouseDown={(event) => onDragStart(node.id, event)}
className={`absolute w-44 h-32 text-left rounded-lg border bg-white shadow-sm transition-shadow cursor-move ${
selected ? 'ring-4 ring-blue-500/15 border-blue-400 shadow-xl' : `${statusFrame} hover:border-blue-200 hover:shadow-md`
}`}
style={{ left: `calc(50% + ${node.x}px)`, top: `calc(50% + ${node.y}px)` }}
>
<div className={`flex items-center justify-between px-3 py-2 border-b ${statusHeader}`}>
<div className="flex items-center gap-2">
<span className={`p-1.5 rounded-md bg-white border ${style.accent}`}>
<Icon className="w-4 h-4" />
</span>
<span className={`text-[10px] font-black tracking-[0.18em] ${style.accent}`}>{style.label}</span>
</div>
<span
className={`text-[9px] font-black uppercase px-2 py-0.5 rounded-full border ${
status === 'error'
? 'bg-red-50 text-red-600 border-red-100'
: status === 'warning'
? 'bg-amber-50 text-amber-600 border-amber-100'
: status === 'success'
? 'bg-emerald-50 text-emerald-600 border-emerald-100'
: 'bg-slate-50 text-slate-500 border-slate-200'
}`}
>
{status}
</span>
</div>
<button
onMouseDown={(event) => event.stopPropagation()}
onClick={(event) => onDelete(node.id, event)}
className="absolute -right-2 -top-2 w-6 h-6 rounded-full bg-white border border-slate-200 shadow-sm text-slate-400 hover:text-red-600 hover:border-red-200 flex items-center justify-center"
title="Delete subject"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
<span className="absolute -left-1.5 top-1/2 -translate-y-1/2 w-3 h-3 rounded-full bg-white border-2 border-slate-300" />
<span className="absolute -right-1.5 top-1/2 -translate-y-1/2 w-3 h-3 rounded-full bg-white border-2 border-slate-300" />
<div className="p-3">
<div>
<h3 className="text-sm font-black text-slate-900">{node.title}</h3>
<p className="text-[11px] text-slate-500 mt-1 leading-snug line-clamp-2">{node.subtitle}</p>
</div>
</div>
</button>
);
};
const OmniEditor: React.FC = () => {
const { t, language } = useLanguage();
const location = useLocation();
const initialWorkflow = useMemo(() => createDefaultWorkflow(), []);
const canvasRef = useRef<HTMLDivElement>(null);
const [workflow, setWorkflow] = useState<WorkflowModel>(initialWorkflow);
const [isAssistantCollapsed, setIsAssistantCollapsed] = useState(true);
const [selectedNodeId, setSelectedNodeId] = useState('example-agent');
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
const [selectedVariableName, setSelectedVariableName] = useState<string | null>(null);
const [linkingFrom, setLinkingFrom] = useState<string | null>(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<InitialSpaceType>('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<string | null>(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<HTMLButtonElement>) => {
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<HTMLDivElement>) => {
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<HTMLButtonElement>) => {
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 (
<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: 40, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: 40, x: '-50%' }}
className="fixed bottom-8 left-1/2 z-[100] bg-white text-slate-900 px-5 py-3 rounded-lg shadow-2xl border border-slate-200 flex items-center gap-3"
>
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
<span className="text-sm font-bold">"{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/70 backdrop-blur-md"
onClick={() => setShowInitModal(false)}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 16 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 16 }}
className="relative w-full max-w-3xl bg-white border border-slate-200 rounded-lg shadow-2xl overflow-hidden"
>
<div className="p-6 border-b border-slate-100">
<h2 className="text-2xl font-black text-slate-900">{t('env_modal_title')}</h2>
<p className="text-slate-500 text-sm mt-2">
{language === 'en'
? 'Choose the spatial subject you need now. You can skip this step and add space later.'
: '选择当前需要的空间主体。你也可以先跳过,之后再添加。'}
</p>
</div>
<div className="p-6 grid grid-cols-2 gap-4">
{[
{ 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) => (
<button
key={item.id}
onClick={() => {
const normalized = item.id.split(' ')[0].toLowerCase() as InitialSpaceType;
setSelectedSpaceType(normalized);
}}
className={`text-left p-4 rounded-lg border transition-all ${
item.id.toLowerCase().startsWith(selectedSpaceType)
? 'border-blue-500 bg-blue-50 ring-4 ring-blue-500/10'
: 'border-slate-200 bg-slate-50 hover:bg-white hover:border-blue-300'
}`}
>
<item.icon className="w-6 h-6 text-blue-600 mb-3" />
<div className="font-black text-slate-900">{item.id}</div>
<p className="text-xs text-slate-500 mt-1 leading-relaxed">{item.desc}</p>
</button>
))}
</div>
<div className="p-6 bg-slate-50 border-t border-slate-100 flex items-center justify-between">
<button
onClick={() => initializeSpace()}
className="px-4 py-2 text-sm font-black text-slate-500 hover:text-slate-900 transition-colors"
>
{t('env_skip')}
</button>
<button
onClick={() => initializeSpace(selectedSpaceType)}
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-black shadow-lg shadow-blue-500/20 transition-all"
>
{t('env_confirm')}
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<header className="h-12 bg-slate-950 border-b border-slate-800 text-slate-200 flex items-center justify-between px-2 z-50 shrink-0 shadow-sm">
<div className="flex items-center gap-2 h-full">
<ModuleSwitcher variant="dark" />
<button
onClick={() => handleAction('Workspace')}
className="h-8 flex items-center gap-1.5 px-2 rounded-md hover:bg-slate-800 text-xs font-semibold transition-colors"
>
Personal <ChevronDown className="w-3.5 h-3.5 text-slate-400" />
</button>
<button
onClick={() => handleAction('GWproduce')}
className="h-8 flex items-center gap-2 px-3 rounded-md bg-slate-800 hover:bg-slate-700 border border-slate-700 min-w-40 text-left transition-colors"
>
<Cloud className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-sm font-semibold text-slate-100">GWproduce</span>
</button>
<button
onClick={() => setIsAssistantCollapsed((collapsed) => !collapsed)}
className="h-8 flex items-center gap-2 px-3 rounded-md bg-slate-800 hover:bg-slate-700 border border-slate-700 text-xs font-semibold transition-colors"
>
<Sparkles className="w-3.5 h-3.5 text-slate-100" />
AI
<span className={`w-8 h-4 rounded-full p-0.5 flex transition-colors ${isAssistantCollapsed ? 'bg-slate-600 justify-start' : 'bg-blue-600 justify-end'}`}>
<span className="w-3 h-3 rounded-full bg-white shadow-sm" />
</span>
</button>
{[
['Share', Share2],
['History', History],
['Import', Upload],
['Export', Download],
].map(([label, Icon]) => {
const actionLabel = label as string;
return (
<button
key={actionLabel}
onClick={() => {
if (actionLabel === 'Export') {
handleExportRuntimeConfig();
return;
}
handleAction(actionLabel === 'Import' ? 'Import Excel will create Associated Data records' : actionLabel);
}}
className="h-8 flex items-center gap-2 px-3 rounded-md bg-slate-800 hover:bg-slate-700 border border-slate-700 text-xs font-semibold transition-colors"
>
<Icon className="w-3.5 h-3.5" />
{actionLabel}
{actionLabel === 'Import' || actionLabel === 'Export' ? <ChevronDown className="w-3.5 h-3.5 text-slate-400" /> : null}
</button>
);
})}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleAction('Upgrade')}
className="h-8 px-3 rounded-md bg-blue-600 hover:bg-blue-500 text-white text-xs font-black transition-colors"
>
Upgrade <ChevronDown className="inline w-3.5 h-3.5 ml-1" />
</button>
<button
onClick={() => handleAction('Help')}
className="h-8 flex items-center gap-2 px-3 rounded-md bg-slate-800 hover:bg-slate-700 border border-slate-700 text-xs font-semibold transition-colors"
>
<HelpCircle className="w-3.5 h-3.5" /> Help <ChevronDown className="w-3.5 h-3.5 text-slate-400" />
</button>
<button
onClick={() => handleAction('Profile')}
className="h-8 w-8 rounded-full bg-slate-800 border border-slate-700 flex items-center justify-center text-emerald-300 hover:bg-slate-700 transition-colors"
>
<UserCircle className="w-5 h-5" />
</button>
</div>
</header>
<div className="flex flex-1 overflow-hidden">
<section
style={{ flex: `0 0 ${isAssistantCollapsed ? 48 : 340}px`, width: isAssistantCollapsed ? 48 : 340 }}
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-black uppercase tracking-[0.2em] text-slate-400 flex items-center gap-2">
<Bot className="w-4 h-4 text-blue-600" /> {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-5 text-sm">
<div className="p-3 rounded-lg bg-blue-50 border border-blue-100 text-blue-800">
<div className="text-[10px] font-black uppercase tracking-widest mb-2">Design Assistant</div>
<p className="text-xs leading-relaxed">
{language === 'en'
? 'Collapsed by default for now: the visual model editor is the main workspace.'
: '当前默认收起 AI:先把右侧图形化模型编辑器作为主工作区。'}
</p>
</div>
<div className="p-3 rounded-lg bg-white border border-slate-200 shadow-sm">
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3">Pipeline Nodes</div>
<div className="grid grid-cols-2 gap-2">
{[
['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]) => (
<button
key={label as string}
onClick={() => handleAction(label as string)}
className="text-left p-3 rounded-lg border border-slate-200 bg-slate-50 hover:bg-white hover:border-blue-200 transition-all"
title={desc as string}
>
<Icon className="w-4 h-4 text-blue-600 mb-2" />
<div className="text-[11px] font-black text-slate-800">{label as string}</div>
<div className="text-[10px] text-slate-400 mt-1 leading-snug">{desc as string}</div>
</button>
))}
</div>
</div>
{messages.map((msg, index) => (
<div key={`${msg.role}-${index}`} className={`flex flex-col gap-1 ${msg.role === 'user' ? 'items-end' : ''}`}>
<span className="text-[10px] text-slate-400 font-black 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-lg rounded-tl-none'
: 'bg-blue-600 text-white border-blue-500 rounded-lg 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-lg p-4 pr-14 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={(event) => setInput(event.target.value)}
placeholder={
language === 'en'
? "Describe a model edit, e.g. 'add a grid space and an agent behavior'"
: "描述模型修改,例如“添加网格空间和一个 agent 行为”"
}
/>
<button
onClick={handleSend}
className="absolute bottom-3 right-3 p-2.5 bg-blue-600 rounded-md 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>
</>
)}
</section>
<main className="flex-1 flex flex-col overflow-hidden">
<section className="flex-1 flex overflow-hidden">
<div className="flex-1 flex flex-col relative bg-white overflow-hidden">
<header className="p-3 border-b border-slate-200 bg-white/90 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-md border border-blue-100">
<Workflow className="w-4 h-4" />
</span>
<div>
<span className="block text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">Model Subjects</span>
<span className="block text-[10px] text-slate-400">Environment, space, agents, observation and behaviors</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setSelectedNodeId('grid-space')}
className="px-3 py-1.5 bg-white hover:bg-slate-50 text-emerald-600 text-[10px] font-black rounded-md border border-slate-200 shadow-sm transition-all uppercase flex items-center gap-2"
>
<Plus className="w-3.5 h-3.5" /> Grid Space
</button>
<button
onClick={() => setSelectedNodeId('example-agent')}
className="px-3 py-1.5 bg-white hover:bg-slate-50 text-indigo-600 text-[10px] font-black rounded-md border border-slate-200 shadow-sm transition-all uppercase flex items-center gap-2"
>
<Plus className="w-3.5 h-3.5" /> Agent Subject
</button>
</div>
</header>
<div
ref={canvasRef}
className="flex-1 relative overflow-hidden bg-[#fbfcfe]"
style={{
backgroundImage:
'linear-gradient(rgba(15,23,42,0.035) 1px, transparent 1px), linear-gradient(90deg, rgba(15,23,42,0.035) 1px, transparent 1px)',
backgroundSize: '32px 32px',
}}
onClick={() => {
setSelectedNodeId('example-agent');
setSelectedEdgeId(null);
setLinkingFrom(null);
}}
onMouseMove={handleCanvasMouseMove}
onMouseUp={stopDragging}
onMouseLeave={stopDragging}
>
<svg className="absolute inset-0 w-full h-full pointer-events-none">
{edgeLines.map((edge) => {
const curveOffset = Math.max(80, Math.min(180, Math.abs(edge!.x2 - edge!.x1) / 2));
const c1 = edge!.x1 <= edge!.x2 ? edge!.x1 + curveOffset : edge!.x1 - curveOffset;
const c2 = edge!.x1 <= edge!.x2 ? edge!.x2 - curveOffset : edge!.x2 + curveOffset;
return (
<g key={edge!.id}>
<path
className="pointer-events-auto cursor-pointer"
d={`M ${edge!.x1} ${edge!.y1} C ${c1} ${edge!.y1}, ${c2} ${edge!.y2}, ${edge!.x2} ${edge!.y2}`}
fill="none"
stroke={portColors[edge!.type]}
strokeWidth={selectedEdgeId === edge!.id ? '6' : '3'}
strokeLinecap="round"
opacity="0.78"
onClick={(event) => {
event.stopPropagation();
setSelectedEdgeId(edge!.id);
}}
/>
<circle cx={edge!.x1} cy={edge!.y1} r="4" fill="white" stroke={portColors[edge!.type]} strokeWidth="3" />
<circle cx={edge!.x2} cy={edge!.y2} r="4" fill={portColors[edge!.type]} />
</g>
);
})}
</svg>
{workflow.nodes.map((node) => (
<WorkflowCard
key={node.id}
node={node}
status={getNodeStatusFromValidation(workflow, node.id)}
selected={selectedNode.id === node.id}
onSelect={handleNodeSelect}
onDragStart={handleNodeMouseDown}
onDelete={deleteNodeById}
/>
))}
</div>
</div>
<aside className="w-[520px] bg-white border-l border-slate-200 flex flex-col">
<header className="p-4 border-b border-slate-100 bg-slate-50/70 flex items-center justify-between">
<div className="flex items-center gap-2">
<Settings className="w-4 h-4 text-slate-400" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Node Inspector</span>
</div>
<button className="p-1.5 hover:bg-slate-100 rounded-md text-slate-400" onClick={() => handleAction('Close inspector')}>
<X className="w-4 h-4" />
</button>
</header>
<div className="flex-1 overflow-y-auto p-4 space-y-5">
{selectedEdge && (
<section className="space-y-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-amber-600">Connection</div>
<h2 className="text-lg font-black text-slate-900 mt-1">{selectedEdge.label}</h2>
<p className="text-xs text-slate-500 mt-1">{selectedEdge.type}</p>
</div>
<button
onClick={deleteSelectedEdge}
className="px-3 py-1.5 bg-red-50 hover:bg-red-100 text-red-600 rounded-md text-[10px] font-black uppercase"
>
Delete
</button>
</div>
<div className="rounded-lg border border-slate-200 bg-slate-50/80 p-3 space-y-3">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[10px] font-black uppercase tracking-[0.18em] text-slate-400">I/O Contract</div>
<p className="text-xs text-slate-500 mt-1">This connection is backed by model input/output rows.</p>
</div>
<span className="text-[10px] font-black text-slate-500 bg-white border border-slate-200 rounded-full px-2 py-1">
{selectedEdgeContracts.length} rows
</span>
</div>
{selectedEdgeContracts.length > 0 ? (
<div className="space-y-2">
{selectedEdgeContracts.map((contract) => (
<div key={contract.id} className="rounded-md border border-white bg-white p-3 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div>
<div className="font-mono text-sm font-black text-blue-600">{contract.ioName}</div>
<div className="text-[11px] text-slate-500 mt-1">
{contract.sourceComponent} {'->'} {contract.targetComponent}
</div>
</div>
<span className={`shrink-0 rounded-full px-2 py-1 text-[9px] font-black uppercase ${
contract.direction === 'input'
? 'bg-emerald-50 text-emerald-700'
: 'bg-blue-50 text-blue-700'
}`}>
{contract.direction}
</span>
</div>
<div className="grid grid-cols-2 gap-2 mt-3 text-[11px] text-slate-500">
<div>
<span className="block text-[9px] font-black uppercase tracking-widest text-slate-400">Schema</span>
<span className="font-mono text-slate-700">{contract.schema}</span>
</div>
<div>
<span className="block text-[9px] font-black uppercase tracking-widest text-slate-400">Snapshot</span>
<span className="font-mono text-slate-700">{contract.snapshotPolicy}</span>
</div>
</div>
</div>
))}
</div>
) : (
<div className="rounded-md border border-dashed border-slate-300 bg-white px-3 py-4 text-xs text-slate-500">
No I/O contract has been bound to this connection yet.
</div>
)}
</div>
</section>
)}
{!selectedEdge && (
<>
<div>
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-blue-600">{kindStyles[selectedNode.kind].label}</div>
<h2 className="text-xl font-black text-slate-900 mt-1">{selectedNode.title}</h2>
<p className="text-xs text-slate-500 mt-1 leading-relaxed">{selectedNode.subtitle}</p>
</div>
<span
className={`shrink-0 text-[9px] font-black uppercase px-2 py-1 rounded-full border ${
selectedNodeStatus === 'error'
? 'bg-red-50 text-red-600 border-red-100'
: selectedNodeStatus === 'warning'
? 'bg-amber-50 text-amber-600 border-amber-100'
: selectedNodeStatus === 'success'
? 'bg-emerald-50 text-emerald-600 border-emerald-100'
: 'bg-slate-50 text-slate-500 border-slate-200'
}`}
>
{selectedNodeStatus}
</span>
</div>
</div>
{selectedVariable && (
<section className="p-3 rounded-lg border border-blue-100 bg-blue-50/70 space-y-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-blue-600">Selected Variable</div>
<h3 className="text-sm font-black text-slate-900 font-mono mt-1">{selectedVariable.name}</h3>
<p className="text-[11px] text-slate-500 mt-1 leading-relaxed">{selectedVariable.description}</p>
</div>
<span className={`shrink-0 px-2 py-1 rounded-full text-[10px] font-black uppercase ${
selectedVariableData?.qualityStatus === 'error'
? 'bg-red-100 text-red-700'
: selectedVariableData
? 'bg-emerald-100 text-emerald-700'
: 'bg-slate-100 text-slate-500'
}`}>
{selectedVariableData?.qualityStatus ?? 'unlinked'}
</span>
</div>
<div className="grid grid-cols-2 gap-2">
{[
['Associated Data', selectedVariableData?.id ?? '-'],
['Source Table', selectedVariableData?.sourceTable ?? '-'],
['Source Column', selectedVariableData?.sourceColumn ?? '-'],
['Version Key', selectedVariableData?.versionKey ?? '-'],
['Used By', selectedVariableData?.usedByBehavior ?? '-'],
['Update', selectedVariableData?.updateFrequency ?? '-'],
].map(([label, value]) => (
<div key={label} className="p-2 rounded-md bg-white border border-blue-100">
<div className="text-[9px] font-black uppercase tracking-widest text-slate-400">{label}</div>
<div className="text-[11px] font-black text-slate-700 mt-1 break-words">{value}</div>
</div>
))}
</div>
</section>
)}
<section className="p-3 rounded-lg border border-slate-200 bg-white space-y-3">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400">Validation Checklist</div>
<p className="text-[11px] text-slate-500 mt-1">Checks related to the current node.</p>
</div>
<span className={`shrink-0 rounded-full px-2 py-1 text-[10px] font-black uppercase ${
selectedNodeStatus === 'error'
? 'bg-red-100 text-red-700'
: selectedNodeStatus === 'warning'
? 'bg-amber-100 text-amber-700'
: 'bg-emerald-100 text-emerald-700'
}`}>
{selectedNodeValidations.length} checks
</span>
</div>
{selectedNodeValidations.length > 0 ? (
<div className="space-y-2">
{selectedNodeValidations.map((validation) => (
<div
key={validation.id}
className={`rounded-md border p-2 ${
validation.status === 'error'
? 'border-red-100 bg-red-50/70'
: validation.status === 'warning'
? 'border-amber-100 bg-amber-50/70'
: 'border-emerald-100 bg-emerald-50/70'
}`}
>
<div className="flex items-start justify-between gap-2">
<div>
<div className="font-mono text-[11px] font-black text-slate-800">{validation.targetName}</div>
<div className="mt-1 text-[10px] text-slate-500">{validation.method} · {validation.metric}</div>
</div>
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[9px] font-black uppercase ${
validation.status === 'error'
? 'bg-red-100 text-red-700'
: validation.status === 'warning'
? 'bg-amber-100 text-amber-700'
: 'bg-emerald-100 text-emerald-700'
}`}>
{validation.status}
</span>
</div>
<p className="mt-2 text-[10px] leading-relaxed text-slate-500">{validation.notes}</p>
</div>
))}
</div>
) : (
<div className="rounded-md border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-xs text-slate-500">
No validation checks are linked to this node yet.
</div>
)}
</section>
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-slate-50 border border-slate-200">
<div className="flex items-center justify-between gap-2">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Inputs</div>
<span className="text-[9px] font-black text-slate-400">{selectedNodeInputContracts.length} contracts</span>
</div>
<div className="mt-2 space-y-1">
{selectedNodeInputContracts.length > 0 ? (
selectedNodeInputContracts.map((contract) => (
<div key={contract.id} className="rounded-md bg-white border border-slate-100 p-2">
<div className="text-[11px] font-black text-slate-800 font-mono">{contract.ioName}</div>
<div className="text-[10px] text-slate-500 mt-1">{contract.sourceComponent} {'->'} {contract.targetComponent}</div>
<div className="text-[10px] text-slate-400 mt-1 font-mono">{contract.schema}</div>
</div>
))
) : (
(selectedNode.inputs.length ? selectedNode.inputs : [{ id: 'none', label: 'none', type: 'REPORT' as WorkflowPortType }]).map((port) => (
<div key={port.id} className="text-[11px] font-black text-slate-700 flex items-center gap-2">
<span className="w-2 h-2 rounded-full" style={{ background: portColors[port.type] }} />
{selectedNode.inputs.length ? port.type : 'NONE'}
</div>
))
)}
</div>
</div>
<div className="p-3 rounded-lg bg-slate-50 border border-slate-200">
<div className="flex items-center justify-between gap-2">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Outputs</div>
<span className="text-[9px] font-black text-slate-400">{selectedNodeOutputContracts.length} contracts</span>
</div>
<div className="mt-2 space-y-1">
{selectedNodeOutputContracts.length > 0 ? (
selectedNodeOutputContracts.map((contract) => (
<div key={contract.id} className="rounded-md bg-white border border-slate-100 p-2">
<div className="text-[11px] font-black text-slate-800 font-mono">{contract.ioName}</div>
<div className="text-[10px] text-slate-500 mt-1">{contract.sourceComponent} {'->'} {contract.targetComponent}</div>
<div className="text-[10px] text-slate-400 mt-1 font-mono">{contract.schema}</div>
</div>
))
) : (
selectedNode.outputs.map((port) => (
<div key={port.id} className="text-[11px] font-black text-slate-700 flex items-center gap-2">
<span className="w-2 h-2 rounded-full" style={{ background: portColors[port.type] }} />
{port.type}
</div>
))
)}
</div>
</div>
</div>
<section className="space-y-3">
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Configuration</h3>
<div className="flex gap-2">
<button
onClick={() => setLinkingFrom(selectedNode.id)}
className={`px-3 py-1.5 rounded-md text-[10px] font-black uppercase ${
linkingFrom === selectedNode.id ? 'bg-blue-600 text-white' : 'bg-blue-50 text-blue-700'
}`}
>
{linkingFrom === selectedNode.id ? 'Select Target' : 'Connect'}
</button>
<button
onClick={deleteSelectedNode}
disabled={selectedNode.id === 'environment'}
className="px-3 py-1.5 rounded-md text-[10px] font-black uppercase bg-red-50 text-red-600 disabled:opacity-40"
>
Delete Subject
</button>
</div>
{selectedNode.config.map((item) => (
<div key={item.label} className="flex items-center justify-between gap-3 p-3 bg-white border border-slate-200 rounded-lg">
<span className="text-xs font-bold text-slate-500">{item.label}</span>
<span className="text-xs font-black text-slate-900 text-right">{item.value}</span>
</div>
))}
</section>
<section className="space-y-3 border-t border-slate-200 pt-4">
<div className="flex items-center justify-between gap-3">
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-widest">VISA Model Tables</h3>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{selectedNode.title}</span>
</div>
<div className="flex flex-wrap gap-2">
{[
['agents', Bot, 'Agents'],
['variables', Table2, 'Variables'],
['sensing', Eye, 'Sensing'],
['internalModels', Workflow, 'Internal Models'],
['schedule', Terminal, 'Schedule'],
['data', Database, 'Data'],
['io', Layers3, 'I/O'],
['validation', CheckCircle, 'Validation'],
].map(([id, Icon, label]) => (
<button
key={id as string}
onClick={() => setBottomTab(id as typeof bottomTab)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[10px] font-black uppercase tracking-wider ${
bottomTab === id
? 'bg-blue-50 text-blue-600 border border-blue-200 shadow-sm'
: 'bg-slate-50 text-slate-400 border border-slate-200 hover:text-slate-700 hover:bg-white'
}`}
>
<Icon className="w-3.5 h-3.5" /> {label as string}
</button>
))}
</div>
<div className="max-h-[460px] overflow-auto rounded-lg border border-slate-200 bg-white">
{bottomTab === 'agents' && (
<table className="min-w-[760px] w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Agent / Subject</th>
<th className="px-4 py-3">Kind</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3">Inputs</th>
<th className="px-4 py-3">Outputs</th>
<th className="px-4 py-3">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{workflow.nodes.map((node) => {
const nodeStatus = getNodeStatusFromValidation(workflow, node.id);
return (
<tr key={node.id} className={selectedNode.id === node.id ? 'bg-blue-50/50' : 'hover:bg-slate-50'}>
<td className="px-4 py-3 font-black text-slate-900">{node.title}</td>
<td className="px-4 py-3 text-slate-500">{kindStyles[node.kind].label}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${
nodeStatus === 'error'
? 'bg-red-100 text-red-700'
: nodeStatus === 'warning'
? 'bg-amber-100 text-amber-700'
: 'bg-emerald-100 text-emerald-700'
}`}>
{nodeStatus}
</span>
</td>
<td className="px-4 py-3 text-slate-500">{node.inputs.map((port) => port.type).join(', ') || 'NONE'}</td>
<td className="px-4 py-3 text-slate-500">{node.outputs.map((port) => port.type).join(', ') || 'NONE'}</td>
<td className="px-4 py-3 text-slate-500">{node.subtitle}</td>
</tr>
);
})}
</tbody>
</table>
)}
{bottomTab === 'variables' && (
<table className="min-w-[1180px] w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Variable</th>
<th className="px-4 py-3">Type</th>
<th className="px-4 py-3">Scope</th>
<th className="px-4 py-3">Source</th>
<th className="px-4 py-3">Value</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3">Associated Data</th>
<th className="px-4 py-3">Version Key</th>
<th className="px-4 py-3">Quality</th>
<th className="px-4 py-3">Used By Behavior</th>
<th className="px-4 py-3">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{(selectedParameters.length ? selectedParameters : workflow.parameters).map((parameter) => {
const associatedData = getAssociatedDataForVariable(parameter.name);
return (
<tr
key={parameter.id}
onClick={() => setSelectedVariableName(parameter.name)}
className={`cursor-pointer ${
selectedVariableName === parameter.name
? 'bg-blue-50'
: parameter.status === 'error' || associatedData?.qualityStatus === 'error'
? 'bg-red-50/40'
: 'hover:bg-slate-50'
}`}
>
<td className="px-4 py-3 font-mono font-black text-blue-700">{parameter.name}</td>
<td className="px-4 py-3 text-slate-600">{parameter.type}</td>
<td className="px-4 py-3 text-slate-500">{parameter.scope}</td>
<td className="px-4 py-3 text-slate-500">{parameter.source}</td>
<td className={`px-4 py-3 font-black ${parameter.status === 'error' ? 'text-red-600' : 'text-slate-900'}`}>{parameter.value}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${
parameter.status === 'error'
? 'bg-red-100 text-red-700'
: parameter.status === 'warning'
? 'bg-amber-100 text-amber-700'
: 'bg-emerald-100 text-emerald-700'
}`}>
{parameter.status}
</span>
</td>
<td className="px-4 py-3 font-mono text-slate-600">{associatedData?.id ?? '-'}</td>
<td className="px-4 py-3 text-slate-500">{associatedData?.versionKey ?? '-'}</td>
<td className="px-4 py-3">
{associatedData ? (
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${associatedData.qualityStatus === 'error' ? 'bg-red-100 text-red-700' : 'bg-emerald-100 text-emerald-700'}`}>
{associatedData.qualityStatus}
</span>
) : (
<span className="text-slate-400">-</span>
)}
</td>
<td className="px-4 py-3 text-slate-500">{associatedData?.usedByBehavior ?? '-'}</td>
<td className="px-4 py-3 text-slate-500">{parameter.description}</td>
</tr>
);
})}
</tbody>
</table>
)}
{bottomTab === 'sensing' && (
<div className="grid grid-cols-2 gap-3 p-4 min-w-[720px]">
{(sensingSections.length ? sensingSections : selectedBehaviorSections).map((section) => (
<section key={section.id} className="bg-white border border-slate-200 rounded-lg overflow-hidden">
<header className="px-3 py-2 bg-slate-50 border-b border-slate-100">
<h3 className="text-[11px] font-black uppercase tracking-widest text-slate-700">{section.title}</h3>
<p className="text-[10px] text-slate-400 mt-1 leading-relaxed">{section.description}</p>
</header>
<div className="p-3 space-y-2">
{section.items.map((item) => (
<div key={`${section.id}-${item.label}`} className="p-2 rounded-md border border-slate-100 bg-slate-50">
<div className="text-[11px] font-black text-blue-700 font-mono">{item.label}</div>
<div className="text-[10px] text-slate-500 mt-1">{item.source}</div>
<div className="text-[10px] font-black text-slate-900 mt-1">{item.value}</div>
</div>
))}
</div>
</section>
))}
</div>
)}
{bottomTab === 'internalModels' && (
<div className="grid grid-cols-3 gap-3 p-4 min-w-[860px]">
{internalModelSections.map((section) => (
<section key={section.id} className="bg-white border border-slate-200 rounded-lg overflow-hidden">
<header className="px-3 py-2 bg-slate-50 border-b border-slate-100">
<h3 className="text-[11px] font-black uppercase tracking-widest text-slate-700">{section.title}</h3>
<p className="text-[10px] text-slate-400 mt-1 leading-relaxed">{section.description}</p>
</header>
<div className="p-3 space-y-2">
{section.items.map((item) => (
<div key={`${section.id}-${item.label}`} className="p-2 rounded-md border border-slate-100 bg-slate-50">
<div className="text-[11px] font-black text-blue-700 font-mono">{item.label}</div>
<div className="text-[10px] text-slate-500 mt-1">{item.source}</div>
<div className="text-[10px] font-black text-slate-900 mt-1">{item.value}</div>
</div>
))}
</div>
</section>
))}
</div>
)}
{bottomTab === 'schedule' && (
<table className="min-w-[1280px] w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Schedule ID</th>
<th className="px-4 py-3">Step Order</th>
<th className="px-4 py-3">Phase</th>
<th className="px-4 py-3">Actor Agent</th>
<th className="px-4 py-3">Target Behavior</th>
<th className="px-4 py-3">Trigger</th>
<th className="px-4 py-3">Execution Mode</th>
<th className="px-4 py-3">Time Unit</th>
<th className="px-4 py-3">Condition</th>
<th className="px-4 py-3">Repeat Rule</th>
<th className="px-4 py-3">Depends On</th>
<th className="px-4 py-3">Writes To</th>
<th className="px-4 py-3">Notes</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{scheduleRows.map((row) => (
<tr key={row.id} className="hover:bg-slate-50">
<td className="px-4 py-3 font-mono font-black text-blue-700">{row.id}</td>
<td className="px-4 py-3 font-black text-slate-900">{row.order}</td>
<td className="px-4 py-3 text-slate-500">{row.phase}</td>
<td className="px-4 py-3 text-slate-700 font-black">{row.actorAgent}</td>
<td className="px-4 py-3 font-mono text-blue-700 font-black">{row.targetBehavior}</td>
<td className="px-4 py-3 text-slate-500">{row.trigger}</td>
<td className="px-4 py-3 text-slate-500">{row.executionMode}</td>
<td className="px-4 py-3 text-slate-500">{row.timeUnit}</td>
<td className="px-4 py-3 text-slate-500">{row.condition}</td>
<td className="px-4 py-3 text-slate-500">{row.repeatRule}</td>
<td className="px-4 py-3 text-slate-500">{row.dependsOn}</td>
<td className="px-4 py-3 text-slate-500">{row.writesTo}</td>
<td className="px-4 py-3 text-slate-500">{row.notes}</td>
</tr>
))}
</tbody>
</table>
)}
{bottomTab === 'data' && (
<table className="min-w-[1280px] w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Data ID</th>
<th className="px-4 py-3">Data</th>
<th className="px-4 py-3">Type</th>
<th className="px-4 py-3">Source Type</th>
<th className="px-4 py-3">Source Name</th>
<th className="px-4 py-3">Source Table</th>
<th className="px-4 py-3">Source Column</th>
<th className="px-4 py-3">Owner Agent</th>
<th className="px-4 py-3">Related Variable</th>
<th className="px-4 py-3">Used By Behavior</th>
<th className="px-4 py-3">Update Frequency</th>
<th className="px-4 py-3">Version Key</th>
<th className="px-4 py-3">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{associatedDataRows.map((row) => (
<tr key={row.id} className={row.qualityStatus === 'error' ? 'bg-red-50/40' : 'hover:bg-slate-50'}>
<td className="px-4 py-3 font-mono font-black text-slate-700">{row.id}</td>
<td className="px-4 py-3 font-mono font-black text-blue-700">{row.name}</td>
<td className="px-4 py-3 text-slate-500">{row.type}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceType}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceName}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceTable}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceColumn}</td>
<td className="px-4 py-3 text-slate-700 font-black">{row.ownerAgent}</td>
<td className="px-4 py-3 text-slate-500">{row.relatedVariable}</td>
<td className="px-4 py-3 text-slate-500">{row.usedByBehavior}</td>
<td className="px-4 py-3 text-slate-500">{row.updateFrequency}</td>
<td className="px-4 py-3 text-slate-500">{row.versionKey}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${row.qualityStatus === 'error' ? 'bg-red-100 text-red-700' : 'bg-emerald-100 text-emerald-700'}`}>
{row.qualityStatus}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
{bottomTab === 'io' && (
<div className="min-w-[1440px]">
<div className="sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 bg-white px-4 py-3">
<div>
<div className="text-[10px] font-black uppercase tracking-[0.2em] text-blue-600">Full Model I/O Contract</div>
<p className="mt-1 text-[11px] text-slate-500">
Showing {ioRows.length} total rows. {contextContracts.length} rows relate to {ioContextLabel}.
</p>
</div>
<span className="rounded-full border border-blue-100 bg-blue-50 px-3 py-1 text-[10px] font-black uppercase text-blue-700">
Context Highlight
</span>
</div>
<table className="w-full text-left text-xs">
<thead className="bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">I/O ID</th>
<th className="px-4 py-3">Component Type</th>
<th className="px-4 py-3">Component ID</th>
<th className="px-4 py-3">Component</th>
<th className="px-4 py-3">Direction</th>
<th className="px-4 py-3">Name</th>
<th className="px-4 py-3">Type</th>
<th className="px-4 py-3">Schema</th>
<th className="px-4 py-3">Source Component</th>
<th className="px-4 py-3">Source Table</th>
<th className="px-4 py-3">Target Component</th>
<th className="px-4 py-3">Target Table</th>
<th className="px-4 py-3">Required</th>
<th className="px-4 py-3">Default</th>
<th className="px-4 py-3">Snapshot Policy</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{ioRows.map((row) => {
const isContextRow = contextContractIds.has(row.id);
return (
<tr key={row.id} className={isContextRow ? 'bg-blue-50/70 ring-1 ring-inset ring-blue-100' : 'hover:bg-slate-50'}>
<td className={`px-4 py-3 font-mono font-black ${isContextRow ? 'text-blue-700' : 'text-slate-700'}`}>{row.id}</td>
<td className="px-4 py-3 text-slate-500">{row.componentType}</td>
<td className="px-4 py-3 font-mono text-slate-500">{row.componentId}</td>
<td className="px-4 py-3 text-slate-700 font-black">{row.componentName}</td>
<td className="px-4 py-3 text-slate-500">{row.direction}</td>
<td className="px-4 py-3 font-mono font-black text-blue-700">{row.ioName}</td>
<td className="px-4 py-3 text-slate-500">{row.ioDataType}</td>
<td className="px-4 py-3 font-mono text-slate-500">{row.schema}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceComponent}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceTable}</td>
<td className="px-4 py-3 text-slate-500">{row.targetComponent}</td>
<td className="px-4 py-3 text-slate-500">{row.targetTable}</td>
<td className="px-4 py-3 text-slate-900 font-black">{row.required}</td>
<td className="px-4 py-3 text-slate-500">{row.defaultValue}</td>
<td className="px-4 py-3 text-slate-500">{row.snapshotPolicy}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{bottomTab === 'validation' && (
<div className="min-w-[1440px]">
<div className="sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 bg-white px-4 py-3">
<div>
<div className="text-[10px] font-black uppercase tracking-[0.2em] text-red-600">Validation Checklist</div>
<p className="mt-1 text-[11px] text-slate-500">
Showing {validationRows.length} total checks. {selectedNodeValidations.length} checks relate to {selectedNode.title}.
</p>
</div>
<span className="rounded-full border border-red-100 bg-red-50 px-3 py-1 text-[10px] font-black uppercase text-red-700">
Context Highlight
</span>
</div>
<table className="w-full text-left text-xs">
<thead className="bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Validation ID</th>
<th className="px-4 py-3">Target Type</th>
<th className="px-4 py-3">Target ID</th>
<th className="px-4 py-3">Target</th>
<th className="px-4 py-3">Validation Type</th>
<th className="px-4 py-3">Method</th>
<th className="px-4 py-3">Metric</th>
<th className="px-4 py-3">Baseline Data</th>
<th className="px-4 py-3">Threshold</th>
<th className="px-4 py-3">Frequency</th>
<th className="px-4 py-3">Result Field</th>
<th className="px-4 py-3">Severity</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3">Notes</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{validationRows.map((row) => {
const isNodeValidation = selectedNodeValidationIds.has(row.id);
const rowTone = row.status === 'error' ? 'bg-red-50/40' : row.status === 'warning' ? 'bg-amber-50/40' : 'hover:bg-slate-50';
return (
<tr key={row.id} className={isNodeValidation ? 'bg-blue-50/70 ring-1 ring-inset ring-blue-100' : rowTone}>
<td className={`px-4 py-3 font-mono font-black ${isNodeValidation ? 'text-blue-700' : 'text-slate-700'}`}>{row.id}</td>
<td className="px-4 py-3 text-slate-500">{row.targetType}</td>
<td className="px-4 py-3 font-mono text-slate-500">{row.targetId}</td>
<td className="px-4 py-3 font-mono font-black text-blue-700">{row.targetName}</td>
<td className="px-4 py-3 text-slate-500">{row.validationType}</td>
<td className="px-4 py-3 text-slate-500">{row.method}</td>
<td className="px-4 py-3 text-slate-500">{row.metric}</td>
<td className="px-4 py-3 text-slate-500">{row.baselineData}</td>
<td className="px-4 py-3 text-slate-900 font-black">{row.threshold}</td>
<td className="px-4 py-3 text-slate-500">{row.frequency}</td>
<td className="px-4 py-3 text-slate-500">{row.resultField}</td>
<td className="px-4 py-3 text-slate-500">{row.severity}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${
row.status === 'error'
? 'bg-red-100 text-red-700'
: row.status === 'warning'
? 'bg-amber-100 text-amber-700'
: 'bg-emerald-100 text-emerald-700'
}`}>
{row.status}
</span>
</td>
<td className="px-4 py-3 text-slate-500">{row.notes}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</section>
</>
)}
</div>
</aside>
</section>
<section className="hidden">
<div
className="h-11 bg-slate-50 border-b border-slate-200 flex items-center justify-between px-4 cursor-pointer"
onClick={() => setIsDetailOpen((open) => !open)}
>
<div className="flex items-center gap-2">
<button
onClick={(event) => {
event.stopPropagation();
setIsDetailOpen((open) => !open);
}}
className="p-1.5 rounded-md border border-slate-200 bg-white text-slate-400 hover:text-blue-600 hover:border-blue-200 transition-colors"
title={isDetailOpen ? 'Collapse model table' : 'Expand model table'}
>
{isDetailOpen ? <PanelLeftClose className="w-4 h-4 -rotate-90" /> : <PanelLeftOpen className="w-4 h-4 rotate-90" />}
</button>
{[
['agents', Bot, 'Agents'],
['variables', Table2, 'Variables'],
['sensing', Eye, 'Sensing'],
['internalModels', Workflow, 'Internal Models'],
['schedule', Terminal, 'Schedule'],
['data', Database, 'Data'],
['io', Layers3, 'I/O'],
['validation', CheckCircle, 'Validation'],
].map(([id, Icon, label]) => (
<button
key={id as string}
onClick={(event) => {
event.stopPropagation();
setBottomTab(id as typeof bottomTab);
setIsDetailOpen(true);
}}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-[10px] font-black uppercase tracking-wider ${
bottomTab === id ? 'bg-white text-blue-600 border border-slate-200 shadow-sm' : 'text-slate-400 hover:text-slate-700'
}`}
>
<Icon className="w-3.5 h-3.5" /> {label as string}
</button>
))}
</div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
{isDetailOpen ? 'Click header to collapse' : 'Click to expand model table'} · {selectedNode.title}
</span>
</div>
{isDetailOpen && <div className="flex-1 overflow-auto">
{bottomTab === 'agents' && (
<table className="w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Agent / Subject</th>
<th className="px-4 py-3">Kind</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3">Inputs</th>
<th className="px-4 py-3">Outputs</th>
<th className="px-4 py-3">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{workflow.nodes.map((node) => {
const nodeStatus = getNodeStatusFromValidation(workflow, node.id);
return (
<tr key={node.id} className={selectedNode.id === node.id ? 'bg-blue-50/50' : 'hover:bg-slate-50'}>
<td className="px-4 py-3 font-black text-slate-900">{node.title}</td>
<td className="px-4 py-3 text-slate-500">{kindStyles[node.kind].label}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${
nodeStatus === 'error'
? 'bg-red-100 text-red-700'
: nodeStatus === 'warning'
? 'bg-amber-100 text-amber-700'
: 'bg-emerald-100 text-emerald-700'
}`}>
{nodeStatus}
</span>
</td>
<td className="px-4 py-3 text-slate-500">{node.inputs.map((port) => port.type).join(', ') || 'NONE'}</td>
<td className="px-4 py-3 text-slate-500">{node.outputs.map((port) => port.type).join(', ') || 'NONE'}</td>
<td className="px-4 py-3 text-slate-500">{node.subtitle}</td>
</tr>
);
})}
</tbody>
</table>
)}
{bottomTab === 'variables' && (
<table className="min-w-[1180px] w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Variable</th>
<th className="px-4 py-3">Type</th>
<th className="px-4 py-3">Scope</th>
<th className="px-4 py-3">Source</th>
<th className="px-4 py-3">Value</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3">Associated Data</th>
<th className="px-4 py-3">Version Key</th>
<th className="px-4 py-3">Quality</th>
<th className="px-4 py-3">Used By Behavior</th>
<th className="px-4 py-3">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{(selectedParameters.length ? selectedParameters : workflow.parameters).map((parameter) => {
const associatedData = getAssociatedDataForVariable(parameter.name);
return (
<tr
key={parameter.id}
onClick={() => setSelectedVariableName(parameter.name)}
className={`cursor-pointer ${
selectedVariableName === parameter.name
? 'bg-blue-50'
: parameter.status === 'error' || associatedData?.qualityStatus === 'error'
? 'bg-red-50/40'
: 'hover:bg-slate-50'
}`}
>
<td className="px-4 py-3 font-mono font-black text-blue-700">{parameter.name}</td>
<td className="px-4 py-3 text-slate-600">{parameter.type}</td>
<td className="px-4 py-3 text-slate-500">{parameter.scope}</td>
<td className="px-4 py-3 text-slate-500">{parameter.source}</td>
<td className={`px-4 py-3 font-black ${parameter.status === 'error' ? 'text-red-600' : 'text-slate-900'}`}>{parameter.value}</td>
<td className="px-4 py-3">
<span
className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${
parameter.status === 'error'
? 'bg-red-100 text-red-700'
: parameter.status === 'warning'
? 'bg-amber-100 text-amber-700'
: 'bg-emerald-100 text-emerald-700'
}`}
>
{parameter.status}
</span>
</td>
<td className="px-4 py-3 font-mono text-slate-600">{associatedData?.id ?? '-'}</td>
<td className="px-4 py-3 text-slate-500">{associatedData?.versionKey ?? '-'}</td>
<td className="px-4 py-3">
{associatedData ? (
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${associatedData.qualityStatus === 'error' ? 'bg-red-100 text-red-700' : 'bg-emerald-100 text-emerald-700'}`}>
{associatedData.qualityStatus}
</span>
) : (
<span className="text-slate-400">-</span>
)}
</td>
<td className="px-4 py-3 text-slate-500">{associatedData?.usedByBehavior ?? '-'}</td>
<td className="px-4 py-3 text-slate-500">{parameter.description}</td>
</tr>
);
})}
</tbody>
</table>
)}
{bottomTab === 'sensing' && (
<div className="grid grid-cols-2 gap-3 p-4 min-w-[720px]">
{(sensingSections.length ? sensingSections : selectedBehaviorSections).map((section) => (
<section key={section.id} className="bg-white border border-slate-200 rounded-lg overflow-hidden">
<header className="px-3 py-2 bg-slate-50 border-b border-slate-100">
<h3 className="text-[11px] font-black uppercase tracking-widest text-slate-700">{section.title}</h3>
<p className="text-[10px] text-slate-400 mt-1 leading-relaxed">{section.description}</p>
</header>
<div className="p-3 space-y-2">
{section.items.map((item) => (
<div key={`${section.id}-${item.label}`} className="p-2 rounded-md border border-slate-100 bg-slate-50">
<div className="text-[11px] font-black text-blue-700 font-mono">{item.label}</div>
<div className="text-[10px] text-slate-500 mt-1">{item.source}</div>
<div className="text-[10px] font-black text-slate-900 mt-1">{item.value}</div>
</div>
))}
</div>
</section>
))}
</div>
)}
{bottomTab === 'internalModels' && (
<div className="grid grid-cols-3 gap-3 p-4 min-w-[860px]">
{internalModelSections.map((section) => (
<section key={section.id} className="bg-white border border-slate-200 rounded-lg overflow-hidden">
<header className="px-3 py-2 bg-slate-50 border-b border-slate-100">
<h3 className="text-[11px] font-black uppercase tracking-widest text-slate-700">{section.title}</h3>
<p className="text-[10px] text-slate-400 mt-1 leading-relaxed">{section.description}</p>
</header>
<div className="p-3 space-y-2">
{section.items.map((item) => (
<div key={`${section.id}-${item.label}`} className="p-2 rounded-md border border-slate-100 bg-slate-50">
<div className="text-[11px] font-black text-blue-700 font-mono">{item.label}</div>
<div className="text-[10px] text-slate-500 mt-1">{item.source}</div>
<div className="text-[10px] font-black text-slate-900 mt-1">{item.value}</div>
</div>
))}
</div>
</section>
))}
</div>
)}
{bottomTab === 'schedule' && (
<table className="w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Step</th>
<th className="px-4 py-3">Phase</th>
<th className="px-4 py-3">Actor</th>
<th className="px-4 py-3">Behavior</th>
<th className="px-4 py-3">Trigger</th>
<th className="px-4 py-3">Mode</th>
<th className="px-4 py-3">Writes To</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{scheduleRows.map((row) => (
<tr key={`${row.order}-${row.targetBehavior}`} className="hover:bg-slate-50">
<td className="px-4 py-3 font-black text-slate-900">{row.order}</td>
<td className="px-4 py-3 text-slate-500">{row.phase}</td>
<td className="px-4 py-3 text-slate-700 font-black">{row.actorAgent}</td>
<td className="px-4 py-3 font-mono text-blue-700 font-black">{row.targetBehavior}</td>
<td className="px-4 py-3 text-slate-500">{row.trigger}</td>
<td className="px-4 py-3 text-slate-500">{row.executionMode}</td>
<td className="px-4 py-3 text-slate-500">{row.writesTo}</td>
</tr>
))}
</tbody>
</table>
)}
{bottomTab === 'data' && (
<table className="min-w-[1280px] w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Data ID</th>
<th className="px-4 py-3">Data</th>
<th className="px-4 py-3">Type</th>
<th className="px-4 py-3">Source Type</th>
<th className="px-4 py-3">Source Name</th>
<th className="px-4 py-3">Source Table</th>
<th className="px-4 py-3">Source Column</th>
<th className="px-4 py-3">Owner Agent</th>
<th className="px-4 py-3">Related Variable</th>
<th className="px-4 py-3">Used By Behavior</th>
<th className="px-4 py-3">Update Frequency</th>
<th className="px-4 py-3">Version Key</th>
<th className="px-4 py-3">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{associatedDataRows.map((row) => (
<tr key={row.id} className={row.qualityStatus === 'error' ? 'bg-red-50/40' : 'hover:bg-slate-50'}>
<td className="px-4 py-3 font-mono font-black text-slate-700">{row.id}</td>
<td className="px-4 py-3 font-mono font-black text-blue-700">{row.name}</td>
<td className="px-4 py-3 text-slate-500">{row.type}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceType}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceName}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceTable}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceColumn}</td>
<td className="px-4 py-3 text-slate-700 font-black">{row.ownerAgent}</td>
<td className="px-4 py-3 text-slate-500">{row.relatedVariable}</td>
<td className="px-4 py-3 text-slate-500">{row.usedByBehavior}</td>
<td className="px-4 py-3 text-slate-500">{row.updateFrequency}</td>
<td className="px-4 py-3 text-slate-500">{row.versionKey}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${row.qualityStatus === 'error' ? 'bg-red-100 text-red-700' : 'bg-emerald-100 text-emerald-700'}`}>
{row.qualityStatus}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
{bottomTab === 'io' && (
<table className="min-w-[1440px] w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">I/O ID</th>
<th className="px-4 py-3">Component Type</th>
<th className="px-4 py-3">Component ID</th>
<th className="px-4 py-3">Component</th>
<th className="px-4 py-3">Direction</th>
<th className="px-4 py-3">Name</th>
<th className="px-4 py-3">Type</th>
<th className="px-4 py-3">Schema</th>
<th className="px-4 py-3">Source Component</th>
<th className="px-4 py-3">Source Table</th>
<th className="px-4 py-3">Target Component</th>
<th className="px-4 py-3">Target Table</th>
<th className="px-4 py-3">Required</th>
<th className="px-4 py-3">Default</th>
<th className="px-4 py-3">Snapshot Policy</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{ioRows.map((row) => (
<tr key={row.id} className="hover:bg-slate-50">
<td className="px-4 py-3 font-mono font-black text-slate-700">{row.id}</td>
<td className="px-4 py-3 text-slate-500">{row.componentType}</td>
<td className="px-4 py-3 font-mono text-slate-500">{row.componentId}</td>
<td className="px-4 py-3 text-slate-700 font-black">{row.componentName}</td>
<td className="px-4 py-3 text-slate-500">{row.direction}</td>
<td className="px-4 py-3 font-mono font-black text-blue-700">{row.ioName}</td>
<td className="px-4 py-3 text-slate-500">{row.ioDataType}</td>
<td className="px-4 py-3 font-mono text-slate-500">{row.schema}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceComponent}</td>
<td className="px-4 py-3 text-slate-500">{row.sourceTable}</td>
<td className="px-4 py-3 text-slate-500">{row.targetComponent}</td>
<td className="px-4 py-3 text-slate-500">{row.targetTable}</td>
<td className="px-4 py-3 text-slate-900 font-black">{row.required}</td>
<td className="px-4 py-3 text-slate-500">{row.defaultValue}</td>
<td className="px-4 py-3 text-slate-500">{row.snapshotPolicy}</td>
</tr>
))}
</tbody>
</table>
)}
{bottomTab === 'validation' && (
<table className="min-w-[1440px] w-full text-left text-xs">
<thead className="sticky top-0 bg-white border-b border-slate-200 text-slate-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3">Validation ID</th>
<th className="px-4 py-3">Target Type</th>
<th className="px-4 py-3">Target ID</th>
<th className="px-4 py-3">Target</th>
<th className="px-4 py-3">Validation Type</th>
<th className="px-4 py-3">Method</th>
<th className="px-4 py-3">Metric</th>
<th className="px-4 py-3">Baseline Data</th>
<th className="px-4 py-3">Threshold</th>
<th className="px-4 py-3">Frequency</th>
<th className="px-4 py-3">Result Field</th>
<th className="px-4 py-3">Severity</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3">Notes</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{validationRows.map((row) => (
<tr key={row.id} className={row.status === 'error' ? 'bg-red-50/40' : 'hover:bg-slate-50'}>
<td className="px-4 py-3 font-mono font-black text-slate-700">{row.id}</td>
<td className="px-4 py-3 text-slate-500">{row.targetType}</td>
<td className="px-4 py-3 font-mono text-slate-500">{row.targetId}</td>
<td className="px-4 py-3 font-mono font-black text-blue-700">{row.targetName}</td>
<td className="px-4 py-3 text-slate-500">{row.validationType}</td>
<td className="px-4 py-3 text-slate-500">{row.method}</td>
<td className="px-4 py-3 text-slate-500">{row.metric}</td>
<td className="px-4 py-3 text-slate-500">{row.baselineData}</td>
<td className="px-4 py-3 text-slate-900 font-black">{row.threshold}</td>
<td className="px-4 py-3 text-slate-500">{row.frequency}</td>
<td className="px-4 py-3 text-slate-500">{row.resultField}</td>
<td className="px-4 py-3 text-slate-500">{row.severity}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-[10px] font-black uppercase ${
row.status === 'error'
? 'bg-red-100 text-red-700'
: row.status === 'warning'
? 'bg-amber-100 text-amber-700'
: 'bg-emerald-100 text-emerald-700'
}`}>
{row.status}
</span>
</td>
<td className="px-4 py-3 text-slate-500">{row.notes}</td>
</tr>
))}
</tbody>
</table>
)}
</div>}
</section>
</main>
</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">
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" /> Workflow Stable
</span>
<div className="flex items-center gap-3">
<span>OmniEditor Pipeline v3.0</span>
<MessageSquare className="w-3 h-3" />
</div>
</footer>
</div>
);
};
export default OmniEditor;