Merge pull request #14 from prateekh777/feature/enhanced-workflow-documentation
Add step-by-step process generation to workflow documentation
This commit is contained in:
commit
800dec47af
@ -113,6 +113,14 @@ class WorkflowAnalyzer:
|
|||||||
# Generate description
|
# Generate description
|
||||||
workflow['description'] = self._generate_description(workflow, trigger_type, integrations)
|
workflow['description'] = self._generate_description(workflow, trigger_type, integrations)
|
||||||
|
|
||||||
|
# Extract or generate step-by-step process
|
||||||
|
steps = self._extract_or_generate_steps(workflow['nodes'], workflow['connections'])
|
||||||
|
workflow['steps'] = steps
|
||||||
|
|
||||||
|
# Debug logging
|
||||||
|
if steps:
|
||||||
|
print(f"Found/Generated {len(steps)} steps in workflow: {workflow['name']}")
|
||||||
|
|
||||||
# Extract raw JSON for viewer
|
# Extract raw JSON for viewer
|
||||||
workflow['rawJson'] = json.dumps(data, indent=2)
|
workflow['rawJson'] = json.dumps(data, indent=2)
|
||||||
|
|
||||||
@ -198,6 +206,204 @@ class WorkflowAnalyzer:
|
|||||||
|
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
|
def _extract_or_generate_steps(self, nodes: List[Dict], connections: Dict) -> List[Dict]:
|
||||||
|
"""Extract notes from nodes or generate steps from workflow structure."""
|
||||||
|
steps = []
|
||||||
|
|
||||||
|
# First, try to extract existing notes
|
||||||
|
nodes_with_notes = []
|
||||||
|
for node in nodes:
|
||||||
|
note = node.get('notes')
|
||||||
|
if note:
|
||||||
|
nodes_with_notes.append({
|
||||||
|
'name': node.get('name', ''),
|
||||||
|
'type': node.get('type', ''),
|
||||||
|
'note': note,
|
||||||
|
'id': node.get('id', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
# If we have notes, use them
|
||||||
|
if nodes_with_notes:
|
||||||
|
return nodes_with_notes
|
||||||
|
|
||||||
|
# Otherwise, generate steps from workflow structure
|
||||||
|
return self._generate_steps_from_structure(nodes, connections)
|
||||||
|
|
||||||
|
def _generate_steps_from_structure(self, nodes: List[Dict], connections: Dict) -> List[Dict]:
|
||||||
|
"""Generate step descriptions from workflow node structure and connections."""
|
||||||
|
if not nodes:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Create a map of nodes by ID for easy lookup
|
||||||
|
node_map = {node.get('id', ''): node for node in nodes}
|
||||||
|
|
||||||
|
# Find the starting node (trigger or first node)
|
||||||
|
start_node = self._find_start_node(nodes, connections)
|
||||||
|
if not start_node:
|
||||||
|
# Fallback: just describe all nodes in order
|
||||||
|
return self._generate_basic_steps(nodes)
|
||||||
|
|
||||||
|
# Follow the workflow path
|
||||||
|
steps = []
|
||||||
|
visited = set()
|
||||||
|
self._traverse_workflow(start_node, node_map, connections, steps, visited)
|
||||||
|
|
||||||
|
return steps
|
||||||
|
|
||||||
|
def _find_start_node(self, nodes: List[Dict], connections: Dict) -> Optional[Dict]:
|
||||||
|
"""Find the starting node of the workflow (trigger or node with no inputs)."""
|
||||||
|
# Look for trigger nodes first
|
||||||
|
for node in nodes:
|
||||||
|
node_type = node.get('type', '').lower()
|
||||||
|
if any(trigger in node_type for trigger in ['trigger', 'webhook', 'cron', 'schedule', 'manual']):
|
||||||
|
return node
|
||||||
|
|
||||||
|
# Find nodes that are not targets of any connections
|
||||||
|
target_nodes = set()
|
||||||
|
for source_connections in connections.values():
|
||||||
|
if isinstance(source_connections, dict) and 'main' in source_connections:
|
||||||
|
for main_connections in source_connections['main']:
|
||||||
|
if isinstance(main_connections, list):
|
||||||
|
for connection in main_connections:
|
||||||
|
if isinstance(connection, dict) and 'node' in connection:
|
||||||
|
target_nodes.add(connection['node'])
|
||||||
|
|
||||||
|
# Return first node that's not a target
|
||||||
|
for node in nodes:
|
||||||
|
if node.get('name', '') not in target_nodes:
|
||||||
|
return node
|
||||||
|
|
||||||
|
# Fallback: return first node
|
||||||
|
return nodes[0] if nodes else None
|
||||||
|
|
||||||
|
def _traverse_workflow(self, current_node: Dict, node_map: Dict, connections: Dict, steps: List[Dict], visited: set):
|
||||||
|
"""Traverse the workflow following connections and generate step descriptions."""
|
||||||
|
node_name = current_node.get('name', '')
|
||||||
|
node_id = current_node.get('id', '')
|
||||||
|
|
||||||
|
if node_id in visited:
|
||||||
|
return
|
||||||
|
|
||||||
|
visited.add(node_id)
|
||||||
|
|
||||||
|
# Generate step description for current node
|
||||||
|
step_description = self._generate_step_description(current_node)
|
||||||
|
if step_description:
|
||||||
|
steps.append({
|
||||||
|
'name': node_name,
|
||||||
|
'type': current_node.get('type', ''),
|
||||||
|
'note': step_description,
|
||||||
|
'id': node_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Find next nodes
|
||||||
|
if node_name in connections:
|
||||||
|
node_connections = connections[node_name]
|
||||||
|
if isinstance(node_connections, dict) and 'main' in node_connections:
|
||||||
|
for main_connections in node_connections['main']:
|
||||||
|
if isinstance(main_connections, list):
|
||||||
|
for connection in main_connections:
|
||||||
|
if isinstance(connection, dict) and 'node' in connection:
|
||||||
|
next_node_name = connection['node']
|
||||||
|
next_node = None
|
||||||
|
for node in node_map.values():
|
||||||
|
if node.get('name') == next_node_name:
|
||||||
|
next_node = node
|
||||||
|
break
|
||||||
|
if next_node:
|
||||||
|
self._traverse_workflow(next_node, node_map, connections, steps, visited)
|
||||||
|
|
||||||
|
def _generate_step_description(self, node: Dict) -> str:
|
||||||
|
"""Generate a meaningful description for a workflow node based on its type and parameters."""
|
||||||
|
node_type = node.get('type', '')
|
||||||
|
node_name = node.get('name', '')
|
||||||
|
parameters = node.get('parameters', {})
|
||||||
|
|
||||||
|
# Clean up node type
|
||||||
|
clean_type = node_type.replace('n8n-nodes-base.', '').replace('Trigger', '').replace('trigger', '')
|
||||||
|
|
||||||
|
# Generate description based on node type
|
||||||
|
if 'webhook' in node_type.lower():
|
||||||
|
return f"Receives incoming webhook requests to trigger the workflow"
|
||||||
|
elif 'cron' in node_type.lower() or 'schedule' in node_type.lower():
|
||||||
|
return f"Runs on a scheduled basis to trigger the workflow automatically"
|
||||||
|
elif 'manual' in node_type.lower():
|
||||||
|
return f"Manual trigger to start the workflow execution"
|
||||||
|
elif 'http' in node_type.lower() or 'httpRequest' in node_type:
|
||||||
|
url = parameters.get('url', '')
|
||||||
|
method = parameters.get('method', 'GET')
|
||||||
|
return f"Makes {method} HTTP request" + (f" to {url}" if url else "")
|
||||||
|
elif 'set' in node_type.lower():
|
||||||
|
return f"Sets and transforms data values for use in subsequent steps"
|
||||||
|
elif 'if' in node_type.lower():
|
||||||
|
return f"Evaluates conditions to determine workflow path"
|
||||||
|
elif 'switch' in node_type.lower():
|
||||||
|
return f"Routes workflow execution based on multiple conditions"
|
||||||
|
elif 'function' in node_type.lower() or 'code' in node_type.lower():
|
||||||
|
return f"Executes custom JavaScript code for data processing"
|
||||||
|
elif 'merge' in node_type.lower():
|
||||||
|
return f"Combines data from multiple workflow branches"
|
||||||
|
elif 'split' in node_type.lower():
|
||||||
|
return f"Splits data into multiple items for parallel processing"
|
||||||
|
elif 'filter' in node_type.lower():
|
||||||
|
return f"Filters data based on specified conditions"
|
||||||
|
elif 'gmail' in node_type.lower():
|
||||||
|
operation = parameters.get('operation', 'send')
|
||||||
|
return f"Performs Gmail {operation} operation"
|
||||||
|
elif 'slack' in node_type.lower():
|
||||||
|
return f"Sends message or performs action in Slack"
|
||||||
|
elif 'discord' in node_type.lower():
|
||||||
|
return f"Sends message or performs action in Discord"
|
||||||
|
elif 'telegram' in node_type.lower():
|
||||||
|
return f"Sends message or performs action in Telegram"
|
||||||
|
elif 'airtable' in node_type.lower():
|
||||||
|
operation = parameters.get('operation', 'create')
|
||||||
|
return f"Performs Airtable {operation} operation on records"
|
||||||
|
elif 'google' in node_type.lower():
|
||||||
|
if 'sheets' in node_type.lower():
|
||||||
|
return f"Reads from or writes to Google Sheets"
|
||||||
|
elif 'drive' in node_type.lower():
|
||||||
|
return f"Manages files in Google Drive"
|
||||||
|
elif 'calendar' in node_type.lower():
|
||||||
|
return f"Manages Google Calendar events"
|
||||||
|
else:
|
||||||
|
return f"Integrates with Google {clean_type} service"
|
||||||
|
elif 'microsoft' in node_type.lower():
|
||||||
|
if 'outlook' in node_type.lower():
|
||||||
|
return f"Manages Microsoft Outlook emails"
|
||||||
|
elif 'excel' in node_type.lower():
|
||||||
|
return f"Works with Microsoft Excel files"
|
||||||
|
else:
|
||||||
|
return f"Integrates with Microsoft {clean_type} service"
|
||||||
|
elif 'openai' in node_type.lower():
|
||||||
|
return f"Processes data using OpenAI AI models"
|
||||||
|
elif 'anthropic' in node_type.lower():
|
||||||
|
return f"Processes data using Anthropic Claude AI"
|
||||||
|
elif 'database' in node_type.lower() or 'mysql' in node_type.lower() or 'postgres' in node_type.lower():
|
||||||
|
return f"Executes database operations"
|
||||||
|
elif 'wait' in node_type.lower():
|
||||||
|
return f"Pauses workflow execution for specified duration"
|
||||||
|
elif 'error' in node_type.lower():
|
||||||
|
return f"Handles errors and stops workflow execution"
|
||||||
|
else:
|
||||||
|
# Generic description based on service name
|
||||||
|
service_name = clean_type.title()
|
||||||
|
return f"Integrates with {service_name} to process data"
|
||||||
|
|
||||||
|
def _generate_basic_steps(self, nodes: List[Dict]) -> List[Dict]:
|
||||||
|
"""Generate basic steps when workflow structure is unclear."""
|
||||||
|
steps = []
|
||||||
|
for i, node in enumerate(nodes, 1):
|
||||||
|
description = self._generate_step_description(node)
|
||||||
|
if description:
|
||||||
|
steps.append({
|
||||||
|
'name': node.get('name', f'Step {i}'),
|
||||||
|
'type': node.get('type', ''),
|
||||||
|
'note': description,
|
||||||
|
'id': node.get('id', '')
|
||||||
|
})
|
||||||
|
return steps
|
||||||
|
|
||||||
def _calculate_stats(self):
|
def _calculate_stats(self):
|
||||||
"""Calculate statistics from analyzed workflows."""
|
"""Calculate statistics from analyzed workflows."""
|
||||||
self.stats['total'] = len(self.workflows)
|
self.stats['total'] = len(self.workflows)
|
||||||
@ -638,13 +844,40 @@ def generate_html_documentation(data: Dict[str, Any]) -> str:
|
|||||||
|
|
||||||
.workflow-description {
|
.workflow-description {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.5;
|
line-height: 1.4;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode .workflow-description {
|
.workflow-integrations {
|
||||||
color: var(--text-muted);
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .workflow-integrations {
|
||||||
|
border-top-color: var(--border-color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integrations-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integrations-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-tag {
|
||||||
|
background: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-footer {
|
.workflow-footer {
|
||||||
@ -652,7 +885,7 @@ def generate_html_documentation(data: Dict[str, Any]) -> str:
|
|||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
border-radius: 0 0 12px 12px;
|
border-radius: 0 0 12px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -745,18 +978,72 @@ def generate_html_documentation(data: Dict[str, Any]) -> str:
|
|||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.integrations-list {
|
.workflow-steps {
|
||||||
display: flex;
|
list-style: none;
|
||||||
flex-wrap: wrap;
|
counter-reset: step-counter;
|
||||||
gap: 6px;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.integration-tag {
|
.workflow-step {
|
||||||
background: var(--secondary-color);
|
counter-increment: step-counter;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--surface-hover);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid var(--accent-color);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .workflow-step {
|
||||||
|
background: var(--surface-hover-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-step::before {
|
||||||
|
content: counter(step-counter);
|
||||||
|
position: absolute;
|
||||||
|
left: -15px;
|
||||||
|
top: 12px;
|
||||||
|
background: var(--accent-color);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 4px 8px;
|
width: 24px;
|
||||||
border-radius: 4px;
|
height: 24px;
|
||||||
font-size: 0.75rem;
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-type {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-note {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .step-note {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-howitworks {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .workflow-howitworks {
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-green {
|
@keyframes pulse-green {
|
||||||
@ -1215,6 +1502,35 @@ def generate_html_documentation(data: Dict[str, Any]) -> str:
|
|||||||
`<span class="integration-tag">${integration}</span>`
|
`<span class="integration-tag">${integration}</span>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
|
// Step-by-step process section
|
||||||
|
let stepsSection = '';
|
||||||
|
if (workflow.steps && workflow.steps.length > 0) {
|
||||||
|
stepsSection = `
|
||||||
|
<div class="details-section">
|
||||||
|
<h4 class="details-title">Step-by-step process (${workflow.steps.length} steps)</h4>
|
||||||
|
<ol class="workflow-steps">
|
||||||
|
${workflow.steps.map(step => `
|
||||||
|
<li class="workflow-step">
|
||||||
|
<div class="step-header">
|
||||||
|
<strong>${step.name ? step.name : '(Unnamed node)'}</strong>
|
||||||
|
<span class="step-type">[${step.type.replace('n8n-nodes-base.', '')}]</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-note">${step.note}</div>
|
||||||
|
</li>
|
||||||
|
`).join('')}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Debug: Show when no steps are found
|
||||||
|
stepsSection = `
|
||||||
|
<div class="details-section">
|
||||||
|
<h4 class="details-title">Step-by-step process</h4>
|
||||||
|
<p style="color: var(--text-muted); font-style: italic;">No step-by-step notes available for this workflow.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="workflow-card" data-trigger="${workflow.triggerType}" data-name="${workflow.name.toLowerCase()}" data-description="${workflow.description.toLowerCase()}" data-integrations="${workflow.integrations.join(' ').toLowerCase()}">
|
<div class="workflow-card" data-trigger="${workflow.triggerType}" data-name="${workflow.name.toLowerCase()}" data-description="${workflow.description.toLowerCase()}" data-integrations="${workflow.integrations.join(' ').toLowerCase()}">
|
||||||
<div class="workflow-header">
|
<div class="workflow-header">
|
||||||
@ -1233,16 +1549,19 @@ def generate_html_documentation(data: Dict[str, Any]) -> str:
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="workflow-title">${workflow.name}</h3>
|
<h3 class="workflow-title">${workflow.name}</h3>
|
||||||
<p class="workflow-description">${workflow.description}</p>
|
<p class="workflow-description">${workflow.description}</p>
|
||||||
</div>
|
|
||||||
|
<!-- Integrations moved to main card -->
|
||||||
<div class="workflow-details">
|
<div class="workflow-integrations">
|
||||||
<div class="details-section">
|
<h4 class="integrations-title">Integrations (${workflow.integrations.length})</h4>
|
||||||
<h4 class="details-title">Integrations (${workflow.integrations.length})</h4>
|
|
||||||
<div class="integrations-list">
|
<div class="integrations-list">
|
||||||
${integrations}
|
${integrations}
|
||||||
${workflow.integrations.length > 5 ? `<span class="integration-tag">+${workflow.integrations.length - 5} more</span>` : ''}
|
${workflow.integrations.length > 5 ? `<span class="integration-tag">+${workflow.integrations.length - 5} more</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workflow-details">
|
||||||
|
${stepsSection}
|
||||||
${workflow.tags.length > 0 ? `
|
${workflow.tags.length > 0 ? `
|
||||||
<div class="details-section">
|
<div class="details-section">
|
||||||
<h4 class="details-title">Tags</h4>
|
<h4 class="details-title">Tags</h4>
|
||||||
@ -1262,7 +1581,6 @@ def generate_html_documentation(data: Dict[str, Any]) -> str:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="workflow-footer">
|
<div class="workflow-footer">
|
||||||
<div class="workflow-tags">${tags}</div>
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="btn toggle-details">View Details</button>
|
<button class="btn toggle-details">View Details</button>
|
||||||
<button class="btn view-json" data-workflow-name="${workflow.name}" data-filename="${workflow.filename}">View File</button>
|
<button class="btn view-json" data-workflow-name="${workflow.name}" data-filename="${workflow.filename}">View File</button>
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user