Implement Visual Workflow Builder: Introduce a web-based drag-and-drop interface for creating and managing workflows, enabling dynamic task and step management.

This commit is contained in:
2026-01-09 14:37:35 +00:00
parent b6a76f5b2a
commit 34dcbd29c9

View File

@@ -95,14 +95,207 @@
<div class="row mt-4 mb-5">
<div class="col-12">
<h2>Workflow (workflow.json)</h2>
<form action="/workflow" method="post">
<textarea name="content" class="form-control" rows="15">{{ workflow_content }}</textarea>
<h2>Workflow Builder (workflow.json)</h2>
<div id="workflow-builder" class="mb-3">
<!-- Visual builder will be rendered here -->
</div>
<form action="/workflow" method="post" id="workflow-form">
<textarea id="workflow-content" name="content" class="form-control d-none" rows="15">{{ workflow_content }}</textarea>
<button type="submit" class="btn btn-success mt-2">Save Workflow</button>
<button type="button" class="btn btn-secondary mt-2" onclick="toggleRawWorkflow()">Toggle Raw JSON</button>
</form>
</div>
</div>
<script>
let workflow = [];
try {
workflow = JSON.parse(document.getElementById('workflow-content').value);
} catch (e) {
console.error("Failed to parse workflow JSON", e);
}
const stepDefinitions = {
'load_context': ['output_key'],
'prepare_messages': ['input_context', 'output_key'],
'llm_gen': ['input_messages', 'output_key'],
'process_response': ['input_response', 'output_key', 'stop_if_no_tools'],
'update_messages': ['input_results', 'target_messages']
};
function renderWorkflow() {
const container = document.getElementById('workflow-builder');
container.innerHTML = '';
workflow.forEach((task, taskIdx) => {
const taskCard = document.createElement('div');
taskCard.className = 'card mb-4 border-primary';
taskCard.innerHTML = `
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<div class="flex-grow-1">
<input type="text" class="form-control form-control-sm d-inline-block w-auto" value="${task.name || ''}" onchange="updateTask(${taskIdx}, 'name', this.value)" placeholder="Task Name">
<select class="form-select form-select-sm d-inline-block w-auto ms-2" onchange="updateTask(${taskIdx}, 'type', this.value)">
<option value="" ${!task.type ? 'selected' : ''}>Standard</option>
<option value="loop" ${task.type === 'loop' ? 'selected' : ''}>Loop</option>
</select>
${task.type === 'loop' ? `<input type="number" class="form-control form-control-sm d-inline-block w-auto ms-2" value="${task.max_iterations || 1}" onchange="updateTask(${taskIdx}, 'max_iterations', parseInt(this.value))" title="Max Iterations">` : ''}
</div>
<div>
<button class="btn btn-sm btn-light" onclick="moveTask(${taskIdx}, -1)" ${taskIdx === 0 ? 'disabled' : ''}>↑</button>
<button class="btn btn-sm btn-light" onclick="moveTask(${taskIdx}, 1)" ${taskIdx === workflow.length - 1 ? 'disabled' : ''}>↓</button>
<button class="btn btn-sm btn-danger ms-2" onclick="removeTask(${taskIdx})">Remove Task</button>
</div>
</div>
<div class="card-body">
<div id="steps-${taskIdx}"></div>
<button class="btn btn-sm btn-outline-primary mt-2" onclick="addStep(${taskIdx})">+ Add Step</button>
</div>
`;
container.appendChild(taskCard);
const stepsContainer = taskCard.querySelector(`#steps-${taskIdx}`);
(task.steps || []).forEach((step, stepIdx) => {
const stepDiv = document.createElement('div');
stepDiv.className = 'card mb-2 bg-light';
let fieldsHtml = `
<select class="form-select form-select-sm mb-2" onchange="updateStepType(${taskIdx}, ${stepIdx}, this.value)">
<option value="">Select Type</option>
${Object.keys(stepDefinitions).map(type => `<option value="${type}" ${step.type === type ? 'selected' : ''}>${type}</option>`).join('')}
</select>
`;
if (step.type && stepDefinitions[step.type]) {
fieldsHtml += '<div class="row g-2">';
stepDefinitions[step.type].forEach(field => {
const val = step[field] !== undefined ? step[field] : '';
const inputType = typeof val === 'boolean' || field === 'stop_if_no_tools' ? 'checkbox' : 'text';
fieldsHtml += `
<div class="col-md-4">
<label class="small">${field}</label>
${inputType === 'checkbox'
? `<div class="form-check"><input type="checkbox" class="form-check-input" ${step[field] ? 'checked' : ''} onchange="updateStepField(${taskIdx}, ${stepIdx}, '${field}', this.checked)"></div>`
: `<input type="text" class="form-control form-control-sm" value="${val}" onchange="updateStepField(${taskIdx}, ${stepIdx}, '${field}', this.value)">`
}
</div>
`;
});
fieldsHtml += '</div>';
}
stepDiv.innerHTML = `
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge bg-secondary">Step ${stepIdx + 1}</span>
<div>
<button class="btn btn-sm btn-link p-0 text-decoration-none" onclick="moveStep(${taskIdx}, ${stepIdx}, -1)" ${stepIdx === 0 ? 'disabled' : ''}>↑</button>
<button class="btn btn-sm btn-link p-0 text-decoration-none ms-1" onclick="moveStep(${taskIdx}, ${stepIdx}, 1)" ${stepIdx === (task.steps.length - 1) ? 'disabled' : ''}>↓</button>
<button class="btn btn-sm btn-link p-0 text-danger text-decoration-none ms-2" onclick="removeStep(${taskIdx}, ${stepIdx})">✕</button>
</div>
</div>
${fieldsHtml}
</div>
`;
stepsContainer.appendChild(stepDiv);
});
});
const addTaskBtn = document.createElement('button');
addTaskBtn.className = 'btn btn-primary mb-5';
addTaskBtn.textContent = '+ Add Task';
addTaskBtn.onclick = addTask;
container.appendChild(addTaskBtn);
syncWorkflow();
}
function syncWorkflow() {
document.getElementById('workflow-content').value = JSON.stringify(workflow, null, 2);
}
function toggleRawWorkflow() {
const textarea = document.getElementById('workflow-content');
textarea.classList.toggle('d-none');
if (!textarea.classList.contains('d-none')) {
textarea.oninput = function() {
try {
workflow = JSON.parse(this.value);
renderWorkflow();
} catch (e) {}
};
}
}
function updateTask(idx, field, value) {
workflow[idx][field] = value;
if (field === 'type' && value !== 'loop') {
delete workflow[idx].max_iterations;
} else if (field === 'type' && value === 'loop' && !workflow[idx].max_iterations) {
workflow[idx].max_iterations = 10;
}
renderWorkflow();
}
function addTask() {
workflow.push({ name: 'New Task', steps: [] });
renderWorkflow();
}
function removeTask(idx) {
if (confirm('Remove this task?')) {
workflow.splice(idx, 1);
renderWorkflow();
}
}
function moveTask(idx, dir) {
const target = idx + dir;
if (target >= 0 && target < workflow.length) {
[workflow[idx], workflow[target]] = [workflow[target], workflow[idx]];
renderWorkflow();
}
}
function updateStepType(taskIdx, stepIdx, type) {
const oldStep = workflow[taskIdx].steps[stepIdx];
const newStep = { type: type };
if (stepDefinitions[type]) {
stepDefinitions[type].forEach(field => {
if (field === 'stop_if_no_tools') newStep[field] = false;
else newStep[field] = '';
});
}
workflow[taskIdx].steps[stepIdx] = newStep;
renderWorkflow();
}
function updateStepField(taskIdx, stepIdx, field, value) {
workflow[taskIdx].steps[stepIdx][field] = value;
syncWorkflow();
}
function addStep(taskIdx) {
if (!workflow[taskIdx].steps) workflow[taskIdx].steps = [];
workflow[taskIdx].steps.push({ type: '' });
renderWorkflow();
}
function removeStep(taskIdx, stepIdx) {
workflow[taskIdx].steps.splice(stepIdx, 1);
renderWorkflow();
}
function moveStep(taskIdx, stepIdx, dir) {
const target = stepIdx + dir;
const steps = workflow[taskIdx].steps;
if (target >= 0 && target < steps.length) {
[steps[stepIdx], steps[target]] = [steps[target], steps[stepIdx]];
renderWorkflow();
}
}
renderWorkflow();
async function refreshLogs() {
try {
const response = await fetch('/api/logs');