feat: Add Canvas dark theme styling and Setup page integration
Canvas Builder Visual Updates: - Update all Canvas components to use Atomaster dark theme - BaseNode: dark background (bg-dark-800), white text, primary selection glow - NodePalette: dark sidebar with hover states - NodeConfigPanel: dark inputs, labels, and panel background - ValidationPanel: semi-transparent error/warning panels with backdrop blur - ChatPanel: dark message area with themed welcome state - ExecuteDialog: dark modal with primary button styling - ConfigImporter: dark tabs, inputs, and file upload zone - TemplateSelector: dark cards with category pills and hover effects Setup Page Integration: - Add Configuration/Canvas Builder tab switcher - Canvas tab renders AtomizerCanvas full-height - Tabs styled to match Atomaster theme Build: Passes TypeScript and Vite build Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,7 +118,7 @@ function CanvasFlow() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full bg-dark-900">
|
||||||
{/* Left: Node Palette */}
|
{/* Left: Node Palette */}
|
||||||
<NodePalette />
|
<NodePalette />
|
||||||
|
|
||||||
@@ -137,10 +137,15 @@ function CanvasFlow() {
|
|||||||
onPaneClick={onPaneClick}
|
onPaneClick={onPaneClick}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
fitView
|
||||||
|
className="bg-dark-900"
|
||||||
>
|
>
|
||||||
<Background />
|
<Background color="#374151" gap={20} />
|
||||||
<Controls />
|
<Controls className="!bg-dark-800 !border-dark-600 !rounded-lg [&>button]:!bg-dark-700 [&>button]:!border-dark-600 [&>button]:!fill-dark-300 [&>button:hover]:!bg-dark-600" />
|
||||||
<MiniMap />
|
<MiniMap
|
||||||
|
className="!bg-dark-800 !border-dark-600 !rounded-lg"
|
||||||
|
nodeColor="#4B5563"
|
||||||
|
maskColor="rgba(0, 0, 0, 0.5)"
|
||||||
|
/>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
@@ -149,8 +154,8 @@ function CanvasFlow() {
|
|||||||
onClick={() => setShowChat(!showChat)}
|
onClick={() => setShowChat(!showChat)}
|
||||||
className={`px-3 py-2 rounded-lg transition-colors ${
|
className={`px-3 py-2 rounded-lg transition-colors ${
|
||||||
showChat
|
showChat
|
||||||
? 'bg-blue-100 text-blue-700'
|
? 'bg-primary-600/20 text-primary-400 border border-primary-500/50'
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: 'bg-dark-800 text-dark-300 hover:bg-dark-700 border border-dark-600'
|
||||||
}`}
|
}`}
|
||||||
title="Toggle Chat"
|
title="Toggle Chat"
|
||||||
>
|
>
|
||||||
@@ -158,17 +163,17 @@ function CanvasFlow() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleValidate}
|
onClick={handleValidate}
|
||||||
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
className="px-4 py-2 bg-dark-700 text-white rounded-lg hover:bg-dark-600 border border-dark-600 transition-colors"
|
||||||
>
|
>
|
||||||
Validate
|
Validate
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleAnalyze}
|
onClick={handleAnalyze}
|
||||||
disabled={!validation.valid}
|
disabled={!validation.valid}
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
className={`px-4 py-2 rounded-lg transition-colors border ${
|
||||||
validation.valid
|
validation.valid
|
||||||
? 'bg-purple-600 text-white hover:bg-purple-700'
|
? 'bg-purple-600 text-white hover:bg-purple-500 border-purple-500'
|
||||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
: 'bg-dark-800 text-dark-500 cursor-not-allowed border-dark-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Analyze
|
Analyze
|
||||||
@@ -176,10 +181,10 @@ function CanvasFlow() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleExecuteClick}
|
onClick={handleExecuteClick}
|
||||||
disabled={!validation.valid}
|
disabled={!validation.valid}
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
className={`px-4 py-2 rounded-lg transition-colors border ${
|
||||||
validation.valid
|
validation.valid
|
||||||
? 'bg-blue-600 text-white hover:bg-blue-700'
|
? 'bg-primary-600 text-white hover:bg-primary-500 border-primary-500'
|
||||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
: 'bg-dark-800 text-dark-500 cursor-not-allowed border-dark-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Execute with Claude
|
Execute with Claude
|
||||||
@@ -194,12 +199,12 @@ function CanvasFlow() {
|
|||||||
|
|
||||||
{/* Right: Config Panel or Chat */}
|
{/* Right: Config Panel or Chat */}
|
||||||
{showChat ? (
|
{showChat ? (
|
||||||
<div className="w-96 border-l border-gray-200 flex flex-col bg-white">
|
<div className="w-96 border-l border-dark-700 flex flex-col bg-dark-850">
|
||||||
<div className="p-3 border-b border-gray-200 flex justify-between items-center">
|
<div className="p-3 border-b border-dark-700 flex justify-between items-center">
|
||||||
<h3 className="font-semibold text-gray-800">Claude Assistant</h3>
|
<h3 className="font-semibold text-white">Claude Assistant</h3>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowChat(false)}
|
onClick={() => setShowChat(false)}
|
||||||
className="text-gray-500 hover:text-gray-700"
|
className="text-dark-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function BaseNodeComponent({
|
|||||||
selected,
|
selected,
|
||||||
icon,
|
icon,
|
||||||
color,
|
color,
|
||||||
colorBg = 'bg-gray-50',
|
colorBg = 'bg-dark-700',
|
||||||
children,
|
children,
|
||||||
inputs = 1,
|
inputs = 1,
|
||||||
outputs = 1,
|
outputs = 1,
|
||||||
@@ -27,13 +27,13 @@ function BaseNodeComponent({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
group relative px-4 py-3 rounded-xl border-2 min-w-[200px] bg-white
|
group relative px-4 py-3 rounded-xl border-2 min-w-[200px] bg-dark-800
|
||||||
transition-all duration-300 ease-out
|
transition-all duration-300 ease-out
|
||||||
${selected
|
${selected
|
||||||
? 'border-blue-500 shadow-xl shadow-blue-100 scale-[1.02]'
|
? 'border-primary-500 shadow-xl shadow-primary-500/20 scale-[1.02]'
|
||||||
: 'border-gray-200 shadow-md hover:shadow-lg hover:border-gray-300'}
|
: 'border-dark-600 shadow-md hover:shadow-lg hover:border-dark-500'}
|
||||||
${!isConfigured ? 'border-dashed opacity-80' : ''}
|
${!isConfigured ? 'border-dashed opacity-80' : ''}
|
||||||
${hasError ? 'border-red-400 shadow-red-100' : ''}
|
${hasError ? 'border-red-500/70 shadow-red-500/20' : ''}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
animation: 'nodeAppear 0.3s ease-out',
|
animation: 'nodeAppear 0.3s ease-out',
|
||||||
@@ -42,9 +42,9 @@ function BaseNodeComponent({
|
|||||||
{/* Glow effect on selection */}
|
{/* Glow effect on selection */}
|
||||||
{selected && (
|
{selected && (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 -z-10 rounded-xl opacity-20"
|
className="absolute inset-0 -z-10 rounded-xl opacity-30"
|
||||||
style={{
|
style={{
|
||||||
background: 'radial-gradient(ellipse at center, #3b82f6 0%, transparent 70%)',
|
background: 'radial-gradient(ellipse at center, #6366f1 0%, transparent 70%)',
|
||||||
transform: 'scale(1.3)',
|
transform: 'scale(1.3)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -56,9 +56,9 @@ function BaseNodeComponent({
|
|||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className={`
|
className={`
|
||||||
!w-4 !h-4 !border-2 !border-white !shadow-md
|
!w-4 !h-4 !border-2 !border-dark-800 !shadow-md
|
||||||
transition-all duration-200
|
transition-all duration-200
|
||||||
${selected ? '!bg-blue-500' : '!bg-gray-400 group-hover:!bg-gray-500'}
|
${selected ? '!bg-primary-500' : '!bg-dark-400 group-hover:!bg-dark-300'}
|
||||||
`}
|
`}
|
||||||
style={{ left: -8 }}
|
style={{ left: -8 }}
|
||||||
/>
|
/>
|
||||||
@@ -74,16 +74,16 @@ function BaseNodeComponent({
|
|||||||
<span className={`text-lg ${color}`}>{icon}</span>
|
<span className={`text-lg ${color}`}>{icon}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<span className="font-semibold text-gray-800 text-sm">{data.label}</span>
|
<span className="font-semibold text-white text-sm">{data.label}</span>
|
||||||
{!isConfigured && (
|
{!isConfigured && (
|
||||||
<span className="ml-2 text-xs text-orange-500 animate-pulse">Needs config</span>
|
<span className="ml-2 text-xs text-amber-400 animate-pulse">Needs config</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{children && (
|
{children && (
|
||||||
<div className="text-sm text-gray-600 mt-2 pl-11">
|
<div className="text-sm text-dark-300 mt-2 pl-11">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -95,14 +95,14 @@ function BaseNodeComponent({
|
|||||||
) : isConfigured ? (
|
) : isConfigured ? (
|
||||||
<span className="w-2 h-2 bg-green-500 rounded-full block" />
|
<span className="w-2 h-2 bg-green-500 rounded-full block" />
|
||||||
) : (
|
) : (
|
||||||
<span className="w-2 h-2 bg-orange-400 rounded-full block" />
|
<span className="w-2 h-2 bg-amber-400 rounded-full block" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Errors */}
|
{/* Errors */}
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<div className="mt-3 pt-2 border-t border-red-100">
|
<div className="mt-3 pt-2 border-t border-red-500/30">
|
||||||
<p className="text-xs text-red-600 flex items-center gap-1">
|
<p className="text-xs text-red-400 flex items-center gap-1">
|
||||||
<span>⚠</span>
|
<span>⚠</span>
|
||||||
{data.errors?.[0]}
|
{data.errors?.[0]}
|
||||||
</p>
|
</p>
|
||||||
@@ -115,9 +115,9 @@ function BaseNodeComponent({
|
|||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className={`
|
className={`
|
||||||
!w-4 !h-4 !border-2 !border-white !shadow-md
|
!w-4 !h-4 !border-2 !border-dark-800 !shadow-md
|
||||||
transition-all duration-200
|
transition-all duration-200
|
||||||
${selected ? '!bg-blue-500' : '!bg-gray-400 group-hover:!bg-gray-500'}
|
${selected ? '!bg-primary-500' : '!bg-dark-400 group-hover:!bg-dark-300'}
|
||||||
`}
|
`}
|
||||||
style={{ right: -8 }}
|
style={{ right: -8 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export function NodePalette() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-64 bg-gray-50 border-r border-gray-200 p-4 overflow-y-auto">
|
<div className="w-64 bg-dark-850 border-r border-dark-700 p-4 overflow-y-auto">
|
||||||
<h3 className="text-sm font-semibold text-gray-500 uppercase mb-4">
|
<h3 className="text-sm font-semibold text-dark-400 uppercase mb-4 tracking-wide">
|
||||||
Components
|
Components
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -36,13 +36,14 @@ export function NodePalette() {
|
|||||||
key={item.type}
|
key={item.type}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => onDragStart(e, item.type)}
|
onDragStart={(e) => onDragStart(e, item.type)}
|
||||||
className="flex items-center gap-3 p-3 bg-white rounded-lg border border-gray-200
|
className="flex items-center gap-3 p-3 bg-dark-800 rounded-lg border border-dark-600
|
||||||
cursor-grab hover:border-blue-300 hover:shadow-sm transition-all"
|
cursor-grab hover:border-primary-500/50 hover:bg-dark-750 transition-all
|
||||||
|
active:cursor-grabbing"
|
||||||
>
|
>
|
||||||
<span className="text-xl">{item.icon}</span>
|
<span className="text-xl">{item.icon}</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-800">{item.label}</div>
|
<div className="font-medium text-white">{item.label}</div>
|
||||||
<div className="text-xs text-gray-500">{item.description}</div>
|
<div className="text-xs text-dark-400">{item.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ export function ChatPanel({ messages, isThinking }: ChatPanelProps) {
|
|||||||
}, [messages, isThinking]);
|
}, [messages, isThinking]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-dark-900">
|
||||||
{/* Welcome message if no messages */}
|
{/* Welcome message if no messages */}
|
||||||
{messages.length === 0 && !isThinking && (
|
{messages.length === 0 && !isThinking && (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center mx-auto mb-4">
|
<div className="w-12 h-12 rounded-xl bg-primary-500/20 flex items-center justify-center mx-auto mb-4">
|
||||||
<span className="text-2xl">🧠</span>
|
<span className="text-2xl">🧠</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-500 text-sm max-w-xs mx-auto">
|
<p className="text-dark-400 text-sm max-w-xs mx-auto">
|
||||||
Use <strong>Validate</strong>, <strong>Analyze</strong>, or <strong>Execute</strong> to interact with Claude about your optimization workflow.
|
Use <strong className="text-white">Validate</strong>, <strong className="text-white">Analyze</strong>, or <strong className="text-white">Execute</strong> to interact with Claude about your optimization workflow.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -110,21 +110,21 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg">
|
<div className="bg-dark-850 rounded-xl shadow-2xl w-full max-w-lg border border-dark-700">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
<div className="px-6 py-4 border-b border-dark-700 flex justify-between items-center">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Import Configuration</h2>
|
<h2 className="text-xl font-semibold text-white">Import Configuration</h2>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="text-gray-500 hover:text-gray-700 text-xl"
|
className="text-dark-400 hover:text-white text-xl transition-colors"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex border-b border-gray-200">
|
<div className="flex border-b border-dark-700">
|
||||||
{[
|
{[
|
||||||
{ id: 'file', label: 'Upload File' },
|
{ id: 'file', label: 'Upload File' },
|
||||||
{ id: 'paste', label: 'Paste JSON' },
|
{ id: 'paste', label: 'Paste JSON' },
|
||||||
@@ -135,8 +135,8 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
onClick={() => setTab(t.id as 'file' | 'paste' | 'study')}
|
onClick={() => setTab(t.id as 'file' | 'paste' | 'study')}
|
||||||
className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${
|
className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${
|
||||||
tab === t.id
|
tab === t.id
|
||||||
? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50'
|
? 'text-primary-400 border-b-2 border-primary-500 bg-dark-800'
|
||||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
: 'text-dark-400 hover:text-white hover:bg-dark-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
@@ -149,8 +149,8 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
{/* File Upload Tab */}
|
{/* File Upload Tab */}
|
||||||
{tab === 'file' && (
|
{tab === 'file' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-dark-300">
|
||||||
Upload an <code className="bg-gray-100 px-1 rounded">optimization_config.json</code> file
|
Upload an <code className="bg-dark-700 px-1 rounded text-primary-300">optimization_config.json</code> file
|
||||||
from an existing study.
|
from an existing study.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
@@ -163,10 +163,10 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
<button
|
<button
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full py-8 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors flex flex-col items-center gap-2"
|
className="w-full py-8 border-2 border-dashed border-dark-600 rounded-lg hover:border-primary-500/50 hover:bg-dark-800 transition-colors flex flex-col items-center gap-2"
|
||||||
>
|
>
|
||||||
<span className="text-3xl">📁</span>
|
<span className="text-3xl">📁</span>
|
||||||
<span className="text-gray-600">
|
<span className="text-dark-300">
|
||||||
{isLoading ? 'Loading...' : 'Click to select file'}
|
{isLoading ? 'Loading...' : 'Click to select file'}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -176,7 +176,7 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
{/* Paste JSON Tab */}
|
{/* Paste JSON Tab */}
|
||||||
{tab === 'paste' && (
|
{tab === 'paste' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-dark-300">
|
||||||
Paste your optimization configuration JSON below.
|
Paste your optimization configuration JSON below.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -187,12 +187,12 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
"objectives": [...],
|
"objectives": [...],
|
||||||
"method": "TPE"
|
"method": "TPE"
|
||||||
}`}
|
}`}
|
||||||
className="w-full h-48 px-3 py-2 border rounded-lg font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="w-full h-48 px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-500 rounded-lg font-mono text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500 focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handlePasteImport}
|
onClick={handlePasteImport}
|
||||||
disabled={!jsonText.trim()}
|
disabled={!jsonText.trim()}
|
||||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="w-full px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
Import JSON
|
Import JSON
|
||||||
</button>
|
</button>
|
||||||
@@ -202,7 +202,7 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
{/* Load Study Tab */}
|
{/* Load Study Tab */}
|
||||||
{tab === 'study' && (
|
{tab === 'study' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-dark-300">
|
||||||
Enter a study name to load its configuration.
|
Enter a study name to load its configuration.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
@@ -210,12 +210,12 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
value={studyPath}
|
value={studyPath}
|
||||||
onChange={(e) => setStudyPath(e.target.value)}
|
onChange={(e) => setStudyPath(e.target.value)}
|
||||||
placeholder="e.g., bracket_mass_v1 or M1_Mirror/m1_mirror_flatback"
|
placeholder="e.g., bracket_mass_v1 or M1_Mirror/m1_mirror_flatback"
|
||||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-400 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleStudyLoad}
|
onClick={handleStudyLoad}
|
||||||
disabled={!studyPath.trim() || isLoading}
|
disabled={!studyPath.trim() || isLoading}
|
||||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="w-full px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
{isLoading ? 'Loading...' : 'Load Study Config'}
|
{isLoading ? 'Loading...' : 'Load Study Config'}
|
||||||
</button>
|
</button>
|
||||||
@@ -224,15 +224,15 @@ export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProp
|
|||||||
|
|
||||||
{/* Error Display */}
|
{/* Error Display */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
<div className="mt-4 p-3 bg-red-900/30 border border-red-500/50 rounded-lg">
|
||||||
<p className="text-sm text-red-700">{error}</p>
|
<p className="text-sm text-red-400">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-lg">
|
<div className="px-6 py-4 border-t border-dark-700 bg-dark-800/50 rounded-b-xl">
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-dark-500">
|
||||||
Imported configurations will replace the current canvas content.
|
Imported configurations will replace the current canvas content.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ export function ExecuteDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
<div className="bg-dark-850 rounded-xl shadow-2xl w-full max-w-md p-6 border border-dark-700">
|
||||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">
|
<h2 className="text-xl font-semibold text-white mb-4">
|
||||||
Execute with Claude
|
Execute with Claude
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export function ExecuteDialog({
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor="study-name"
|
htmlFor="study-name"
|
||||||
className="block text-sm font-medium text-gray-600 mb-1"
|
className="block text-sm font-medium text-dark-300 mb-1"
|
||||||
>
|
>
|
||||||
Study Name
|
Study Name
|
||||||
</label>
|
</label>
|
||||||
@@ -71,14 +71,14 @@ export function ExecuteDialog({
|
|||||||
value={studyName}
|
value={studyName}
|
||||||
onChange={(e) => setStudyName(e.target.value.toLowerCase().replace(/\s+/g, '_'))}
|
onChange={(e) => setStudyName(e.target.value.toLowerCase().replace(/\s+/g, '_'))}
|
||||||
placeholder="my_optimization_study"
|
placeholder="my_optimization_study"
|
||||||
className="w-full px-3 py-2 border rounded-lg font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-400 rounded-lg font-mono focus:ring-2 focus:ring-primary-500 focus:border-primary-500 focus:outline-none transition-colors"
|
||||||
disabled={isExecuting}
|
disabled={isExecuting}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="mt-1 text-sm text-red-600">{error}</p>
|
<p className="mt-1 text-sm text-red-400">{error}</p>
|
||||||
)}
|
)}
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-dark-500">
|
||||||
Use snake_case (e.g., bracket_mass_v1, mirror_wfe_optimization)
|
Use snake_case (e.g., bracket_mass_v1, mirror_wfe_optimization)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,9 +90,9 @@ export function ExecuteDialog({
|
|||||||
checked={autoRun}
|
checked={autoRun}
|
||||||
onChange={(e) => setAutoRun(e.target.checked)}
|
onChange={(e) => setAutoRun(e.target.checked)}
|
||||||
disabled={isExecuting}
|
disabled={isExecuting}
|
||||||
className="w-4 h-4"
|
className="w-4 h-4 rounded bg-dark-800 border-dark-600 text-primary-500 focus:ring-primary-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700">
|
<span className="text-sm text-dark-300">
|
||||||
Start optimization immediately after creation
|
Start optimization immediately after creation
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -103,14 +103,14 @@ export function ExecuteDialog({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
disabled={isExecuting}
|
disabled={isExecuting}
|
||||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50"
|
className="px-4 py-2 text-dark-300 hover:text-white disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isExecuting}
|
disabled={isExecuting}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors"
|
||||||
>
|
>
|
||||||
{isExecuting ? (
|
{isExecuting ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ interface NodeConfigPanelProps {
|
|||||||
nodeId: string;
|
nodeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Common input class for dark theme
|
||||||
|
const inputClass = "w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-400 rounded-lg focus:border-primary-500 focus:outline-none transition-colors";
|
||||||
|
const selectClass = "w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white rounded-lg focus:border-primary-500 focus:outline-none transition-colors";
|
||||||
|
const labelClass = "block text-sm font-medium text-dark-300 mb-1";
|
||||||
|
|
||||||
export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
||||||
const { nodes, updateNodeData, deleteSelected } = useCanvasStore();
|
const { nodes, updateNodeData, deleteSelected } = useCanvasStore();
|
||||||
const node = nodes.find((n) => n.id === nodeId);
|
const node = nodes.find((n) => n.id === nodeId);
|
||||||
@@ -27,12 +32,12 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 bg-white border-l border-gray-200 p-4 overflow-y-auto">
|
<div className="w-80 bg-dark-850 border-l border-dark-700 p-4 overflow-y-auto">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="font-semibold text-gray-800">Configure {data.label}</h3>
|
<h3 className="font-semibold text-white">Configure {data.label}</h3>
|
||||||
<button
|
<button
|
||||||
onClick={deleteSelected}
|
onClick={deleteSelected}
|
||||||
className="text-red-500 hover:text-red-700 text-sm"
|
className="text-red-400 hover:text-red-300 text-sm transition-colors"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -41,14 +46,14 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Common: Label */}
|
{/* Common: Label */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Label
|
Label
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.label}
|
value={data.label}
|
||||||
onChange={(e) => handleChange('label', e.target.value)}
|
onChange={(e) => handleChange('label', e.target.value)}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,7 +61,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
{data.type === 'model' && (
|
{data.type === 'model' && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
File Path
|
File Path
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -64,17 +69,17 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
value={(data as ModelNodeData).filePath || ''}
|
value={(data as ModelNodeData).filePath || ''}
|
||||||
onChange={(e) => handleChange('filePath', e.target.value)}
|
onChange={(e) => handleChange('filePath', e.target.value)}
|
||||||
placeholder="path/to/model.prt"
|
placeholder="path/to/model.prt"
|
||||||
className="w-full px-3 py-2 border rounded-lg font-mono text-sm"
|
className={`${inputClass} font-mono text-sm`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
File Type
|
File Type
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={(data as ModelNodeData).fileType || ''}
|
value={(data as ModelNodeData).fileType || ''}
|
||||||
onChange={(e) => handleChange('fileType', e.target.value)}
|
onChange={(e) => handleChange('fileType', e.target.value)}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={selectClass}
|
||||||
>
|
>
|
||||||
<option value="">Select...</option>
|
<option value="">Select...</option>
|
||||||
<option value="prt">Part (.prt)</option>
|
<option value="prt">Part (.prt)</option>
|
||||||
@@ -87,13 +92,13 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
|
|
||||||
{data.type === 'solver' && (
|
{data.type === 'solver' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Solution Type
|
Solution Type
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={(data as SolverNodeData).solverType || ''}
|
value={(data as SolverNodeData).solverType || ''}
|
||||||
onChange={(e) => handleChange('solverType', e.target.value)}
|
onChange={(e) => handleChange('solverType', e.target.value)}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={selectClass}
|
||||||
>
|
>
|
||||||
<option value="">Select...</option>
|
<option value="">Select...</option>
|
||||||
<option value="SOL101">SOL 101 - Linear Static</option>
|
<option value="SOL101">SOL 101 - Linear Static</option>
|
||||||
@@ -109,7 +114,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
{data.type === 'designVar' && (
|
{data.type === 'designVar' && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Expression Name
|
Expression Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -117,35 +122,35 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
value={(data as DesignVarNodeData).expressionName || ''}
|
value={(data as DesignVarNodeData).expressionName || ''}
|
||||||
onChange={(e) => handleChange('expressionName', e.target.value)}
|
onChange={(e) => handleChange('expressionName', e.target.value)}
|
||||||
placeholder="thickness"
|
placeholder="thickness"
|
||||||
className="w-full px-3 py-2 border rounded-lg font-mono"
|
className={`${inputClass} font-mono`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Min
|
Min
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={(data as DesignVarNodeData).minValue ?? ''}
|
value={(data as DesignVarNodeData).minValue ?? ''}
|
||||||
onChange={(e) => handleChange('minValue', parseFloat(e.target.value))}
|
onChange={(e) => handleChange('minValue', parseFloat(e.target.value))}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Max
|
Max
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={(data as DesignVarNodeData).maxValue ?? ''}
|
value={(data as DesignVarNodeData).maxValue ?? ''}
|
||||||
onChange={(e) => handleChange('maxValue', parseFloat(e.target.value))}
|
onChange={(e) => handleChange('maxValue', parseFloat(e.target.value))}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Unit
|
Unit
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -153,7 +158,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
value={(data as DesignVarNodeData).unit || ''}
|
value={(data as DesignVarNodeData).unit || ''}
|
||||||
onChange={(e) => handleChange('unit', e.target.value)}
|
onChange={(e) => handleChange('unit', e.target.value)}
|
||||||
placeholder="mm"
|
placeholder="mm"
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -162,7 +167,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
{data.type === 'extractor' && (
|
{data.type === 'extractor' && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Extractor ID
|
Extractor ID
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@@ -182,7 +187,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
handleChange('extractorId', id);
|
handleChange('extractorId', id);
|
||||||
handleChange('extractorName', names[id] || id);
|
handleChange('extractorName', names[id] || id);
|
||||||
}}
|
}}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={selectClass}
|
||||||
>
|
>
|
||||||
<option value="">Select...</option>
|
<option value="">Select...</option>
|
||||||
<option value="E1">E1 - Displacement</option>
|
<option value="E1">E1 - Displacement</option>
|
||||||
@@ -201,13 +206,13 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
{data.type === 'algorithm' && (
|
{data.type === 'algorithm' && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Method
|
Method
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={(data as AlgorithmNodeData).method || ''}
|
value={(data as AlgorithmNodeData).method || ''}
|
||||||
onChange={(e) => handleChange('method', e.target.value)}
|
onChange={(e) => handleChange('method', e.target.value)}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={selectClass}
|
||||||
>
|
>
|
||||||
<option value="">Select...</option>
|
<option value="">Select...</option>
|
||||||
<option value="TPE">TPE (Tree Parzen Estimator)</option>
|
<option value="TPE">TPE (Tree Parzen Estimator)</option>
|
||||||
@@ -218,7 +223,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Max Trials
|
Max Trials
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -226,7 +231,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
value={(data as AlgorithmNodeData).maxTrials ?? ''}
|
value={(data as AlgorithmNodeData).maxTrials ?? ''}
|
||||||
onChange={(e) => handleChange('maxTrials', parseInt(e.target.value))}
|
onChange={(e) => handleChange('maxTrials', parseInt(e.target.value))}
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -235,7 +240,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
{data.type === 'objective' && (
|
{data.type === 'objective' && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Objective Name
|
Objective Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -243,24 +248,24 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
value={(data as ObjectiveNodeData).name || ''}
|
value={(data as ObjectiveNodeData).name || ''}
|
||||||
onChange={(e) => handleChange('name', e.target.value)}
|
onChange={(e) => handleChange('name', e.target.value)}
|
||||||
placeholder="mass"
|
placeholder="mass"
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Direction
|
Direction
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={(data as ObjectiveNodeData).direction || 'minimize'}
|
value={(data as ObjectiveNodeData).direction || 'minimize'}
|
||||||
onChange={(e) => handleChange('direction', e.target.value)}
|
onChange={(e) => handleChange('direction', e.target.value)}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={selectClass}
|
||||||
>
|
>
|
||||||
<option value="minimize">Minimize</option>
|
<option value="minimize">Minimize</option>
|
||||||
<option value="maximize">Maximize</option>
|
<option value="maximize">Maximize</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Weight
|
Weight
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -268,7 +273,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
step="0.1"
|
step="0.1"
|
||||||
value={(data as ObjectiveNodeData).weight ?? 1}
|
value={(data as ObjectiveNodeData).weight ?? 1}
|
||||||
onChange={(e) => handleChange('weight', parseFloat(e.target.value))}
|
onChange={(e) => handleChange('weight', parseFloat(e.target.value))}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -277,7 +282,7 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
{data.type === 'constraint' && (
|
{data.type === 'constraint' && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Constraint Name
|
Constraint Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -285,18 +290,18 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
value={(data as ConstraintNodeData).name || ''}
|
value={(data as ConstraintNodeData).name || ''}
|
||||||
onChange={(e) => handleChange('name', e.target.value)}
|
onChange={(e) => handleChange('name', e.target.value)}
|
||||||
placeholder="max_stress"
|
placeholder="max_stress"
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Operator
|
Operator
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={(data as ConstraintNodeData).operator || '<='}
|
value={(data as ConstraintNodeData).operator || '<='}
|
||||||
onChange={(e) => handleChange('operator', e.target.value)}
|
onChange={(e) => handleChange('operator', e.target.value)}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={selectClass}
|
||||||
>
|
>
|
||||||
<option value="<"><</option>
|
<option value="<"><</option>
|
||||||
<option value="<="><=</option>
|
<option value="<="><=</option>
|
||||||
@@ -306,14 +311,14 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Value
|
Value
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={(data as ConstraintNodeData).value ?? ''}
|
value={(data as ConstraintNodeData).value ?? ''}
|
||||||
onChange={(e) => handleChange('value', parseFloat(e.target.value))}
|
onChange={(e) => handleChange('value', parseFloat(e.target.value))}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,22 +333,22 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
id="surrogate-enabled"
|
id="surrogate-enabled"
|
||||||
checked={(data as SurrogateNodeData).enabled || false}
|
checked={(data as SurrogateNodeData).enabled || false}
|
||||||
onChange={(e) => handleChange('enabled', e.target.checked)}
|
onChange={(e) => handleChange('enabled', e.target.checked)}
|
||||||
className="w-4 h-4"
|
className="w-4 h-4 rounded bg-dark-800 border-dark-600 text-primary-500 focus:ring-primary-500"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="surrogate-enabled" className="text-sm font-medium text-gray-600">
|
<label htmlFor="surrogate-enabled" className="text-sm font-medium text-dark-300">
|
||||||
Enable Neural Surrogate
|
Enable Neural Surrogate
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{(data as SurrogateNodeData).enabled && (
|
{(data as SurrogateNodeData).enabled && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Model Type
|
Model Type
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={(data as SurrogateNodeData).modelType || ''}
|
value={(data as SurrogateNodeData).modelType || ''}
|
||||||
onChange={(e) => handleChange('modelType', e.target.value)}
|
onChange={(e) => handleChange('modelType', e.target.value)}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={selectClass}
|
||||||
>
|
>
|
||||||
<option value="">Select...</option>
|
<option value="">Select...</option>
|
||||||
<option value="MLP">MLP (Multi-Layer Perceptron)</option>
|
<option value="MLP">MLP (Multi-Layer Perceptron)</option>
|
||||||
@@ -352,14 +357,14 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
<label className={labelClass}>
|
||||||
Min Trials Before Activation
|
Min Trials Before Activation
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={(data as SurrogateNodeData).minTrials ?? 20}
|
value={(data as SurrogateNodeData).minTrials ?? 20}
|
||||||
onChange={(e) => handleChange('minTrials', parseInt(e.target.value))}
|
onChange={(e) => handleChange('minTrials', parseInt(e.target.value))}
|
||||||
className="w-full px-3 py-2 border rounded-lg"
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -42,39 +42,39 @@ export function TemplateSelector({ isOpen, onClose, onSelect }: TemplateSelector
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm">
|
||||||
<div
|
<div
|
||||||
className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col overflow-hidden"
|
className="bg-dark-850 rounded-xl shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col overflow-hidden border border-dark-700"
|
||||||
style={{ animation: 'fadeIn 0.2s ease-out' }}
|
style={{ animation: 'fadeIn 0.2s ease-out' }}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gradient-to-r from-blue-50 to-purple-50">
|
<div className="px-6 py-4 border-b border-dark-700 flex justify-between items-center bg-gradient-to-r from-primary-900/30 to-purple-900/30">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Choose a Template</h2>
|
<h2 className="text-xl font-semibold text-white">Choose a Template</h2>
|
||||||
<p className="text-sm text-gray-500 mt-0.5">Start with a pre-configured optimization workflow</p>
|
<p className="text-sm text-dark-400 mt-0.5">Start with a pre-configured optimization workflow</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="w-8 h-8 flex items-center justify-center text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-colors"
|
className="w-8 h-8 flex items-center justify-center text-dark-400 hover:text-white hover:bg-dark-700 rounded-full transition-colors"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category Filters */}
|
{/* Category Filters */}
|
||||||
<div className="px-6 py-3 border-b border-gray-100 flex gap-2 bg-gray-50">
|
<div className="px-6 py-3 border-b border-dark-700 flex gap-2 bg-dark-800">
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<button
|
<button
|
||||||
key={cat.id}
|
key={cat.id}
|
||||||
onClick={() => setSelectedCategory(cat.id as typeof selectedCategory)}
|
onClick={() => setSelectedCategory(cat.id as typeof selectedCategory)}
|
||||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-all ${
|
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-all ${
|
||||||
selectedCategory === cat.id
|
selectedCategory === cat.id
|
||||||
? 'bg-blue-600 text-white shadow-sm'
|
? 'bg-primary-600 text-white shadow-sm'
|
||||||
: 'bg-white text-gray-600 hover:bg-gray-100 border border-gray-200'
|
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 border border-dark-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{cat.label}
|
{cat.label}
|
||||||
<span className={`ml-1.5 ${selectedCategory === cat.id ? 'text-blue-200' : 'text-gray-400'}`}>
|
<span className={`ml-1.5 ${selectedCategory === cat.id ? 'text-primary-200' : 'text-dark-500'}`}>
|
||||||
({cat.count})
|
({cat.count})
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -82,7 +82,7 @@ export function TemplateSelector({ isOpen, onClose, onSelect }: TemplateSelector
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Template Grid */}
|
{/* Template Grid */}
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex-1 overflow-y-auto p-6 bg-dark-900">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{filteredTemplates.map((template) => (
|
{filteredTemplates.map((template) => (
|
||||||
<TemplateCard
|
<TemplateCard
|
||||||
@@ -98,19 +98,19 @@ export function TemplateSelector({ isOpen, onClose, onSelect }: TemplateSelector
|
|||||||
|
|
||||||
{filteredTemplates.length === 0 && (
|
{filteredTemplates.length === 0 && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-gray-500">No templates in this category</p>
|
<p className="text-dark-500">No templates in this category</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-6 py-3 border-t border-gray-200 bg-gray-50 flex justify-between items-center">
|
<div className="px-6 py-3 border-t border-dark-700 bg-dark-800 flex justify-between items-center">
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-dark-500">
|
||||||
Templates provide a starting point - customize parameters after loading
|
Templates provide a starting point - customize parameters after loading
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 text-sm"
|
className="px-4 py-2 text-dark-400 hover:text-white text-sm transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -147,17 +147,17 @@ function TemplateCard({ template, isHovered, onHover, onLeave, onSelect }: Templ
|
|||||||
};
|
};
|
||||||
|
|
||||||
const categoryColors = {
|
const categoryColors = {
|
||||||
structural: 'bg-orange-100 text-orange-700',
|
structural: 'bg-orange-500/20 text-orange-400',
|
||||||
optical: 'bg-purple-100 text-purple-700',
|
optical: 'bg-purple-500/20 text-purple-400',
|
||||||
general: 'bg-blue-100 text-blue-700',
|
general: 'bg-blue-500/20 text-blue-400',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`border rounded-xl p-4 cursor-pointer transition-all duration-200 ${
|
className={`border rounded-xl p-4 cursor-pointer transition-all duration-200 ${
|
||||||
isHovered
|
isHovered
|
||||||
? 'border-blue-400 shadow-lg transform -translate-y-0.5 bg-blue-50/30'
|
? 'border-primary-500/70 shadow-lg shadow-primary-500/10 transform -translate-y-0.5 bg-dark-750'
|
||||||
: 'border-gray-200 hover:border-gray-300 bg-white'
|
: 'border-dark-600 hover:border-dark-500 bg-dark-800'
|
||||||
}`}
|
}`}
|
||||||
onMouseEnter={onHover}
|
onMouseEnter={onHover}
|
||||||
onMouseLeave={onLeave}
|
onMouseLeave={onLeave}
|
||||||
@@ -168,24 +168,24 @@ function TemplateCard({ template, isHovered, onHover, onLeave, onSelect }: Templ
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-2xl">{template.icon}</span>
|
<span className="text-2xl">{template.icon}</span>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-800">{template.name}</h3>
|
<h3 className="font-semibold text-white">{template.name}</h3>
|
||||||
<span className={`text-xs px-2 py-0.5 rounded-full ${categoryColors[template.category]}`}>
|
<span className={`text-xs px-2 py-0.5 rounded-full ${categoryColors[template.category]}`}>
|
||||||
{template.category}
|
{template.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{stats.hasSurrogate && (
|
{stats.hasSurrogate && (
|
||||||
<span className="text-xs px-2 py-1 bg-green-100 text-green-700 rounded-full">
|
<span className="text-xs px-2 py-1 bg-green-500/20 text-green-400 rounded-full">
|
||||||
Turbo
|
Turbo
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2">{template.description}</p>
|
<p className="text-sm text-dark-300 mb-4 line-clamp-2">{template.description}</p>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="flex gap-4 text-xs text-gray-500">
|
<div className="flex gap-4 text-xs text-dark-400">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="w-2 h-2 rounded-full bg-blue-400"></span>
|
<span className="w-2 h-2 rounded-full bg-blue-400"></span>
|
||||||
<span>{stats.designVars} vars</span>
|
<span>{stats.designVars} vars</span>
|
||||||
@@ -199,7 +199,7 @@ function TemplateCard({ template, isHovered, onHover, onLeave, onSelect }: Templ
|
|||||||
<span>{stats.constraints} con</span>
|
<span>{stats.constraints} con</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="font-medium text-gray-600">{stats.method}</span>
|
<span className="font-medium text-dark-300">{stats.method}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ function TemplateCard({ template, isHovered, onHover, onLeave, onSelect }: Templ
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="w-full py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
|
className="w-full py-2 bg-primary-600 text-white text-sm rounded-lg hover:bg-primary-500 transition-colors"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelect();
|
onSelect();
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export function ValidationPanel({ validation }: ValidationPanelProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 max-w-md w-full z-10">
|
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 max-w-md w-full z-10">
|
||||||
{validation.errors.length > 0 && (
|
{validation.errors.length > 0 && (
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3 mb-2">
|
<div className="bg-red-900/30 border border-red-500/50 rounded-lg p-3 mb-2 backdrop-blur-sm">
|
||||||
<div className="font-medium text-red-800 mb-1">Errors</div>
|
<div className="font-medium text-red-400 mb-1">Errors</div>
|
||||||
<ul className="text-sm text-red-600 list-disc list-inside">
|
<ul className="text-sm text-red-300 list-disc list-inside">
|
||||||
{validation.errors.map((error, i) => (
|
{validation.errors.map((error, i) => (
|
||||||
<li key={i}>{error}</li>
|
<li key={i}>{error}</li>
|
||||||
))}
|
))}
|
||||||
@@ -18,9 +18,9 @@ export function ValidationPanel({ validation }: ValidationPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{validation.warnings.length > 0 && (
|
{validation.warnings.length > 0 && (
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
<div className="bg-amber-900/30 border border-amber-500/50 rounded-lg p-3 backdrop-blur-sm">
|
||||||
<div className="font-medium text-yellow-800 mb-1">Warnings</div>
|
<div className="font-medium text-amber-400 mb-1">Warnings</div>
|
||||||
<ul className="text-sm text-yellow-600 list-disc list-inside">
|
<ul className="text-sm text-amber-300 list-disc list-inside">
|
||||||
{validation.warnings.map((warning, i) => (
|
{validation.warnings.map((warning, i) => (
|
||||||
<li key={i}>{warning}</li>
|
<li key={i}>{warning}</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
FileBox,
|
FileBox,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
File
|
File,
|
||||||
|
Layout,
|
||||||
|
Grid3X3
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useStudy } from '../context/StudyContext';
|
import { useStudy } from '../context/StudyContext';
|
||||||
import { Card } from '../components/common/Card';
|
import { Card } from '../components/common/Card';
|
||||||
import { Button } from '../components/common/Button';
|
import { Button } from '../components/common/Button';
|
||||||
import { apiClient, ModelFile } from '../api/client';
|
import { apiClient, ModelFile } from '../api/client';
|
||||||
|
import { AtomizerCanvas } from '../components/canvas/AtomizerCanvas';
|
||||||
|
|
||||||
interface StudyConfig {
|
interface StudyConfig {
|
||||||
study_name: string;
|
study_name: string;
|
||||||
@@ -71,9 +74,12 @@ interface StudyConfig {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TabType = 'config' | 'canvas';
|
||||||
|
|
||||||
export default function Setup() {
|
export default function Setup() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { selectedStudy, isInitialized } = useStudy();
|
const { selectedStudy, isInitialized } = useStudy();
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('config');
|
||||||
const [config, setConfig] = useState<StudyConfig | null>(null);
|
const [config, setConfig] = useState<StudyConfig | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -248,8 +254,59 @@ export default function Setup() {
|
|||||||
return acc * 1000; // Approximate for continuous
|
return acc * 1000; // Approximate for continuous
|
||||||
}, 1) || 0;
|
}, 1) || 0;
|
||||||
|
|
||||||
|
// Canvas tab - full height
|
||||||
|
if (activeTab === 'canvas') {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col">
|
||||||
|
{/* Tab Bar */}
|
||||||
|
<div className="flex items-center gap-2 mb-4 border-b border-dark-700 pb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('config')}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-dark-800 text-dark-300 hover:text-white hover:bg-dark-700"
|
||||||
|
>
|
||||||
|
<Layout className="w-4 h-4" />
|
||||||
|
Configuration
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('canvas')}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-primary-600 text-white"
|
||||||
|
>
|
||||||
|
<Grid3X3 className="w-4 h-4" />
|
||||||
|
Canvas Builder
|
||||||
|
</button>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<span className="text-dark-400 text-sm">
|
||||||
|
{selectedStudy?.name || 'Study'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Canvas - takes remaining height */}
|
||||||
|
<div className="flex-1 min-h-0 rounded-lg overflow-hidden border border-dark-700">
|
||||||
|
<AtomizerCanvas />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
{/* Tab Bar */}
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('config')}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-primary-600 text-white"
|
||||||
|
>
|
||||||
|
<Layout className="w-4 h-4" />
|
||||||
|
Configuration
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('canvas')}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-dark-800 text-dark-300 hover:text-white hover:bg-dark-700"
|
||||||
|
>
|
||||||
|
<Grid3X3 className="w-4 h-4" />
|
||||||
|
Canvas Builder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
|
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user