@@ -364,20 +592,23 @@ const onClearSearch = async () => {
-
+
Application Package
-
-
+
No data or failed to fetch.
+
+
-
\ No newline at end of file
+
+
+
diff --git a/public/cwl-demo/cwl-svg-custom.js b/public/cwl-demo/cwl-svg-custom.js
new file mode 100644
index 0000000..f819166
--- /dev/null
+++ b/public/cwl-demo/cwl-svg-custom.js
@@ -0,0 +1,2883 @@
+/**
+ * CWL-SVG Custom - Custom version for CWL visualizer
+ * Features: CWL parsing, SVG rendering, user interactions
+ */
+
+class CWLSVGCustom {
+ constructor(container, options = {}) {
+ this.container = typeof container === 'string' ? document.getElementById(container) : container;
+ this.options = {
+ width: options.width || 900,
+ height: options.height || 600,
+ nodeWidth: options.nodeWidth || 140,
+ nodeHeight: options.nodeHeight || 70,
+ nodeSpacing: options.nodeSpacing || 80,
+ levelSpacing: options.levelSpacing || 180,
+ ...options
+ };
+
+ this.workflow = null;
+ this.svg = null;
+ this.zoomLevel = 1;
+ this.panX = 0;
+ this.panY = 0;
+ this.draggedNode = null;
+ this.selectedNodes = new Set();
+
+ // Public API
+ this._api = null;
+
+ this.initializeSVG();
+ this.setupEventHandlers();
+ }
+
+ /**
+ * Get the public API instance
+ * @returns {CWLVisualizerAPI}
+ */
+ api() {
+ if (!this._api) {
+ this._api = new CWLVisualizerAPI(this);
+ }
+ return this._api;
+ }
+
+ /**
+ * Initialize the main SVG element
+ */
+ initializeSVG() {
+ // Clear container
+ this.container.innerHTML = '';
+
+ // Create wrapper with controls
+ const wrapper = document.createElement('div');
+ wrapper.className = 'cwl-svg-wrapper';
+ wrapper.style.cssText = `
+ position: relative;
+ width: 100%;
+ height: ${this.options.height}px;
+ border: 1px solid #333;
+ border-radius: 8px;
+ overflow: hidden;
+ background: #303030;
+ `;
+
+ // Create controls
+ const controls = document.createElement('div');
+ controls.className = 'cwl-svg-controls';
+ controls.style.cssText = `
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 100;
+ display: flex;
+ gap: 5px;
+ `;
+
+ const createButton = (text, onclick, title = '') => {
+ const btn = document.createElement('button');
+ btn.textContent = text;
+ btn.title = title;
+ btn.style.cssText = `
+ padding: 5px 10px;
+ background: #4f46e5;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ `;
+ btn.onclick = onclick;
+ return btn;
+ };
+
+ controls.appendChild(createButton('🔍+', () => this.zoom(1.2), 'Zoom in'));
+ controls.appendChild(createButton('🔍−', () => this.zoom(0.8), 'Zoom out'));
+ controls.appendChild(createButton('🔄', () => this.resetView(), 'Reset view'));
+ controls.appendChild(createButton('�', () => this.autoLayout(), 'Auto-arrange'));
+ controls.appendChild(createButton('�💾', () => this.downloadSVG(), 'Download SVG'));
+
+ // Create SVG element
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.svg.setAttribute('width', '100%');
+ this.svg.setAttribute('height', '100%');
+ this.svg.setAttribute('viewBox', `0 0 ${this.options.width} ${this.options.height}`);
+ this.svg.style.cursor = 'grab';
+
+ // Groupe principal pour les transformations
+ this.mainGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.mainGroup.setAttribute('class', 'main-group');
+ this.svg.appendChild(this.mainGroup);
+
+ // Definitions for vue-cwl styles
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
+ defs.innerHTML = `
+
+
+
+
+
+
+
+
+
+ `;
+ this.svg.appendChild(defs);
+
+ wrapper.appendChild(this.svg);
+ wrapper.appendChild(controls);
+ this.container.appendChild(wrapper);
+ }
+
+ /**
+ * Configure event handlers
+ */
+ setupEventHandlers() {
+ let isPanning = false;
+ let startPoint = { x: 0, y: 0 };
+ let startPan = { x: 0, y: 0 };
+
+ // Zoom avec la molette
+ this.svg.addEventListener('wheel', (e) => {
+ e.preventDefault();
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
+ this.zoom(delta);
+ });
+
+ // Pan avec clic-glisser
+ this.svg.addEventListener('mousedown', (e) => {
+ if (e.target === this.svg || e.target === this.mainGroup) {
+ isPanning = true;
+ startPoint = { x: e.clientX, y: e.clientY };
+ startPan = { x: this.panX, y: this.panY };
+ this.svg.style.cursor = 'grabbing';
+ // Close port details when clicking on background
+ this.clearPortDetails();
+ }
+ });
+
+ this.svg.addEventListener('mousemove', (e) => {
+ if (isPanning) {
+ const dx = e.clientX - startPoint.x;
+ const dy = e.clientY - startPoint.y;
+ this.panX = startPan.x + dx;
+ this.panY = startPan.y + dy;
+ this.updateTransform();
+ }
+ });
+
+ this.svg.addEventListener('mouseup', () => {
+ isPanning = false;
+ this.svg.style.cursor = 'grab';
+ });
+
+ this.svg.addEventListener('mouseleave', () => {
+ isPanning = false;
+ this.svg.style.cursor = 'grab';
+ });
+ }
+
+ /**
+ * Parse a CWL workflow from a JSON/YAML string
+ */
+ async loadWorkflow(cwlContent, format = 'auto') {
+ try {
+ let workflowData;
+
+ if (format === 'auto') {
+ // Detect format
+ const trimmed = cwlContent.trim();
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
+ format = 'json';
+ } else {
+ format = 'yaml';
+ }
+ }
+
+ if (format === 'yaml') {
+ if (typeof jsyaml === 'undefined') {
+ throw new Error('js-yaml library not loaded');
+ }
+ workflowData = jsyaml.load(cwlContent);
+ } else {
+ workflowData = JSON.parse(cwlContent);
+ }
+
+ console.log('🔍 Parsed CWL data:', workflowData);
+ this.workflow = this.processWorkflow(workflowData);
+ console.log('🔍 Workflow after processing:', this.workflow);
+ console.log('🔍 Inputs:', Object.keys(this.workflow.inputs));
+ console.log('🔍 Steps:', Object.keys(this.workflow.steps));
+ console.log('🔍 Outputs:', Object.keys(this.workflow.outputs));
+ this.render();
+
+ return { success: true, workflow: this.workflow };
+ } catch (error) {
+ console.error('Error parsing CWL:', error);
+ return { success: false, error: error.message };
+ }
+ }
+
+ /**
+ * Process and structure workflow data
+ */
+ processWorkflow(data) {
+ // If it's a file with $graph, extract the main workflow
+ if (data.$graph) {
+ return this.processGraphWorkflow(data);
+ }
+
+ // Si c'est un CommandLineTool, le convertir en workflow simple
+ if (data.class === 'CommandLineTool') {
+ return this.processCommandLineTool(data);
+ }
+
+ const workflow = {
+ class: data.class || 'Workflow',
+ id: data.id || 'workflow',
+ label: data.label || data.id || 'Workflow',
+ inputs: {},
+ outputs: {},
+ steps: {},
+ connections: []
+ };
+
+ // Traiter les inputs
+ if (data.inputs) {
+ if (Array.isArray(data.inputs)) {
+ data.inputs.forEach((input, idx) => {
+ const id = input.id || `input_${idx}`;
+ workflow.inputs[id] = {
+ id,
+ type: input.type || 'any',
+ label: input.label || id,
+ doc: input.doc || ''
+ };
+ });
+ } else {
+ Object.entries(data.inputs).forEach(([id, input]) => {
+ workflow.inputs[id] = {
+ id,
+ type: input.type || input || 'any',
+ label: input.label || id,
+ doc: input.doc || ''
+ };
+ });
+ }
+ }
+
+ // Traiter les outputs
+ if (data.outputs) {
+ if (Array.isArray(data.outputs)) {
+ data.outputs.forEach((output, idx) => {
+ const id = output.id || `output_${idx}`;
+ workflow.outputs[id] = {
+ id,
+ type: output.type || 'any',
+ label: output.label || id,
+ outputSource: output.outputSource
+ };
+ });
+ } else {
+ Object.entries(data.outputs).forEach(([id, output]) => {
+ workflow.outputs[id] = {
+ id,
+ type: output.type || output || 'any',
+ label: output.label || id,
+ outputSource: output.outputSource
+ };
+ });
+ }
+ }
+
+ // Process steps
+ console.log('🔍 Processing steps. data.steps:', data.steps);
+ if (data.steps) {
+ if (Array.isArray(data.steps)) {
+ console.log('🔍 Steps in array format');
+ data.steps.forEach((step, idx) => {
+ const id = step.id || `step_${idx}`;
+ workflow.steps[id] = {
+ id,
+ label: step.label || id,
+ run: step.run,
+ in: step.in || {},
+ out: step.out || []
+ };
+ console.log(`🔍 Step added: ${id}`, workflow.steps[id]);
+ });
+ } else {
+ console.log('🔍 Steps in object format');
+ Object.entries(data.steps).forEach(([id, step]) => {
+ workflow.steps[id] = {
+ id,
+ label: step.label || id,
+ run: step.run,
+ in: step.in || {},
+ out: step.out || []
+ };
+ console.log(`🔍 Step added: ${id}`, workflow.steps[id]);
+ });
+ }
+ }
+
+ // Analyze connections
+ this.analyzeConnections(workflow);
+
+ return workflow;
+ }
+
+ /**
+ * Convertit un CommandLineTool en workflow simple visualisable
+ */
+ processCommandLineTool(data) {
+ console.log('🔧 Processing CommandLineTool:', data);
+
+ const toolId = data.id || 'tool';
+ const toolLabel = data.label || (Array.isArray(data.baseCommand) ? data.baseCommand.join(' ') : data.baseCommand) || toolId;
+
+ const workflow = {
+ class: 'Workflow',
+ id: 'workflow_' + toolId,
+ label: `Workflow: ${toolLabel}`,
+ inputs: {},
+ outputs: {},
+ steps: {},
+ connections: []
+ };
+
+ // Traiter les inputs du tool
+ if (data.inputs) {
+ Object.entries(data.inputs).forEach(([inputId, inputDef]) => {
+ workflow.inputs[inputId] = {
+ id: inputId,
+ label: inputDef.label || inputId,
+ type: inputDef.type || 'Any',
+ doc: inputDef.doc
+ };
+ });
+ }
+
+ // Traiter les outputs du tool
+ if (data.outputs) {
+ Object.entries(data.outputs).forEach(([outputId, outputDef]) => {
+ workflow.outputs[outputId] = {
+ id: outputId,
+ label: outputDef.label || outputId,
+ type: outputDef.type || 'Any',
+ outputSource: `${toolId}/${outputId}`
+ };
+ });
+ }
+
+ // Create single step representing the tool
+ workflow.steps[toolId] = {
+ id: toolId,
+ label: toolLabel,
+ run: Array.isArray(data.baseCommand) ? data.baseCommand.join(' ') : data.baseCommand || 'tool',
+ in: {},
+ out: []
+ };
+
+ // Connect all inputs to the step
+ Object.keys(workflow.inputs).forEach(inputId => {
+ workflow.steps[toolId].in[inputId] = inputId;
+
+ // Create connection input -> step
+ workflow.connections.push({
+ from: { id: inputId, type: 'input', port: 'output' },
+ to: { id: toolId, type: 'step', port: inputId }
+ });
+ });
+
+ // Add outputs to the step
+ Object.keys(workflow.outputs).forEach(outputId => {
+ workflow.steps[toolId].out.push(outputId);
+
+ // Create connection step -> output
+ workflow.connections.push({
+ from: { id: toolId, type: 'step', port: outputId },
+ to: { id: outputId, type: 'output', port: 'input' }
+ });
+ });
+
+ console.log('🔧 CommandLineTool converted to workflow:', workflow);
+ return workflow;
+ }
+
+ /**
+ * Process CWL files with $graph (containing multiple components)
+ */
+ processGraphWorkflow(data) {
+ console.log('📊 Processing $graph file:', data);
+
+ if (!data.$graph || !Array.isArray(data.$graph)) {
+ throw new Error('Invalid $graph format');
+ }
+
+ // Find main workflow in the graph
+ let mainWorkflow = data.$graph.find(item => item.class === 'Workflow');
+
+ if (!mainWorkflow) {
+ throw new Error('No workflow found in $graph');
+ }
+
+ console.log('📊 Main workflow found:', mainWorkflow);
+
+ // Process main workflow normally
+ const workflow = {
+ class: mainWorkflow.class,
+ id: mainWorkflow.id || 'workflow',
+ label: mainWorkflow.label || mainWorkflow.id || 'Workflow',
+ inputs: {},
+ outputs: {},
+ steps: {},
+ connections: []
+ };
+
+ // Traiter les inputs
+ if (mainWorkflow.inputs) {
+ Object.entries(mainWorkflow.inputs).forEach(([inputId, inputDef]) => {
+ workflow.inputs[inputId] = {
+ id: inputId,
+ label: inputDef.label || inputId,
+ type: inputDef.type || 'Any',
+ doc: inputDef.doc
+ };
+ });
+ }
+
+ // Traiter les outputs
+ if (mainWorkflow.outputs) {
+ Object.entries(mainWorkflow.outputs).forEach(([outputId, outputDef]) => {
+ workflow.outputs[outputId] = {
+ id: outputId,
+ label: outputDef.label || outputId,
+ type: outputDef.type || 'Any',
+ outputSource: outputDef.outputSource
+ };
+ });
+ }
+
+ // Traiter les steps
+ if (mainWorkflow.steps) {
+ Object.entries(mainWorkflow.steps).forEach(([stepId, stepDef]) => {
+ workflow.steps[stepId] = {
+ id: stepId,
+ label: stepDef.label || stepId,
+ run: stepDef.run,
+ in: stepDef.in || {},
+ out: stepDef.out || []
+ };
+ console.log(`🔍 Step added: ${stepId}`, workflow.steps[stepId]);
+ });
+ }
+
+ // Analyze connections
+ this.analyzeConnections(workflow);
+
+ console.log('📊 $graph workflow processed:', workflow);
+ return workflow;
+ }
+
+ /**
+ * Analyse les connexions entre les éléments
+ */
+ analyzeConnections(workflow) {
+ // Connexions des inputs vers les steps
+ Object.values(workflow.steps).forEach(step => {
+ if (step.in) {
+ Object.entries(step.in).forEach(([inputId, source]) => {
+ let sourceId, sourcePort;
+
+ if (typeof source === 'string') {
+ sourceId = source;
+ sourcePort = 'output';
+ } else if (source.source) {
+ sourceId = source.source;
+ sourcePort = 'output';
+ } else {
+ return;
+ }
+
+ // Determine source type
+ if (workflow.inputs[sourceId]) {
+ workflow.connections.push({
+ from: { id: sourceId, type: 'input', port: sourcePort },
+ to: { id: step.id, type: 'step', port: inputId }
+ });
+ } else {
+ // Chercher dans les outputs des autres steps
+ Object.values(workflow.steps).forEach(otherStep => {
+ if (otherStep.id !== step.id &&
+ typeof sourceId === 'string' &&
+ (sourceId.startsWith(otherStep.id + '/') || sourceId === otherStep.id)) {
+
+ // Extraire le nom du port depuis step1/processed_file
+ let fromPortName = sourcePort;
+ if (sourceId.includes('/')) {
+ fromPortName = sourceId.split('/')[1];
+ }
+
+ workflow.connections.push({
+ from: { id: otherStep.id, type: 'step', port: fromPortName },
+ to: { id: step.id, type: 'step', port: inputId }
+ });
+ }
+ });
+ }
+ });
+ }
+ });
+
+ // Connections from steps to outputs
+ Object.values(workflow.outputs).forEach(output => {
+ if (output.outputSource) {
+ // outputSource can be a string or an array
+ let sourceSources = Array.isArray(output.outputSource) ? output.outputSource : [output.outputSource];
+
+ sourceSources.forEach(sourceId => {
+ Object.values(workflow.steps).forEach(step => {
+ if (typeof sourceId === 'string' &&
+ (sourceId.startsWith(step.id + '/') || sourceId === step.id)) {
+
+ // Extraire le nom du port depuis step_1/output_directory
+ let fromPortName = 'output';
+ if (sourceId.includes('/')) {
+ fromPortName = sourceId.split('/')[1];
+ }
+
+ workflow.connections.push({
+ from: { id: step.id, type: 'step', port: fromPortName },
+ to: { id: output.id, type: 'output', port: 'input' }
+ });
+ }
+ });
+ });
+ }
+ });
+ }
+
+ /**
+ * Calcule le layout automatique des éléments avec organisation gauche-droite
+ */
+ calculateLayout() {
+ const layout = { nodes: {}, levels: [] };
+ const { nodeWidth, nodeHeight, nodeSpacing, levelSpacing } = this.options;
+
+ // Topological analysis for left-right arrangement
+ const levels = this.calculateTopologicalLevels();
+ console.log('🔍 Levels calculated:', levels);
+
+ let currentX = 50;
+
+ levels.forEach((level, levelIndex) => {
+ console.log(`🔍 Traitement niveau ${levelIndex}:`, level);
+ const levelHeight = level.length * nodeHeight + (level.length - 1) * nodeSpacing;
+ let startY = Math.max(50, (this.options.height - levelHeight) / 2);
+
+ level.forEach((id, index) => {
+ const x = currentX;
+ const y = startY + index * (nodeHeight + nodeSpacing);
+
+ console.log(`🔍 Position calculée pour ${id}: x=${x}, y=${y}`);
+ layout.nodes[id] = { x, y, width: nodeWidth, height: nodeHeight };
+ });
+
+ currentX += nodeWidth + levelSpacing;
+ });
+
+ return layout;
+ }
+
+ /**
+ * Calcule les niveaux topologiques avec gestion spéciale des workflows simples
+ */
+ calculateTopologicalLevels() {
+ const inputIds = Object.keys(this.workflow.inputs);
+ const stepIds = Object.keys(this.workflow.steps);
+ const outputIds = Object.keys(this.workflow.outputs);
+
+ console.log('🔍 Checking simple workflow - stepIds.length:', stepIds.length);
+ // Special case: simple workflow with single step
+ if (stepIds.length <= 1) {
+ console.log('🔍 Using calculateSimpleWorkflowLevels');
+ return this.calculateSimpleWorkflowLevels(inputIds, stepIds, outputIds);
+ }
+
+ // Workflow complexe : tri topologique standard
+ const levels = [];
+ const processed = new Set();
+ const inDegree = new Map();
+
+ const allNodes = [
+ ...inputIds.map(id => ({ id, type: 'input' })),
+ ...stepIds.map(id => ({ id, type: 'step' })),
+ ...outputIds.map(id => ({ id, type: 'output' }))
+ ];
+
+ allNodes.forEach(node => inDegree.set(node.id, 0));
+
+ // Calculate incoming degrees
+ this.workflow.connections.forEach(conn => {
+ const current = inDegree.get(conn.to.id) || 0;
+ inDegree.set(conn.to.id, current + 1);
+ });
+
+ // Topological sort by levels
+ while (processed.size < allNodes.length) {
+ const currentLevel = [];
+
+ // Find nodes without unprocessed dependencies
+ allNodes.forEach(node => {
+ if (!processed.has(node.id) && (inDegree.get(node.id) === 0)) {
+ currentLevel.push(node.id);
+ }
+ });
+
+ if (currentLevel.length === 0) {
+ // Avoid loops - add remaining nodes
+ allNodes.forEach(node => {
+ if (!processed.has(node.id)) {
+ currentLevel.push(node.id);
+ }
+ });
+ }
+
+ // Organiser par type (inputs -> steps -> outputs)
+ currentLevel.sort((a, b) => {
+ const typeOrder = { input: 0, step: 1, output: 2 };
+ const typeA = this.getNodeType(a);
+ const typeB = this.getNodeType(b);
+ return typeOrder[typeA] - typeOrder[typeB];
+ });
+
+ levels.push(currentLevel);
+
+ // Mark as processed
+ currentLevel.forEach(nodeId => {
+ processed.add(nodeId);
+
+ // Reduce degree of connected nodes
+ this.workflow.connections.forEach(conn => {
+ if (conn.from.id === nodeId) {
+ const current = inDegree.get(conn.to.id);
+ inDegree.set(conn.to.id, Math.max(0, current - 1));
+ }
+ });
+ });
+ }
+
+ return levels;
+ }
+
+ /**
+ * Calculate layout for a simple workflow (single step)
+ */
+ calculateSimpleWorkflowLevels(inputIds, stepIds, outputIds) {
+ console.log('🔍 calculateSimpleWorkflowLevels called with:', {inputIds, stepIds, outputIds});
+ const levels = [];
+
+ // Level 1: Inputs (always on left)
+ if (inputIds.length > 0) {
+ console.log('🔍 Adding inputs level:', inputIds);
+ levels.push(inputIds);
+ }
+
+ // Level 2: Step(s) (in center)
+ if (stepIds.length > 0) {
+ console.log('🔍 Adding steps level:', stepIds);
+ levels.push(stepIds);
+ }
+
+ // Level 3: Outputs (always on right)
+ if (outputIds.length > 0) {
+ console.log('🔍 Adding outputs level:', outputIds);
+ levels.push(outputIds);
+ }
+
+ console.log('🔍 Levels returned:', levels);
+ return levels;
+ }
+
+ /**
+ * Détermine le type d'un noeud
+ */
+ getNodeType(nodeId) {
+ if (this.workflow.inputs[nodeId]) return 'input';
+ if (this.workflow.outputs[nodeId]) return 'output';
+ return 'step';
+ }
+
+ /**
+ * Crée les ports d'entrée/sortie avec métadonnées CWL (style vue-cwl)
+ */
+ createNodePorts(nodeId, type, position, color) {
+ const ports = [];
+ const portRadius = 4;
+
+ // Input ports (left side)
+ if (type === 'step' || type === 'output') {
+ const inputPorts = this.getNodeInputPorts(nodeId);
+ inputPorts.forEach((portInfo, i) => {
+ const port = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ const y = position.y + position.height * (0.25 + 0.5 * i / Math.max(1, inputPorts.length - 1));
+
+ port.setAttribute('cx', position.x - 2);
+ port.setAttribute('cy', isNaN(y) ? position.y + position.height * 0.5 : y);
+ port.setAttribute('r', portRadius);
+ port.setAttribute('fill', this.getPortColor(portInfo.type));
+ port.setAttribute('stroke', color.border);
+ port.setAttribute('stroke-width', '2');
+ port.setAttribute('class', 'cwl-port cwl-port-input');
+ port.setAttribute('data-port-type', 'input');
+ port.setAttribute('data-port-id', portInfo.id);
+ port.setAttribute('data-port-cwl-type', portInfo.type);
+ port.setAttribute('data-port-name', portInfo.name || portInfo.id);
+ port.setAttribute('data-node-id', nodeId);
+
+ // Tooltip avec informations du port
+ port.setAttribute('title', `${portInfo.name || portInfo.id} (${portInfo.type})`);
+
+ // Gestionnaires d'événements pour les ports
+ this.setupPortInteraction(port, portInfo, nodeId);
+
+ ports.push(port);
+ });
+ }
+
+ // Output ports (right side)
+ if (type === 'input' || type === 'step') {
+ const outputPorts = this.getNodeOutputPorts(nodeId);
+ outputPorts.forEach((portInfo, i) => {
+ const port = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ const y = position.y + position.height * (0.25 + 0.5 * i / Math.max(1, outputPorts.length - 1));
+
+ port.setAttribute('cx', position.x + position.width + 2);
+ port.setAttribute('cy', isNaN(y) ? position.y + position.height * 0.5 : y);
+ port.setAttribute('r', portRadius);
+ port.setAttribute('fill', this.getPortColor(portInfo.type));
+ port.setAttribute('stroke', color.shadow);
+ port.setAttribute('stroke-width', '2');
+ port.setAttribute('class', 'cwl-port cwl-port-output');
+ port.setAttribute('data-port-type', 'output');
+ port.setAttribute('data-port-id', portInfo.id);
+ port.setAttribute('data-port-cwl-type', portInfo.type);
+ port.setAttribute('data-port-name', portInfo.name || portInfo.id);
+ port.setAttribute('data-node-id', nodeId);
+
+ // Tooltip avec informations du port
+ port.setAttribute('title', `${portInfo.name || portInfo.id} (${portInfo.type})`);
+
+ // Gestionnaires d'événements pour les ports
+ this.setupPortInteraction(port, portInfo, nodeId);
+
+ ports.push(port);
+ });
+ }
+
+ return ports;
+ }
+
+ /**
+ * Retrieve node information for icon and style
+ */
+ getNodeInfo(nodeId) {
+ // Check if it's an input
+ if (this.workflow.inputs[nodeId]) {
+ const input = this.workflow.inputs[nodeId];
+ return {
+ id: nodeId,
+ type: 'input',
+ cwlType: input.type,
+ cwlItems: this.extractArrayItemType(input.type),
+ label: input.label || nodeId
+ };
+ }
+
+ // Check if it's a step
+ if (this.workflow.steps[nodeId]) {
+ const step = this.workflow.steps[nodeId];
+ return {
+ id: nodeId,
+ type: 'step',
+ cwlClass: this.extractCwlClass(step.run),
+ label: step.label || nodeId
+ };
+ }
+
+ // Check if it's an output
+ if (this.workflow.outputs[nodeId]) {
+ const output = this.workflow.outputs[nodeId];
+ return {
+ id: nodeId,
+ type: 'output',
+ cwlType: output.type,
+ cwlItems: this.extractArrayItemType(output.type),
+ label: output.label || nodeId
+ };
+ }
+
+ // Fallback
+ return {
+ id: nodeId,
+ type: 'unknown',
+ label: nodeId
+ };
+ }
+
+ /**
+ * Extrait le type d'élément d'un array CWL
+ */
+ extractArrayItemType(type) {
+ if (Array.isArray(type)) {
+ const arrayType = type.find(t => typeof t === 'object' && t.type === 'array');
+ return arrayType ? arrayType.items : null;
+ }
+ if (typeof type === 'object' && type.type === 'array') {
+ return type.items;
+ }
+ return null;
+ }
+
+ /**
+ * Extrait la classe CWL d'un run path
+ */
+ extractCwlClass(runPath) {
+ if (typeof runPath === 'string') {
+ if (runPath.includes('workflow') || runPath.endsWith('.cwl')) {
+ return 'Workflow';
+ }
+ }
+ return 'CommandLineTool'; // Default
+ }
+
+ /**
+ * Get SVG icon for a node type (authentic cwl-svg)
+ */
+ getNodeIcon(node) {
+ // Detect exact type according to cwl-svg logic
+ if (node.type === 'input' && node.cwlType) {
+ if (node.cwlType === 'File' || (node.cwlType === 'array' && node.cwlItems === 'File')) {
+ return '
';
+ } else {
+ return '
';
+ }
+ } else if (node.type === 'output' && node.cwlType) {
+ if (node.cwlType === 'File' || (node.cwlType === 'array' && node.cwlItems === 'File')) {
+ return '
';
+ } else {
+ return '
';
+ }
+ } else if (node.type === 'step' && node.cwlClass) {
+ if (node.cwlClass === 'Workflow') {
+ return '
';
+ } else {
+ return '
';
+ }
+ }
+
+ // Default fallback - tool icon
+ return '
';
+ }
+
+ /**
+ * Create icon element from SVG
+ */
+ createIconElement(iconSvg) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(iconSvg, 'image/svg+xml');
+ const iconElement = document.importNode(doc.documentElement, true);
+ return iconElement;
+ }
+
+ /**
+ * Create ports with authentic cwl-svg style
+ */
+ createNodePortsCWLSVG(nodeId, nodeType, nodeRadius, inputPorts, outputPorts) {
+ const ports = [];
+
+ // Input ports (left side)
+ if (nodeType === 'step' || nodeType === 'output') {
+ inputPorts.forEach((portInfo, index) => {
+ const portMatrix = this.createPortMatrix(inputPorts.length, index, nodeRadius, 'input');
+ const port = this.createPortElement(portInfo, 'input', portMatrix, nodeId);
+ ports.push(port);
+ });
+ }
+
+ // Output ports (right side)
+ if (nodeType === 'input' || nodeType === 'step') {
+ outputPorts.forEach((portInfo, index) => {
+ const portMatrix = this.createPortMatrix(outputPorts.length, index, nodeRadius, 'output');
+ const port = this.createPortElement(portInfo, 'output', portMatrix, nodeId);
+ ports.push(port);
+ });
+ }
+
+ return ports;
+ }
+
+ /**
+ * Calculate transformation matrix for a port (exactly like cwl-svg GraphNode.createPortMatrix)
+ */
+ createPortMatrix(totalPortLength, portIndex, radius, type) {
+ const availableAngle = 140; // Exactly like cwl-svg
+
+ let rotationAngle;
+ if (type === "output") {
+ // For output ports (right side)
+ rotationAngle = (-availableAngle / 2) + ((portIndex + 1) * availableAngle / (totalPortLength + 1));
+ } else {
+ // For input ports (left side)
+ rotationAngle = 180 - (availableAngle / -2) - ((portIndex + 1) * availableAngle / (totalPortLength + 1));
+ }
+
+ // Convert to radians
+ const radians = (rotationAngle * Math.PI) / 180;
+
+ // Calculate position on circle (identical to cwl-svg)
+ const x = radius * Math.cos(radians);
+ const y = radius * Math.sin(radians);
+
+ return { x, y };
+ }
+
+ /**
+ * Crée un élément port avec le style cwl-svg authentique
+ */
+ createPortElement(portInfo, portType, matrix, nodeId) {
+ const portClass = portType === "input" ? "input-port" : "output-port";
+ const label = portInfo.label || portInfo.id;
+
+ // Create port group directly with SVG DOM
+ const portGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ portGroup.setAttribute('class', `port ${portClass} cwl-port cwl-port-${portType}`);
+ portGroup.setAttribute('transform', `matrix(1, 0, 0, 1, ${matrix.x}, ${matrix.y})`);
+ portGroup.setAttribute('data-connection-id', portInfo.connectionId || portInfo.id);
+ portGroup.setAttribute('data-port-id', portInfo.id);
+ portGroup.setAttribute('data-port-type', portType);
+ // Add cx, cy attributes for getPortPosition
+ portGroup.setAttribute('cx', matrix.x);
+ portGroup.setAttribute('cy', matrix.y);
+
+ // Create io-port group
+ const ioPortGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ ioPortGroup.setAttribute('class', 'io-port');
+
+ // Create port circle (visible!)
+ const portCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ portCircle.setAttribute('cx', '0');
+ portCircle.setAttribute('cy', '0');
+ portCircle.setAttribute('r', '5');
+ portCircle.setAttribute('class', 'port-handle');
+
+ // Create port label
+ const labelText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ labelText.setAttribute('x', portType === 'input' ? '-15' : '15');
+ labelText.setAttribute('y', '4');
+ labelText.setAttribute('text-anchor', portType === 'input' ? 'end' : 'start');
+ labelText.setAttribute('class', 'label unselectable');
+ labelText.setAttribute('font-size', '10');
+ labelText.setAttribute('fill', '#666');
+ labelText.textContent = label;
+
+ // Assemble elements
+ ioPortGroup.appendChild(portCircle);
+ portGroup.appendChild(ioPortGroup);
+ portGroup.appendChild(labelText);
+
+ // Event handlers
+ this.setupPortInteraction(portGroup, portInfo, nodeId);
+
+ return portGroup;
+ }
+
+ /**
+ * Configure interaction with a port
+ */
+ setupPortInteraction(portElement, portInfo, nodeId) {
+ // Prevent propagation to parent node
+ portElement.style.cursor = 'pointer';
+
+ // Display port name on hover
+ portElement.addEventListener('mouseenter', (e) => {
+ e.stopPropagation();
+ this.showPortLabel(portElement, portInfo);
+ });
+
+ portElement.addEventListener('mouseleave', (e) => {
+ e.stopPropagation();
+ this.hidePortLabel();
+ });
+
+ // Click on port to display details
+ portElement.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.showPortDetails(nodeId, portInfo);
+ });
+
+ // Allow mousedown/mousemove to propagate so both node-drag and port-drag
+ // handlers (delegated on the workflow root) can start. Previously we
+ // stopped propagation here which prevented the NodeMove plugin from
+ // receiving the events when ports were clicked/dragged.
+ // Keep click/hover propagation blocked to avoid accidental selection.
+ portElement.addEventListener('mousedown', (e) => {
+ // Intentionally do not stop propagation here.
+ });
+
+ portElement.addEventListener('mousemove', (e) => {
+ // Intentionally do not stop propagation here.
+ });
+ }
+
+ /**
+ * Get SVG icon for a port type
+ */
+ getPortTypeIcon(portType, size = 12) {
+ const type = this.formatPortType(portType).toLowerCase();
+
+ // Icons based on cwl-svg
+ const icons = {
+ 'file': `
`,
+ 'directory': `
`,
+ 'string': `
`,
+ 'int': `
`,
+ 'long': `
`,
+ 'float': `
`,
+ 'double': `
`,
+ 'boolean': `
`,
+ 'array': `
`
+ };
+
+ return icons[type] || icons['string']; // Default: string icon
+ }
+
+ /**
+ * Format port type for display
+ */
+ formatPortType(portType) {
+ if (!portType) return '';
+
+ // Si c'est un string simple
+ if (typeof portType === 'string') {
+ return portType;
+ }
+
+ // Si c'est un objet avec type
+ if (typeof portType === 'object') {
+ if (portType.type) {
+ return this.formatPortType(portType.type);
+ }
+ if (Array.isArray(portType)) {
+ // Pour les types union (ex: [null, string])
+ return portType.map(t => this.formatPortType(t)).join('|');
+ }
+ }
+
+ return String(portType);
+ }
+
+ /**
+ * Return appropriate SVG icon for a port type
+ */
+ getPortTypeIcon(portType, isInput = true) {
+ const type = this.formatPortType(portType).toLowerCase();
+
+ // Icons for File types
+ if (type.includes('file')) {
+ if (isInput) {
+ return '
';
+ } else {
+ return '
';
+ }
+ }
+
+ // Icons for Directory types
+ if (type.includes('directory')) {
+ return '
';
+ }
+
+ // Icons for numeric types
+ if (type.includes('int') || type.includes('float') || type.includes('double') || type.includes('long')) {
+ return '
';
+ }
+
+ // Icons for boolean types
+ if (type.includes('boolean')) {
+ return '
';
+ }
+
+ // Icons for string/text types
+ if (type.includes('string') || type.includes('text')) {
+ return '
';
+ }
+
+ // Icons for array types
+ if (type.includes('array') || type.includes('[]')) {
+ return '
';
+ }
+
+ // Default icon for unknown types
+ return '
';
+ }
+
+ /**
+ * Try to infer a basic type based on port name
+ */
+ inferPortType(portName) {
+ const name = portName.toLowerCase();
+
+ if (name.includes('file') || name.includes('input') || name.includes('output')) {
+ return 'File';
+ }
+ if (name.includes('param') || name.includes('value')) {
+ return 'string';
+ }
+ if (name.includes('count') || name.includes('number') || name.includes('size')) {
+ return 'int';
+ }
+ if (name.includes('flag') || name.includes('enable') || name.includes('disable')) {
+ return 'boolean';
+ }
+
+ return null; // No type inferred
+ }
+
+ /**
+ * Display port label on hover
+ */
+ showPortLabel(portElement, portInfo, portType = 'input') {
+ const portName = portInfo.name || portInfo.id;
+ const typeStr = this.formatPortType(portInfo.type);
+ const displayText = typeStr ? `${portName}: ${typeStr}` : portName;
+
+ // Calculer la position absolue du port
+ const nodeGroup = portElement.closest('[data-id]');
+ if (!nodeGroup) return;
+
+ const nodeId = nodeGroup.getAttribute('data-id');
+ const nodeTransform = nodeGroup.getAttribute('transform');
+ const nodeMatches = nodeTransform.match(/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/);
+ if (!nodeMatches) return;
+
+ const nodeX = parseFloat(nodeMatches[1]);
+ const nodeY = parseFloat(nodeMatches[2]);
+ const portCx = parseFloat(portElement.getAttribute('cx'));
+ const portCy = parseFloat(portElement.getAttribute('cy'));
+
+ const absoluteX = nodeX + portCx;
+ const absoluteY = nodeY + portCy;
+
+ // With larger nodes, no need for Y adjustment - ports are well spaced
+
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ label.setAttribute('class', 'port-label');
+ label.setAttribute('data-node-id', nodeId);
+ label.setAttribute('data-port-id', portInfo.id || portInfo.name);
+ label.setAttribute('data-port-type', portType);
+
+ // Calculate actual text width (improved approximation)
+ const baseIconWidth = portInfo.type ? 16 : 0; // Icon width + margin
+ const textWidth = displayText.length * 7; // Approximate text width with type
+ const padding = 4; // Further reduced padding for more compact backgrounds
+ const rectWidth = textWidth + padding + baseIconWidth;
+
+ // Get port index for intelligent spacing
+ const allPortsOfType = nodeGroup.querySelectorAll(`[data-port-type="${portType}"]`);
+ let portIndex = Array.from(allPortsOfType).indexOf(portElement);
+
+ // Get SVG bounds to avoid overflow
+ const viewBox = this.svg.getAttribute('viewBox').split(' ');
+ const svgWidth = parseFloat(viewBox[2]);
+ const svgHeight = parseFloat(viewBox[3]);
+
+ // Label position based on port type - with spacing to avoid covering the port
+ let rectX, textX, textAnchor;
+ if (portType === 'input') {
+ // Labels on left for input ports - spacing to avoid port overlap
+ rectX = absoluteX - rectWidth - 8; // 8px left of port to avoid overlap
+
+ // Prevent overflow on left
+ if (rectX < 10) {
+ rectX = 10;
+ }
+
+ textX = rectX + padding/2;
+ textAnchor = 'start';
+ } else {
+ // Labels on right for output ports - spacing to avoid port overlap
+ rectX = absoluteX + 8; // 8px right of port to avoid overlap
+
+ // Prevent overflow on right
+ if (rectX + rectWidth > svgWidth - 10) {
+ rectX = svgWidth - rectWidth - 10;
+ }
+
+ textX = rectX + padding/2;
+ textAnchor = 'start';
+ }
+
+ // Couleur de fond du visualiseur (#303030)
+ const bgColor = '#303030';
+
+ // Obtenir l'icône pour le type de port
+ const typeIcon = portInfo.type ? this.getPortTypeIcon(portInfo.type) : '';
+ const hasIcon = typeIcon.length > 0;
+
+ // Ajuster la largeur et position si on a une icône
+ let finalRectX = rectX;
+ let finalTextX = textX;
+ let iconX = rectX;
+
+ if (hasIcon) {
+ if (portType === 'input') {
+ // Pour les inputs, icône à gauche du texte
+ iconX = rectX + 2;
+ finalTextX = textX + 16; // Décaler le texte pour laisser place à l'icône
+ } else {
+ // Pour les outputs, icône à droite du texte
+ iconX = rectX + rectWidth - 14; // Position l'icône à droite dans le rectangle
+ }
+ }
+
+ // Fond du label - même couleur que le background du visualiseur
+ const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ labelBg.setAttribute('x', finalRectX);
+ labelBg.setAttribute('y', absoluteY - 8); // Position verticale ajustée pour être moins haute
+ labelBg.setAttribute('width', rectWidth);
+ labelBg.setAttribute('height', 16); // Hauteur réduite pour un background plus compact
+ labelBg.setAttribute('fill', bgColor); // Même couleur que le fond
+ labelBg.setAttribute('opacity', '1'); // Opacité complète pour se fondre dans le background
+ labelBg.setAttribute('rx', '4');
+ // Pas de bordure pour se fondre parfaitement dans le background
+
+ // Texte du label - blanc pour contraster avec #303030
+ const labelText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ labelText.setAttribute('x', finalTextX);
+ labelText.setAttribute('y', absoluteY + 3); // Position ajustée avec la nouvelle hauteur du background
+ labelText.setAttribute('fill', '#ffffff'); // Texte blanc sur fond #303030
+ labelText.setAttribute('font-size', '11');
+ labelText.setAttribute('font-family', 'system-ui, -apple-system, sans-serif');
+ labelText.setAttribute('text-anchor', textAnchor);
+ labelText.textContent = displayText;
+
+ // Ajouter l'icône SVG si disponible
+ let iconElement = null;
+ if (hasIcon) {
+ iconElement = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ iconElement.setAttribute('transform', `translate(${iconX}, ${absoluteY - 6})`);
+ iconElement.innerHTML = typeIcon;
+
+ // Appliquer la couleur blanche à l'icône
+ const iconSvg = iconElement.querySelector('svg');
+ if (iconSvg) {
+ iconSvg.setAttribute('fill', '#ffffff');
+ iconSvg.style.color = '#ffffff';
+ }
+ }
+
+ label.appendChild(labelBg);
+ label.appendChild(labelText);
+ if (iconElement) {
+ label.appendChild(iconElement);
+ }
+
+ this.mainGroup.appendChild(label);
+ }
+
+ /**
+ * Cache le label du port
+ */
+ hidePortLabel() {
+ const existingLabels = this.mainGroup.querySelectorAll('.port-label:not([data-node-id])');
+ existingLabels.forEach(label => label.remove());
+ }
+
+ /**
+ * Affiche les détails d'un port spécifique
+ */
+ showPortDetails(nodeId, portInfo) {
+ // Supprimer les anciens détails
+ this.clearPortDetails();
+
+ const nodeGroup = this.mainGroup.querySelector(`[data-id="${nodeId}"]`);
+ const nodeRect = nodeGroup.querySelector('rect');
+ const nodeX = parseFloat(nodeRect.getAttribute('x'));
+ const nodeY = parseFloat(nodeRect.getAttribute('y'));
+ const nodeWidth = parseFloat(nodeRect.getAttribute('width'));
+
+ // Créer le panneau de détails du port
+ const detailsPanel = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ detailsPanel.setAttribute('class', 'port-details-panel');
+
+ // Fond du panneau
+ const panelWidth = 180;
+ const panelHeight = 80;
+ const panelX = nodeX + nodeWidth + 20;
+ const panelY = nodeY - 10;
+
+ const panelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ panelBg.setAttribute('x', panelX);
+ panelBg.setAttribute('y', panelY);
+ panelBg.setAttribute('width', panelWidth);
+ panelBg.setAttribute('height', panelHeight);
+ panelBg.setAttribute('fill', '#ffffff');
+ panelBg.setAttribute('stroke', '#e5e7eb');
+ panelBg.setAttribute('stroke-width', '1');
+ panelBg.setAttribute('rx', '8');
+ panelBg.setAttribute('filter', 'drop-shadow(2px 2px 8px rgba(0,0,0,0.15))');
+
+ detailsPanel.appendChild(panelBg);
+
+ // Titre
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', panelX + 10);
+ title.setAttribute('y', panelY + 20);
+ title.setAttribute('fill', '#1f2937');
+ title.setAttribute('font-size', '12');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = 'Port Details';
+
+ detailsPanel.appendChild(title);
+
+ // Nom du port
+ const nameLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ nameLabel.setAttribute('x', panelX + 10);
+ nameLabel.setAttribute('y', panelY + 38);
+ nameLabel.setAttribute('fill', '#374151');
+ nameLabel.setAttribute('font-size', '11');
+ nameLabel.textContent = `Nom: ${portInfo.name || portInfo.id}`;
+
+ detailsPanel.appendChild(nameLabel);
+
+ // Type du port
+ const typeLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ typeLabel.setAttribute('x', panelX + 10);
+ typeLabel.setAttribute('y', panelY + 54);
+ typeLabel.setAttribute('fill', '#374151');
+ typeLabel.setAttribute('font-size', '11');
+ typeLabel.textContent = `Type: ${portInfo.type}`;
+
+ detailsPanel.appendChild(typeLabel);
+
+ // Indicateur de couleur du type
+ const colorIndicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ colorIndicator.setAttribute('cx', panelX + panelWidth - 15);
+ colorIndicator.setAttribute('cy', panelY + 15);
+ colorIndicator.setAttribute('r', '6');
+ colorIndicator.setAttribute('fill', this.getPortColor(portInfo.type));
+ colorIndicator.setAttribute('stroke', '#ffffff');
+ colorIndicator.setAttribute('stroke-width', '2');
+
+ detailsPanel.appendChild(colorIndicator);
+
+ // Bouton de fermeture
+ const closeButton = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ closeButton.setAttribute('x', panelX + panelWidth - 12);
+ closeButton.setAttribute('y', panelY + 12);
+ closeButton.setAttribute('fill', '#6b7280');
+ closeButton.setAttribute('font-size', '14');
+ closeButton.setAttribute('font-weight', 'bold');
+ closeButton.setAttribute('cursor', 'pointer');
+ closeButton.textContent = '×';
+
+ closeButton.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.clearPortDetails();
+ });
+
+ detailsPanel.appendChild(closeButton);
+
+ this.mainGroup.appendChild(detailsPanel);
+ }
+
+ /**
+ * Retourne la couleur d'un port selon son type CWL
+ */
+ getPortColor(cwlType) {
+ const typeColors = {
+ 'string': '#22c55e',
+ 'int': '#3b82f6',
+ 'float': '#8b5cf6',
+ 'boolean': '#f59e0b',
+ 'File': '#ef4444',
+ 'Directory': '#84cc16',
+ 'Any': '#6b7280',
+ 'null': '#9ca3af'
+ };
+
+ // Extraire le type de base (supprimer array, optional, etc.)
+ const baseType = this.extractBaseType(cwlType);
+ return typeColors[baseType] || '#64748b';
+ }
+
+ /**
+ * Extrait le type de base d'un type CWL complexe
+ */
+ extractBaseType(cwlType) {
+ if (typeof cwlType === 'string') {
+ return cwlType.replace(/[\[\]?]/g, ''); // Supprimer array et optional
+ }
+
+ if (Array.isArray(cwlType)) {
+ // Union type - prendre le premier non-null
+ const nonNullType = cwlType.find(t => t !== 'null');
+ return nonNullType ? this.extractBaseType(nonNullType) : 'Any';
+ }
+
+ if (typeof cwlType === 'object' && cwlType.type) {
+ return this.extractBaseType(cwlType.type);
+ }
+
+ return 'Any';
+ }
+
+ /**
+ * Récupère les informations détaillées des ports d'entrée
+ */
+ getNodeInputPorts(nodeId) {
+ const ports = [];
+
+ if (this.workflow.steps[nodeId] && this.workflow.steps[nodeId].in) {
+ Object.entries(this.workflow.steps[nodeId].in).forEach(([portId, portDef]) => {
+ ports.push({
+ id: portId,
+ name: portId,
+ type: this.inferPortType(portDef) || 'Any'
+ });
+ });
+ } else if (this.workflow.outputs[nodeId]) {
+ // Port d'entrée pour un output (connecté à une source)
+ ports.push({
+ id: 'input',
+ name: 'input',
+ type: this.workflow.outputs[nodeId].type || 'Any'
+ });
+ }
+
+ return ports;
+ }
+
+ /**
+ * Récupère les informations détaillées des ports de sortie
+ */
+ getNodeOutputPorts(nodeId) {
+ const ports = [];
+
+ if (this.workflow.inputs[nodeId]) {
+ // Input node - un seul port de sortie
+ ports.push({
+ id: 'output',
+ name: this.workflow.inputs[nodeId].label || nodeId,
+ type: this.workflow.inputs[nodeId].type || 'Any'
+ });
+ } else if (this.workflow.steps[nodeId] && this.workflow.steps[nodeId].out) {
+ // Step node - ports basés sur les outputs
+ this.workflow.steps[nodeId].out.forEach(outId => {
+ ports.push({
+ id: outId,
+ name: outId,
+ type: 'Any' // Type à inférer depuis le tool
+ });
+ });
+ }
+
+ console.log(`🔍 Output ports for ${nodeId}:`, ports.map(p => p.id));
+ return ports;
+ }
+
+ /**
+ * Infère le type d'un port à partir de sa définition
+ */
+ inferPortType(portDef) {
+ if (typeof portDef === 'string') {
+ return 'Any';
+ }
+
+ if (portDef && portDef.type) {
+ return portDef.type;
+ }
+
+ if (portDef && portDef.source) {
+ // Type basé sur la source - nécessite résolution
+ return this.resolveSourceType(portDef.source);
+ }
+
+ return 'Any';
+ }
+
+ /**
+ * Résout le type d'une source de connexion
+ */
+ resolveSourceType(source) {
+ // Vérifier dans les inputs du workflow
+ if (this.workflow.inputs[source]) {
+ return this.workflow.inputs[source].type || 'Any';
+ }
+
+ // Vérifier dans les steps (format step/output)
+ const stepMatch = source.match(/^(.+)\/(.+)$/);
+ if (stepMatch) {
+ const [, stepId, outputId] = stepMatch;
+ // Ici on pourrait analyser le tool du step pour connaître le type
+ // Pour l'instant, retourner Any
+ return 'Any';
+ }
+
+ return 'Any';
+ }
+
+ /**
+ * Compte les entrées d'un nœud (méthode de compatibilité)
+ */
+ getNodeInputCount(nodeId) {
+ return this.getNodeInputPorts(nodeId).length;
+ return this.workflow.connections.filter(conn => conn.to.id === nodeId).length || 1;
+ }
+
+ /**
+ * Compte les sorties d'un nœud
+ */
+ getNodeOutputCount(nodeId) {
+ if (this.workflow.steps[nodeId] && this.workflow.steps[nodeId].out) {
+ return this.workflow.steps[nodeId].out.length;
+ }
+ return this.workflow.connections.filter(conn => conn.from.id === nodeId).length || 1;
+ }
+
+ /**
+ * Rend le workflow en SVG
+ */
+ render() {
+ console.log('🔍 Starting render, workflow:', this.workflow);
+ if (!this.workflow) {
+ console.log('❌ No workflow to render');
+ return;
+ }
+
+ // Clean SVG
+ this.mainGroup.innerHTML = '';
+
+ const layout = this.calculateLayout();
+ console.log('🔍 Layout calculated:', layout);
+
+ // Render nodes first
+ this.renderNodes(layout);
+
+ console.log('🔍 Nodes added to DOM:', this.mainGroup.querySelectorAll('.node').length);
+
+ // Render connections after (so they can find ports)
+ this.renderConnections(layout);
+
+ // Ajuster la vue
+ this.fitToContent();
+ }
+
+ /**
+ * Rend les connexions directes entre ports (sans flèches)
+ */
+ renderConnections(layout) {
+ console.log('🔗 Starting connection rendering:', this.workflow.connections.length);
+
+ this.workflow.connections.forEach((conn, index) => {
+ console.log(`🔗 Connection ${index}:`, conn);
+
+ const fromPos = layout.nodes[conn.from.id];
+ const toPos = layout.nodes[conn.to.id];
+
+ console.log(`🔗 Positions - From: ${conn.from.id}:`, fromPos, `To: ${conn.to.id}:`, toPos);
+
+ if (!fromPos || !toPos) {
+ console.log('❌ Position manquante pour la connexion');
+ return;
+ }
+
+ // Calculer positions exactes des ports (authentique cwl-svg)
+ let fromPortPos = this.getPortPosition(conn.from.id, conn.from.port, 'output', fromPos);
+ let toPortPos = this.getPortPosition(conn.to.id, conn.to.port, 'input', toPos);
+
+ console.log(`🔗 Port positions - From: ${conn.from.port}:`, fromPortPos, `To: ${conn.to.port}:`, toPortPos);
+
+ if (!fromPortPos || !toPortPos) {
+ console.log('⚠️ Utilisation des positions de fallback');
+ console.log(`⚠️ From port missing: ${!fromPortPos}, To port missing: ${!toPortPos}`);
+
+ // Fallback intelligent vers les bords des nœuds
+ if (!fromPortPos) {
+ // Pour le nœud source, utiliser le bord droit
+ fromPortPos = { x: fromPos.x + 60, y: fromPos.y };
+ }
+ if (!toPortPos) {
+ // Pour le nœud cible, utiliser le bord gauche
+ toPortPos = { x: toPos.x - 60, y: toPos.y };
+ }
+
+ console.log(`⚠️ Fallback positions - From: (${fromPortPos.x}, ${fromPortPos.y}), To: (${toPortPos.x}, ${toPortPos.y})`);
+ }
+
+ // Générer le chemin avec l'algorithme authentique cwl-svg
+ const pathData = this.makeConnectionPath(
+ fromPortPos.x,
+ fromPortPos.y,
+ toPortPos.x,
+ toPortPos.y,
+ 'right'
+ );
+
+ // Créer le groupe d'edge authentique cwl-svg (avec double path)
+ const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ edgeGroup.setAttribute('class', 'edge');
+ edgeGroup.setAttribute('tabindex', '-1');
+ edgeGroup.setAttribute('data-connection-index', index);
+ edgeGroup.setAttribute('data-source-connection', `${conn.from.id}/${conn.from.port}`);
+ edgeGroup.setAttribute('data-destination-connection', `${conn.to.id}/${conn.to.port}`);
+ edgeGroup.setAttribute('data-source-node', conn.from.id);
+ edgeGroup.setAttribute('data-destination-node', conn.to.id);
+ edgeGroup.setAttribute('data-source-port', conn.from.port);
+ edgeGroup.setAttribute('data-destination-port', conn.to.port);
+
+ // Path outer (authentique cwl-svg)
+ const outerPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ outerPath.setAttribute('class', 'sub-edge outer');
+ outerPath.setAttribute('d', pathData);
+
+ // Path inner (authentique cwl-svg)
+ const innerPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ innerPath.setAttribute('class', 'sub-edge inner');
+ innerPath.setAttribute('d', pathData);
+
+ edgeGroup.appendChild(outerPath);
+ edgeGroup.appendChild(innerPath);
+
+ // Insérer avant les nœuds (ordre Z authentique)
+ const firstNode = this.mainGroup.querySelector('.node');
+ if (firstNode) {
+ this.mainGroup.insertBefore(edgeGroup, firstNode);
+ } else {
+ this.mainGroup.appendChild(edgeGroup);
+ }
+ });
+ }
+
+ /**
+ * Fonction de connexion authentique cwl-svg (adaptée de Workflow.makeConnectionPath)
+ */
+ makeConnectionPath(x1, y1, x2, y2, forceDirection = 'right') {
+ if (!forceDirection) {
+ return `M ${x1} ${y1} C ${(x1 + x2) / 2} ${y1} ${(x1 + x2) / 2} ${y2} ${x2} ${y2}`;
+ } else if (forceDirection === 'right') {
+ const outDir = x1 + Math.abs(x1 - x2) / 2;
+ const inDir = x2 - Math.abs(x1 - x2) / 2;
+ return `M ${x1} ${y1} C ${outDir} ${y1} ${inDir} ${y2} ${x2} ${y2}`;
+ } else if (forceDirection === 'left') {
+ const outDir = x1 - Math.abs(x1 - x2) / 2;
+ const inDir = x2 + Math.abs(x1 - x2) / 2;
+ return `M ${x1} ${y1} C ${outDir} ${y1} ${inDir} ${y2} ${x2} ${y2}`;
+ }
+ }
+
+ /**
+ * Calcule la position exacte d'un port sur un nœud
+ */
+ getPortPosition(nodeId, portId, portType, nodeLayout) {
+ const nodeGroup = this.mainGroup.querySelector(`[data-id="${nodeId}"]`);
+ if (!nodeGroup) {
+ console.log(`❌ Node group not found: ${nodeId}`);
+ return null;
+ }
+
+ const port = nodeGroup.querySelector(`.cwl-port[data-port-id="${portId}"]`);
+ if (!port) {
+ console.log(`❌ Port not found: ${portId} in node ${nodeId}`);
+ // Debug: lister tous les ports disponibles
+ const allPorts = nodeGroup.querySelectorAll('.cwl-port');
+ console.log(`🔍 Available ports in ${nodeId}:`, Array.from(allPorts).map(p => p.getAttribute('data-port-id')));
+ return null;
+ }
+
+ // Récupérer la position du nœud depuis son transform
+ const nodeTransform = nodeGroup.getAttribute('transform');
+ const nodeMatches = nodeTransform.match(/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/);
+ if (!nodeMatches) {
+ console.log(`❌ Could not parse node transform: ${nodeTransform}`);
+ return null;
+ }
+
+ const nodeX = parseFloat(nodeMatches[1]);
+ const nodeY = parseFloat(nodeMatches[2]);
+
+ // Position relative du port depuis son transform (pas cx/cy)
+ let portX = 0, portY = 0;
+ const portTransform = port.getAttribute('transform');
+ if (portTransform) {
+ const portMatches = portTransform.match(/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/);
+ if (portMatches) {
+ portX = parseFloat(portMatches[1]);
+ portY = parseFloat(portMatches[2]);
+ }
+ }
+
+ // Fallback: try cx/cy attributes if transform parsing failed
+ if (portX === 0 && portY === 0) {
+ const cx = port.getAttribute('cx');
+ const cy = port.getAttribute('cy');
+ if (cx && cy) {
+ portX = parseFloat(cx);
+ portY = parseFloat(cy);
+ }
+ }
+
+ // Position absolue du port
+ const absoluteX = nodeX + portX;
+ const absoluteY = nodeY + portY;
+
+ console.log(`✅ Port position found: ${portId} at (${absoluteX}, ${absoluteY}) (node: ${nodeX}, ${nodeY}, port: ${portX}, ${portY})`);
+
+ return { x: absoluteX, y: absoluteY };
+ }
+
+ /**
+ * Render nodes (inputs, steps, outputs)
+ */
+ renderNodes(layout) {
+ console.log('🔍 Starting node rendering. Layout nodes:', Object.keys(layout.nodes));
+
+ // Render inputs
+ Object.values(this.workflow.inputs).forEach(input => {
+ console.log(`🔍 Attempting to render input ${input.id}:`, layout.nodes[input.id]);
+ if (layout.nodes[input.id]) {
+ this.renderNode(input.id, input.label, 'input', layout.nodes[input.id]);
+ } else {
+ console.log(`❌ No layout position for input ${input.id}`);
+ }
+ });
+
+ // Rendre les steps
+ Object.values(this.workflow.steps).forEach(step => {
+ console.log(`🔍 Attempting to render step ${step.id}:`, layout.nodes[step.id]);
+ if (layout.nodes[step.id]) {
+ this.renderNode(step.id, step.label, 'step', layout.nodes[step.id]);
+ } else {
+ console.log(`❌ No layout position for step ${step.id}`);
+ }
+ });
+
+ // Rendre les outputs
+ Object.values(this.workflow.outputs).forEach(output => {
+ console.log(`🔍 Attempting to render output ${output.id}:`, layout.nodes[output.id]);
+ if (layout.nodes[output.id]) {
+ this.renderNode(output.id, output.label, 'output', layout.nodes[output.id]);
+ } else {
+ console.log(`❌ No layout position for output ${output.id}`);
+ }
+ });
+ }
+
+ /**
+ * Render an individual node with the Rabix/CWL-SVG styling
+ */
+ renderNode(id, label, type, position) {
+ if (!position || typeof position.x === 'undefined' || typeof position.y === 'undefined') {
+ // Create a fallback position instead of aborting
+ position = {
+ x: 100 + Math.random() * 200,
+ y: 100 + Math.random() * 100,
+ width: this.options.nodeWidth || 120,
+ height: this.options.nodeHeight || 60
+ };
+ }
+
+ // Calculer le nombre de ports pour dimensionner le nœud
+ const inputPorts = this.getNodeInputPorts(id);
+ const outputPorts = this.getNodeOutputPorts(id);
+ const maxPorts = Math.max(inputPorts.length, outputPorts.length);
+
+ // Calculer le rayon selon le style cwl-svg original - augmenté pour éviter superposition des labels
+ const baseRadius = 35; // GraphNode.radius augmenté
+ const portRadius = 6; // IOPort.radius augmenté pour plus d'espacement
+ const nodeRadius = baseRadius + maxPorts * portRadius;
+
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ group.setAttribute('class', 'node');
+ group.setAttribute('tabindex', '-1');
+ group.classList.add(type);
+ group.setAttribute('data-connection-id', id);
+ group.setAttribute('data-id', id);
+ group.setAttribute('transform', `matrix(1, 0, 0, 1, ${position.x}, ${position.y})`);
+
+ // Créer le groupe principal (core)
+ const coreGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ coreGroup.setAttribute('class', 'core');
+ coreGroup.setAttribute('transform', 'matrix(1, 0, 0, 1, 0, 0)');
+
+ // Cercle extérieur (style cwl-svg original)
+ const outerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ outerCircle.setAttribute('cx', '0');
+ outerCircle.setAttribute('cy', '0');
+ outerCircle.setAttribute('r', nodeRadius.toString());
+ outerCircle.setAttribute('class', 'outer');
+
+ // Cercle intérieur (style cwl-svg original)
+ const innerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ innerCircle.setAttribute('cx', '0');
+ innerCircle.setAttribute('cy', '0');
+ innerCircle.setAttribute('r', (nodeRadius * 0.75).toString());
+ innerCircle.setAttribute('class', 'inner');
+
+ // Icône selon le type (style cwl-svg authentique)
+ const nodeInfo = this.getNodeInfo(id);
+ const iconSvg = this.getNodeIcon(nodeInfo);
+
+ coreGroup.appendChild(outerCircle);
+ coreGroup.appendChild(innerCircle);
+ if (iconSvg) {
+ const iconElement = this.createIconElement(iconSvg);
+ coreGroup.appendChild(iconElement);
+ }
+
+ // Label sous le nœud (style cwl-svg original)
+ const labelText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ labelText.setAttribute('transform', `matrix(${this.labelScale || 1},0,0,${this.labelScale || 1},0,${nodeRadius + 30})`);
+ labelText.setAttribute('class', 'title label');
+ labelText.textContent = label || id;
+
+ // Créer les ports avec positionnement cwl-svg authentique
+ const ports = this.createNodePortsCWLSVG(id, type, nodeRadius, inputPorts, outputPorts);
+
+ // Assembler le nœud
+ group.appendChild(coreGroup);
+ group.appendChild(labelText);
+ ports.forEach(port => group.appendChild(port));
+
+ // Gestionnaires d'événements pour l'interaction
+ this.setupNodeInteraction(group, id);
+
+ this.mainGroup.appendChild(group);
+ }
+
+ /**
+ * Configure l'interaction avec un noeud
+ */
+ setupNodeInteraction(nodeGroup, nodeId) {
+ let isDragging = false;
+ let startPos = { x: 0, y: 0 };
+ let nodeStartPos = { x: 0, y: 0 };
+
+ nodeGroup.addEventListener('mousedown', (e) => {
+ // Ne pas démarrer le drag si on clique sur un port
+ if (e.target.classList.contains('port-handle') || e.target.closest('.port')) {
+ return;
+ }
+
+ e.stopPropagation();
+ isDragging = true;
+ startPos = { x: e.clientX, y: e.clientY };
+
+ // Pour les nœuds circulaires, récupérer la position depuis transform
+ const transform = nodeGroup.getAttribute('transform');
+ const matches = transform.match(/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/);
+ if (matches) {
+ nodeStartPos = {
+ x: parseFloat(matches[1]),
+ y: parseFloat(matches[2])
+ };
+ }
+
+ // Gestion de la sélection
+ if (!e.ctrlKey && !e.metaKey) {
+ this.clearSelection();
+ }
+ this.selectNode(nodeId);
+
+ // Effet visuel de drag
+ nodeGroup.classList.add('dragging');
+ nodeGroup.style.cursor = 'grabbing';
+
+ // Ajouter les listeners de drag au niveau document
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ });
+
+ // Gestion du drag au niveau document pour capturer la souris même hors du nœud
+ const handleMouseMove = (e) => {
+ if (isDragging) {
+ e.preventDefault();
+ const dx = e.clientX - startPos.x;
+ const dy = e.clientY - startPos.y;
+
+ const newX = nodeStartPos.x + dx / (this.zoomLevel || 1);
+ const newY = nodeStartPos.y + dy / (this.zoomLevel || 1);
+
+ this.moveNode(nodeGroup, newX, newY);
+ }
+ };
+
+ const handleMouseUp = () => {
+ if (isDragging) {
+ isDragging = false;
+ nodeGroup.classList.remove('dragging');
+ nodeGroup.style.cursor = 'move';
+
+ // Masquer les labels après le drag si la souris n'est plus sur le nœud
+ setTimeout(() => {
+ if (!nodeGroup.matches(':hover')) {
+ this.hideNodePortLabels(nodeId);
+ }
+ }, 100);
+
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ }
+ };
+
+ // Affichage des ports au survol du nœud
+ nodeGroup.addEventListener('mouseenter', (e) => {
+ if (!isDragging) {
+ this.showNodePortLabels(nodeGroup, nodeId);
+ }
+ });
+
+ nodeGroup.addEventListener('mouseleave', (e) => {
+ // Masquer les labels seulement si on ne drag pas et si on quitte vraiment le nœud
+ if (!isDragging && !e.relatedTarget?.closest(`[data-id="${nodeId}"]`)) {
+ this.hideNodePortLabels(nodeId);
+ }
+ });
+
+ nodeGroup.addEventListener('click', (e) => {
+ // Vérifier si le clic provient d'un port
+ if (e.target.classList.contains('cwl-port')) {
+ return; // Ne pas traiter le clic sur le nœud si c'est un port
+ }
+
+ e.stopPropagation();
+ // Afficher les détails des ports pour les steps
+ if (this.getNodeType(nodeId) === 'step') {
+ this.showStepPortDetails(nodeId);
+ }
+ });
+ }
+
+ /**
+ * Déplace un noeud circulaire vers une nouvelle position
+ */
+ moveNode(nodeGroup, x, y) {
+ const nodeId = nodeGroup.getAttribute('data-id');
+
+ // Mettre à jour le transform du groupe principal
+ nodeGroup.setAttribute('transform', `matrix(1, 0, 0, 1, ${x}, ${y})`);
+
+ // Les ports et autres éléments suivront automatiquement car ils sont dans le groupe
+
+ // Mettre à jour les labels de ports s'ils sont affichés
+ this.updatePortLabelsPosition(nodeGroup, nodeId);
+
+ // Mettre à jour les connexions qui touchent ce nœud
+ this.updateConnectionsForNode(nodeGroup);
+ }
+
+ /**
+ * Met à jour les connexions après déplacement d'un nœud
+ */
+ updateConnectionsForNode(movedNode) {
+ const nodeId = movedNode.getAttribute('data-id');
+ if (!nodeId) return;
+
+ // Trouver toutes les connexions impliquant ce nœud
+ const connections = this.mainGroup.querySelectorAll('.edge');
+
+ connections.forEach(edge => {
+ const sourceNodeId = edge.getAttribute('data-source-node');
+ const destNodeId = edge.getAttribute('data-destination-node');
+
+ // Si cette connexion implique le nœud déplacé
+ if (sourceNodeId === nodeId || destNodeId === nodeId) {
+ const sourcePort = edge.getAttribute('data-source-port');
+ const destPort = edge.getAttribute('data-destination-port');
+
+ // Recalculer les positions des ports
+ let fromPos = this.getPortPosition(sourceNodeId, sourcePort, 'output', null);
+ let toPos = this.getPortPosition(destNodeId, destPort, 'input', null);
+
+ if (!fromPos || !toPos) {
+ // Fallback vers les bords des nœuds
+ const sourceNode = this.mainGroup.querySelector(`[data-id="${sourceNodeId}"]`);
+ const destNode = this.mainGroup.querySelector(`[data-id="${destNodeId}"]`);
+
+ if (sourceNode && destNode) {
+ fromPos = this.getNodeEdgePosition(sourceNode, 'right');
+ toPos = this.getNodeEdgePosition(destNode, 'left');
+ }
+ }
+
+ if (fromPos && toPos) {
+ // Regenerer le chemin de connexion
+ const newPath = this.makeConnectionPath(
+ fromPos.x, fromPos.y,
+ toPos.x, toPos.y,
+ 'right'
+ );
+
+ // Mettre à jour les deux paths (outer et inner)
+ const outerPath = edge.querySelector('.sub-edge.outer');
+ const innerPath = edge.querySelector('.sub-edge.inner');
+
+ if (outerPath) outerPath.setAttribute('d', newPath);
+ if (innerPath) innerPath.setAttribute('d', newPath);
+ }
+ }
+ });
+ }
+
+ /**
+ * Affiche les labels de tous les ports d'un nœud au survol
+ */
+ showNodePortLabels(nodeGroup, nodeId) {
+ // Supprimer les anciens labels
+ this.hideNodePortLabels(nodeId);
+
+ const ports = nodeGroup.querySelectorAll('.cwl-port');
+
+ ports.forEach(port => {
+ const portId = port.getAttribute('data-port-id');
+ const portType = port.getAttribute('data-port-type');
+
+ if (portId) {
+ // Récupérer les informations complètes du port depuis le workflow
+ let portInfo = { id: portId, name: portId };
+
+ if (this.workflow) {
+ // D'abord essayer de récupérer le type depuis les inputs/outputs globaux du workflow
+ if (portType === 'input' && this.workflow.inputs) {
+ const globalInput = this.workflow.inputs[portId];
+ if (globalInput && globalInput.type) {
+ portInfo.type = globalInput.type;
+ }
+ } else if (portType === 'output' && this.workflow.outputs) {
+ const globalOutput = this.workflow.outputs[portId];
+ if (globalOutput && globalOutput.type) {
+ portInfo.type = globalOutput.type;
+ }
+ }
+
+ // Si pas trouvé dans les inputs/outputs globaux, essayer dans les étapes
+ if (!portInfo.type && this.workflow.steps) {
+ const step = this.workflow.steps[nodeId];
+ if (step) {
+ if (portType === 'input' && step.in) {
+ let inputPort = null;
+
+ // step.in peut être un tableau ou un objet
+ if (Array.isArray(step.in)) {
+ inputPort = step.in.find(p => (p.id || p) === portId);
+ } else if (typeof step.in === 'object') {
+ // Si c'est un objet, chercher par clé
+ inputPort = step.in[portId];
+ }
+
+ if (inputPort && inputPort.type) {
+ portInfo.type = inputPort.type;
+ }
+ } else if (portType === 'output' && step.out) {
+ let outputPort = null;
+
+ // step.out peut être un tableau ou un objet
+ if (Array.isArray(step.out)) {
+ outputPort = step.out.find(p => (p.id || p) === portId);
+ } else if (typeof step.out === 'object') {
+ // Si c'est un objet, chercher par clé
+ outputPort = step.out[portId];
+ }
+
+ if (outputPort && outputPort.type) {
+ portInfo.type = outputPort.type;
+ }
+ }
+ }
+ }
+
+ // Si toujours pas de type, essayer d'inférer depuis les connexions
+ if (!portInfo.type && this.workflow.steps) {
+ // Pour les ports d'entrée, chercher d'où viennent les données
+ if (portType === 'input') {
+ const step = this.workflow.steps[nodeId];
+ if (step && step.in) {
+ let connectionSource = null;
+
+ if (Array.isArray(step.in)) {
+ const connection = step.in.find(p => (p.id || p) === portId);
+ connectionSource = connection ? connection.source || connection : null;
+ } else if (typeof step.in === 'object') {
+ connectionSource = step.in[portId];
+ }
+
+ // Si la connexion pointe vers un input global, récupérer son type
+ if (typeof connectionSource === 'string' && this.workflow.inputs) {
+ const globalInput = this.workflow.inputs[connectionSource];
+ if (globalInput && globalInput.type) {
+ portInfo.type = globalInput.type;
+ }
+ }
+ }
+ }
+ }
+
+ // En dernier recours, essayer d'inférer le type depuis le nom
+ if (!portInfo.type) {
+ const inferredType = this.inferPortType(portId);
+ if (inferredType) {
+ portInfo.type = inferredType;
+ }
+ }
+
+ // Debug: log pour voir ce qu'on trouve
+ console.log(`Port ${portId} (${portType}):`, portInfo);
+ }
+
+ this.showPortLabel(port, portInfo, portType);
+ }
+ });
+ }
+
+ /**
+ * Masque tous les labels de ports d'un nœud
+ */
+ hideNodePortLabels(nodeId) {
+ const labels = this.mainGroup.querySelectorAll(`.port-label[data-node-id="${nodeId}"]`);
+ labels.forEach(label => label.remove());
+ }
+
+ /**
+ * Met à jour la position des labels de ports pendant le déplacement
+ */
+ updatePortLabelsPosition(nodeGroup, nodeId) {
+ const labels = this.mainGroup.querySelectorAll(`.port-label[data-node-id="${nodeId}"]`);
+
+ if (labels.length === 0) return; // Pas de labels à mettre à jour
+
+ // Obtenir la nouvelle position du nœud
+ const nodeTransform = nodeGroup.getAttribute('transform');
+ const nodeMatches = nodeTransform.match(/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/);
+ if (!nodeMatches) return;
+
+ const nodeX = parseFloat(nodeMatches[1]);
+ const nodeY = parseFloat(nodeMatches[2]);
+
+ // Mettre à jour la position de chaque label existant (plus efficace que recréer)
+ labels.forEach(label => {
+ const portId = label.getAttribute('data-port-id');
+ const portType = label.getAttribute('data-port-type');
+
+ if (portId && portType) {
+ // Trouver le port correspondant dans le nœud
+ const port = nodeGroup.querySelector(`.cwl-port[data-port-id="${portId}"]`);
+ if (port) {
+ const portCx = parseFloat(port.getAttribute('cx'));
+ const portCy = parseFloat(port.getAttribute('cy'));
+
+ const absoluteX = nodeX + portCx;
+ const absoluteY = nodeY + portCy;
+
+ // Calculer la nouvelle position du label
+ const allPortsOfType = nodeGroup.querySelectorAll(`[data-port-type="${portType}"]`);
+
+ // Récupérer le texte réel du label (qui peut inclure le type)
+ const labelTextElement = label.querySelector('text');
+ const actualText = labelTextElement ? labelTextElement.textContent : portId;
+ const textWidth = actualText.length * 7;
+ const padding = 4; // Padding réduit pour correspondre à showPortLabel
+ const rectWidth = textWidth + padding;
+
+ // Vérifier si le label a une icône
+ const hasIcon = label.querySelector('.port-type-icon') !== null;
+ const iconWidth = hasIcon ? 16 : 0;
+ const totalWidth = rectWidth + iconWidth;
+
+ // Obtenir les limites du SVG pour éviter les débordements
+ const svgBounds = this.svg.getBoundingClientRect();
+ const viewBox = this.svg.getAttribute('viewBox').split(' ');
+ const svgWidth = parseFloat(viewBox[2]);
+ const svgHeight = parseFloat(viewBox[3]);
+
+ let rectX, textX, iconX;
+ if (portType === 'input') {
+ rectX = absoluteX - totalWidth - 8; // Espacement augmenté pour correspondre à showPortLabel
+
+ // Empêcher le débordement à gauche
+ if (rectX < 10) {
+ rectX = 10;
+ }
+
+ if (hasIcon) {
+ iconX = rectX + 2;
+ textX = rectX + padding/2 + iconWidth;
+ } else {
+ textX = rectX + padding/2;
+ }
+ } else {
+ rectX = absoluteX + 8; // Espacement augmenté pour correspondre à showPortLabel
+
+ // Empêcher le débordement à droite
+ if (rectX + totalWidth > svgWidth - 10) {
+ rectX = svgWidth - totalWidth - 10;
+ }
+
+ textX = rectX + padding/2;
+ if (hasIcon) {
+ iconX = rectX + rectWidth + 2;
+ }
+ }
+
+ // Mettre à jour directement les éléments existants
+ const labelBg = label.querySelector('rect');
+ const labelText = label.querySelector('text');
+ const iconElement = label.querySelector('g');
+
+ if (labelBg && labelText) {
+ labelBg.setAttribute('x', rectX);
+ labelBg.setAttribute('y', absoluteY - 8); // Position ajustée pour être moins haute
+ labelBg.setAttribute('width', totalWidth); // Largeur mise à jour pour inclure l'icône
+ labelBg.setAttribute('height', 16); // Hauteur réduite pour correspondre à showPortLabel
+ labelText.setAttribute('x', textX);
+ labelText.setAttribute('y', absoluteY + 3); // Position ajustée avec la nouvelle hauteur
+
+ // Mettre à jour la position de l'icône si elle existe
+ if (iconElement && hasIcon) {
+ iconElement.setAttribute('transform', `translate(${iconX}, ${absoluteY - 6})`);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Calcule la position du bord d'un nœud circulaire
+ */
+ getNodeEdgePosition(nodeGroup, side) {
+ const transform = nodeGroup.getAttribute('transform');
+ const matches = transform.match(/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/);
+
+ if (!matches) return null;
+
+ const nodeX = parseFloat(matches[1]);
+ const nodeY = parseFloat(matches[2]);
+
+ // Calculer le rayon réel du nœud basé sur ses ports (comme dans createNodeCWLSVG)
+ const nodeId = nodeGroup.getAttribute('data-id');
+ const inputPorts = this.getNodeInputPorts(nodeId);
+ const outputPorts = this.getNodeOutputPorts(nodeId);
+ const maxPorts = Math.max(inputPorts.length, outputPorts.length);
+ const radius = 35 + maxPorts * 6; // Même formule que createNodeCWLSVG
+
+ switch (side) {
+ case 'right':
+ return { x: nodeX + radius, y: nodeY };
+ case 'left':
+ return { x: nodeX - radius, y: nodeY };
+ case 'top':
+ return { x: nodeX, y: nodeY - radius };
+ case 'bottom':
+ return { x: nodeX, y: nodeY + radius };
+ default:
+ return { x: nodeX, y: nodeY };
+ }
+ }
+
+ /**
+ * Redessine toutes les connexions (authentique cwl-svg)
+ */
+ redrawConnections() {
+ // Supprimer les anciennes connexions avec classes authentiques
+ const connections = this.mainGroup.querySelectorAll('.edge');
+ connections.forEach(conn => conn.remove());
+
+ // Recalculer les positions actuelles des nœuds circulaires
+ const currentLayout = { nodes: {} };
+ this.mainGroup.querySelectorAll('.node').forEach(node => {
+ const id = node.getAttribute('data-id');
+ const transform = node.getAttribute('transform');
+
+ // Parser la transformation matrix pour obtenir x,y
+ const matrixMatch = transform.match(/matrix\(1,\s*0,\s*0,\s*1,\s*([^,]+),\s*([^)]+)\)/);
+ if (matrixMatch) {
+ currentLayout.nodes[id] = {
+ x: parseFloat(matrixMatch[1]),
+ y: parseFloat(matrixMatch[2]),
+ width: 60, // Approximation pour nœud circulaire
+ height: 60
+ };
+ }
+ });
+
+ // Redessiner les connexions
+ this.renderConnections(currentLayout);
+ }
+
+ /**
+ * Gestion de la sélection des noeuds
+ */
+ selectNode(nodeId) {
+ this.selectedNodes.add(nodeId);
+ const nodeGroup = this.mainGroup.querySelector(`[data-id="${nodeId}"]`);
+ if (nodeGroup) {
+ nodeGroup.classList.add('selected');
+ }
+ // Mettre en évidence les connexions liées
+ this.highlightConnections(nodeId);
+ }
+
+ clearSelection() {
+ this.selectedNodes.clear();
+ this.mainGroup.querySelectorAll('.cwl-node.selected').forEach(node => {
+ node.classList.remove('selected');
+ });
+ // Effacer la mise en évidence des connexions
+ this.clearConnectionHighlights();
+ }
+
+ /**
+ * Met en évidence les connexions liées au noeud sélectionné
+ */
+ highlightConnections(nodeId) {
+ // Effacer les anciennes mises en évidence
+ this.clearConnectionHighlights();
+
+ const connections = this.mainGroup.querySelectorAll('.cwl-connection');
+ const relatedConnections = new Set();
+
+ // Trouver toutes les connexions liées au noeud
+ this.workflow.connections.forEach((conn, index) => {
+ if (conn.from.id === nodeId || conn.to.id === nodeId) {
+ relatedConnections.add(index);
+ }
+ });
+
+ // Appliquer la mise en évidence
+ connections.forEach((connection, index) => {
+ if (relatedConnections.has(index)) {
+ connection.classList.add('highlighted');
+ } else {
+ connection.classList.add('dimmed');
+ }
+ });
+ }
+
+ /**
+ * Efface la mise en évidence des connexions
+ */
+ clearConnectionHighlights() {
+ this.mainGroup.querySelectorAll('.cwl-connection').forEach(connection => {
+ connection.classList.remove('highlighted', 'dimmed');
+ });
+ }
+
+ /**
+ * Supprime tous les panneaux de détails des ports
+ */
+ clearPortDetails() {
+ const existingPanels = this.mainGroup.querySelectorAll('.port-details-panel');
+ existingPanels.forEach(panel => panel.remove());
+ }
+
+ /**
+ * Affiche les détails des ports d'une étape (version pour les nœuds complets)
+ */
+ showStepPortDetails(stepId) {
+ // Supprimer les anciens détails
+ this.clearPortDetails();
+
+ const step = this.workflow.steps[stepId];
+ if (!step) return;
+
+ // Créer le panneau de détails
+ const detailsPanel = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ detailsPanel.setAttribute('class', 'port-details-panel');
+ detailsPanel.setAttribute('data-step-id', stepId);
+
+ // Position du panneau près du nœud
+ const stepNode = this.mainGroup.querySelector(`[data-id="${stepId}"]`);
+ if (!stepNode) {
+ console.error('Step node not found:', stepId);
+ return;
+ }
+
+ // Pour les nœuds circulaires, utiliser la position du transform
+ const transform = stepNode.getAttribute('transform');
+ if (!transform) {
+ console.error('Step transform not found for node:', stepId);
+ return;
+ }
+
+ // Extraire x,y du transform matrix(1, 0, 0, 1, x, y)
+ const matches = transform.match(/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/);
+ if (!matches) {
+ console.error('Could not parse transform for node:', stepId);
+ return;
+ }
+
+ const stepX = parseFloat(matches[1]);
+ const stepY = parseFloat(matches[2]);
+ const stepWidth = 60; // Largeur approximative du cercle pour positionner le panneau
+
+ // Fond du panneau
+ const panelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ const panelWidth = 200;
+ const panelHeight = this.calculatePanelHeight(stepId);
+
+ panelBg.setAttribute('x', stepX + stepWidth + 20);
+ panelBg.setAttribute('y', stepY);
+ panelBg.setAttribute('width', panelWidth);
+ panelBg.setAttribute('height', panelHeight);
+ panelBg.setAttribute('fill', '#ffffff');
+ panelBg.setAttribute('stroke', '#e5e7eb');
+ panelBg.setAttribute('stroke-width', '1');
+ panelBg.setAttribute('rx', '8');
+ panelBg.setAttribute('filter', 'drop-shadow(2px 2px 8px rgba(0,0,0,0.1))');
+
+ detailsPanel.appendChild(panelBg);
+
+ // Titre du panneau
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', stepX + stepWidth + 30);
+ title.setAttribute('y', stepY + 20);
+ title.setAttribute('fill', '#1f2937');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = `Ports: ${step.label || stepId}`;
+
+ detailsPanel.appendChild(title);
+
+ // Détails des ports d'entrée
+ const inputPorts = this.getNodeInputPorts(stepId);
+ let currentY = stepY + 40;
+
+ if (inputPorts.length > 0) {
+ const inputTitle = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ inputTitle.setAttribute('x', stepX + stepWidth + 35);
+ inputTitle.setAttribute('y', currentY);
+ inputTitle.setAttribute('fill', '#4b5563');
+ inputTitle.setAttribute('font-size', '12');
+ inputTitle.setAttribute('font-weight', '600');
+ inputTitle.textContent = 'Inputs:';
+
+ detailsPanel.appendChild(inputTitle);
+ currentY += 20;
+
+ inputPorts.forEach(port => {
+ const portText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ portText.setAttribute('x', stepX + stepWidth + 45);
+ portText.setAttribute('y', currentY);
+ portText.setAttribute('fill', '#6b7280');
+ portText.setAttribute('font-size', '11');
+ portText.textContent = `${port.name}: ${port.type}`;
+
+ // Color type indicator
+ const typeIndicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ typeIndicator.setAttribute('cx', stepX + stepWidth + 40);
+ typeIndicator.setAttribute('cy', currentY - 4);
+ typeIndicator.setAttribute('r', '3');
+ typeIndicator.setAttribute('fill', this.getPortColor(port.type));
+
+ detailsPanel.appendChild(typeIndicator);
+ detailsPanel.appendChild(portText);
+ currentY += 18;
+ });
+ }
+
+ // Output ports details
+ const outputPorts = this.getNodeOutputPorts(stepId);
+ if (outputPorts.length > 0) {
+ currentY += 10;
+ const outputTitle = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ outputTitle.setAttribute('x', stepX + stepWidth + 35);
+ outputTitle.setAttribute('y', currentY);
+ outputTitle.setAttribute('fill', '#4b5563');
+ outputTitle.setAttribute('font-size', '12');
+ outputTitle.setAttribute('font-weight', '600');
+ outputTitle.textContent = 'Outputs:';
+
+ detailsPanel.appendChild(outputTitle);
+ currentY += 20;
+
+ outputPorts.forEach(port => {
+ const portText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ portText.setAttribute('x', stepX + stepWidth + 45);
+ portText.setAttribute('y', currentY);
+ portText.setAttribute('fill', '#6b7280');
+ portText.setAttribute('font-size', '11');
+ portText.textContent = `${port.name}: ${port.type}`;
+
+ // Colorier l'indicateur de type
+ const typeIndicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ typeIndicator.setAttribute('cx', stepX + stepWidth + 40);
+ typeIndicator.setAttribute('cy', currentY - 4);
+ typeIndicator.setAttribute('r', '3');
+ typeIndicator.setAttribute('fill', this.getPortColor(port.type));
+
+ detailsPanel.appendChild(typeIndicator);
+ detailsPanel.appendChild(portText);
+ currentY += 18;
+ });
+ }
+
+ // Bouton de fermeture
+ const closeBtn = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ closeBtn.setAttribute('cx', stepX + stepWidth + panelWidth + 10);
+ closeBtn.setAttribute('cy', stepY + 10);
+ closeBtn.setAttribute('r', '8');
+ closeBtn.setAttribute('fill', '#ef4444');
+ closeBtn.setAttribute('cursor', 'pointer');
+
+ const closeX = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ closeX.setAttribute('x', stepX + stepWidth + panelWidth + 10);
+ closeX.setAttribute('y', stepY + 14);
+ closeX.setAttribute('fill', '#ffffff');
+ closeX.setAttribute('font-size', '12');
+ closeX.setAttribute('text-anchor', 'middle');
+ closeX.setAttribute('cursor', 'pointer');
+ closeX.textContent = '×';
+
+ closeBtn.addEventListener('click', () => this.clearPortDetails());
+ closeX.addEventListener('click', () => this.clearPortDetails());
+
+ detailsPanel.appendChild(closeBtn);
+ detailsPanel.appendChild(closeX);
+
+ this.mainGroup.appendChild(detailsPanel);
+ }
+
+ /**
+ * Calcule la hauteur nécessaire pour le panneau de détails
+ */
+ calculatePanelHeight(stepId) {
+ const inputCount = this.getNodeInputPorts(stepId).length;
+ const outputCount = this.getNodeOutputPorts(stepId).length;
+
+ let height = 50; // Titre + marges
+ if (inputCount > 0) height += 30 + inputCount * 18;
+ if (outputCount > 0) height += 40 + outputCount * 18;
+
+ return Math.max(80, height);
+ }
+
+ /**
+ * Supprime les panneaux de détails des ports
+ */
+ clearPortDetails() {
+ this.mainGroup.querySelectorAll('.port-details-panel').forEach(panel => {
+ panel.remove();
+ });
+ }
+
+ /**
+ * Utilitaires
+ */
+ truncateLabel(label, maxLength) {
+ if (label.length <= maxLength) return label;
+ return label.substring(0, maxLength - 3) + '...';
+ }
+
+ zoom(factor) {
+ this.zoomLevel *= factor;
+ this.zoomLevel = Math.max(0.1, Math.min(5, this.zoomLevel));
+ this.updateTransform();
+ }
+
+ resetView() {
+ this.zoomLevel = 1;
+ this.panX = 0;
+ this.panY = 0;
+ this.updateTransform();
+ this.fitToContent();
+ }
+
+ /**
+ * Réorganise automatiquement le layout
+ */
+ autoLayout() {
+ if (!this.workflow) return;
+
+ const layout = this.calculateLayout();
+
+ // Animer le déplacement des noeuds
+ this.mainGroup.querySelectorAll('.cwl-node').forEach(node => {
+ const id = node.getAttribute('data-id');
+ const targetPos = layout.nodes[id];
+
+ if (targetPos) {
+ // Animation CSS pour un déplacement fluide
+ node.style.transition = 'all 0.5s ease-in-out';
+ this.moveNode(node, targetPos.x, targetPos.y);
+
+ // Retirer la transition après animation
+ setTimeout(() => {
+ node.style.transition = '';
+ }, 500);
+ }
+ });
+
+ // Ajuster la vue après réorganisation
+ setTimeout(() => {
+ this.fitToContent();
+ }, 600);
+ }
+
+ updateTransform() {
+ this.mainGroup.setAttribute('transform',
+ `translate(${this.panX}, ${this.panY}) scale(${this.zoomLevel})`
+ );
+ }
+
+ fitToContent() {
+ if (!this.workflow) return;
+
+ // Calculer la bounding box du contenu
+ const bbox = this.mainGroup.getBBox();
+ if (bbox.width === 0 || bbox.height === 0) return;
+
+ const padding = 50;
+ const scaleX = (this.options.width - padding * 2) / bbox.width;
+ const scaleY = (this.options.height - padding * 2) / bbox.height;
+ const scale = Math.min(scaleX, scaleY, 1);
+
+ this.zoomLevel = scale;
+ this.panX = (this.options.width - bbox.width * scale) / 2 - bbox.x * scale;
+ this.panY = (this.options.height - bbox.height * scale) / 2 - bbox.y * scale;
+
+ this.updateTransform();
+ }
+
+ /**
+ * Télécharge le SVG actuel
+ */
+ downloadSVG() {
+ const svgData = new XMLSerializer().serializeToString(this.svg);
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
+ const url = URL.createObjectURL(svgBlob);
+
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'workflow.svg';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ }
+}
+
+// Export pour utilisation globale
+window.CWLSVGCustom = CWLSVGCustom;
+
+console.log('✅ CWL-SVG Custom loaded successfully');
\ No newline at end of file
diff --git a/public/cwl-demo/cwl-svg-simple-bundle.js b/public/cwl-demo/cwl-svg-simple-bundle.js
new file mode 100644
index 0000000..289623b
--- /dev/null
+++ b/public/cwl-demo/cwl-svg-simple-bundle.js
@@ -0,0 +1,396 @@
+// Self-executing bundle for CWLSVGCustom
+console.log('🚀 Loading simple CWLSVGCustom bundle...');
+
+// Immediate self-execution
+(function() {
+ 'use strict';
+ console.log('📦 Starting CWLSVGCustom bundle execution');
+
+ try {
+ // Check if already available
+ if (typeof window.CWLSVGCustom !== 'undefined') {
+ console.log('✅ CWLSVGCustom already available');
+ return;
+ }
+
+ // Define CWLSVGCustom directly
+ window.CWLSVGCustom = class {
+ constructor(container, options = {}) {
+ this.container = typeof container === 'string' ? document.getElementById(container) : container;
+ this.options = {
+ width: options.width || 900,
+ height: options.height || 600,
+ nodeWidth: options.nodeWidth || 140,
+ nodeHeight: options.nodeHeight || 70,
+ nodeSpacing: options.nodeSpacing || 80,
+ levelSpacing: options.levelSpacing || 180,
+ ...options
+ };
+ this.workflow = null;
+ this.svg = null;
+ this.zoomLevel = 1;
+ this.panX = 0;
+ this.panY = 0;
+ this.draggedNode = null;
+ this.selectedNodes = new Set();
+
+ this.initializeSVG();
+ this.setupEventHandlers();
+ }
+
+ initializeSVG() {
+ console.log('🎨 Initializing CWLSVGCustom SVG interface');
+ this.container.innerHTML = '';
+
+ // Create the main wrapper
+ const wrapper = document.createElement('div');
+ wrapper.className = 'cwl-svg-wrapper';
+ wrapper.style.cssText = `
+ position: relative;
+ width: 100%;
+ height: ${this.options.height}px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ overflow: hidden;
+ background: #fafafa;
+ `;
+
+ // Create controls
+ const controls = document.createElement('div');
+ controls.className = 'cwl-svg-controls';
+ controls.style.cssText = `
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 100;
+ display: flex;
+ gap: 5px;
+ `;
+
+ const createButton = (text, handler, title = '') => {
+ const btn = document.createElement('button');
+ btn.textContent = text;
+ btn.title = title;
+ btn.style.cssText = `
+ padding: 5px 10px;
+ background: #4f46e5;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ `;
+ btn.onclick = handler;
+ return btn;
+ };
+
+ controls.appendChild(createButton('🔍+', () => this.zoom(1.2), 'Zoom in'));
+ controls.appendChild(createButton('🔍−', () => this.zoom(0.8), 'Zoom out'));
+ controls.appendChild(createButton('🔄', () => this.resetView(), 'Reset view'));
+ controls.appendChild(createButton('📐', () => this.autoLayout(), 'Auto-arrange'));
+ controls.appendChild(createButton('💾', () => this.downloadSVG(), 'Download SVG'));
+
+ // Create the SVG
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.svg.setAttribute('width', '100%');
+ this.svg.setAttribute('height', '100%');
+ this.svg.setAttribute('viewBox', `0 0 ${this.options.width} ${this.options.height}`);
+ this.svg.style.cursor = 'grab';
+
+ this.mainGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.mainGroup.setAttribute('class', 'main-group');
+ this.svg.appendChild(this.mainGroup);
+
+ // Add styles
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
+ defs.innerHTML = `
+
+ `;
+
+ this.svg.appendChild(defs);
+ wrapper.appendChild(this.svg);
+ wrapper.appendChild(controls);
+ this.container.appendChild(wrapper);
+ }
+
+ setupEventHandlers() {
+ console.log('🎛️ Configuring event handlers');
+ // Basic handlers for zoom, pan, etc.
+ }
+
+ async loadWorkflow(data, format = 'auto') {
+ console.log('📄 Chargement du workflow CWL', { format });
+ try {
+ let parsed;
+
+ if (format === 'auto') {
+ const trimmed = data.trim();
+ format = (trimmed.startsWith('{') || trimmed.startsWith('[')) ? 'json' : 'yaml';
+ }
+
+ if (format === 'yaml') {
+ if (typeof jsyaml === 'undefined') {
+ throw new Error('js-yaml library not loaded');
+ }
+ parsed = jsyaml.load(data);
+ } else {
+ parsed = JSON.parse(data);
+ }
+
+ this.workflow = this.processWorkflow(parsed);
+ this.render();
+
+ return { success: true, workflow: this.workflow };
+ } catch (error) {
+ console.error('Erreur lors du parsing CWL:', error);
+ return { success: false, error: error.message };
+ }
+ }
+
+ processWorkflow(data) {
+ console.log('⚙️ Traitement du workflow CWL');
+ const workflow = {
+ class: data.class || 'Workflow',
+ id: data.id || 'workflow',
+ label: data.label || data.id || 'Workflow',
+ inputs: {},
+ outputs: {},
+ steps: {},
+ connections: []
+ };
+
+ // Traiter les inputs
+ if (data.inputs) {
+ if (Array.isArray(data.inputs)) {
+ data.inputs.forEach((input, index) => {
+ const id = input.id || `input_${index}`;
+ workflow.inputs[id] = {
+ id: id,
+ type: input.type || 'any',
+ label: input.label || id,
+ doc: input.doc || ''
+ };
+ });
+ } else {
+ Object.entries(data.inputs).forEach(([id, input]) => {
+ workflow.inputs[id] = {
+ id: id,
+ type: input.type || input || 'any',
+ label: input.label || id,
+ doc: input.doc || ''
+ };
+ });
+ }
+ }
+
+ // Traiter les steps
+ if (data.steps) {
+ if (Array.isArray(data.steps)) {
+ data.steps.forEach((step, index) => {
+ const id = step.id || `step_${index}`;
+ workflow.steps[id] = {
+ id: id,
+ label: step.label || id,
+ run: step.run,
+ in: step.in || {},
+ out: step.out || []
+ };
+ });
+ } else {
+ Object.entries(data.steps).forEach(([id, step]) => {
+ workflow.steps[id] = {
+ id: id,
+ label: step.label || id,
+ run: step.run,
+ in: step.in || {},
+ out: step.out || []
+ };
+ });
+ }
+ }
+
+ // Traiter les outputs
+ if (data.outputs) {
+ if (Array.isArray(data.outputs)) {
+ data.outputs.forEach((output, index) => {
+ const id = output.id || `output_${index}`;
+ workflow.outputs[id] = {
+ id: id,
+ type: output.type || 'any',
+ label: output.label || id,
+ outputSource: output.outputSource
+ };
+ });
+ } else {
+ Object.entries(data.outputs).forEach(([id, output]) => {
+ workflow.outputs[id] = {
+ id: id,
+ type: output.type || output || 'any',
+ label: output.label || id,
+ outputSource: output.outputSource
+ };
+ });
+ }
+ }
+
+ return workflow;
+ }
+
+ render() {
+ console.log('🎨 Rendering CWL workflow with authentic cwl-svg style');
+ if (!this.workflow) return;
+
+ this.mainGroup.innerHTML = '';
+
+ // Create a simple visualization for testing
+ const inputCount = Object.keys(this.workflow.inputs).length;
+ const stepCount = Object.keys(this.workflow.steps).length;
+ const outputCount = Object.keys(this.workflow.outputs).length;
+
+ let y = 100;
+ const spacing = 150;
+
+ // Render inputs
+ Object.values(this.workflow.inputs).forEach((input, index) => {
+ this.renderNode(input.id, input.label || input.id, 'input', {
+ x: 100,
+ y: y + index * spacing
+ });
+ });
+
+ // Render steps
+ Object.values(this.workflow.steps).forEach((step, index) => {
+ this.renderNode(step.id, step.label || step.id, 'step', {
+ x: 300,
+ y: y + index * spacing
+ });
+ });
+
+ // Render outputs
+ Object.values(this.workflow.outputs).forEach((output, index) => {
+ this.renderNode(output.id, output.label || output.id, 'output', {
+ x: 500,
+ y: y + index * spacing
+ });
+ });
+ }
+
+ renderNode(id, label, type, position) {
+ const radius = 30;
+
+ // Create the node group
+ const nodeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ nodeGroup.setAttribute('class', 'node');
+ nodeGroup.classList.add(type);
+ nodeGroup.setAttribute('data-id', id);
+ nodeGroup.setAttribute('transform', `translate(${position.x}, ${position.y})`);
+
+ // Outer circle
+ const outerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ outerCircle.setAttribute('cx', '0');
+ outerCircle.setAttribute('cy', '0');
+ outerCircle.setAttribute('r', radius.toString());
+ outerCircle.setAttribute('class', 'outer');
+
+ // Inner circle
+ const innerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ innerCircle.setAttribute('cx', '0');
+ innerCircle.setAttribute('cy', '0');
+ innerCircle.setAttribute('r', (radius * 0.75).toString());
+ innerCircle.setAttribute('class', 'inner');
+
+ // Label
+ const labelText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ labelText.setAttribute('x', '0');
+ labelText.setAttribute('y', radius + 20);
+ labelText.setAttribute('class', 'label');
+ labelText.textContent = label;
+
+ nodeGroup.appendChild(outerCircle);
+ nodeGroup.appendChild(innerCircle);
+ nodeGroup.appendChild(labelText);
+
+ this.mainGroup.appendChild(nodeGroup);
+ }
+
+ // Utility methods
+ zoom(factor) { console.log('🔍 Zoom:', factor); }
+ resetView() { console.log('🔄 Reset view'); }
+ autoLayout() { console.log('📐 Auto layout'); }
+ downloadSVG() { console.log('💾 Download SVG'); }
+ };
+
+ console.log('✅ CWLSVGCustom defined successfully');
+ console.log('📋 Type:', typeof window.CWLSVGCustom);
+
+ // Success signal
+ window.CWLSVGCustomReady = true;
+
+ // Trigger a custom event
+ if (typeof window.dispatchEvent === 'function') {
+ const event = new CustomEvent('CWLSVGCustomReady', {
+ detail: { ready: true }
+ });
+ window.dispatchEvent(event);
+ }
+
+ } catch (error) {
+ console.error('❌ Error initializing CWLSVGCustom:', error);
+ window.CWLSVGCustomError = error;
+ }
+})();
+
+console.log('🏁 Simple CWLSVGCustom bundle completed');
\ No newline at end of file
diff --git a/public/cwl-demo/cwl-svg.bundle.js b/public/cwl-demo/cwl-svg.bundle.js
new file mode 100644
index 0000000..ce52613
--- /dev/null
+++ b/public/cwl-demo/cwl-svg.bundle.js
@@ -0,0 +1 @@
+(()=>{var t={55:function(t,e,o){"use strict";var n,r=this&&this.__extends||(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var o in e)e.hasOwnProperty(o)&&(t[o]=e[o])},function(t,e){function o(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&t[Symbol.iterator],o=0;return e?e.call(t):{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}};Object.defineProperty(e,"__esModule",{value:!0});var s=o(239),a=o(547),l=o(430),c=function(t){function e(e){void 0===e&&(e={});var o=t.call(this)||this;return o.scrollMargin=50,o.movementSpeed=10,o.wheelPrevent=function(t){return t.stopPropagation()},o.boundMoveHandler=o.onMove.bind(o),o.boundMoveStartHandler=o.onMoveStart.bind(o),o.boundMoveEndHandler=o.onMoveEnd.bind(o),o.detachDragListenerFn=void 0,Object.assign(o,e),o}return r(e,t),e.prototype.onEditableStateChange=function(t){t?this.attachDrag():this.detachDrag()},e.prototype.afterRender=function(){this.workflow.editingEnabled&&this.attachDrag()},e.prototype.destroy=function(){this.detachDrag()},e.prototype.registerWorkflow=function(e){t.prototype.registerWorkflow.call(this,e),this.edgePanner=new l.EdgePanner(this.workflow,{scrollMargin:this.scrollMargin,movementSpeed:this.movementSpeed})},e.prototype.detachDrag=function(){"function"==typeof this.detachDragListenerFn&&this.detachDragListenerFn(),this.detachDragListenerFn=void 0},e.prototype.attachDrag=function(){this.detachDrag(),this.detachDragListenerFn=this.workflow.domEvents.drag(".node .core",this.boundMoveHandler,this.boundMoveStartHandler,this.boundMoveEndHandler)},e.prototype.getWorkflowMatrix=function(){return this.workflow.workflow.transform.baseVal.getItem(0).matrix},e.prototype.onMove=function(t,e,o){var n=this,r=this.workflow.scale,i=this.getWorkflowMatrix().e-this.startWorkflowTranslation.x,s=this.getWorkflowMatrix().f-this.startWorkflowTranslation.y;this.edgePanner.triggerCollisionDetection(o.clientX,o.clientY,function(t,e){n.sdx+=t,n.sdy+=e,n.translateNodeBy(n.movingNode,t,e),n.redrawEdges(n.sdx,n.sdy)}),this.sdx=(t-i)/r,this.sdy=(e-s)/r;var a=this.sdx+this.startX,l=this.sdy+this.startY;this.translateNodeTo(this.movingNode,a,l),this.redrawEdges(this.sdx,this.sdy)},e.prototype.onMoveStart=function(t,e){var o=this.workflow.svgRoot;document.addEventListener("mousewheel",this.wheelPrevent,!0);var n=e.parentNode,r=n.transform.baseVal.getItem(0).matrix;this.startX=r.e,this.startY=r.f;var s=n.getAttribute("data-id");this.startWorkflowTranslation={x:this.getWorkflowMatrix().e,y:this.getWorkflowMatrix().f},this.boundingClientRect=o.getBoundingClientRect(),this.movingNode=e.parentNode,this.inputEdges=new Map,this.outputEdges=new Map;var a,l,c=".edge[data-source-node='"+s+"'] .sub-edge",d=".edge[data-destination-node='"+s+"'] .sub-edge",u=o.querySelectorAll([d,c].join(", "));try{for(var h=i(u),f=h.next();!f.done;f=h.next()){var p=f.value,g=p.parentElement.getAttribute("data-destination-node")===s,v=p.getAttribute("d").split(" ").map(Number).filter(function(t){return!isNaN(t)});g?this.inputEdges.set(p,v):this.outputEdges.set(p,v)}}catch(t){a={error:t}}finally{try{f&&!f.done&&(l=h.return)&&l.call(h)}finally{if(a)throw a.error}}},e.prototype.translateNodeBy=function(t,e,o){var n=t.transform.baseVal.getItem(0).matrix;this.translateNodeTo(t,n.e+e,n.f+o)},e.prototype.translateNodeTo=function(t,e,o){t.transform.baseVal.getItem(0).setTranslate(e,o)},e.prototype.redrawEdges=function(t,e){this.inputEdges.forEach(function(o,n){var r=s.Workflow.makeConnectionPath(o[0],o[1],o[6]+t,o[7]+e);n.setAttribute("d",r)}),this.outputEdges.forEach(function(o,n){var r=s.Workflow.makeConnectionPath(o[0]+t,o[1]+e,o[6],o[7]);n.setAttribute("d",r)})},e.prototype.onMoveEnd=function(){this.edgePanner.stop();var t=this.movingNode.getAttribute("data-connection-id"),e=this.workflow.model.findById(t);e.customProps||(e.customProps={});var o=this.movingNode.transform.baseVal.getItem(0).matrix;Object.assign(e.customProps,{"sbg:x":o.e,"sbg:y":o.f}),this.onAfterChange({type:"node-move"}),document.removeEventListener("mousewheel",this.wheelPrevent,!0),delete this.startX,delete this.startY,delete this.movingNode,delete this.inputEdges,delete this.outputEdges,delete this.boundingClientRect,delete this.startWorkflowTranslation},e}(a.PluginBase);e.SVGNodeMovePlugin=c},99:function(t,e,o){"use strict";var n,r=this&&this.__extends||(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var o in e)e.hasOwnProperty(o)&&(t[o]=e[o])},function(t,e){function o(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)});Object.defineProperty(e,"__esModule",{value:!0});var i=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return r(e,t),e.prototype.registerWorkflow=function(e){t.prototype.registerWorkflow.call(this,e),this.svg=e.svgRoot,this.dispose=this.attachWheelListener()},e.prototype.attachWheelListener=function(){var t=this,e=this.onMouseWheel.bind(this);return this.svg.addEventListener("wheel",e,!0),function(){return t.svg.removeEventListener("wheel",e,!0)}},e.prototype.onMouseWheel=function(t){var e=this.workflow.scale,o=e-t.deltaY/500,n=o
e&&this.workflow.maxScaleo||(this.workflow.scaleAtPoint(o,t.clientX,t.clientY),t.stopPropagation())},e.prototype.destroy=function(){"function"==typeof this.dispose&&this.dispose(),this.dispose=void 0},e}(o(547).PluginBase);e.ZoomPlugin=i},159:function(t,e){"use strict";var o=this&&this.__read||function(t,e){var o="function"==typeof Symbol&&t[Symbol.iterator];if(!o)return t;var n,r,i=o.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}return s},n=this&&this.__spread||function(){for(var t=[],e=0;e=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}},r=this&&this.__read||function(t,e){var o="function"==typeof Symbol&&t[Symbol.iterator];if(!o)return t;var n,r,i=o.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}return s},i=this&&this.__spread||function(){for(var t=[],e=0;eu&&(u=n),t.el.setAttribute("transform",a.SVGUtils.matrixToTransformAttr(e)),h[t.connectionID]={x:e.e,y:e.f}}),d+=o.width});var f=Object.keys(o).sort(function(t,e){var o=t.startsWith("out/"),n=t.startsWith("in/"),r=e.startsWith("out/"),i=e.startsWith("in/"),s=t.toLowerCase(),a=e.toLowerCase();return n?i?a.localeCompare(s):1:o?i?-1:r?a.localeCompare(s):1:i||r?-1:a.localeCompare(s)}),p=5*s.GraphNode.radius,g=0,v=0,w=new Map,y=new Map;d=0;var m=Math.max(i.width,3*p);f.forEach(function(t,e){var n=o[t].firstElementChild.getBoundingClientRect();w.set(e,n.width),0===d&&(d-=n.width/2),n.height>g&&(g=n.height),(d+=n.width+30+Math.max(150-n.width,0))>=m&&e=m&&(b+=Math.ceil(i)+30,d=0,g=0,v++)}),this.workflow.redrawEdges(),this.workflow.fitToViewport(),this.onAfterChange(h),this.triggerAfterRender(),h){var S=h[k],P=this.workflow.model.findById(k);P.customProps||(P.customProps={}),Object.assign(P.customProps,{"sbg:x":S.x,"sbg:y":S.y})}return h},t.prototype.calculateColumnSizes=function(t){for(var e={width:0,height:0},o=[],n=1;ne.height&&(e.height=i)}return{columnDimensions:o,distributionArea:e}},t.prototype.distributeNodesIntoColumns=function(t){for(var e={},o=Object.keys(t).sort(function(t,e){return e.localeCompare(t)}),n=[],r=0;r{try{const e=o(256),{WorkflowFactory:n}=e,r=o(558);if("undefined"!=typeof window){window.WorkflowFactory=n,window.Workflow=r.Workflow,window.SVGArrangePlugin=r.SVGArrangePlugin,window.SelectionPlugin=r.SelectionPlugin,window.SVGPortDragPlugin=r.SVGPortDragPlugin,window.SVGNodeMovePlugin=r.SVGNodeMovePlugin,window.ZoomPlugin=r.ZoomPlugin,window.SVGValidatePlugin=r.SVGValidatePlugin,window.SVGEdgeHoverPlugin=r.SVGEdgeHoverPlugin,window.DeletionPlugin=r.DeletionPlugin,window.CWL={WorkflowFactory:window.WorkflowFactory,Workflow:window.Workflow,SVGArrangePlugin:window.SVGArrangePlugin,SelectionPlugin:window.SelectionPlugin,SVGPortDragPlugin:window.SVGPortDragPlugin,SVGNodeMovePlugin:window.SVGNodeMovePlugin,ZoomPlugin:window.ZoomPlugin,SVGValidatePlugin:window.SVGValidatePlugin,SVGEdgeHoverPlugin:window.SVGEdgeHoverPlugin,DeletionPlugin:window.DeletionPlugin},console.log("CWL objects loaded:",Object.keys(window.CWL));try{const t=o(793);t&&t.CWLComponent&&(window.CWLComponent=t.CWLComponent,console.log("✅ CWLComponent exposé dans window.CWLComponent"))}catch(t){console.warn("⚠️ CWLComponent non disponible:",t)}}t.exports=r}catch(t){console.error("Erreur lors du chargement de cwl-svg:",t),"undefined"!=typeof window&&(window.CWL={},window.WorkflowFactory=null,window.Workflow=null)}},237:function(t,e,o){"use strict";var n,r=this&&this.__extends||(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var o in e)e.hasOwnProperty(o)&&(t[o]=e[o])},function(t,e){function o(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),i=this&&this.__read||function(t,e){var o="function"==typeof Symbol&&t[Symbol.iterator];if(!o)return t;var n,r,i=o.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}return s};Object.defineProperty(e,"__esModule",{value:!0});var s=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.boundEdgeEnterFunction=e.onEdgeEnter.bind(e),e.modelListener={dispose:function(){}},e}return r(e,t),e.prototype.afterRender=function(){this.attachEdgeHoverBehavior()},e.prototype.destroy=function(){this.detachEdgeHoverBehavior(),this.modelListener.dispose()},e.prototype.attachEdgeHoverBehavior=function(){this.detachEdgeHoverBehavior(),this.workflow.workflow.addEventListener("mouseenter",this.boundEdgeEnterFunction,!0)},e.prototype.detachEdgeHoverBehavior=function(){this.workflow.workflow.removeEventListener("mouseenter",this.boundEdgeEnterFunction,!0)},e.prototype.onEdgeEnter=function(t){var e=this;if(t.srcElement.classList.contains("edge")){var o,n=t.srcElement,r=function(t){var n=e.workflow.transformScreenCTMtoCanvas(t.clientX,t.clientY);o.setAttribute("x",String(n.x)),o.setAttribute("y",String(n.y-16))}.bind(this),s=function(t){o.remove(),n.removeEventListener("mousemove",r),n.removeEventListener("mouseleave",s)}.bind(this);this.modelListener=this.workflow.model.on("connection.remove",function(t,e){if(o){var n=i(o.getAttribute("data-source-destination").split("$!$"),2),r=n[0],s=n[1];r===t.connectionId&&s===e.connectionId&&o.remove()}});var a=n.getAttribute("data-source-node"),l=n.getAttribute("data-destination-node"),c=n.getAttribute("data-source-port"),d=n.getAttribute("data-destination-port"),u=n.getAttribute("data-source-connection"),h=n.getAttribute("data-destination-connection"),f=a===c?a:a+" ("+c+")",p=l===d?l:l+" ("+d+")",g=this.workflow.transformScreenCTMtoCanvas(t.clientX,t.clientY);(o=document.createElementNS("http://www.w3.org/2000/svg","text")).classList.add("label"),o.classList.add("label-edge"),o.setAttribute("x",String(g.x)),o.setAttribute("y",String(g.y)),o.setAttribute("data-source-destination",u+"$!$"+h),o.innerHTML=f+" → "+p,this.workflow.workflow.appendChild(o),n.addEventListener("mousemove",r),n.addEventListener("mouseleave",s)}},e}(o(547).PluginBase);e.SVGEdgeHoverPlugin=s},239:(t,e,o)=>{"use strict";function n(t){for(var o in t)e.hasOwnProperty(o)||(e[o]=t[o])}Object.defineProperty(e,"__esModule",{value:!0}),n(o(759)),n(o(99)),n(o(187)),n(o(961)),n(o(55)),n(o(319)),n(o(771)),n(o(237)),n(o(705)),n(o(629))},256:(t,e,o)=>{"use strict";o.r(e),o.d(e,{CWLModel:()=>n,CommandInputParameterModel:()=>u,CommandLineToolModel:()=>s,CommandOutputParameterModel:()=>h,ModelFactory:()=>f,WorkflowInputParameterModel:()=>a,WorkflowModel:()=>r,WorkflowOutputParameterModel:()=>l,WorkflowStepInputModel:()=>c,WorkflowStepModel:()=>i,WorkflowStepOutputModel:()=>d,default:()=>p});class n{constructor(t={}){this.id=t.id||"",this.label=t.label||"",this.doc=t.doc||"",this.cwlVersion=t.cwlVersion||"v1.0"}serialize(){return JSON.stringify(this)}}class r extends n{constructor(t={}){super(t),this.class="Workflow",this.inputs=t.inputs||[],this.outputs=t.outputs||[],this.steps=t.steps||[],this.requirements=t.requirements||[],this.hints=t.hints||[]}addStep(t){this.steps.push(t)}getStep(t){return this.steps.find(e=>e.id===t)}}class i extends n{constructor(t={}){super(t),this.run=t.run||"",this.in=t.in||[],this.out=t.out||[],this.scatter=t.scatter||null,this.when=t.when||null}}class s extends n{constructor(t={}){super(t),this.class="CommandLineTool",this.baseCommand=t.baseCommand||[],this.arguments=t.arguments||[],this.inputs=t.inputs||[],this.outputs=t.outputs||[],this.requirements=t.requirements||[],this.hints=t.hints||[]}}class a extends n{constructor(t={}){super(t),this.type=t.type||"string",this.default=t.default||null,this.format=t.format||null,this.streamable=t.streamable||!1}}class l extends n{constructor(t={}){super(t),this.type=t.type||"string",this.outputSource=t.outputSource||null,this.linkMerge=t.linkMerge||null,this.format=t.format||null}}class c extends n{constructor(t={}){super(t),this.source=t.source||null,this.linkMerge=t.linkMerge||null,this.default=t.default||null,this.valueFrom=t.valueFrom||null}}class d extends n{constructor(t={}){super(t),this.id=t.id||""}}class u extends n{constructor(t={}){super(t),this.type=t.type||"string",this.inputBinding=t.inputBinding||null,this.default=t.default||null,this.format=t.format||null}}class h extends n{constructor(t={}){super(t),this.type=t.type||"string",this.outputBinding=t.outputBinding||null,this.format=t.format||null}}class f{static createModel(t){if(!t||"object"!=typeof t)return null;switch(t.class){case"Workflow":return new r(t);case"CommandLineTool":return new s(t);default:return new n(t)}}static createWorkflow(t){return new r(t)}static createCommandLineTool(t){return new s(t)}}const p={CWLModel:n,WorkflowModel:r,WorkflowStepModel:i,CommandLineToolModel:s,WorkflowInputParameterModel:a,WorkflowOutputParameterModel:l,WorkflowStepInputModel:c,WorkflowStepOutputModel:d,CommandInputParameterModel:u,CommandOutputParameterModel:h,ModelFactory:f}},319:function(t,e,o){"use strict";var n,r=this&&this.__extends||(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var o in e)e.hasOwnProperty(o)&&(t[o]=e[o])},function(t,e){function o(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&t[Symbol.iterator],o=0;return e?e.call(t):{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}};Object.defineProperty(e,"__esModule",{value:!0});var s=o(547),a=o(239),l=o(589),c=o(465),d=o(427),u=o(430),h=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.snapRadius=120,e.css={plugin:"__plugin-port-drag",snap:"__port-drag-snap",dragging:"__port-drag-dragging",suggestion:"__port-drag-suggestion"},e.detachDragListenerFn=void 0,e.wheelPrevent=function(t){return t.stopPropagation()},e.ghostX=0,e.ghostY=0,e}return r(e,t),e.prototype.registerWorkflow=function(e){t.prototype.registerWorkflow.call(this,e),this.panner=new u.EdgePanner(this.workflow),this.workflow.svgRoot.classList.add(this.css.plugin)},e.prototype.afterRender=function(){this.workflow.editingEnabled&&this.attachPortDrag()},e.prototype.onEditableStateChange=function(t){t?this.attachPortDrag():this.detachPortDrag()},e.prototype.destroy=function(){this.detachPortDrag()},e.prototype.detachPortDrag=function(){"function"==typeof this.detachDragListenerFn&&this.detachDragListenerFn(),this.detachDragListenerFn=void 0},e.prototype.attachPortDrag=function(){this.detachPortDrag(),this.detachDragListenerFn=this.workflow.domEvents.drag(".port",this.onMove.bind(this),this.onMoveStart.bind(this),this.onMoveEnd.bind(this))},e.prototype.onMove=function(t,e,o,n){var r=this;document.addEventListener("mousewheel",this.wheelPrevent,!0);var i=this.workflow.transformScreenCTMtoCanvas(o.clientX,o.clientY),s=this.workflow.scale,a=(t-this.lastMouseMove.x)/s,l=(e-this.lastMouseMove.y)/s;this.panner.triggerCollisionDetection(o.clientX,o.clientY,function(t,e){r.ghostX+=t,r.ghostY+=e,r.translateGhostNode(r.ghostX,r.ghostY),r.updateEdge(r.portOnCanvas.x,r.portOnCanvas.y,r.ghostX,r.ghostY)});var d=c.Geometry.distance(this.nodeCoords.x,this.nodeCoords.y,i.x,i.y),u=this.findClosestPort(i.x,i.y);this.updateSnapPort(u.portEl,u.distance),this.ghostX+=a,this.ghostY+=l,this.translateGhostNode(this.ghostX,this.ghostY),this.updateGhostNodeVisibility(d,u.distance),this.updateEdge(this.portOnCanvas.x,this.portOnCanvas.y,this.ghostX,this.ghostY),this.lastMouseMove={x:t,y:e}},e.prototype.onMoveStart=function(t,e){this.lastMouseMove={x:0,y:0},this.originPort=e;var o=e.getScreenCTM();this.portOnCanvas=this.workflow.transformScreenCTMtoCanvas(o.e,o.f),this.ghostX=this.portOnCanvas.x,this.ghostY=this.portOnCanvas.y,this.boundingClientRect=this.workflow.svgRoot.getBoundingClientRect();var n=this.workflow.findParent(e).transform.baseVal.getItem(0).matrix;this.nodeCoords={x:n.e,y:n.f};var r=this.workflow.workflow;this.portType=e.classList.contains("input-port")?"input":"output",this.ghostNode=this.createGhostNode(this.portType),r.appendChild(this.ghostNode),this.edgeGroup=d.Edge.spawn(),this.edgeGroup.classList.add(this.css.dragging),r.appendChild(this.edgeGroup),this.workflow.svgRoot.classList.add(this.css.dragging),this.portOrigins=this.getPortCandidateTransformations(e),this.highlightSuggestedPorts(e.getAttribute("data-connection-id"))},e.prototype.onMoveEnd=function(t){document.removeEventListener("mousewheel",this.wheelPrevent,!0),this.panner.stop();var e=this.ghostNode.getAttribute("data-type"),o=!this.ghostNode.classList.contains("hidden"),n=void 0!==this.snapPort,r=o&&"input"===e,i=o&&"output"===e,s=this.originPort.getAttribute("data-connection-id");if(n)this.createEdgeBetweenPorts(this.originPort,this.snapPort);else if(r||i){var a=this.workflow.transformScreenCTMtoCanvas(t.clientX,t.clientY),l={"sbg:x":a.x,"sbg:y":a.y};r?this.workflow.model.createInputFromPort(s,{customProps:l}):this.workflow.model.createOutputFromPort(s,{customProps:l})}this.cleanMemory(),this.cleanStyles()},e.prototype.updateSnapPort=function(t,e){var o=t!==this.snapPort,n=e>this.snapRadius;if(this.snapPort&&(o||n)){var r=this.workflow.findParent(this.snapPort);this.snapPort.classList.remove(this.css.snap),r.classList.remove(this.css.snap),delete this.snapPort}if(!(e>this.snapRadius)){var i=this.originPort.getAttribute("data-connection-id"),s=t.getAttribute("data-connection-id");if(this.findEdge(i,s))delete this.snapPort;else{this.snapPort=t;var a=this.workflow.findParent(t),l="input"===this.portType?"output":"input";t.classList.add(this.css.snap),a.classList.add(this.css.snap),a.classList.add(this.css.snap+"-"+l)}}},e.prototype.updateEdge=function(t,e,o,n){var r,s,l=this.edgeGroup.children;try{for(var c=i(l),d=c.next();!d.done;d=c.next()){var u=d.value,h=a.Workflow.makeConnectionPath(t,e,o,n,"input"===this.portType?"left":"right");u.setAttribute("d",h)}}catch(t){r={error:t}}finally{try{d&&!d.done&&(s=c.return)&&s.call(c)}finally{if(r)throw r.error}}},e.prototype.updateGhostNodeVisibility=function(t,e){var o=this.ghostNode.classList.contains("hidden"),n=t>this.snapRadius&&e>this.snapRadius;n&&o?this.ghostNode.classList.remove("hidden"):n||o||this.ghostNode.classList.add("hidden")},e.prototype.translateGhostNode=function(t,e){this.ghostNode.transform.baseVal.getItem(0).setTranslate(t,e)},e.prototype.getPortCandidateTransformations=function(t){var e,o,n='.node:not([data-connection-id="'+this.workflow.findParent(t).getAttribute("data-connection-id")+'"]) .port.'+("input"===this.portType?"output":"input")+"-port",r=this.workflow.workflow.querySelectorAll(n),s=new Map;try{for(var a=i(r),l=a.next();!l.done;l=a.next()){var d=l.value;s.set(d,c.Geometry.getTransformToElement(d,this.workflow.workflow))}}catch(t){e={error:t}}finally{try{l&&!l.done&&(o=a.return)&&o.call(a)}finally{if(e)throw e.error}}return s},e.prototype.highlightSuggestedPorts=function(t){for(var e=this.workflow.model.gatherValidConnectionPoints(t),o=0;o',e},e.prototype.findClosestPort=function(t,e){var o=void 0,n=1/0;return this.portOrigins.forEach(function(r,i){var s=c.Geometry.distance(t,e,r.e,r.f);s0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}return s},r=this&&this.__values||function(t){var e="function"==typeof Symbol&&t[Symbol.iterator],o=0;return e?e.call(t):{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}};Object.defineProperty(e,"__esModule",{value:!0});var i=o(465),s=o(564),a=o(759),l=function(){function t(){}return t.makeTemplate=function(t,e,o){if(!t.isVisible||"Step"===t.source.type||"Step"===t.destination.type)return"";var r=n(t.source.id.split("/"),3),i=(r[0],r[1]),s=r[2],l=n(t.destination.id.split("/"),3),c=(l[0],l[1]),d=l[2],u=e.querySelector('.node[data-id="'+i+'"] .output-port[data-port-id="'+s+'"] .io-port'),h=e.querySelector('.node[data-id="'+c+'"] .input-port[data-port-id="'+d+'"] .io-port');if(t.source.type!==t.destination.type)if(u){if(h){var f=u.getCTM(),p=h.getCTM(),g=e.transform.baseVal.getItem(0).matrix,v=a.Workflow.makeConnectionPath((f.e-g.e)/f.a,(f.f-g.f)/f.a,(p.e-g.e)/f.a,(p.f-g.f)/f.a);return'\n \n \n \n \n '}console.error("Destination vertex not found for edge "+t.destination.id,t)}else console.error("Source vertex not found for edge "+t.source.id,t);else console.error("Can't update edge between nodes of the same type.",t)},t.spawn=function(t,e){void 0===t&&(t=""),void 0===e&&(e={});var o=document.createElementNS("http://www.w3.org/2000/svg","g"),r=n((e.source||"//").split("/"),3),i=(r[0],r[1]),s=r[2],a=n((e.destination||"//").split("/"),3),l=(a[0],a[1]),c=a[2];return o.classList.add("edge"),i&&o.classList.add(i),l&&o.classList.add(l),o.setAttribute("tabindex","-1"),o.setAttribute("data-destination-node",l),o.setAttribute("data-destination-port",c),o.setAttribute("data-source-port",s),o.setAttribute("data-source-node",i),o.setAttribute("data-source-connection",e.source),o.setAttribute("data-destination-connection",e.destination),o.innerHTML='\n \n \n ',o},t.spawnBetweenConnectionIDs=function(e,o,n){if(o.startsWith("in")){var a=o;o=n,n=a}var l=e.querySelector('.port[data-connection-id="'+o+'"]'),c=e.querySelector('.port[data-connection-id="'+n+'"]'),d=i.Geometry.getTransformToElement(l,e),u=i.Geometry.getTransformToElement(c,e),h=s.IOPort.makeConnectionPath(d.e,d.f,u.e,u.f),f=e.querySelector('.edge[data-source-connection="'+o+'"][data-destination-connection="'+n+'"]');if(f){console.log("Updating existing edge");try{for(var p=r(f.querySelectorAll(".sub-edge")),g=p.next();!g.done;g=p.next())g.value.setAttribute("d",h)}catch(t){v={error:t}}finally{try{g&&!g.done&&(w=p.return)&&w.call(p)}finally{if(v)throw v.error}}return f}var v,w,y=t.spawn(h,{source:o,destination:n}),m=e.querySelector(".node");return e.insertBefore(y,m),y},t.findEdge=function(t,e,o){return t.querySelector('[data-source-connection="'+e+'"][data-destination-connection="'+o+'"]')},t.parseConnectionID=function(t){var e=n((t||"//").split("/"),3);return{side:e[0],stepID:e[1],portID:e[2]}},t}();e.Edge=l},430:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(t,e){void 0===e&&(e={scrollMargin:100,movementSpeed:10}),this.movementSpeed=10,this.scrollMargin=100,this.collision={x:0,y:0},this.panningCallback=function(t,e){};var o=Object.assign({scrollMargin:100,movementSpeed:10},e);this.workflow=t,this.scrollMargin=o.scrollMargin,this.movementSpeed=o.movementSpeed,this.viewportClientRect=this.workflow.svgRoot.getBoundingClientRect()}return t.prototype.triggerCollisionDetection=function(t,e,o){var n={x:0,y:0};this.panningCallback=o;var r=this.viewportClientRect,i=r.left,s=r.right,a=r.top,l=r.bottom;if(i+=this.scrollMargin,s-=this.scrollMargin,a+=this.scrollMargin,l-=this.scrollMargin,ts&&(n.x=t-s),el&&(n.y=e-l),Math.sign(n.x)!==Math.sign(this.collision.x)||Math.sign(n.y)!==Math.sign(this.collision.y)){var c=this.collision;this.collision=n,this.onBoundaryCollisionChange(n,c)}},t.prototype.onBoundaryCollisionChange=function(t,e){this.stop(),0===t.x&&0===t.y||this.start(this.collision)},t.prototype.start=function(t){var e,o=this,n=this.workflow.scale,r=this.workflow.workflow.transform.baseVal.getItem(0).matrix,i=16.6666,s=function(a){var l=a-(e||a);if(e=a,0===l||o.panAnimationFrame){var c=Math.sign(t.x)*o.movementSpeed*l/i,d=Math.sign(t.y)*o.movementSpeed*l/i;r.e-=c,r.f-=d;var u=c/n,h=d/n;o.panningCallback(u,h),o.panAnimationFrame=window.requestAnimationFrame(s)}else e=void 0};this.panAnimationFrame=window.requestAnimationFrame(s)},t.prototype.stop=function(){window.cancelAnimationFrame(this.panAnimationFrame),this.panAnimationFrame=void 0},t}();e.EdgePanner=o},465:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){}return t.distance=function(t,e,o,n){return Math.sqrt(Math.pow(o-t,2)+Math.pow(n-e,2))},t.getTransformToElement=function(t,e){var o=function(t,e,n){if(void 0===e&&(e=0),void 0===n&&(n=0),t.ownerSVGElement){var r=t.transform.baseVal.getItem(0).matrix,i=r.e,s=r.f;return o(t.parentNode,i+e,s+n)}var a=t.createSVGMatrix();return a.e=e,a.f=n,a},n=o(e),r=o(t),i=t.ownerSVGElement.createSVGMatrix();return i.e=n.e-r.e,i.f=n.f-r.f,i.inverse()},t}();e.Geometry=o},510:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){}return t.parse=function(t){var e=document.createElementNS("http://www.w3.org/2000/svg","g");return e.innerHTML=t,e.firstElementChild},t}();e.TemplateParser=o},547:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){}return t.prototype.registerWorkflow=function(t){this.workflow=t},t.prototype.registerOnBeforeChange=function(t){this.onBeforeChange=t},t.prototype.registerOnAfterChange=function(t){this.onAfterChange=t},t.prototype.registerOnAfterRender=function(t){this.onAfterRender=t},t}();e.PluginBase=o},558:(t,e,o)=>{"use strict";function n(t){for(var o in t)e.hasOwnProperty(o)||(e[o]=t[o])}Object.defineProperty(e,"__esModule",{value:!0}),n(o(759)),n(o(669)),n(o(319)),n(o(187)),n(o(237)),n(o(55)),n(o(961)),n(o(771)),n(o(99)),n(o(705)),n(o(629)),n(o(547))},564:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){}return t.makeConnectionPath=function(t,e,o,n,r){return void 0===r&&(r="right"),r?"right"===r?"M "+t+" "+e+" C "+(t+Math.abs(t-o)/2)+" "+e+" "+(o-Math.abs(t-o)/2)+" "+n+" "+o+" "+n:"left"===r?"M "+t+" "+e+" C "+(t-Math.abs(t-o)/2)+" "+e+" "+(o+Math.abs(t-o)/2)+" "+n+" "+o+" "+n:void 0:"M "+t+" "+e+" C "+(t+o)/2+" "+e+" "+(t+o)/2+" "+n+" "+o+" "+n},t.radius=7,t}();e.IOPort=o},567:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(t){this.root=t,this.handlers=new Map}return t.prototype.on=function(){for(var t=this,e=[],o=0;o=3&&"function"==typeof e&&e(n,a,t,i,l.root)}},f=function(t){if(d>=3){c&&"function"==typeof n&&n(t,i,l.root);var e=i.parentNode,o=function(t){t.stopPropagation(),e.removeEventListener("click",o,!0)};e.addEventListener("click",o,!0)}for(var s in c=!1,i=void 0,r=void 0,d=0,document.removeEventListener("mouseup",f),document.removeEventListener("mousemove",h),a)l.root.addEventListener("mouseover",a[s]),l.handlers.get(l.root).mouseover=[],l.handlers.get(l.root).mouseover.push(a[s])};return u},t.prototype.drag=function(t,e,o,n){var r,i,s,a,l=this,c=!1,d=0,u=this.on("mousedown",t,function(t,e,o){return c=!0,r=t,i=e,s=t,t.preventDefault(),a=l.detachHandlers("mouseover"),document.addEventListener("mousemove",h),document.addEventListener("mouseup",f),!1}),h=function(t){if(c){var n=t.screenX-r.screenX,a=t.screenY-r.screenY;3===++d&&"function"==typeof o&&o(s,i,l.root),d>=3&&"function"==typeof e&&e(n,a,t,i,l.root)}},f=function(t){if(d>=3&&(c&&"function"==typeof n&&n(t,i,l.root),i.contains(t.target))){var e=i.parentNode,o=function(t){t.stopPropagation(),e.removeEventListener("click",o,!0)};e.addEventListener("click",o,!0)}for(var s in c=!1,i=void 0,r=void 0,d=0,document.removeEventListener("mouseup",f),document.removeEventListener("mousemove",h),a)l.root.addEventListener("mouseover",a[s]),l.handlers.get(l.root).mouseover=[],l.handlers.get(l.root).mouseover.push(a[s])};return u},t.prototype.hover=function(t,e,o,n){var r=this;void 0===e&&(e=function(){}),void 0===o&&(o=function(){}),void 0===n&&(n=function(){});var i=!1;t.addEventListener("mouseenter",function(e){i=!0,o(e,t,r.root)}),t.addEventListener("mouseleave",function(e){i=!1,n(e,t,r.root)}),t.addEventListener("mousemove",function(o){i&&e(o,t,r.root)})},t.prototype.detachHandlers=function(t,e){e=e||this.root;var o=[];return this.handlers.forEach(function(n,r){if(r.id===e.id&&r===e){var i=function(e){if(e!==t)return"continue";n[e].forEach(function(t){o.push(t),r.removeEventListener(e,t)})};for(var s in n)i(s)}}),delete this.handlers.get(this.root)[t],o},t.prototype.detachAll=function(){this.handlers.forEach(function(t,e){var o=function(o){t[o].forEach(function(t){return e.removeEventListener(o,t)})};for(var n in t)o(n)}),this.handlers.clear()},t}();e.DomEvents=o},589:(t,e,o)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=o(256),r=o(784),i=o(695),s=o(564),a=function(){function t(t,e){this.dataModel=e,this.position={x:0,y:0},this.dataModel=e,Object.assign(this.position,t)}return t.makeIconFragment=function(t){var e="";return t instanceof n.StepModel&&t.run?"Workflow"===t.run.class?e=this.workflowIconSvg:"CommandLineTool"===t.run.class&&(e=this.toolIconSvg):t instanceof n.WorkflowInputParameterModel&&t.type?e="File"===t.type.type||"array"===t.type.type&&"File"===t.type.items?this.fileInputIconSvg:this.inputIconSvg:t instanceof n.WorkflowOutputParameterModel&&t.type&&(e="File"===t.type.type||"array"===t.type.type&&"File"===t.type.items?this.fileOutputIconSvg:this.outputIconSvg),e},t.makeTemplate=function(e,o){void 0===o&&(o=1);var a=~~(e.customProps&&e.customProps["sbg:x"]),l=~~(e.customProps&&e.customProps["sbg:y"]),c="step";e instanceof n.WorkflowInputParameterModel?c="input":e instanceof n.WorkflowOutputParameterModel&&(c="output");var d=(e.in||[]).filter(function(t){return t.isVisible}),u=(e.out||[]).filter(function(t){return t.isVisible}),h=Math.max(d.length,u.length),f=t.radius+h*s.IOPort.radius,p="",g="";e.type&&(p="type-"+e.type.type,e.type.items&&(g="items-"+e.type.items));var v=d.sort(function(t,e){return-t.id.localeCompare(e.id)}).map(function(e,o,n){return t.makePortTemplate(e,"input",i.SVGUtils.matrixToTransformAttr(t.createPortMatrix(n.length,o,f,"input")))}).reduce(function(t,e){return t+e},""),w=u.sort(function(t,e){return-t.id.localeCompare(e.id)}).map(function(e,o,n){return t.makePortTemplate(e,"output",i.SVGUtils.matrixToTransformAttr(t.createPortMatrix(n.length,o,f,"output")))}).reduce(function(t,e){return t+e},"");return'\n \n \n \n \n \n \n '+t.makeIconFragment(e)+'\n \n \n '+r.HtmlUtils.escapeHTML(e.label||e.id)+"\n \n "+v+"\n "+w+"\n \n "},t.makePortTemplate=function(t,e,o){void 0===o&&(o="matrix(1, 0, 0, 1, 0, 0)");var n="input"===e?"input-port":"output-port",i=r.HtmlUtils.escapeHTML(t.label||t.id);return'\n \n \n \n \n '+i+"\n \n \n "},t.createPortMatrix=function(t,e,o,n){var r=140*(e+1)/(t+1)-70;return"input"===n&&(r=250-140*(e+1)/(t+1)),i.SVGUtils.createMatrix().rotate(r).translate(o,0).rotate(-r)},t.patchModelPorts=function(t){var e=[{connectionId:t.connectionId,isVisible:!0,id:t.id}];if(t instanceof n.WorkflowInputParameterModel){var o=Object.create(t);return Object.assign(o,{out:e})}return t instanceof n.WorkflowOutputParameterModel?(o=Object.create(t),Object.assign(o,{in:e})):t},t.radius=30,t.workflowIconSvg='',t.toolIconSvg='',t.fileInputIconSvg='',t.fileOutputIconSvg='',t.inputIconSvg='',t.outputIconSvg='',t}();e.GraphNode=a},629:function(t,e){"use strict";var o=this&&this.__values||function(t){var e="function"==typeof Symbol&&t[Symbol.iterator],o=0;return e?e.call(t):{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}};Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(t){this.containerElements=["svg","g"],this.embeddableStyles={rect:["fill","stroke","stroke-width"],path:["fill","stroke","stroke-width"],circle:["fill","stroke","stroke-width"],line:["stroke","stroke-width"],text:["fill","font-size","text-anchor","font-family"],polygon:["stroke","fill"]},this.svg=t}return t.prototype.dump=function(t){var e=(void 0===t?{padding:50}:t).padding;this.adaptViewbox(this.svg,e);var n=this.svg.cloneNode(!0),r=n.querySelectorAll(".port .label");try{for(var i=o(r),s=i.next();!s.done;s=i.next()){var a=s.value;a.parentNode.removeChild(a)}}catch(t){l={error:t}}finally{try{s&&!s.done&&(c=i.return)&&c.call(i)}finally{if(l)throw l.error}}this.treeShakeStyles(n,this.svg);var l,c,d=n.querySelector(".pan-handle");return d&&n.removeChild(d),(new XMLSerializer).serializeToString(n)},t.prototype.adaptViewbox=function(t,e){void 0===e&&(e=50);var o=t.querySelector(".workflow").getBoundingClientRect(),n=this.getPointOnSVG(o.left,o.top),r=this.svg.viewBox.baseVal;r.x=n.x-e/2,r.y=n.y-e/2,r.height=o.height+e,r.width=o.width+e},t.prototype.getPointOnSVG=function(t,e){var o=this.svg.getScreenCTM(),n=this.svg.createSVGPoint();return n.x=t,n.y=e,n.matrixTransform(o.inverse())},t.prototype.treeShakeStyles=function(t,e){for(var o=t.childNodes,n=e.childNodes,r=0;r0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}return s},r=this&&this.__spread||function(){for(var t=[],e=0;e=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}};Object.defineProperty(e,"__esModule",{value:!0});var s=o(427),a=o(589),l=o(510),c=function(){function t(t,e){this.stepEl=t,this.svg=t.ownerSVGElement,this.model=e}return t.prototype.update=function(){var t=a.GraphNode.makeTemplate(this.model),e=l.TemplateParser.parse(t);this.stepEl.innerHTML=e.innerHTML;var o,n,c=this.svg.querySelectorAll('.edge[data-destination-node="'+this.model.connectionId+'"]'),d=this.svg.querySelectorAll('.edge[data-source-node="'+this.model.connectionId+'"]');try{for(var u=i(r(c,d)),h=u.next();!h.done;h=u.next()){var f=h.value;s.Edge.spawnBetweenConnectionIDs(this.svg.querySelector(".workflow"),f.getAttribute("data-source-connection"),f.getAttribute("data-destination-connection"))}}catch(t){o={error:t}}finally{try{h&&!h.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}console.log("Should redraw input port",c)},t}();e.StepNode=c},695:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){}return t.matrixToTransformAttr=function(t){return"matrix("+t.a+", "+t.b+", "+t.c+", "+t.d+", "+t.e+", "+t.f+")"},t.createMatrix=function(){return document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGMatrix()},t}();e.SVGUtils=o},705:function(t,e,o){"use strict";var n,r=this&&this.__extends||(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var o in e)e.hasOwnProperty(o)&&(t[o]=e[o])},function(t,e){function o(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),i=this&&this.__read||function(t,e){var o="function"==typeof Symbol&&t[Symbol.iterator];if(!o)return t;var n,r,i=o.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}return s};Object.defineProperty(e,"__esModule",{value:!0});var s=o(547),a=o(771),l=o(256),c=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.boundDeleteFunction=e.onDelete.bind(e),e}return r(e,t),e.prototype.afterRender=function(){this.attachDeleteBehavior()},e.prototype.onEditableStateChange=function(t){t?this.attachDeleteBehavior():this.detachDeleteBehavior()},e.prototype.attachDeleteBehavior=function(){this.detachDeleteBehavior(),window.addEventListener("keyup",this.boundDeleteFunction,!0)},e.prototype.detachDeleteBehavior=function(){window.removeEventListener("keyup",this.boundDeleteFunction,!0)},e.prototype.onDelete=function(t){(8===t.which||46===t.which)&&t.target instanceof SVGElement&&this.deleteSelection()},e.prototype.deleteSelection=function(){var t=this,e=this.workflow.getPlugin(a.SelectionPlugin);e&&this.workflow.editingEnabled&&e.getSelection().forEach(function(o,n){if("node"===o){var r=t.workflow.model.findById(n);r instanceof l.StepModel?(t.workflow.model.removeStep(r),e.clearSelection()):r instanceof l.WorkflowInputParameterModel?(t.workflow.model.removeInput(r),e.clearSelection()):r instanceof l.WorkflowOutputParameterModel&&(t.workflow.model.removeOutput(r),e.clearSelection())}else{var s=i(n.split(a.SelectionPlugin.edgePortsDelimiter),2),c=s[0],d=s[1];t.workflow.model.disconnect(c,d),e.clearSelection()}})},e.prototype.destroy=function(){this.detachDeleteBehavior()},e}(s.PluginBase);e.DeletionPlugin=c},759:function(t,e,o){"use strict";var n=this&&this.__read||function(t,e){var o="function"==typeof Symbol&&t[Symbol.iterator];if(!o)return t;var n,r,i=o.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}return s},r=this&&this.__spread||function(){for(var t=[],e=0;e=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}};Object.defineProperty(e,"__esModule",{value:!0});var s=o(567),a=o(159),l=o(427),c=o(589),d=o(669),u=o(510),h=function(){function t(t){var e=this;this.svgID=this.makeID(),this.minScale=.2,this.maxScale=2,this.editingEnabled=!0,this.labelScale=1,this.plugins=[],this.disposers=[],this.pendingFirstDraw=!0,this.isDestroyed=!1,this._scale=1,this.svgRoot=t.svgRoot,this.plugins=t.plugins||[],this.domEvents=new s.DomEvents(this.svgRoot),this.model=t.model,this.editingEnabled=!1!==t.editingEnabled,this.svgRoot.classList.add(this.svgID),this.svgRoot.innerHTML='\n \n \n ',this.workflow=this.svgRoot.querySelector(".workflow"),this.invokePlugins("registerWorkflow",this),this.eventHub=new a.EventHub(["connection.create","app.create.step","app.create.input","app.create.output","beforeChange","afterChange","afterRender","selectionChange"]),this.hookPlugins(),this.draw(t.model),this.eventHub.on("afterRender",function(){return e.invokePlugins("afterRender")})}return Object.defineProperty(t.prototype,"scale",{get:function(){return this._scale},set:function(t){this.workflowBoundingClientRect=this.svgRoot.getBoundingClientRect();var e=(this.workflowBoundingClientRect.right+this.workflowBoundingClientRect.left)/2,o=(this.workflowBoundingClientRect.top+this.workflowBoundingClientRect.bottom)/2;this.scaleAtPoint(t,e,o)},enumerable:!0,configurable:!0}),t.canDrawIn=function(t){return 0!==t.getBoundingClientRect().width},t.makeConnectionPath=function(t,e,o,n,r){return void 0===r&&(r="right"),r?"right"===r?"M "+t+" "+e+" C "+(t+Math.abs(t-o)/2)+" "+e+" "+(o-Math.abs(t-o)/2)+" "+n+" "+o+" "+n:"left"===r?"M "+t+" "+e+" C "+(t-Math.abs(t-o)/2)+" "+e+" "+(o+Math.abs(t-o)/2)+" "+n+" "+o+" "+n:void 0:"M "+t+" "+e+" C "+(t+o)/2+" "+e+" "+(t+o)/2+" "+n+" "+o+" "+n},t.prototype.draw=function(t){var e=this;void 0===t&&(t=this.model),this.assertNotDestroyed("draw");var o=this.workflow.getAttribute("transform");if(this.model!==t||this.pendingFirstDraw){this.pendingFirstDraw=!1,this.model=t;var n=this.model.on("step.change",this.onStepChange.bind(this)),s=this.model.on("step.create",this.onStepCreate.bind(this)),a=this.model.on("step.remove",this.onStepRemove.bind(this)),l=this.model.on("input.create",this.onInputCreate.bind(this)),d=this.model.on("input.remove",this.onInputRemove.bind(this)),u=this.model.on("output.create",this.onOutputCreate.bind(this)),h=this.model.on("output.remove",this.onOutputRemove.bind(this)),f=this.model.on("step.inPort.show",this.onInputPortShow.bind(this)),p=this.model.on("step.inPort.hide",this.onInputPortHide.bind(this)),g=this.model.on("connection.create",this.onConnectionCreate.bind(this)),v=this.model.on("connection.remove",this.onConnectionRemove.bind(this)),w=this.model.on("step.outPort.create",this.onOutputPortCreate.bind(this)),y=this.model.on("step.outPort.remove",this.onOutputPortRemove.bind(this));this.disposers.push(function(){n.dispose(),s.dispose(),a.dispose(),l.dispose(),d.dispose(),u.dispose(),h.dispose(),f.dispose(),p.dispose(),g.dispose(),v.dispose(),w.dispose(),y.dispose()}),this.invokePlugins("afterModelChange")}this.clearCanvas();var m,b,k=r(this.model.steps,this.model.inputs,this.model.outputs).filter(function(t){return t.isVisible}),S="";try{for(var P=i(k),x=P.next();!x.done;x=P.next()){var C=x.value,M=c.GraphNode.patchModelPorts(C);isNaN(parseInt(M.customProps["sbg:x"])),isNaN(parseInt(M.customProps["sbg:y"]));S+=c.GraphNode.makeTemplate(M)}}catch(t){m={error:t}}finally{try{x&&!x.done&&(b=P.return)&&b.call(P)}finally{if(m)throw m.error}}this.workflow.innerHTML+=S,this.redrawEdges(),Array.from(this.workflow.querySelectorAll(".node")).forEach(function(t){e.workflow.appendChild(t)}),this.addEventListeners(),this.workflow.setAttribute("transform",o),this.scaleAtPoint(this.scale),this.invokePlugins("afterRender")},t.prototype.findParent=function(t,e){void 0===e&&(e="node");for(var o=t;o;){if(o.classList.contains(e))return o;o=o.parentElement}},t.prototype.getPlugin=function(t){return this.plugins.find(function(e){return e instanceof t})},t.prototype.on=function(t,e){this.eventHub.on(t,e)},t.prototype.off=function(t,e){this.eventHub.off(t,e)},t.prototype.fitToViewport=function(t){void 0===t&&(t=!1),this.scaleAtPoint(1),Object.assign(this.workflow.transform.baseVal.getItem(0).matrix,{e:0,f:0});var e=this.svgRoot.getBoundingClientRect(),o=this.workflow.getBoundingClientRect();if(0===e.width||0===e.height)throw new Error("Cannot fit workflow to the area that has no visible viewport.");var n=o.height/(e.height-100),r=o.width/(e.width-100),i=Math.max(n,r),s=Math.min(this.scale/i,1);t||(s=Math.max(s,this.minScale)),this.scaleAtPoint(s);var a=this.workflow.getBoundingClientRect(),l=e.top-a.top+Math.abs(e.height-a.height)/2,c=e.left-a.left+Math.abs(e.width-a.width)/2,d=this.workflow.transform.baseVal.getItem(0).matrix;d.e+=c,d.f+=l},t.prototype.redrawEdges=function(){var t=this,e=new Set;Array.from(this.workflow.querySelectorAll(".edge")).forEach(function(t){if(t.classList.contains("highlighted")){var o=t.attributes["data-source-connection"].value+t.attributes["data-destination-connection"].value;e.add(o)}t.remove()});var o=this.model.connections.map(function(o){var n=o.source.id+o.destination.id,r=e.has(n)?"highlighted":"";return l.Edge.makeTemplate(o,t.workflow,r)}).reduce(function(t,e){return t+e},"");this.workflow.innerHTML=o+this.workflow.innerHTML},t.prototype.scaleAtPoint=function(t,e,o){void 0===t&&(t=1),void 0===e&&(e=0),void 0===o&&(o=0),this._scale=t,this.labelScale=1+(1-this._scale)/(2*this._scale);var n=this.workflow.transform.baseVal.getItem(0).matrix,r=this.transformScreenCTMtoCanvas(e,o);n.e+=n.a*r.x,n.f+=n.a*r.y,n.a=n.d=t,n.e-=t*r.x,n.f-=t*r.y;var s,a,l=this.workflow.querySelectorAll(".node .label");try{for(var c=i(l),d=c.next();!d.done;d=c.next()){var u=d.value.transform.baseVal.getItem(0).matrix;Object.assign(u,{a:this.labelScale,d:this.labelScale})}}catch(t){s={error:t}}finally{try{d&&!d.done&&(a=c.return)&&a.call(c)}finally{if(s)throw s.error}}},t.prototype.transformScreenCTMtoCanvas=function(t,e){var o=this.svgRoot,n=this.workflow.getScreenCTM(),r=o.createSVGPoint();r.x=t,r.y=e;var i=r.matrixTransform(n.inverse());return{x:i.x,y:i.y}},t.prototype.enableEditing=function(t){this.invokePlugins("onEditableStateChange",t),this.editingEnabled=t},t.prototype.destroy=function(){this.svgRoot.classList.remove(this.svgID),this.clearCanvas(),this.eventHub.empty(),this.invokePlugins("destroy");try{for(var t=i(this.disposers),e=t.next();!e.done;e=t.next())(0,e.value)()}catch(t){o={error:t}}finally{try{e&&!e.done&&(n=t.return)&&n.call(t)}finally{if(o)throw o.error}}var o,n;this.isDestroyed=!0},t.prototype.resetTransform=function(){this.workflow.setAttribute("transform","matrix(1,0,0,1,0,0)"),this.scaleAtPoint()},t.prototype.assertNotDestroyed=function(t){if(this.isDestroyed)throw new Error("Cannot call the "+t+" method on a destroyed graph. Destroying this object removes DOM listeners, and reusing it would result in unexpected things not working. Instead, you can just call the “draw” method with a different model, or create a new Workflow object.")},t.prototype.addEventListeners=function(){var t,e,o,n;this.domEvents.drag(".pan-handle",function(t,r){n.e=e+t,n.f=o+r},function(r,i,s){t=s.querySelector(".workflow"),n=t.transform.baseVal.getItem(0).matrix,e=n.e,o=n.f},function(){t=void 0,n=void 0})},t.prototype.clearCanvas=function(){this.domEvents.detachAll(),this.workflow.innerHTML="",this.workflow.setAttribute("transform","matrix(1,0,0,1,0,0)"),this.workflow.setAttribute("class","workflow")},t.prototype.hookPlugins=function(){var t=this;this.plugins.forEach(function(e){e.registerOnBeforeChange(function(e){t.eventHub.emit("beforeChange",e)}),e.registerOnAfterChange(function(e){t.eventHub.emit("afterChange",e)}),e.registerOnAfterRender(function(e){t.eventHub.emit("afterRender",e)})})},t.prototype.invokePlugins=function(t){for(var e=[],o=1;o=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}},s=this&&this.__read||function(t,e){var o="function"==typeof Symbol&&t[Symbol.iterator];if(!o)return t;var n,r,i=o.call(t),s=[];try{for(;(void 0===e||e-- >0)&&!(n=i.next()).done;)s.push(n.value)}catch(t){r={error:t}}finally{try{n&&!n.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}return s};Object.defineProperty(e,"__esModule",{value:!0});var a=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.selection=new Map,e.cleanups=[],e.selectionChangeCallbacks=[],e.css={selected:"__selection-plugin-selected",highlight:"__selection-plugin-highlight",fade:"__selection-plugin-fade",plugin:"__plugin-selection"},e}return r(e,t),e.prototype.registerWorkflow=function(e){var o=this;t.prototype.registerWorkflow.call(this,e),this.svg=this.workflow.svgRoot,this.svg.classList.add(this.css.plugin);var n=this.onClick.bind(this);this.svg.addEventListener("click",n),this.cleanups.push(function(){return o.svg.removeEventListener("click",n)})},e.prototype.afterRender=function(){this.restoreSelection()},e.prototype.afterModelChange=function(){"function"==typeof this.detachModelEvents&&this.detachModelEvents(),this.detachModelEvents=this.bindModelEvents()},e.prototype.destroy=function(){this.detachModelEvents(),this.detachModelEvents=void 0,this.svg.classList.remove(this.css.plugin);try{for(var t=i(this.cleanups),e=t.next();!e.done;e=t.next())(0,e.value)()}catch(t){o={error:t}}finally{try{e&&!e.done&&(n=t.return)&&n.call(t)}finally{if(o)throw o.error}}var o,n},e.prototype.clearSelection=function(){var t,e,o,n,r=this.svg.querySelectorAll("."+this.css.selected),s=this.svg.querySelectorAll("."+this.css.highlight);try{for(var a=i(r),l=a.next();!l.done;l=a.next())l.value.classList.remove(this.css.selected)}catch(e){t={error:e}}finally{try{l&&!l.done&&(e=a.return)&&e.call(a)}finally{if(t)throw t.error}}try{for(var c=i(s),d=c.next();!d.done;d=c.next())d.value.classList.remove(this.css.highlight)}catch(t){o={error:t}}finally{try{d&&!d.done&&(n=c.return)&&n.call(c)}finally{if(o)throw o.error}}this.svg.classList.remove(this.css.fade),this.selection.clear(),this.emitChange(null)},e.prototype.getSelection=function(){return this.selection},e.prototype.registerOnSelectionChange=function(t){this.selectionChangeCallbacks.push(t)},e.prototype.selectStep=function(t){var e='[data-connection-id="'+t+'"]',o=this.svg.querySelector(e);o&&this.materializeClickOnElement(o)},e.prototype.bindModelEvents=function(){var t,e,o=this,n=function(){return o.restoreSelection()},r=[],s=function(t){var e=a.workflow.model.on(t,n);r.push(function(){return e.dispose()})},a=this;try{for(var l=i(["connection.create","connection.remove"]),c=l.next();!c.done;c=l.next())s(c.value)}catch(e){t={error:e}}finally{try{c&&!c.done&&(e=l.return)&&e.call(l)}finally{if(t)throw t.error}}return function(){return r.forEach(function(t){return t()})}},e.prototype.restoreSelection=function(){var t=this;this.selection.forEach(function(o,n){if("node"===o){var r=t.svg.querySelector('[data-connection-id="'+n+'"]');r&&t.selectNode(r)}else if("edge"===o){var i=s(n.split(e.edgePortsDelimiter),2),a='[data-source-connection="'+i[0]+'"][data-destination-connection="'+i[1]+'"]',l=t.svg.querySelector(a);l&&t.selectEdge(l)}})},e.prototype.onClick=function(t){var e=t.target;this.clearSelection(),this.materializeClickOnElement(e)},e.prototype.materializeClickOnElement=function(t){var o;if(o=this.workflow.findParent(t,"node"))this.selectNode(o),this.selection.set(o.getAttribute("data-connection-id"),"node"),this.emitChange(o);else if(o=this.workflow.findParent(t,"edge")){this.selectEdge(o);var n=[o.getAttribute("data-source-connection"),e.edgePortsDelimiter,o.getAttribute("data-destination-connection")].join("");this.selection.set(n,"edge"),this.emitChange(n)}},e.prototype.selectNode=function(t){t.parentElement.appendChild(t),this.svg.classList.add(this.css.fade),t.classList.add(this.css.selected),t.classList.add(this.css.highlight);var e,o,n,r,s=t.getAttribute("data-id"),a=this.svg.querySelectorAll('.edge[data-source-node="'+s+'"],.edge[data-destination-node="'+s+'"]'),l=this.svg.getElementsByClassName("node")[0];try{for(var c=i(a),d=c.next();!d.done;d=c.next()){var u=d.value;u.classList.add(this.css.highlight),this.workflow.workflow.insertBefore(u,l);var h=u.getAttribute("data-source-node"),f=u.getAttribute("data-destination-node"),p=this.svg.querySelectorAll('.node[data-id="'+h+'"],.node[data-id="'+f+'"]');try{for(var g=i(p),v=g.next();!v.done;v=g.next())v.value.classList.add(this.css.highlight)}catch(t){n={error:t}}finally{try{v&&!v.done&&(r=g.return)&&r.call(g)}finally{if(n)throw n.error}}}}catch(t){e={error:t}}finally{try{d&&!d.done&&(o=c.return)&&o.call(c)}finally{if(e)throw e.error}}},e.prototype.selectEdge=function(t){t.classList.add(this.css.highlight),t.classList.add(this.css.selected);var e,o,n=t.getAttribute("data-source-node"),r=t.getAttribute("data-destination-node"),s=t.getAttribute("data-source-port"),a='.node[data-id="'+r+'"] .input-port[data-port-id="'+t.getAttribute("data-destination-port")+'"]',l='.node[data-id="'+n+'"] .output-port[data-port-id="'+s+'"]',c=this.svg.querySelectorAll(a+", "+l);try{for(var d=i(c),u=d.next();!u.done;u=d.next())u.value.classList.add(this.css.highlight)}catch(t){e={error:t}}finally{try{u&&!u.done&&(o=d.return)&&o.call(d)}finally{if(e)throw e.error}}},e.prototype.emitChange=function(t){try{for(var e=i(this.selectionChangeCallbacks),o=e.next();!o.done;o=e.next())(0,o.value)(t)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=e.return)&&r.call(e)}finally{if(n)throw n.error}}var n,r},e.edgePortsDelimiter="$!$",e}(o(547).PluginBase);e.SelectionPlugin=a},784:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){}return t.escapeHTML=function(e){return String(e).replace(/[&<>"'\/]/g,function(e){return t.entityMap[e]})},t.entityMap={"&":"&","<":"<",">":">",'"':""",'""':""","'":"'","/":"/"},t}();e.HtmlUtils=o},793:()=>{"undefined"!=typeof window&&(window.CWLComponent=class{constructor(t={}){this.container=t.container,this.cwlData=t.cwl||null,this.cwlUrl=t.cwlUrl||null,this.editingEnabled=t.editingEnabled||!1,this.plugins=t.plugins||[],this.width=t.width||"100%",this.height=t.height||"600px",this.workflow=null,this.cwlModel=null,this.svgElement=null,this.isInitialized=!1,this.onWorkflowChanged=t.onWorkflowChanged||(()=>{}),this.onSelectionChanged=t.onSelectionChanged||(()=>{}),this.onError=t.onError||(()=>{}),this.init()}async init(){try{if(console.log("🎨 Initialisation du composant CWL..."),!this.container)throw new Error("Container requis pour le composant CWL");this.createSVGElement(),this.cwlUrl?await this.loadCwlFromUrl():this.cwlData&&await this.loadCwlFromData(this.cwlData),this.isInitialized=!0,console.log("✅ Composant CWL initialisé avec succès")}catch(t){console.error("❌ Erreur lors de l'initialisation du composant CWL:",t),this.onError(t)}}createSVGElement(){this.container.innerHTML="",this.svgElement=document.createElementNS("http://www.w3.org/2000/svg","svg"),this.svgElement.classList.add("cwl-workflow"),this.svgElement.style.width=this.width,this.svgElement.style.height=this.height,this.svgElement.style.border="1px solid #e5e7eb",this.svgElement.style.borderRadius="8px",this.container.appendChild(this.svgElement)}async loadCwlFromUrl(){try{console.log(`📥 Chargement CWL depuis URL: ${this.cwlUrl}`);const t=await fetch(this.cwlUrl,{headers:{Accept:"application/json","Content-Type":"application/json"}});if(!t.ok)throw new Error(`Erreur HTTP ${t.status}: ${t.statusText}`);const e=await t.json();await this.loadCwlFromData(e)}catch(t){throw console.error("❌ Erreur lors du chargement CWL depuis URL:",t),this.showError(`Impossible de charger le workflow depuis ${this.cwlUrl}`),t}}async loadCwlFromData(t){try{if(console.log("🔄 Traitement des données CWL...",t),!t)throw new Error("Données CWL vides ou invalides");this.cwlData=t,await this.createWorkflow()}catch(t){throw console.error("❌ Erreur lors du traitement des données CWL:",t),this.showError("Erreur lors du traitement des données CWL"),t}}async createWorkflow(){try{if(console.log("🏗️ Création du workflow cwl-svg..."),!this.isCwlSvgAvailable())throw new Error("cwl-svg non disponible");this.cwlModel=window.WorkflowFactory.from(this.cwlData),console.log("📋 Modèle CWL créé:",this.cwlModel);const t=[...this.getDefaultPluginInstances(),...this.plugins];this.workflow=new window.Workflow({editingEnabled:this.editingEnabled,model:this.cwlModel,svgRoot:this.svgElement,plugins:t}),this.setupWorkflowHandlers(),this.arrangeWorkflow(),console.log("✅ Workflow créé avec succès"),this.onWorkflowChanged(this.workflow)}catch(t){console.error("❌ Erreur lors de la création du workflow:",t),await this.createFallbackVisualization()}}isCwlSvgAvailable(){if("undefined"==typeof window)return!1;const t=window.WorkflowFactory&&"function"==typeof window.WorkflowFactory.from,e=window.Workflow&&"function"==typeof window.Workflow,o=window.SVGArrangePlugin||window.SelectionPlugin;return console.log("🔍 Détection cwl-svg:",{WorkflowFactory:!!window.WorkflowFactory,Workflow:!!window.Workflow,SVGArrangePlugin:!!window.SVGArrangePlugin,SelectionPlugin:!!window.SelectionPlugin,hasWorkflowFactory:t,hasWorkflow:e,hasBasicPlugins:o}),t&&e}getDefaultPluginInstances(){const t=[];try{window.SVGArrangePlugin&&(t.push(new window.SVGArrangePlugin),console.log("✅ SVGArrangePlugin ajouté")),window.SelectionPlugin&&(t.push(new window.SelectionPlugin),console.log("✅ SelectionPlugin ajouté")),window.SVGNodeMovePlugin&&(t.push(new window.SVGNodeMovePlugin),console.log("✅ SVGNodeMovePlugin ajouté")),window.SVGEdgeHoverPlugin&&(t.push(new window.SVGEdgeHoverPlugin),console.log("✅ SVGEdgeHoverPlugin ajouté")),window.SVGPortDragPlugin&&(t.push(new window.SVGPortDragPlugin),console.log("✅ SVGPortDragPlugin ajouté")),window.ZoomPlugin&&(t.push(new window.ZoomPlugin),console.log("✅ ZoomPlugin ajouté"))}catch(t){console.warn("⚠️ Erreur lors de la création des plugins:",t)}return console.log(`🔌 Plugins créés: ${t.length}`),t}setupWorkflowHandlers(){try{const t=this.workflow.getPlugin("SelectionPlugin");t&&(t.registerOnSelectionChange(t=>{if(t){const e=t.getAttribute("data-connection-id"),o=this.workflow.model.findById(e);this.onSelectionChanged(o)}}),console.log("🎯 Gestionnaire de sélection configuré")),this.workflow.on&&(this.workflow.on("afterRender",()=>{console.log("🎨 Workflow rendu")}),this.workflow.on("afterChange",t=>{console.log("🔄 Workflow modifié:",t)}))}catch(t){console.warn("⚠️ Erreur lors de la configuration des gestionnaires:",t)}}arrangeWorkflow(){try{const t=this.workflow.getPlugin("SVGArrangePlugin");t&&t.arrange?(t.arrange(),console.log("📐 Workflow réarrangé automatiquement")):this.workflow.fitToViewport&&(this.workflow.fitToViewport(),console.log("📐 Workflow ajusté à la vue"))}catch(t){console.warn("⚠️ Erreur lors de l'arrangement:",t)}}async createFallbackVisualization(){console.log("🔄 Création de la visualisation de fallback...");try{const t=await this.generateFallbackSVG(this.cwlData);this.container.innerHTML=t,this.showInfo("Visualisation en mode de compatibilité (cwl-svg non disponible)")}catch(t){console.error("❌ Erreur lors de la création du fallback:",t),this.showError("Impossible de visualiser le workflow")}}async generateFallbackSVG(t){const e=t,o=e.inputs||{},n=e.outputs||{},r=e.steps||{},i=Object.keys(o),s=Object.keys(n),a=Object.keys(r),l=i.length+a.length+s.length;let c=`",c}showError(t){const e=document.createElement("div");e.className="cwl-error bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg",e.innerHTML=`\n \n `,this.container.appendChild(e)}showInfo(t){const e=document.createElement("div");e.className="cwl-info bg-blue-50 border border-blue-200 text-blue-700 px-4 py-3 rounded-lg mb-4",e.innerHTML=`\n \n `,this.container.insertBefore(e,this.container.firstChild),setTimeout(()=>{e.parentNode&&e.parentNode.removeChild(e)},5e3)}async updateCwl(t){this.cwlData=t,await this.loadCwlFromData(t)}async updateFromUrl(t){this.cwlUrl=t,await this.loadCwlFromUrl()}enableEditing(){this.editingEnabled=!0,this.workflow&&(this.workflow.editingEnabled=!0)}disableEditing(){this.editingEnabled=!1,this.workflow&&(this.workflow.editingEnabled=!1)}getSelectedNode(){if(this.workflow){const t=this.workflow.getPlugin("SelectionPlugin");return t?t.getSelection():null}return null}destroy(){if(this.workflow)try{this.workflow.destroy()}catch(t){console.warn("⚠️ Erreur lors de la destruction du workflow:",t)}this.container&&(this.container.innerHTML=""),console.log("🗑️ Composant CWL détruit")}})},961:function(t,e,o){"use strict";var n,r=this&&this.__extends||(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var o in e)e.hasOwnProperty(o)&&(t[o]=e[o])},function(t,e){function o(){this.constructor=t}n(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}),i=this&&this.__values||function(t){var e="function"==typeof Symbol&&t[Symbol.iterator],o=0;return e?e.call(t):{next:function(){return t&&o>=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}}};Object.defineProperty(e,"__esModule",{value:!0});var s=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.modelDisposers=[],e.css={plugin:"__plugin-validate",invalid:"__validate-invalid"},e}return r(e,t),e.prototype.registerWorkflow=function(e){t.prototype.registerWorkflow.call(this,e),this.workflow.svgRoot.classList.add(this.css.plugin)},e.prototype.afterModelChange=function(){this.disposeModelListeners();var t=this.workflow.model.on("connections.updated",this.renderEdgeValidation.bind(this)),e=this.workflow.model.on("connection.create",this.renderEdgeValidation.bind(this));this.modelDisposers.concat([t.dispose,e.dispose])},e.prototype.destroy=function(){this.disposeModelListeners()},e.prototype.afterRender=function(){this.renderEdgeValidation()},e.prototype.onEditableStateChange=function(t){t?this.renderEdgeValidation():this.removeClasses(this.workflow.workflow.querySelectorAll(".edge"))},e.prototype.disposeModelListeners=function(){try{for(var t=i(this.modelDisposers),e=t.next();!e.done;e=t.next())(0,e.value)()}catch(t){o={error:t}}finally{try{e&&!e.done&&(n=t.return)&&n.call(t)}finally{if(o)throw o.error}}var o,n;this.modelDisposers=[]},e.prototype.removeClasses=function(t){try{for(var e=i(t),o=e.next();!o.done;o=e.next())o.value.classList.remove(this.css.invalid)}catch(t){n={error:t}}finally{try{o&&!o.done&&(r=e.return)&&r.call(e)}finally{if(n)throw n.error}}var n,r},e.prototype.renderEdgeValidation=function(){var t=this,e=this.workflow.workflow.querySelectorAll(".edge");this.removeClasses(e),this.workflow.model.connections.forEach(function(o){if(!o.isValid)try{for(var n=i(e),r=n.next();!r.done;r=n.next()){var s=r.value,a=s.getAttribute("data-source-connection"),l=s.getAttribute("data-destination-connection");if(o.source.id===a&&o.destination.id===l){s.classList.add(t.css.invalid);break}}}catch(t){c={error:t}}finally{try{r&&!r.done&&(d=n.return)&&d.call(n)}finally{if(c)throw c.error}}var c,d})},e}(o(547).PluginBase);e.SVGValidatePlugin=s}},e={};function o(n){var r=e[n];if(void 0!==r)return r.exports;var i=e[n]={exports:{}};return t[n].call(i.exports,i,i.exports,o),i.exports}o.d=(t,e)=>{for(var n in e)o.o(e,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},o.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),o.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var n=o(228);window.CwlSvg=n})();
\ No newline at end of file
diff --git a/public/cwl-demo/cwl-visualizer-api.js b/public/cwl-demo/cwl-visualizer-api.js
new file mode 100644
index 0000000..5ec546e
--- /dev/null
+++ b/public/cwl-demo/cwl-visualizer-api.js
@@ -0,0 +1,177 @@
+/**
+ * CWL Visualizer API - Public API for CWL-SVG Custom
+ * Provides public methods to load and visualize CWL workflows programmatically
+ */
+
+class CWLVisualizerAPI {
+ constructor(visualizer) {
+ this.visualizer = visualizer;
+ }
+
+ /**
+ * Load a CWL workflow from a URL
+ * @param {string} url - URL to the CWL file
+ * @returns {Promise}
+ */
+ async loadFromURL(url) {
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const text = await response.text();
+ return this.loadFromText(text);
+ } catch (error) {
+ console.error('Error loading CWL from URL:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Load a CWL workflow from a text string
+ * @param {string} cwlText - CWL content as text (YAML or JSON)
+ * @returns {Promise}
+ */
+ async loadFromText(cwlText) {
+ try {
+ let cwlObject;
+
+ // Try parsing as JSON first
+ try {
+ cwlObject = JSON.parse(cwlText);
+ } catch (e) {
+ // If JSON parsing fails, try YAML
+ if (window.jsyaml) {
+ cwlObject = jsyaml.load(cwlText);
+ } else {
+ throw new Error('YAML parser not available. Include js-yaml library.');
+ }
+ }
+
+ return this.loadFromObject(cwlObject);
+ } catch (error) {
+ console.error('Error parsing CWL text:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Load a CWL workflow from a JavaScript object
+ * @param {Object} cwlObject - Parsed CWL workflow object
+ * @returns {Promise}
+ */
+ async loadFromObject(cwlObject) {
+ try {
+ if (!cwlObject) {
+ throw new Error('CWL object is null or undefined');
+ }
+
+ // Validate that it's a CWL workflow
+ if (!cwlObject.class || !cwlObject.cwlVersion) {
+ throw new Error('Invalid CWL format: missing class or cwlVersion');
+ }
+
+ // Load the workflow
+ this.visualizer.loadWorkflow(cwlObject);
+
+ return Promise.resolve();
+ } catch (error) {
+ console.error('Error loading CWL object:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Load from a File object (for file upload)
+ * @param {File} file - File object from input element
+ * @returns {Promise}
+ */
+ async loadFromFile(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onload = async (e) => {
+ try {
+ await this.loadFromText(e.target.result);
+ resolve();
+ } catch (error) {
+ reject(error);
+ }
+ };
+
+ reader.onerror = () => {
+ reject(new Error('Failed to read file'));
+ };
+
+ reader.readAsText(file);
+ });
+ }
+
+ /**
+ * Clear the current visualization
+ */
+ clear() {
+ this.visualizer.workflow = null;
+ if (this.visualizer.mainGroup) {
+ this.visualizer.mainGroup.innerHTML = '';
+ }
+ }
+
+ /**
+ * Get the current workflow object
+ * @returns {Object|null}
+ */
+ getWorkflow() {
+ return this.visualizer.workflow;
+ }
+
+ /**
+ * Export current visualization as SVG
+ * @returns {string} - SVG content as string
+ */
+ exportSVG() {
+ return this.visualizer.svg ? this.visualizer.svg.outerHTML : '';
+ }
+
+ /**
+ * Download the current visualization as SVG file
+ * @param {string} filename - Name for the downloaded file
+ */
+ downloadSVG(filename = 'cwl-workflow.svg') {
+ this.visualizer.downloadSVG(filename);
+ }
+
+ /**
+ * Zoom in/out
+ * @param {number} factor - Zoom factor (>1 = zoom in, <1 = zoom out)
+ */
+ zoom(factor) {
+ this.visualizer.zoom(factor);
+ }
+
+ /**
+ * Reset view to default
+ */
+ resetView() {
+ this.visualizer.resetView();
+ }
+
+ /**
+ * Auto-layout the workflow
+ */
+ autoLayout() {
+ this.visualizer.autoLayout();
+ }
+
+ /**
+ * Fit visualization to content
+ */
+ fitToContent() {
+ this.visualizer.fitToContent();
+ }
+}
+
+// Export for use in different environments
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = CWLVisualizerAPI;
+}
diff --git a/public/cwl-demo/cwl-visualizer.js b/public/cwl-demo/cwl-visualizer.js
new file mode 100644
index 0000000..0d0b87d
--- /dev/null
+++ b/public/cwl-demo/cwl-visualizer.js
@@ -0,0 +1,859 @@
+// CWL Visualizer - JavaScript Application with CWL-SVG Custom
+console.log('📜 cwl-visualizer.js loading...');
+
+// Avoid redeclaration if already loaded
+if (typeof window.CWLVisualizer === 'undefined') {
+
+class CWLVisualizer {
+ constructor() {
+ this.currentFile = null;
+ this.svgContent = null;
+ this.cwlSvgRenderer = null; // Instance CWLSVGCustom
+ this.cwlSvgAvailable = false;
+ this.cwlSvgSource = 'NONE';
+
+
+ // Wait for CWLSVGCustom to load
+ this.waitForCwlSvg();
+ }
+
+ waitForCwlSvg() {
+ const checkInterval = setInterval(() => {
+ if (this.detectCwlSvgLibrary()) {
+ console.log(`✅ CWL-SVG Custom détecté et prêt`);
+ clearInterval(checkInterval);
+ this.initializeCwlSvg();
+ }
+ }, 100);
+
+ // Timeout after 10 seconds
+ setTimeout(() => {
+ clearInterval(checkInterval);
+ if (!this.cwlSvgAvailable) {
+ console.warn('⚠️ Timeout: CWL-SVG Custom not available after 10s, using fallback');
+ this.cwlSvgSource = 'FALLBACK';
+ }
+ }, 10000);
+ }
+
+ detectCwlSvgLibrary() {
+ // Detailed debug of window objects
+ console.log('🔍 DEBUG - Available window objects:');
+ console.log('CWLComponent:', typeof window.CWLComponent, window.CWLComponent);
+ console.log('WorkflowFactory:', typeof window.WorkflowFactory, window.WorkflowFactory);
+ console.log('Workflow:', typeof window.Workflow, window.Workflow);
+ console.log('CWLSVGCustom:', typeof window.CWLSVGCustom, window.CWLSVGCustom);
+ console.log('cwlSvg:', typeof window.cwlSvg, window.cwlSvg);
+
+ // List all CWL properties in window
+ const cwlKeys = Object.keys(window).filter(key =>
+ key.toLowerCase().includes('cwl') || key.toLowerCase().includes('svg')
+ );
+ console.log('CWL object keys:', cwlKeys.length > 0 ? cwlKeys : 'none');
+ console.log('cwlSvgAvailable:', this.cwlSvgAvailable);
+ console.log('cwlSvgSource:', this.cwlSvgSource);
+ console.log('isCwlSvgReady:', this.isCwlSvgReady());
+
+ // Check if CWLSVGCustom is available
+ if (typeof window !== 'undefined' && window.CWLSVGCustom) {
+ console.log('✅ CWLSVGCustom detected!');
+ this.cwlSvgAvailable = true;
+ this.cwlSvgSource = 'CUSTOM';
+ return true;
+ }
+
+ console.log('❌ CWLSVGCustom non disponible');
+ return false;
+ }
+
+ isCwlSvgReady() {
+ return this.cwlSvgAvailable &&
+ typeof window.CWLSVGCustom !== 'undefined' &&
+ window.CWLSVGCustom !== null;
+ }
+
+ initializeCwlSvg() {
+ if (!this.cwlSvgAvailable) return;
+
+ console.log('✅ CWL-SVG Custom initialized successfully');
+ this.updateUIForCwlSvg();
+ }
+
+ showBasicFallback() {
+ const svgContainer = document.getElementById('svg-container');
+ const svgContent = document.getElementById('svg-content');
+
+ if (svgContent) {
+ svgContent.innerHTML = `
+
+
+ Compatibility Mode
+
+
+ Interactive visualization is not available.
+ The CWL file has been processed but advanced visualization requires a compatible browser.
+
+
+
Format détecté: ${this.currentFile ? this.currentFile.name : 'CWL'}
+
Source CWL-SVG: ${this.cwlSvgSource}
+
+
+ `;
+ }
+ }
+
+ updateUIForCwlSvg() {
+ // Ajouter un indicateur visuel que cwl-svg est disponible
+ const header = document.querySelector('header h1');
+ if (header && this.cwlSvgAvailable) {
+ const badge = document.createElement('span');
+ badge.className = 'inline-block bg-green-100 text-green-800 text-xs font-semibold ml-2 px-2.5 py-0.5 rounded';
+ badge.textContent = `cwl-svg ${this.cwlSvgSource}`;
+ badge.title = 'cwl-svg is available for advanced visualization';
+ header.appendChild(badge);
+ }
+ }
+
+
+
+ handleFileSelect(e) {
+ const file = e.target.files[0];
+ if (file) {
+ this.processFile(file);
+ }
+ }
+
+ processFile(file) {
+ // Validate file type
+ const validExtensions = ['.cwl', '.yml', '.yaml'];
+ const fileName = file.name.toLowerCase();
+ const isValid = validExtensions.some(ext => fileName.endsWith(ext));
+
+ if (!isValid) {
+ this.showError(`Unsupported file type. Please select a ${validExtensions.join(', ')} file`);
+ return;
+ }
+
+ this.currentFile = file;
+ this.showFileInfo(file);
+ this.hideError();
+
+ // Read file content
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ // Use modern visualization
+ this.generateModernVisualization(e.target.result);
+ };
+ reader.onerror = () => {
+ this.showError('Error reading file');
+ };
+ reader.readAsText(file);
+ }
+
+ showFileInfo(file) {
+ document.getElementById('file-name').textContent = file.name;
+ document.getElementById('file-size').textContent = this.formatFileSize(file.size);
+ document.getElementById('file-info').classList.remove('hidden');
+ }
+
+
+
+
+ async generateVisualization(cwlContent) {
+ this.showLoading();
+
+ try {
+ // Parse CWL content
+ let cwlData;
+ try {
+ // Try JSON first
+ cwlData = JSON.parse(cwlContent);
+ } catch (e) {
+ // If JSON fails, try YAML parsing
+ cwlData = this.parseYAML(cwlContent);
+ }
+
+ console.log('CWL Data parsed:', cwlData);
+
+ // Use CWLSVGCustom to generate visualization
+ await this.generateSVGFromCWLCustom(cwlContent);
+
+ } catch (error) {
+ console.error('Visualization error:', error);
+ this.showError('Error generating visualization: ' + error.message);
+ this.hideLoading();
+ }
+ }
+
+ parseYAML(yamlContent) {
+ // Utiliser js-yaml si disponible
+ if (typeof jsyaml !== 'undefined') {
+ try {
+ return jsyaml.load(yamlContent);
+ } catch (e) {
+ console.warn('Erreur avec js-yaml, utilisation du parser simple:', e);
+ return this.parseSimpleYAML(yamlContent);
+ }
+ } else {
+ return this.parseSimpleYAML(yamlContent);
+ }
+ }
+
+ // Generate visualization with CWLSVGCustom
+ async generateSVGFromCWLCustom(cwlContent) {
+ try {
+ if (!window.CWLSVGCustom) {
+ throw new Error('CWLSVGCustom not available');
+ }
+
+ // Prepare existing visualization container
+ const svgContainer = document.getElementById('svg-container');
+ const svgContent = document.getElementById('svg-content');
+
+ if (!svgContainer || !svgContent) {
+ throw new Error('SVG containers not found in DOM');
+ }
+
+ // Clear and prepare container
+ svgContent.innerHTML = '';
+
+ // Create CWLSVGCustom instance with dynamic dimensions
+ const containerRect = svgContainer.getBoundingClientRect();
+ this.cwlSvgRenderer = new window.CWLSVGCustom(svgContent, {
+ width: Math.max(1200, containerRect.width || 1200),
+ height: Math.max(600, containerRect.height || 600)
+ });
+
+ // Load workflow
+ const result = await this.cwlSvgRenderer.loadWorkflow(cwlContent);
+
+ if (!result.success) {
+ throw new Error(result.error);
+ }
+
+ // Display visualization (no more viz-controls to display)
+ svgContainer.classList.remove('hidden');
+
+ console.log('✅ CWL visualization generated successfully');
+ this.hideLoading();
+
+ } catch (error) {
+ console.error('Error generating with CWLSVGCustom:', error);
+ // Fallback to basic method
+ this.showBasicFallback();
+ this.hideLoading();
+ }
+ }
+
+ // Use js-yaml for parsing YAML files
+ parseSimpleYAML(yamlContent) {
+ if (typeof jsyaml !== 'undefined') {
+ try {
+ return jsyaml.load(yamlContent);
+ } catch (e) {
+ console.warn('js-yaml parsing failed, falling back to simple parser:', e);
+ }
+ }
+
+ // Fallback simple parser
+ const lines = yamlContent.split('\n');
+ const result = {};
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (trimmed && !trimmed.startsWith('#')) {
+ const colonIndex = trimmed.indexOf(':');
+ if (colonIndex > 0) {
+ const key = trimmed.substring(0, colonIndex).trim();
+ const value = trimmed.substring(colonIndex + 1).trim();
+
+ if (value && !value.startsWith('#')) {
+ result[key] = value.replace(/['"]/g, '');
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ async generateSVGFromCWL(cwlData) {
+ // Try to use the real cwl-svg library with all its interactive features
+ try {
+ // Check if we have the cwl-svg library and cwlts models available
+ const hasWorkflow = typeof window !== 'undefined' && window.Workflow;
+ const hasWorkflowFactory = typeof window !== 'undefined' && window.WorkflowFactory;
+ const hasPlugins = typeof window !== 'undefined' &&
+ window.SVGArrangePlugin &&
+ window.SVGNodeMovePlugin &&
+ window.SVGPortDragPlugin;
+
+ if (hasWorkflow && hasWorkflowFactory && hasPlugins) {
+ console.log('Using cwl-svg with full interactive capabilities');
+ return await this.createInteractiveWorkflow(cwlData);
+ }
+
+ // Fallback to older detection methods for other cwl-svg variants
+ if (typeof window !== 'undefined' && window.CwlSvg) {
+ console.log('Using window.CwlSvg constructor');
+ const cwlSvg = new window.CwlSvg({
+ svgId: 'cwl-workflow-svg',
+ workflow: cwlData
+ });
+ const svgElement = await cwlSvg.generateSVG();
+ return svgElement.outerHTML;
+ }
+
+ // Method: Check for cwl-svg module functions
+ if (typeof window !== 'undefined' && window.cwlSvg) {
+ console.log('Using window.cwlSvg module functions');
+ const lib = window.cwlSvg;
+
+ if (typeof lib.render === 'function') {
+ const result = await lib.render(cwlData);
+ return typeof result === 'string' ? result : result.outerHTML;
+ }
+ if (typeof lib.generate === 'function') {
+ const result = await lib.generate(cwlData);
+ return typeof result === 'string' ? result : result.outerHTML;
+ }
+ }
+
+ // Method: Dynamic import fallback
+ try {
+ const importFunction = new Function('specifier', 'return import(specifier)');
+ const cwlSvgModule = await importFunction('cwl-svg');
+ console.log('Using dynamic import of cwl-svg');
+
+ const CwlSvgClass = cwlSvgModule.default || cwlSvgModule.CwlSvg || cwlSvgModule;
+ if (typeof CwlSvgClass === 'function') {
+ const cwlSvg = new CwlSvgClass({
+ svgId: 'cwl-workflow-svg',
+ workflow: cwlData
+ });
+ const svgElement = await cwlSvg.generateSVG();
+ return svgElement.outerHTML;
+ }
+ } catch (importError) {
+ console.info('Dynamic import not available or failed:', importError.message);
+ }
+
+ console.log('cwl-svg not available, using fallback generator');
+
+ } catch (error) {
+ console.warn('Error trying to use cwl-svg:', error.message);
+ }
+
+ // Fallback to custom generator
+ return await this.generateFallbackSVG(cwlData);
+ }
+
+ async createInteractiveWorkflow(cwlData) {
+ try {
+ console.log('Creating interactive workflow with cwl-svg');
+
+ // Clear any existing workflow
+ if (this.workflow) {
+ this.workflow.destroy();
+ this.workflow = null;
+ }
+
+ // Create the model using WorkflowFactory from CWL namespace
+ const { WorkflowFactory, Workflow, SVGArrangePlugin, SVGNodeMovePlugin,
+ SVGPortDragPlugin, SelectionPlugin, SVGEdgeHoverPlugin, ZoomPlugin } = window.CWL;
+
+ // Parse CWL data using WorkflowFactory
+ this.cwlModel = WorkflowFactory.from(cwlData);
+ console.log('CWL Model created:', this.cwlModel);
+
+ // Get or create the SVG container
+ const container = document.getElementById('visualization-container');
+ if (!container) {
+ throw new Error('Visualization container not found');
+ }
+
+ // Create SVG element for cwl-svg
+ let svgElement = container.querySelector('svg.cwl-workflow');
+ if (!svgElement) {
+ svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svgElement.setAttribute('class', 'cwl-workflow');
+ svgElement.style.width = '100%';
+ svgElement.style.height = '100%';
+ svgElement.style.minHeight = '500px';
+ container.innerHTML = '';
+ container.appendChild(svgElement);
+ }
+
+ // Create the interactive workflow with all plugins
+ this.workflow = new Workflow({
+ model: this.cwlModel,
+ svgRoot: svgElement,
+ editingEnabled: true, // Enable interactive editing
+ plugins: [
+ new SVGArrangePlugin(), // Auto-arrange nodes
+ new SVGEdgeHoverPlugin(), // Edge highlighting on hover
+ new SVGNodeMovePlugin({ // Drag and drop nodes
+ movementSpeed: 10
+ }),
+ new SVGPortDragPlugin(), // Connect ports by dragging
+ new SelectionPlugin(), // Select and highlight elements
+ new ZoomPlugin(), // Zoom and pan functionality
+ ]
+ });
+
+ console.log('Interactive workflow created successfully');
+
+ // Fit the workflow to viewport and arrange if needed
+ setTimeout(() => {
+ try {
+ this.workflow.fitToViewport();
+
+ // Auto-arrange if nodes don't have positions
+ const arrangePlugin = this.workflow.getPlugin(SVGArrangePlugin);
+ if (arrangePlugin) {
+ arrangePlugin.arrange();
+ }
+
+ console.log('Workflow arranged and fitted to viewport');
+ } catch (e) {
+ console.warn('Error arranging workflow:', e);
+ }
+ }, 100);
+
+ // Setup event listeners for interactive features
+ this.setupWorkflowEventListeners();
+
+ // Return a placeholder - the actual SVG is now interactive in the DOM
+ return 'Interactive CWL Workflow loaded successfully!
';
+
+ } catch (error) {
+ console.error('Error creating interactive workflow:', error);
+ throw new Error('Failed to create interactive workflow: ' + error.message);
+ }
+ }
+
+ setupWorkflowEventListeners() {
+ if (!this.workflow) return;
+
+ // Listen for selection changes
+ const selectionPlugin = this.workflow.getPlugin(window.SelectionPlugin);
+ if (selectionPlugin) {
+ selectionPlugin.registerOnSelectionChange((selection) => {
+ console.log('Selection changed:', selection);
+ // Could show selection details in UI here
+ });
+ }
+
+ // Listen for workflow changes
+ this.workflow.on('afterChange', (change) => {
+ console.log('Workflow changed:', change);
+ });
+
+ // Listen for workflow render events
+ this.workflow.on('afterRender', () => {
+ console.log('Workflow rendered');
+ });
+ }
+
+ // New modern method inspired by vue-cwl
+ async generateModernVisualization(cwlContent) {
+ console.log('🎨 Generating modern visualization...');
+ this.showLoading();
+
+ try {
+ // Parse CWL content
+ let cwlData;
+ try {
+ // Try JSON first
+ cwlData = JSON.parse(cwlContent);
+ } catch (e) {
+ // If JSON fails, try YAML parsing
+ cwlData = this.parseYAML(cwlContent);
+ }
+
+ console.log('CWL Data parsed:', cwlData);
+
+ // Debug available objects
+ console.log('🔍 Debug object state:');
+ console.log('- window.CWLComponent:', typeof window.CWLComponent);
+ console.log('- window.WorkflowFactory:', typeof window.WorkflowFactory);
+ console.log('- window.Workflow:', typeof window.Workflow);
+ console.log('- window.CWL:', window.CWL);
+ console.log('- cwlSvgAvailable:', this.cwlSvgAvailable);
+ console.log('- cwlSvgSource:', this.cwlSvgSource);
+
+ // Also surface debug info into a visible panel for users without devtools
+ try {
+ const debugPanel = document.getElementById('debug-panel');
+ if (debugPanel) {
+ debugPanel.innerText = [
+ `CWLComponent: ${typeof window.CWLComponent}`,
+ `WorkflowFactory: ${window.CWL ? typeof window.CWL.WorkflowFactory : typeof window.WorkflowFactory}`,
+ `Workflow: ${window.CWL ? typeof window.CWL.Workflow : typeof window.Workflow}`,
+ `CWL object keys: ${window.CWL ? Object.keys(window.CWL).join(', ') : 'none'}`,
+ `cwlSvgAvailable: ${this.cwlSvgAvailable}`,
+ `cwlSvgSource: ${this.cwlSvgSource}`,
+ `isCwlSvgReady: ${this.isCwlSvgReady()}`
+ ].join('\n');
+ }
+ } catch (e) {
+ console.warn('Impossible d\'afficher debug dans le panneau:', e);
+ }
+
+ // Obtenir le conteneur de visualisation
+ const container = document.getElementById('svg-content');
+ if (!container) {
+ throw new Error('Visualization container not found');
+ }
+
+ // Destroy any existing instance
+ if (this.cwlComponent) {
+ this.cwlComponent.destroy();
+ }
+
+ // Check availability of modern CWL component
+ if (typeof window.CWLComponent === 'undefined') {
+ console.warn('⚠️ CWLComponent not available, using fallback');
+ return await this.generateVisualization(cwlContent);
+ }
+
+ // Wait for cwl-svg to be fully loaded
+ let attempts = 0;
+ const maxAttempts = 50; // 5 secondes max
+ while (attempts < maxAttempts && !this.isCwlSvgReady()) {
+ console.log(`⏳ Waiting for cwl-svg... (${attempts + 1}/${maxAttempts})`);
+ console.log('Debug cwl-svg state:', {
+ cwlSvgAvailable: this.cwlSvgAvailable,
+ windowCWL: typeof window.CWL,
+ workflowFactory: window.CWL ? typeof window.CWL.WorkflowFactory : 'no CWL',
+ workflow: window.CWL ? typeof window.CWL.Workflow : 'no CWL'
+ });
+ await new Promise(resolve => setTimeout(resolve, 100));
+ attempts++;
+ }
+
+ if (!this.isCwlSvgReady()) {
+ console.warn('⚠️ cwl-svg not available after waiting, using fallback');
+ return await this.generateVisualization(cwlContent);
+ }
+
+ // Create new modern CWL component
+ this.cwlComponent = new window.CWLComponent({
+ container: container,
+ cwl: cwlData,
+ editingEnabled: true,
+ width: '100%',
+ height: '600px',
+ plugins: this.getModernPlugins(),
+ onWorkflowChanged: (workflow) => {
+ console.log('🔄 Workflow changed:', workflow);
+ this.onWorkflowChanged(workflow);
+ },
+ onSelectionChanged: (selection) => {
+ console.log('🎯 Selection changed:', selection);
+ this.onSelectionChanged(selection);
+ },
+ onError: (error) => {
+ console.error('❌ CWL Component error:', error);
+ this.showError(`Erreur du composant: ${error.message}`);
+ }
+ });
+
+ this.hideLoading();
+ console.log('✅ Modern visualization generated successfully');
+
+ } catch (error) {
+ console.error('Visualization error:', error);
+ this.showError('Error generating visualization: ' + error.message);
+ this.hideLoading();
+ }
+ }
+
+ getModernPlugins() {
+ const plugins = [];
+
+ // Plugins cwl-svg si disponibles
+ if (typeof window !== 'undefined') {
+ if (window.SVGArrangePlugin) plugins.push(window.SVGArrangePlugin);
+ if (window.SelectionPlugin) plugins.push(window.SelectionPlugin);
+ if (window.SVGNodeMovePlugin) plugins.push(window.SVGNodeMovePlugin);
+ if (window.SVGEdgeHoverPlugin) plugins.push(window.SVGEdgeHoverPlugin);
+ if (window.SVGPortDragPlugin) plugins.push(window.SVGPortDragPlugin);
+ if (window.ZoomPlugin) plugins.push(window.ZoomPlugin);
+ }
+
+ console.log(`🔌 Plugins modernes chargés: ${plugins.length}`);
+ return plugins;
+ }
+
+ onWorkflowChanged(workflow) {
+ // Callback for workflow changes
+ console.log('Workflow updated:', workflow);
+
+ // Update interface if needed
+ if (workflow && workflow.model) {
+ // Display workflow information
+ this.updateWorkflowInfo(workflow.model);
+ }
+ }
+
+ onSelectionChanged(selection) {
+ // Callback for selection changes
+ console.log('Selection updated:', selection);
+
+ // Display selected element details
+ if (selection) {
+ this.showSelectionDetails(selection);
+ }
+ }
+
+ updateWorkflowInfo(model) {
+ // Update workflow information in interface
+ try {
+ const info = {
+ inputs: Object.keys(model.inputs || {}).length,
+ outputs: Object.keys(model.outputs || {}).length,
+ steps: Object.keys(model.steps || {}).length
+ };
+
+ console.log('📊 Workflow info:', info);
+
+ // Optional: update UI with this info
+ const infoElement = document.getElementById('workflow-info');
+ if (infoElement) {
+ infoElement.innerHTML = `
+
+ Inputs: ${info.inputs}
+ Steps: ${info.steps}
+ Outputs: ${info.outputs}
+
+ `;
+ }
+ } catch (error) {
+ console.warn('Error updating workflow info:', error);
+ }
+ }
+
+ showSelectionDetails(selection) {
+ // Display details of selected element
+ try {
+ console.log('🔍 Selection details:', selection);
+
+ // Optional: display in side panel
+ const detailsElement = document.getElementById('selection-details');
+ if (detailsElement && selection) {
+ detailsElement.innerHTML = `
+
+
Selected Element
+
ID: ${selection.id || 'N/A'}
+
Type: ${selection.type || selection.class || 'N/A'}
+
+ `;
+ }
+ } catch (error) {
+ console.warn('Error displaying details:', error);
+ }
+ }
+
+ async generateFallbackSVG(cwlData) {
+ // Simulate asynchronous processing
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ const workflowClass = cwlData.class || cwlData.cwlVersion || 'Workflow';
+ const inputs = cwlData.inputs || {};
+ const outputs = cwlData.outputs || {};
+ const steps = cwlData.steps || {};
+
+ // Generate custom SVG based on CWL structure
+ const svg = this.createWorkflowSVG(workflowClass, inputs, outputs, steps);
+ return svg;
+ }
+
+ createWorkflowSVG(workflowClass, inputs, outputs, steps) {
+ const width = 600;
+ const height = 400;
+ const padding = 40;
+
+ // Count elements
+ const inputCount = Object.keys(inputs).length || 1;
+ const outputCount = Object.keys(outputs).length || 1;
+ const stepCount = Object.keys(steps).length || 1;
+
+ let svg = `
+ ';
+ return svg;
+ }
+
+ createNode(x, y, label, type, fill) {
+ const nodeWidth = 100;
+ const nodeHeight = 50;
+ const truncatedLabel = label.length > 12 ? label.substring(0, 12) + '...' : label;
+
+ return `
+
+
+
+ ${truncatedLabel}
+
+
+ `;
+ }
+
+ drawConnections(width, inputY, stepY, outputY, inputCount, stepCount, outputCount) {
+ let connections = '';
+
+ // Input to steps connections
+ for (let i = 0; i < Math.min(inputCount, stepCount); i++) {
+ const inputX = 40 + (i * 120) + 60;
+ const stepX = 40 + (i * 140) + 70;
+ connections += ``;
+ }
+
+ // Steps to output connections
+ for (let i = 0; i < Math.min(stepCount, outputCount); i++) {
+ const stepX = 40 + (i * 140) + 70;
+ const outputX = 40 + (i * 120) + 60;
+ connections += ``;
+ }
+
+ return connections;
+ }
+
+ showLoading() {
+ document.getElementById('svg-container').classList.add('hidden');
+ }
+
+
+ showVisualization(svg) {
+ this.svgContent = svg;
+ document.getElementById('svg-container').classList.remove('hidden');
+
+ const svgElement = document.getElementById('svg-content');
+ svgElement.innerHTML = svg;
+ svgElement.style.transformOrigin = 'top left';
+
+ this.updateZoomDisplay();
+ }
+
+ hideVisualization() {
+ const svgContainer = document.getElementById('svg-container');
+
+ if (loading) loading.classList.add('hidden');
+ if (svgContainer) svgContainer.classList.add('hidden');
+ if (empty) empty.classList.remove('hidden');
+
+ this.svgContent = null;
+ }
+
+
+
+ formatFileSize(bytes) {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ }
+}
+
+// Initialize the application when DOM is loaded or immediately if already loaded
+function initializeCWLVisualizer() {
+ console.log('🎯 Initialisation de CWLVisualizer...');
+ new CWLVisualizer();
+}
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializeCWLVisualizer);
+} else {
+ // DOM already loaded, initialize immediately
+ initializeCWLVisualizer();
+}
+
+// Close condition and export to window
+window.CWLVisualizer = CWLVisualizer;
+
+} else {
+ console.log('⚠️ CWLVisualizer already declared, using existing instance.');
+}
\ No newline at end of file
diff --git a/public/cwl-demo/cwlts/index.js b/public/cwl-demo/cwlts/index.js
new file mode 100644
index 0000000..16c68d5
--- /dev/null
+++ b/public/cwl-demo/cwlts/index.js
@@ -0,0 +1,10 @@
+// Main entry point for our version of cwlts
+import * as models from './models.js';
+
+// Export all models
+export { models };
+
+// Default export
+export default {
+ models
+};
\ No newline at end of file
diff --git a/public/cwl-demo/cwlts/model.js b/public/cwl-demo/cwlts/model.js
new file mode 100644
index 0000000..1985e0a
--- /dev/null
+++ b/public/cwl-demo/cwlts/model.js
@@ -0,0 +1,190 @@
+// Adapted version of cwlts/models for recent Node.js modules
+// Simplified to work with cwl-svg
+
+/**
+ * Base class for all CWL models
+ */
+class CWLModel {
+ constructor(data = {}) {
+ this.id = data.id || '';
+ this.label = data.label || '';
+ this.doc = data.doc || '';
+ this.cwlVersion = data.cwlVersion || 'v1.0';
+ }
+
+ serialize() {
+ return JSON.stringify(this);
+ }
+}
+
+/**
+ * Model for CWL workflows
+ */
+class WorkflowModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.class = 'Workflow';
+ this.inputs = data.inputs || [];
+ this.outputs = data.outputs || [];
+ this.steps = data.steps || [];
+ this.requirements = data.requirements || [];
+ this.hints = data.hints || [];
+ }
+
+ addStep(step) {
+ this.steps.push(step);
+ }
+
+ getStep(id) {
+ return this.steps.find(step => step.id === id);
+ }
+}
+
+/**
+ * Model for workflow steps
+ */
+class WorkflowStepModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.run = data.run || '';
+ this.in = data.in || [];
+ this.out = data.out || [];
+ this.scatter = data.scatter || null;
+ this.when = data.when || null;
+ }
+}
+
+/**
+ * Model for command line tools
+ */
+class CommandLineToolModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.class = 'CommandLineTool';
+ this.baseCommand = data.baseCommand || [];
+ this.arguments = data.arguments || [];
+ this.inputs = data.inputs || [];
+ this.outputs = data.outputs || [];
+ this.requirements = data.requirements || [];
+ this.hints = data.hints || [];
+ }
+}
+
+/**
+ * Model for input parameters
+ */
+class WorkflowInputParameterModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.type = data.type || 'string';
+ this.default = data.default || null;
+ this.format = data.format || null;
+ this.streamable = data.streamable || false;
+ }
+}
+
+/**
+ * Model for output parameters
+ */
+class WorkflowOutputParameterModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.type = data.type || 'string';
+ this.outputSource = data.outputSource || null;
+ this.linkMerge = data.linkMerge || null;
+ this.format = data.format || null;
+ }
+}
+
+/**
+ * Model for step inputs
+ */
+class WorkflowStepInputModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.source = data.source || null;
+ this.linkMerge = data.linkMerge || null;
+ this.default = data.default || null;
+ this.valueFrom = data.valueFrom || null;
+ }
+}
+
+/**
+ * Model for step outputs
+ */
+class WorkflowStepOutputModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.id = data.id || '';
+ }
+}
+
+/**
+ * Model for tool inputs
+ */
+class CommandInputParameterModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.type = data.type || 'string';
+ this.inputBinding = data.inputBinding || null;
+ this.default = data.default || null;
+ this.format = data.format || null;
+ }
+}
+
+/**
+ * Model for tool outputs
+ */
+class CommandOutputParameterModel extends CWLModel {
+ constructor(data = {}) {
+ super(data);
+ this.type = data.type || 'string';
+ this.outputBinding = data.outputBinding || null;
+ this.format = data.format || null;
+ }
+}
+
+/**
+ * Utility class to create models from CWL data
+ */
+class ModelFactory {
+ static createModel(data) {
+ if (!data || typeof data !== 'object') {
+ return null;
+ }
+
+ switch (data.class) {
+ case 'Workflow':
+ return new WorkflowModel(data);
+ case 'CommandLineTool':
+ return new CommandLineToolModel(data);
+ default:
+ return new CWLModel(data);
+ }
+ }
+
+ static createWorkflow(data) {
+ return new WorkflowModel(data);
+ }
+
+ static createCommandLineTool(data) {
+ return new CommandLineToolModel(data);
+ }
+}
+
+// Exportation ES6 pour webpack
+export {
+ CWLModel,
+ WorkflowModel,
+ WorkflowStepModel,
+ CommandLineToolModel,
+ WorkflowInputParameterModel,
+ WorkflowOutputParameterModel,
+ WorkflowStepInputModel,
+ WorkflowStepOutputModel,
+ CommandInputParameterModel,
+ CommandOutputParameterModel,
+ ModelFactory
+};
+
+export default ModelFactory;
\ No newline at end of file
diff --git a/public/cwl-demo/example-workflow.cwl b/public/cwl-demo/example-workflow.cwl
new file mode 100644
index 0000000..4d00601
--- /dev/null
+++ b/public/cwl-demo/example-workflow.cwl
@@ -0,0 +1,37 @@
+cwlVersion: v1.0
+class: Workflow
+
+inputs:
+ input_file:
+ type: File
+ doc: "Fichier d'entrée à traiter"
+
+ parameter1:
+ type: string
+ default: "default_value"
+ doc: "Premier paramètre du workflow"
+
+steps:
+ step1:
+ run: tool1.cwl
+ in:
+ input: input_file
+ param: parameter1
+ out: [processed_file]
+
+ step2:
+ run: tool2.cwl
+ in:
+ input: step1/processed_file
+ out: [final_result, log_file]
+
+outputs:
+ result:
+ type: File
+ outputSource: step2/final_result
+ doc: "Résultat final du traitement"
+
+ log:
+ type: File
+ outputSource: step2/log_file
+ doc: "Fichier de log du traitement"
\ No newline at end of file