diff --git a/package-lock.json b/package-lock.json index 1302562..852485d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1904,6 +1905,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2371,6 +2373,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -3774,6 +3777,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3800,6 +3804,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3897,6 +3902,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3906,6 +3912,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3925,6 +3932,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -4037,7 +4045,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4398,6 +4407,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -4538,6 +4548,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/src/App.tsx b/src/App.tsx index 851146e..d12d367 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,6 @@ import Landing from './pages/Landing'; import Login from './pages/Login'; import SciHub from './pages/SciHub'; import OmniEditor from './pages/OmniEditor'; -import SciDashboard from './pages/SciDashboard'; import SimuAnalysis from './pages/SimuAnalysis'; import { LanguageProvider } from './context/LanguageContext'; import LanguageSwitcher from './components/LanguageSwitcher'; @@ -22,7 +21,6 @@ export default function App() { } /> } /> } /> - } /> } /> } /> diff --git a/src/components/ModuleSwitcher.tsx b/src/components/ModuleSwitcher.tsx index 2467d94..97d9452 100644 --- a/src/components/ModuleSwitcher.tsx +++ b/src/components/ModuleSwitcher.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { LayoutGrid, Home, Edit3, Table, Activity, ChevronDown, Globe } from 'lucide-react'; +import { LayoutGrid, Home, Edit3, Activity, ChevronDown, Globe } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; const ModuleSwitcher: React.FC = () => { @@ -10,7 +10,6 @@ const ModuleSwitcher: React.FC = () => { const modules = [ { name: 'Landing', path: '/', icon: Globe, desc: 'Home' }, { name: 'OmniEditor', path: '/editor', icon: Edit3, desc: 'Visual Modeling' }, - { name: 'SciDashboard', path: '/dashboard', icon: Table, desc: 'Parameters' }, { name: 'SimuAnalysis', path: '/analysis', icon: Activity, desc: 'Simulation' }, { name: 'SciHub', path: '/hub', icon: Home, desc: 'Knowledge Base' }, ]; diff --git a/src/pages/OmniEditor.tsx b/src/pages/OmniEditor.tsx index 54a94c1..bf7452e 100644 --- a/src/pages/OmniEditor.tsx +++ b/src/pages/OmniEditor.tsx @@ -1,235 +1,332 @@ -import React, { useState } from 'react'; -import { - Play, - Settings, - Plus, - Minus, - Maximize, - Send, - Cpu, - Hexagon, - Book, - ChevronRight, - ChevronLeft, - Zap, - Terminal, - Users, - X, +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + AlertCircle, + Bot, + CheckCircle, + ChevronDown, + Database, + Eye, + Grid3X3, + Layers3, MessageSquare, PanelLeftClose, PanelLeftOpen, - Upload, - Download, - CheckCircle, - LayoutGrid, - Share2, - Move, - Map, - Mic, + Play, + Plus, + Send, + Settings, + Sparkles, + Table2, + Terminal, Trash2, - Menu, - ChevronDown, - Grid3X3, - Box, - Compass, - AlertCircle, - Link as LinkIcon, - ArrowRight + Workflow, + X, + Zap, } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; -import { Link, useLocation } from 'react-router-dom'; -import { useLanguage } from '../context/LanguageContext'; - +import { useLocation } from 'react-router-dom'; import ModuleSwitcher from '../components/ModuleSwitcher'; +import { useLanguage } from '../context/LanguageContext'; +import { + createDefaultWorkflow, + getBehaviorSectionsForNode, + getNodeConnectionAnchor, + getParametersForNode, + InitialSpaceType, + WorkflowEdge, + WorkflowModel, + WorkflowNode, + WorkflowNodeKind, + WorkflowPortType, +} from '../workflowModel'; -interface Node { - id: string; - type: 'environment' | 'agent' | 'parameter' | 'space'; - x: number; - y: number; - data: any; +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; + selected: boolean; + onSelect: (id: string) => void; + onDragStart: (id: string, event: React.MouseEvent) => void; + onDelete: (id: string, event: React.MouseEvent) => void; } -interface Edge { - id: string; - source: string; - target: string; - direction: 'none' | 'forward' | 'backward' | 'both'; - isDashed: boolean; -} +const WorkflowCard: React.FC = ({ + node, + selected, + onSelect, + onDragStart, + onDelete, +}) => { + const style = kindStyles[node.kind]; + const Icon = style.icon; + + return ( + + + +
+
+

{node.title}

+

{node.subtitle}

+
+
+ + ); +}; const OmniEditor: React.FC = () => { const { t, language } = useLanguage(); const location = useLocation(); - const [isAssistantCollapsed, setIsAssistantCollapsed] = useState(false); - const [selectedNode, setSelectedNode] = useState(null); - const [selectedEdge, setSelectedEdge] = useState(null); - const [showInitModal, setShowInitModal] = useState(!!location.state?.fromLanding); - const [selectedSpace, setSelectedSpace] = useState('grid'); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - - // Connection state + 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 [linkingFrom, setLinkingFrom] = useState(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(() => { - 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([]); - - // 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 [dragState, setDragState] = useState<{ id: string; startX: number; startY: number; nodeX: number; nodeY: number } | null>(null); + const [bottomTab, setBottomTab] = useState<'attributes' | 'behavior' | 'json' | 'history'>('attributes'); + 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've analyzed the Evolutionary Stable Strategy. Based on your inputs, I recommend adding a \"Cost of Sensing\" parameter to the Agent logic." - : "我分析了演化稳定策略。根据您的输入,我建议在代理逻辑中添加“感知成本”参数。" - } + { + 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); - 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: {} }]); + 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 selectedEdge = workflow.edges.find((edge) => edge.id === selectedEdgeId) ?? null; + const selectedParameters = getParametersForNode(workflow, selectedNode.id); + const selectedBehaviorSections = getBehaviorSectionsForNode( + workflow, + selectedNode.kind === 'agent' ? 'on-sensing' : selectedNode.id, + ); + + 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 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 userMsg = input.trim(); - setMessages([...messages, { role: 'user', content: userMsg }]); + const content = input.trim(); + setMessages((prev) => [...prev, { role: 'user', content }]); 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); + setMessages((prev) => [ + ...prev, + { + role: 'assistant', + content: + language === 'en' + ? 'I would map that request onto workflow nodes first, then update the parameter table and generated code preview.' + : '我会先把这个需求映射到工作流节点,再更新参数表和生成代码预览。', + }, + ]); + }, 500); }; return (
{showToast && ( - -
- "{showToast}" {t('coming_soon')} + + "{showToast}" {t('coming_soon')} )} @@ -237,62 +334,62 @@ const OmniEditor: React.FC = () => { {showInitModal && (
- setShowInitModal(false)} /> - -
-

{t('env_modal_title')}

-

{t('env_modal_subtitle')}

+
+

{t('env_modal_title')}

+

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

-
+
{[ - { 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' } + { 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) => ( ))}
-
- - + @@ -303,618 +400,489 @@ const OmniEditor: React.FC = () => {
- {/* Top Row: Context & System */}
-
-
- +
+
+ + MODEL CANVAS
-
- Schema: - Evolutionary Stable Strategy +
-
- -
- - {t('visa_connected')} - +
+ + {t('visa_connected')} +
- {/* Bottom Row: ABM Workbench Menus */} -
+
{[ - { 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) => ( - ))} -
-
- - -
+
+ +
-
- {/* AI Assistant */} - -
- {!isAssistantCollapsed && {t('ai_assistant')}} - -
- +
+
{!isAssistantCollapsed && ( - <> -
- {messages.map((msg, i) => ( -
- {msg.role === 'user' ? t('user') : t('ai_researcher')} -
- {msg.content} -
-
- ))} + + {t('ai_assistant')} + + )} + +
+ + {!isAssistantCollapsed && ( + <> +
+
+
Design Assistant
+

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

-
-
-