import {
getA3SFlowCoreNode,
isA3SFlowCorePortAvailable,
localizeA3SFlowDagManifest,
type A3SFlowDagPortDefinition,
} from '@a3s-lab/flow-ui';
import { A3SFlowDagNodePreview } from '@a3s-lab/flow-ui/react';
import { Copy, Play, Trash } from '@phosphor-icons/react';
import { Handle, NodeToolbar, Position, type NodeProps } from '@xyflow/react';
import { memo } from 'react';
import { workflowPlaygroundCopy } from './WorkflowPlayground.copy';
import type { PlaygroundNode } from './WorkflowPlayground.model';
import { useWorkflowPlaygroundRegistry } from './WorkflowPlayground.registry';
function handleTop(index: number, total: number, container: boolean): string {
if (container) return '76px';
const start = total === 1 ? 50 : 30;
const end = total === 1 ? 50 : 72;
const progress = total <= 1 ? 0 : index / (total - 1);
return `${start + (end - start) * progress}%`;
}
function portClassName(port: A3SFlowDagPortDefinition): string {
return `flow-playground-node__handle is-${port.kind}`;
}
function WorkflowPlaygroundNodeComponent({
data,
selected,
isConnectable,
}: NodeProps<PlaygroundNode>) {
const registry = useWorkflowPlaygroundRegistry();
const manifest = registry.require(data.dagNode.data.type);
const localized = localizeA3SFlowDagManifest(manifest, data.locale);
const text = workflowPlaygroundCopy[data.locale];
const coreDefinition = getA3SFlowCoreNode(manifest.type);
const outputs = localized.ports.outputs.filter((port) => {
const corePort = coreDefinition?.ports.outputs.find(
({ id }) => id === port.id,
);
return !corePort || isA3SFlowCorePortAvailable(corePort, data.dagNode.data);
});
return (
<div
className={`a3s-workflow-node-shell flow-playground-node${data.container ? ' is-container' : ''}${data.internal ? ' is-internal' : ''}${selected ? ' is-selected' : ''}`}
data-flow-node-type={manifest.type}
>
<NodeToolbar
className="a3s-node-toolbar"
isVisible={selected}
position={Position.Top}
>
<button
aria-label={text.runStep}
onClick={(event) => {
event.stopPropagation();
data.onRun?.(data.dagNode.id);
}}
title={text.runStep}
type="button"
>
<Play aria-hidden="true" weight="fill" />
</button>
<button
aria-label={text.duplicate}
onClick={(event) => {
event.stopPropagation();
data.onDuplicate?.(data.dagNode.id);
}}
title={text.duplicate}
type="button"
>
<Copy aria-hidden="true" />
</button>
<button
aria-label={text.delete}
onClick={(event) => {
event.stopPropagation();
data.onDelete?.(data.dagNode.id);
}}
title={text.delete}
type="button"
>
<Trash aria-hidden="true" />
</button>
</NodeToolbar>
{localized.ports.inputs.map((port, index) => (
<Handle
aria-label={text.targetHandle(port.label)}
className={`${portClassName(port)} a3s-node-handle`}
id={port.id}
isConnectable={isConnectable}
key={port.id}
position={Position.Left}
style={{
top: handleTop(
index,
localized.ports.inputs.length,
data.container,
),
}}
title={text.targetHandle(port.label)}
type="target"
/>
))}
<A3SFlowDagNodePreview
dagNode={data.dagNode}
locale={data.locale}
manifest={manifest}
selected={selected}
status={data.runtimeStatus ?? 'idle'}
/>
{data.container && (
<div className="flow-playground-node__child-label">
<span>{text.childCanvas}</span>
<code>{data.dagNode.id}</code>
</div>
)}
{data.internal && (
<span className="flow-playground-node__internal-label">
{text.internalNode}
</span>
)}
{outputs.map((port, index) => (
<Handle
aria-label={text.sourceHandle(port.label)}
className={`${portClassName(port)} a3s-node-handle`}
id={port.id}
isConnectable={isConnectable}
key={port.id}
position={Position.Right}
style={{ top: handleTop(index, outputs.length, data.container) }}
title={text.sourceHandle(port.label)}
type="source"
/>
))}
</div>
);
}
export const WorkflowPlaygroundNode = memo(WorkflowPlaygroundNodeComponent);