systemforge-ai / frontend /components /workflow /FlowchartNode.tsx
JacobJA's picture
Clean fresh deployment
34b6cef
Raw
History Blame Contribute Delete
6.1 kB
'use client';
import React from 'react';
import { motion } from 'framer-motion';
export type FlowchartNodeType =
| 'input'
| 'task'
| 'decision'
| 'automation'
| 'approval'
| 'output'
| 'api'
| 'queue'
| 'llm'
| 'human_review'
| 'notification';
interface FlowchartNodeProps {
title: string;
type: FlowchartNodeType;
subtitle?: string;
isLast?: boolean;
}
function getNodeAccent(
type: FlowchartNodeType
) {
switch (type) {
case 'input':
return '#06B6D4';
case 'automation':
return '#F97316';
case 'output':
return '#22C55E';
case 'decision':
return '#F59E0B';
case 'api':
return '#3B82F6';
case 'queue':
return '#8B5CF6';
case 'llm':
return '#EC4899';
case 'approval':
return '#22C55E';
case 'human_review':
return '#F97316';
case 'notification':
return '#06B6D4';
default:
return '#00D4FF';
}
}
function getNodeIcon(
type: FlowchartNodeType
) {
switch (type) {
case 'input':
return '□';
case 'automation':
return '⚡';
case 'output':
return '✓';
case 'decision':
return '?';
case 'api':
return '⚡';
case 'queue':
return '⇄';
case 'llm':
return '🤖';
case 'approval':
return '✓';
case 'human_review':
return '🧑';
case 'notification':
return '📩';
default:
return '□';
}
}
export default function FlowchartNode({
title,
type,
subtitle,
isLast = false,
}: FlowchartNodeProps) {
const accent = getNodeAccent(type);
const icon = getNodeIcon(type);
const isDecision =
type === 'decision';
return (
<motion.div
initial={{
opacity: 0,
y: 20,
}}
whileInView={{
opacity: 1,
y: 0,
}}
viewport={{
once: true,
}}
transition={{
duration: 0.45,
}}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginBottom: isLast ? 0 : 30,
}}
>
{/* NODE */}
<div
style={{
width: isDecision ? 220 : 360,
minHeight: isDecision
? 220
: 150,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: `1px solid ${accent}35`,
background: `${accent}08`,
backdropFilter: 'blur(14px)',
padding: isDecision
? '20px'
: '28px',
textAlign: 'center',
borderRadius: isDecision
? '28px'
: '22px',
transform: isDecision
? 'rotate(45deg)'
: 'none',
boxShadow: `0 0 0 1px ${accent}08`,
}}
>
<div
style={{
width: '100%',
maxWidth: isDecision
? '78%'
: '100%',
transform: isDecision
? 'rotate(-45deg)'
: 'none',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
{/* ICON ONLY */}
<div
style={{
color: accent,
fontSize: '24px',
fontWeight: 700,
marginBottom: '14px',
lineHeight: 1,
}}
>
{icon}
</div>
{/* TITLE ONLY */}
<div
style={{
color: '#F0F0FF',
fontSize: isDecision
? '15px'
: '16px',
fontWeight: 600,
lineHeight: 1.8,
letterSpacing: '0.01em',
wordBreak: 'break-word',
whiteSpace: 'normal',
}}
>
{title}
</div>
{/* Optional subtitle */}
{subtitle && (
<div
style={{
marginTop: '10px',
color: '#94A3B8',
fontSize: '12px',
lineHeight: 1.6,
}}
>
{subtitle}
</div>
)}
</div>
</div>
{/* CONNECTOR LINE */}
{!isLast && (
<div
style={{
width: '2px',
height: '38px',
marginTop: '14px',
background:
'rgba(255,255,255,0.08)',
}}
/>
)}
</motion.div>
);
}