feat(canvas): Replace emoji icons with professional Lucide React icons
Phase 1 of Canvas Professional Upgrade: - Replace all emoji icons in nodes with Lucide components - Update BaseNode to use iconColor prop instead of color/colorBg - Update NodePalette with matching Lucide icons - Update templates.ts with Lucide icon names (Box, Scale, BarChart3, etc.) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { BrainCircuit } from 'lucide-react';
|
||||
import { BaseNode } from './BaseNode';
|
||||
import { AlgorithmNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
function AlgorithmNodeComponent(props: NodeProps<AlgorithmNodeData>) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<BaseNode {...props} icon={<span>🧠</span>} color="text-indigo-600" colorBg="bg-indigo-50">
|
||||
{data.method && <div>{data.method}</div>}
|
||||
{data.maxTrials && (
|
||||
<div className="text-xs text-gray-400">{data.maxTrials} trials</div>
|
||||
)}
|
||||
<BaseNode {...props} icon={<BrainCircuit size={16} />} iconColor="text-indigo-400">
|
||||
{data.method ? `${data.method} (${data.maxTrials || 100} trials)` : 'Select method'}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { memo, ReactNode } from 'react';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { BaseNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
interface BaseNodeProps extends NodeProps<BaseNodeData> {
|
||||
icon: ReactNode;
|
||||
color: string;
|
||||
colorBg?: string;
|
||||
iconColor: string;
|
||||
children?: ReactNode;
|
||||
inputs?: number;
|
||||
outputs?: number;
|
||||
@@ -15,127 +15,63 @@ function BaseNodeComponent({
|
||||
data,
|
||||
selected,
|
||||
icon,
|
||||
color,
|
||||
colorBg = 'bg-dark-700',
|
||||
iconColor,
|
||||
children,
|
||||
inputs = 1,
|
||||
outputs = 1,
|
||||
}: BaseNodeProps) {
|
||||
const hasError = data.errors && data.errors.length > 0;
|
||||
const isConfigured = data.configured;
|
||||
const hasErrors = data.errors && data.errors.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
group relative px-4 py-3 rounded-xl border-2 min-w-[200px] bg-dark-800
|
||||
transition-all duration-300 ease-out
|
||||
${selected
|
||||
? 'border-primary-500 shadow-xl shadow-primary-500/20 scale-[1.02]'
|
||||
: 'border-dark-600 shadow-md hover:shadow-lg hover:border-dark-500'}
|
||||
${!isConfigured ? 'border-dashed opacity-80' : ''}
|
||||
${hasError ? 'border-red-500/70 shadow-red-500/20' : ''}
|
||||
relative px-3 py-2.5 rounded-lg border min-w-[160px] max-w-[200px]
|
||||
bg-dark-850 shadow-lg transition-all duration-150
|
||||
${selected ? 'border-primary-400 ring-2 ring-primary-400/20' : 'border-dark-600'}
|
||||
${!data.configured ? 'border-dashed border-dark-500' : ''}
|
||||
${hasErrors ? 'border-red-500/70' : ''}
|
||||
`}
|
||||
style={{
|
||||
animation: 'nodeAppear 0.3s ease-out',
|
||||
}}
|
||||
>
|
||||
{/* Glow effect on selection */}
|
||||
{selected && (
|
||||
<div
|
||||
className="absolute inset-0 -z-10 rounded-xl opacity-30"
|
||||
style={{
|
||||
background: 'radial-gradient(ellipse at center, #6366f1 0%, transparent 70%)',
|
||||
transform: 'scale(1.3)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Input handles */}
|
||||
{inputs > 0 && (
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className={`
|
||||
!w-4 !h-4 !border-2 !border-dark-800 !shadow-md
|
||||
transition-all duration-200
|
||||
${selected ? '!bg-primary-500' : '!bg-dark-400 group-hover:!bg-dark-300'}
|
||||
`}
|
||||
style={{ left: -8 }}
|
||||
className="!w-2.5 !h-2.5 !bg-dark-500 !border-2 !border-dark-700 hover:!bg-primary-400 hover:!border-primary-500 transition-colors"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className={`
|
||||
w-8 h-8 rounded-lg ${colorBg} flex items-center justify-center
|
||||
transition-transform duration-200
|
||||
${selected ? 'scale-110' : 'group-hover:scale-105'}
|
||||
`}>
|
||||
<span className={`text-lg ${color}`}>{icon}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`${iconColor} flex-shrink-0`}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="font-semibold text-white text-sm">{data.label}</span>
|
||||
{!isConfigured && (
|
||||
<span className="ml-2 text-xs text-amber-400 animate-pulse">Needs config</span>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-white text-sm truncate">{data.label}</div>
|
||||
</div>
|
||||
{!data.configured && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{children && (
|
||||
<div className="text-sm text-dark-300 mt-2 pl-11">
|
||||
<div className="mt-1.5 text-xs text-dark-400 truncate">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status indicator */}
|
||||
<div className="absolute top-2 right-2">
|
||||
{hasError ? (
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full block animate-pulse" />
|
||||
) : isConfigured ? (
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full block" />
|
||||
) : (
|
||||
<span className="w-2 h-2 bg-amber-400 rounded-full block" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Errors */}
|
||||
{hasError && (
|
||||
<div className="mt-3 pt-2 border-t border-red-500/30">
|
||||
<p className="text-xs text-red-400 flex items-center gap-1">
|
||||
<span>⚠</span>
|
||||
{data.errors?.[0]}
|
||||
</p>
|
||||
{hasErrors && (
|
||||
<div className="mt-1.5 flex items-center gap-1 text-xs text-red-400">
|
||||
<AlertCircle size={10} />
|
||||
<span className="truncate">{data.errors![0]}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Output handles */}
|
||||
{outputs > 0 && (
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className={`
|
||||
!w-4 !h-4 !border-2 !border-dark-800 !shadow-md
|
||||
transition-all duration-200
|
||||
${selected ? '!bg-primary-500' : '!bg-dark-400 group-hover:!bg-dark-300'}
|
||||
`}
|
||||
style={{ right: -8 }}
|
||||
className="!w-2.5 !h-2.5 !bg-dark-500 !border-2 !border-dark-700 hover:!bg-primary-400 hover:!border-primary-500 transition-colors"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Inline styles for animation */}
|
||||
<style>{`
|
||||
@keyframes nodeAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { ShieldAlert } from 'lucide-react';
|
||||
import { BaseNode } from './BaseNode';
|
||||
import { ConstraintNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
function ConstraintNodeComponent(props: NodeProps<ConstraintNodeData>) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<BaseNode {...props} icon={<span>🚧</span>} color="text-orange-600" colorBg="bg-orange-50">
|
||||
{data.name && <div>{data.name}</div>}
|
||||
{data.operator && data.value !== undefined && (
|
||||
<div className="text-xs text-gray-400">
|
||||
{data.operator} {data.value}
|
||||
</div>
|
||||
)}
|
||||
<BaseNode {...props} icon={<ShieldAlert size={16} />} iconColor="text-amber-400">
|
||||
{data.name && data.operator && data.value !== undefined
|
||||
? `${data.name} ${data.operator} ${data.value}`
|
||||
: 'Set constraint'}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { SlidersHorizontal } from 'lucide-react';
|
||||
import { BaseNode } from './BaseNode';
|
||||
import { DesignVarNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
function DesignVarNodeComponent(props: NodeProps<DesignVarNodeData>) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<BaseNode {...props} icon={<span>📐</span>} color="text-green-600" colorBg="bg-green-50">
|
||||
{data.expressionName && <div className="font-mono">{data.expressionName}</div>}
|
||||
{data.minValue !== undefined && data.maxValue !== undefined && (
|
||||
<div className="text-xs text-gray-400">
|
||||
[{data.minValue} - {data.maxValue}] {data.unit || ''}
|
||||
</div>
|
||||
<BaseNode {...props} icon={<SlidersHorizontal size={16} />} iconColor="text-emerald-400">
|
||||
{data.expressionName ? (
|
||||
<span className="font-mono">{data.expressionName}</span>
|
||||
) : (
|
||||
'Select expression'
|
||||
)}
|
||||
</BaseNode>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { FlaskConical } from 'lucide-react';
|
||||
import { BaseNode } from './BaseNode';
|
||||
import { ExtractorNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
function ExtractorNodeComponent(props: NodeProps<ExtractorNodeData>) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<BaseNode {...props} icon={<span>🔬</span>} color="text-cyan-600" colorBg="bg-cyan-50">
|
||||
{data.extractorName && <div>{data.extractorName}</div>}
|
||||
{data.extractorId && (
|
||||
<div className="text-xs text-gray-400">{data.extractorId}</div>
|
||||
)}
|
||||
<BaseNode {...props} icon={<FlaskConical size={16} />} iconColor="text-cyan-400">
|
||||
{data.extractorName || 'Select extractor'}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box } from 'lucide-react';
|
||||
import { BaseNode } from './BaseNode';
|
||||
import { ModelNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
function ModelNodeComponent(props: NodeProps<ModelNodeData>) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<BaseNode {...props} icon={<span>📦</span>} color="text-blue-600" colorBg="bg-blue-50" inputs={0}>
|
||||
{data.filePath && (
|
||||
<div className="truncate max-w-[150px]">{data.filePath.split('/').pop()}</div>
|
||||
)}
|
||||
{data.fileType && (
|
||||
<div className="text-xs text-gray-400">{data.fileType.toUpperCase()}</div>
|
||||
)}
|
||||
<BaseNode {...props} icon={<Box size={16} />} iconColor="text-blue-400" inputs={0}>
|
||||
{data.filePath ? data.filePath.split(/[/\\]/).pop() : 'No file selected'}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Target } from 'lucide-react';
|
||||
import { BaseNode } from './BaseNode';
|
||||
import { ObjectiveNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
function ObjectiveNodeComponent(props: NodeProps<ObjectiveNodeData>) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<BaseNode {...props} icon={<span>🎯</span>} color="text-red-600" colorBg="bg-red-50">
|
||||
{data.name && <div>{data.name}</div>}
|
||||
{data.direction && (
|
||||
<div className="text-xs text-gray-400">
|
||||
{data.direction === 'minimize' ? '↓ Minimize' : '↑ Maximize'}
|
||||
{data.weight !== 1 && ` (w=${data.weight})`}
|
||||
</div>
|
||||
)}
|
||||
<BaseNode {...props} icon={<Target size={16} />} iconColor="text-rose-400">
|
||||
{data.name ? `${data.direction === 'maximize' ? '↑' : '↓'} ${data.name}` : 'Set objective'}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Cpu } from 'lucide-react';
|
||||
import { BaseNode } from './BaseNode';
|
||||
import { SolverNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
function SolverNodeComponent(props: NodeProps<SolverNodeData>) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<BaseNode {...props} icon={<span>⚙️</span>} color="text-purple-600" colorBg="bg-purple-50">
|
||||
{data.solverType && <div>{data.solverType}</div>}
|
||||
<BaseNode {...props} icon={<Cpu size={16} />} iconColor="text-violet-400">
|
||||
{data.solverType || 'Select solution'}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Rocket } from 'lucide-react';
|
||||
import { BaseNode } from './BaseNode';
|
||||
import { SurrogateNodeData } from '../../../lib/canvas/schema';
|
||||
|
||||
function SurrogateNodeComponent(props: NodeProps<SurrogateNodeData>) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<BaseNode {...props} icon={<span>🚀</span>} color="text-pink-600" colorBg="bg-pink-50" outputs={0}>
|
||||
<div>{data.enabled ? 'Enabled' : 'Disabled'}</div>
|
||||
{data.enabled && data.modelType && (
|
||||
<div className="text-xs text-gray-400">{data.modelType}</div>
|
||||
)}
|
||||
<BaseNode {...props} icon={<Rocket size={16} />} iconColor="text-pink-400" outputs={0}>
|
||||
{data.enabled ? (data.modelType || 'Auto') : 'Disabled'}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import { DragEvent } from 'react';
|
||||
import { NodeType } from '../../../lib/canvas/schema';
|
||||
import {
|
||||
Box,
|
||||
Cpu,
|
||||
SlidersHorizontal,
|
||||
FlaskConical,
|
||||
Target,
|
||||
ShieldAlert,
|
||||
BrainCircuit,
|
||||
Rocket,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface PaletteItem {
|
||||
type: NodeType;
|
||||
label: string;
|
||||
icon: string;
|
||||
icon: React.ReactNode;
|
||||
description: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const PALETTE_ITEMS: PaletteItem[] = [
|
||||
{ type: 'model', label: 'Model', icon: '📦', description: 'NX model file' },
|
||||
{ type: 'solver', label: 'Solver', icon: '⚙️', description: 'Nastran solution' },
|
||||
{ type: 'designVar', label: 'Design Variable', icon: '📐', description: 'Parameter to vary' },
|
||||
{ type: 'extractor', label: 'Extractor', icon: '🔬', description: 'Physics extraction' },
|
||||
{ type: 'objective', label: 'Objective', icon: '🎯', description: 'Optimization goal' },
|
||||
{ type: 'constraint', label: 'Constraint', icon: '🚧', description: 'Limit condition' },
|
||||
{ type: 'algorithm', label: 'Algorithm', icon: '🧠', description: 'Optimization method' },
|
||||
{ type: 'surrogate', label: 'Surrogate', icon: '🚀', description: 'Neural acceleration' },
|
||||
{ type: 'model', label: 'Model', icon: <Box size={18} />, description: 'NX model file (.prt, .sim)', color: 'text-blue-400' },
|
||||
{ type: 'solver', label: 'Solver', icon: <Cpu size={18} />, description: 'Nastran solution type', color: 'text-violet-400' },
|
||||
{ type: 'designVar', label: 'Design Variable', icon: <SlidersHorizontal size={18} />, description: 'Parameter to optimize', color: 'text-emerald-400' },
|
||||
{ type: 'extractor', label: 'Extractor', icon: <FlaskConical size={18} />, description: 'Physics result extraction', color: 'text-cyan-400' },
|
||||
{ type: 'objective', label: 'Objective', icon: <Target size={18} />, description: 'Optimization goal', color: 'text-rose-400' },
|
||||
{ type: 'constraint', label: 'Constraint', icon: <ShieldAlert size={18} />, description: 'Design constraint', color: 'text-amber-400' },
|
||||
{ type: 'algorithm', label: 'Algorithm', icon: <BrainCircuit size={18} />, description: 'Optimization method', color: 'text-indigo-400' },
|
||||
{ type: 'surrogate', label: 'Surrogate', icon: <Rocket size={18} />, description: 'Neural acceleration', color: 'text-pink-400' },
|
||||
];
|
||||
|
||||
export function NodePalette() {
|
||||
@@ -26,24 +37,31 @@ export function NodePalette() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-dark-850 border-r border-dark-700 p-4 overflow-y-auto">
|
||||
<h3 className="text-sm font-semibold text-dark-400 uppercase mb-4 tracking-wide">
|
||||
Components
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="w-56 bg-dark-850 border-r border-dark-700 flex flex-col">
|
||||
<div className="p-4 border-b border-dark-700">
|
||||
<h3 className="text-xs font-semibold text-dark-400 uppercase tracking-wider">
|
||||
Components
|
||||
</h3>
|
||||
<p className="text-xs text-dark-500 mt-1">
|
||||
Drag to canvas
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-1.5">
|
||||
{PALETTE_ITEMS.map((item) => (
|
||||
<div
|
||||
key={item.type}
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, item.type)}
|
||||
className="flex items-center gap-3 p-3 bg-dark-800 rounded-lg border border-dark-600
|
||||
cursor-grab hover:border-primary-500/50 hover:bg-dark-750 transition-all
|
||||
active:cursor-grabbing"
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 bg-dark-800/50 rounded-lg border border-dark-700/50
|
||||
cursor-grab hover:border-primary-500/50 hover:bg-dark-800
|
||||
active:cursor-grabbing transition-all group"
|
||||
>
|
||||
<span className="text-xl">{item.icon}</span>
|
||||
<div>
|
||||
<div className="font-medium text-white">{item.label}</div>
|
||||
<div className="text-xs text-dark-400">{item.description}</div>
|
||||
<div className={`${item.color} opacity-80 group-hover:opacity-100 transition-opacity`}>
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-dark-200 text-sm leading-tight">{item.label}</div>
|
||||
<div className="text-[10px] text-dark-500 truncate">{item.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -24,7 +24,7 @@ const massMinimizationTemplate: CanvasTemplate = {
|
||||
name: 'Mass Minimization',
|
||||
description: 'Minimize structural mass while maintaining stiffness constraints. Ideal for brackets, housings, and weight-critical components.',
|
||||
category: 'structural',
|
||||
icon: '⚖️',
|
||||
icon: 'Scale',
|
||||
intent: {
|
||||
version: '1.0',
|
||||
source: 'canvas',
|
||||
@@ -67,7 +67,7 @@ const multiObjectiveTemplate: CanvasTemplate = {
|
||||
name: 'Multi-Objective Pareto',
|
||||
description: 'Explore trade-offs between competing objectives. Generates a Pareto front for informed decision-making.',
|
||||
category: 'structural',
|
||||
icon: '📊',
|
||||
icon: 'BarChart3',
|
||||
intent: {
|
||||
version: '1.0',
|
||||
source: 'canvas',
|
||||
@@ -112,7 +112,7 @@ const turboModeTemplate: CanvasTemplate = {
|
||||
name: 'Turbo Mode',
|
||||
description: 'Accelerated optimization using neural network surrogates. Run thousands of virtual trials with periodic FEA validation.',
|
||||
category: 'general',
|
||||
icon: '🚀',
|
||||
icon: 'Rocket',
|
||||
intent: {
|
||||
version: '1.0',
|
||||
source: 'canvas',
|
||||
@@ -158,7 +158,7 @@ const mirrorZernikeTemplate: CanvasTemplate = {
|
||||
name: 'Mirror Zernike',
|
||||
description: 'Optimize optical mirror surface quality using Zernike wavefront error metrics. Specialized for precision optics.',
|
||||
category: 'optical',
|
||||
icon: '🔭',
|
||||
icon: 'Telescope',
|
||||
intent: {
|
||||
version: '1.0',
|
||||
source: 'canvas',
|
||||
@@ -208,7 +208,7 @@ const frequencyOptimizationTemplate: CanvasTemplate = {
|
||||
name: 'Frequency Optimization',
|
||||
description: 'Maximize natural frequencies to avoid resonance. Essential for rotating machinery and vibration-sensitive equipment.',
|
||||
category: 'structural',
|
||||
icon: '📈',
|
||||
icon: 'TrendingUp',
|
||||
intent: {
|
||||
version: '1.0',
|
||||
source: 'canvas',
|
||||
|
||||
Reference in New Issue
Block a user