gateway/static/60_workflow.js
2025-04-30 00:39:37 +02:00

717 lines
No EOL
24 KiB
JavaScript

/**
* workflow.js
* The main coordinator module for the workflow functionality
* Acts as the entry point and orchestrates interactions between all other modules
* Implements a state machine for workflow status management
*/
import api from '../shared/apiCalls.js';
import * as WorkflowCoordination from './workflowCoordination.js';
import * as WorkflowUI from './workflowUi.js';
import * as WorkflowData from './workflowData.js';
// DOM elements mapping
const domElements = {};
// State machine constants
const WORKFLOW_STATES = {
NULL: null,
RUNNING: 'running',
COMPLETED: 'completed',
FAILED: 'failed',
STOPPED: 'stopped'
};
/**
* Initializes the workflow module
* @param {Object} globalStateObj - Global application state
*/
function initWorkflowModule(globalStateObj) {
console.log("Initializing workflow module...");
try {
// Initialize DOM elements
initDomElements();
// Initialize coordination layer with initial state
WorkflowCoordination.initCoordination({
status: WORKFLOW_STATES.NULL,
workflowId: "",
logs: [],
chatMessages: [],
lastPolledLogId: null,
lastPolledMessageId: null,
dataStats: {
bytesSent: 0,
bytesReceived: 0,
tokensUsed: 0
},
pollFailCount: 0
});
// Make DOM elements available to coordination layer
WorkflowCoordination.userInputState.domElements = domElements;
// Initialize UI layer with callbacks
WorkflowUI.initUI(
WorkflowCoordination.getWorkflowState(),
{
onResetWorkflow: resetWorkflow,
onStopWorkflow: stopWorkflow,
onLayoutChange: handleLayoutChange
}
);
// Initialize data layer
WorkflowData.initDataLayer(globalStateObj);
// Setup event listeners
setupEventListeners();
// Initialize file handling
initFileHandling();
// Load prompt options
if (globalStateObj && globalStateObj.mainView) {
loadPromptOptions(globalStateObj.mainView.availablePrompts || []);
}
// Show initial prompt view
WorkflowCoordination.showInitialPromptView();
console.log("Workflow module successfully initialized with state:", WORKFLOW_STATES.NULL);
} catch (error) {
console.error("Error initializing workflow module:", error);
window.utils.ui.showError("Failed to initialize workflow module: " + error.message);
}
}
/**
* Initializes all DOM element references
*/
function initDomElements() {
console.log("Initializing DOM elements");
// Main containers
domElements.workflowContainer = document.querySelector('.workflow-container');
domElements.workflowHeader = document.querySelector('.workflow-header');
domElements.chatSection = document.querySelector('.chat-section');
domElements.workflowFooter = document.querySelector('.workflow-footer');
// UI components
domElements.resetBtn = document.getElementById('reset-btn');
domElements.stopWorkflowBtn = document.getElementById('stop-workflow-btn');
domElements.executionLog = document.getElementById('execution-log');
domElements.agentChatMessages = document.getElementById('agent-chat-messages');
domElements.emptyChatState = document.getElementById('empty-chat-state');
domElements.userMessageInput = document.getElementById('user-message-input');
domElements.sendUserMessageBtn = document.getElementById('send-user-message-btn');
domElements.toggleHeaderBtn = document.getElementById('toggle-header-btn');
domElements.userInputArea = document.getElementById('user-input-area');
// File handling
domElements.filePreviewContainer = document.getElementById('file-preview-container');
domElements.filePreviewContent = document.getElementById('file-preview-content');
domElements.downloadFileBtn = document.getElementById('download-file-btn');
domElements.copyFileBtn = document.getElementById('copy-file-btn');
domElements.uploadAdditionalFileBtn = document.getElementById('upload-additional-file-btn');
domElements.additionalFileInput = document.getElementById('additional-file-input');
domElements.additionalFilesContainer = document.getElementById('additional-files-container');
// Prompt selection
domElements.promptSelectMain = document.getElementById('prompt-select-main');
// Statistics
domElements.dataStatisticsEl = document.getElementById('data-statistics');
// Log found elements vs missing elements
const foundElements = Object.keys(domElements).filter(key => domElements[key] !== null).length;
const missingElements = Object.keys(domElements).filter(key => domElements[key] === null);
console.log(`DOM elements initialized: ${foundElements} found, ${missingElements.length} missing`);
if (missingElements.length > 0) {
console.warn("Missing DOM elements:", missingElements.join(', '));
}
}
/**
* Sets up event listeners
*/
function setupEventListeners() {
console.log("Setting up event listeners");
// User input handling
if (domElements.userMessageInput) {
// Track input changes
domElements.userMessageInput.addEventListener('input', (e) => {
WorkflowCoordination.userInputState.promptText = e.target.value;
});
// Handle Enter key for submission
domElements.userMessageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendUserResponse();
}
});
}
// Send button
if (domElements.sendUserMessageBtn) {
// Remove any existing listeners
const newButton = domElements.sendUserMessageBtn.cloneNode(true);
if (domElements.sendUserMessageBtn.parentNode) {
domElements.sendUserMessageBtn.parentNode.replaceChild(newButton, domElements.sendUserMessageBtn);
}
domElements.sendUserMessageBtn = newButton;
newButton.addEventListener('click', sendUserResponse);
}
// Reset button
if (domElements.resetBtn) {
domElements.resetBtn.addEventListener('click', resetWorkflow);
}
// Stop workflow button
if (domElements.stopWorkflowBtn) {
domElements.stopWorkflowBtn.addEventListener('click', stopWorkflow);
}
// Prompt selection
if (domElements.promptSelectMain) {
domElements.promptSelectMain.addEventListener('change', handlePromptSelection);
}
// Add custom event listeners for workflow state changes
document.addEventListener('workflowStatusChanged', handleWorkflowStatusChange);
}
/**
* Handler for workflow status change events
* @param {CustomEvent} event - The status change event
*/
function handleWorkflowStatusChange(event) {
const { status, previousStatus, options } = event.detail;
console.log(`Workflow status transition: ${previousStatus}${status}`);
// Update UI based on state transitions
switch (status) {
case WORKFLOW_STATES.RUNNING:
// Show running UI state
if (domElements.stopWorkflowBtn) {
domElements.stopWorkflowBtn.style.display = 'inline-block';
}
if (domElements.emptyChatState) {
domElements.emptyChatState.style.display = 'none';
}
if (domElements.agentChatMessages) {
domElements.agentChatMessages.style.display = 'block';
}
break;
case WORKFLOW_STATES.COMPLETED:
// Show completed UI state
if (domElements.stopWorkflowBtn) {
domElements.stopWorkflowBtn.style.display = 'none';
}
WorkflowCoordination.showInitialPromptView();
break;
case WORKFLOW_STATES.FAILED:
// Show failed UI state with retry option
if (domElements.stopWorkflowBtn) {
domElements.stopWorkflowBtn.style.display = 'none';
}
WorkflowCoordination.showInitialPromptView();
// Optionally show error UI
window.utils.ui.showToast(options.message || "Workflow failed", "error");
break;
case WORKFLOW_STATES.STOPPED:
// Show stopped UI state with resume option
if (domElements.stopWorkflowBtn) {
domElements.stopWorkflowBtn.style.display = 'none';
}
WorkflowCoordination.showInitialPromptView();
break;
case WORKFLOW_STATES.NULL:
// Reset to initial state
if (domElements.stopWorkflowBtn) {
domElements.stopWorkflowBtn.style.display = 'none';
}
if (domElements.emptyChatState) {
domElements.emptyChatState.style.display = 'flex';
}
if (domElements.agentChatMessages) {
domElements.agentChatMessages.style.display = 'none';
}
WorkflowCoordination.showInitialPromptView();
break;
}
}
/**
* Initializes file handling functionality
*/
function initFileHandling() {
// Setup file input handling
if (domElements.uploadAdditionalFileBtn && domElements.additionalFileInput) {
domElements.uploadAdditionalFileBtn.addEventListener('click', () => {
domElements.additionalFileInput.click();
});
domElements.additionalFileInput.addEventListener('change', (event) => {
const files = event.target.files;
for (let file of files) {
handleFileSelection(file);
}
event.target.value = '';
});
}
// Initialize drag & drop
if (domElements.userInputArea) {
initDragAndDrop(domElements.userInputArea);
}
}
/**
* Initializes drag and drop functionality
* @param {HTMLElement} dropArea - The area where files can be dropped
*/
function initDragAndDrop(dropArea) {
console.log("Initializing drag & drop for files");
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
// Highlight drop area when item is dragged over it
dropArea.addEventListener('dragenter', () => {
dropArea.classList.add('dragging');
});
dropArea.addEventListener('dragover', () => {
dropArea.classList.add('dragging');
});
// Remove highlight when item is dragged out or dropped
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('dragging');
});
dropArea.addEventListener('drop', (e) => {
dropArea.classList.remove('dragging');
const files = e.dataTransfer.files;
if (files.length > 0) {
for (let file of files) {
handleFileSelection(file);
}
}
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
}
/**
* Handles file selection from input or drop
* @param {File} file - The selected file
*/
async function handleFileSelection(file) {
try {
// Upload and process file
const processedFile = await api.uploadFile(file);
// Add to additional files list if successful
if (processedFile) {
const fileExists = WorkflowCoordination.userInputState.additionalFiles.some(f => f.id === processedFile.id);
if (!fileExists) {
WorkflowCoordination.userInputState.additionalFiles.push(processedFile);
WorkflowUI.renderAdditionalFiles(WorkflowCoordination.userInputState.additionalFiles);
WorkflowUI.updatePromptVisualization();
}
}
} catch (error) {
console.error(`Error processing file ${file.name}:`, error);
window.utils.ui.showToast("File Processing Error", `Failed to process file ${file.name}: ${error.message}`, "error");
}
}
/**
* Loads prompt options into the select dropdown
* @param {Array} prompts - Available prompt templates
*/
function loadPromptOptions(prompts) {
if (!domElements.promptSelectMain || !prompts) {
return;
}
// Clear existing options, keeping the default
while (domElements.promptSelectMain.options.length > 1) {
domElements.promptSelectMain.remove(1);
}
// Add prompts to select
prompts.forEach(prompt => {
const option = document.createElement('option');
option.value = prompt.id;
option.textContent = prompt.name || `Prompt ${prompt.id}`;
domElements.promptSelectMain.appendChild(option);
});
}
/**
* Handles selection from the prompt dropdown
*/
function handlePromptSelection() {
if (!domElements.promptSelectMain || !domElements.userMessageInput) {
return;
}
const selectedPromptId = domElements.promptSelectMain.value;
if (!selectedPromptId) {
domElements.userMessageInput.value = '';
WorkflowCoordination.userInputState.promptText = '';
return;
}
// Find selected prompt
const prompts = window.globalState?.mainView?.availablePrompts || [];
const selectedPrompt = prompts.find(p => String(p.id) === selectedPromptId);
if (selectedPrompt) {
domElements.userMessageInput.value = selectedPrompt.content;
WorkflowCoordination.userInputState.promptText = selectedPrompt.content;
domElements.userMessageInput.focus();
}
}
/**
* Sends a user response to the workflow based on current state
*/
async function sendUserResponse() {
if (!domElements.userMessageInput) {
console.error("Error: userMessageInput not found");
window.utils.ui.showToast("No input available", "error");
return;
}
// Get user message
const userMessage = WorkflowCoordination.userInputState.promptText.trim();
if (!userMessage) {
window.utils.ui.showToast("Missing Input. Please enter a message", "warning");
return;
}
// Get current workflow state
const workflowState = WorkflowCoordination.getWorkflowState();
// Set loading state
WorkflowCoordination.setLoadingState(true);
try {
// Create or continue workflow based on current state
if (workflowState.workflowId &&
[WORKFLOW_STATES.COMPLETED, WORKFLOW_STATES.FAILED, WORKFLOW_STATES.STOPPED].includes(workflowState.status)) {
// This is a continuation of a completed/failed/stopped workflow
await continueExistingWorkflow(workflowState.workflowId, userMessage);
} else if (workflowState.workflowId && workflowState.status === WORKFLOW_STATES.RUNNING) {
// Continuing a running workflow
await continueRunningWorkflow(workflowState.workflowId, userMessage);
} else {
// Starting a new workflow
await createNewWorkflow(userMessage);
}
// Reset input after successful submission
domElements.userMessageInput.value = '';
WorkflowCoordination.userInputState.promptText = '';
WorkflowCoordination.clearAttachedFiles();
} catch (error) {
console.error("Error sending message:", error);
// Transition to failed state if we were in running state
if (workflowState.status === WORKFLOW_STATES.RUNNING) {
WorkflowCoordination.updateWorkflowStatus(WORKFLOW_STATES.FAILED, {
message: `Communication error: ${error.message}`,
systemMessage: "Failed to process your request"
});
}
window.utils.ui.showToast(`Communication Error: Failed to send message: ${error.message}`, "error");
} finally {
// Unlock UI
WorkflowCoordination.setLoadingState(false);
}
}
/**
* Creates a new workflow with user prompt
* @param {string} userMessage - User prompt
*/
async function createNewWorkflow(userMessage) {
console.log("Starting new workflow");
// Add log entry for starting workflow
const startLog = WorkflowCoordination.addLogEntry("Starting new workflow...", "info");
try {
// Transition state to running before API call
WorkflowCoordination.updateWorkflowStatus(WORKFLOW_STATES.RUNNING, {
message: "Preparing workflow",
logId: startLog.id
});
// Create new workflow
const response = await WorkflowData.createWorkflow(
userMessage,
WorkflowCoordination.userInputState.additionalFiles
);
if (!response || !response.id) {
throw new Error("Failed to start workflow: No workflow ID received");
}
// Set active workflow
WorkflowCoordination.setActiveWorkflow(response.id);
// Start polling for updates
WorkflowData.pollWorkflowStatus(response.id).catch(error => {
console.error("Error in polling process:", error);
// Optionally handle the error (e.g., show a notification)
});
} catch (error) {
console.error("Error creating workflow:", error);
// Revert to null state
WorkflowCoordination.updateWorkflowStatus(WORKFLOW_STATES.NULL, {
message: `Failed to create workflow: ${error.message}`,
systemMessage: "Failed to start workflow"
});
throw error;
}
}
/**
* Continues a completed, failed, or stopped workflow
* @param {string} workflowId - ID of the workflow
* @param {string} userMessage - User prompt
*/
async function continueExistingWorkflow(workflowId, userMessage) {
console.log(`Continuing workflow ${workflowId} from ${WorkflowCoordination.getWorkflowStatus()} state`);
// Add log entry
const continueLog = WorkflowCoordination.addLogEntry("Continuing workflow...", "info");
try {
// Transition state to running
WorkflowCoordination.updateWorkflowStatus(WORKFLOW_STATES.RUNNING, {
message: "Resuming workflow",
logId: continueLog.id
});
// Get file IDs
const additionalFileIds = WorkflowCoordination.userInputState.additionalFiles.map(file => file.id);
// Submit user input
await WorkflowData.submitUserInput(
workflowId,
userMessage,
additionalFileIds
);
// Start polling
WorkflowData.pollWorkflowStatus(workflowId);
} catch (error) {
console.error("Error continuing workflow:", error);
// Revert to previous state
WorkflowCoordination.updateWorkflowStatus(WORKFLOW_STATES.FAILED, {
message: `Failed to continue workflow: ${error.message}`,
systemMessage: "Failed to process your request"
});
throw error;
}
}
/**
* Continues a running workflow
* @param {string} workflowId - ID of the workflow
* @param {string} userMessage - User prompt
*/
async function continueRunningWorkflow(workflowId, userMessage) {
console.log(`Continuing running workflow ${workflowId}`);
try {
// Get file IDs
const additionalFileIds = WorkflowCoordination.userInputState.additionalFiles.map(file => file.id);
// Submit user input
await WorkflowData.submitUserInput(
workflowId,
userMessage,
additionalFileIds
);
// Update logs
WorkflowCoordination.addLogEntry("Continuing workflow with user input", "info");
// Make sure we're still in running state
if (WorkflowCoordination.getWorkflowStatus() !== WORKFLOW_STATES.RUNNING) {
WorkflowCoordination.updateWorkflowStatus(WORKFLOW_STATES.RUNNING, {
message: "Processing user input"
});
}
// Poll for updates
WorkflowData.pollWorkflowStatus(workflowId);
} catch (error) {
console.error("Error submitting to running workflow:", error);
throw error;
}
}
/**
* Stops the current workflow
*/
async function stopWorkflow() {
const workflowState = WorkflowCoordination.getWorkflowState();
if (!workflowState.workflowId || workflowState.status !== WORKFLOW_STATES.RUNNING) {
console.warn("No running workflow to stop");
return;
}
try {
// IMMEDIATELY modify state before async operations
// This prevents race conditions during workflow stopping
workflowState.pollActive = false;
// Clear polling sequence to prevent orphaned polling
if (workflowState._pollingSequenceId) {
workflowState._pollingSequenceId = null;
}
// Add log entry
WorkflowCoordination.addLogEntry("Stopping workflow...", "warning");
// Call API to stop workflow
await WorkflowData.stopWorkflow(workflowState.workflowId);
// Update to stopped status
WorkflowCoordination.updateWorkflowStatus(WORKFLOW_STATES.STOPPED, {
message: "Workflow has been stopped",
systemMessage: "Workflow was manually stopped"
});
console.log("Workflow stopped successfully, polling disabled");
} catch (error) {
console.error("Error stopping workflow:", error);
// Log error but still try to update UI state
WorkflowCoordination.addLogEntry(`Error stopping workflow: ${error.message}`, "error");
// Force status to stopped even if API call failed
WorkflowCoordination.updateWorkflowStatus(WORKFLOW_STATES.STOPPED, {
message: "Workflow stopped with errors",
systemMessage: "Workflow was stopped but there were errors"
});
}
}
/**
* Resets the workflow completely
*/
function resetWorkflow() {
// Stop any ongoing polling
workflowState.pollActive = false;
// Clear polling sequence to prevent orphaned polling
if (workflowState._pollingSequenceId) {
workflowState._pollingSequenceId = null;
}
// Reset state
workflowState = {
status: null,
workflowId: "",
logs: [],
chatMessages: [],
lastPolledLogId: null,
lastPolledMessageId: null,
dataStats: {
bytesSent: 0,
bytesReceived: 0,
tokensUsed: 0
},
pollFailCount: 0,
pollActive: false
};
// Reset user input
userInputState.promptText = "";
userInputState.additionalFiles = [];
// Stop animations
stopWaitingAnimation();
// Dispatch event for UI update
const event = new CustomEvent('workflowReset');
document.dispatchEvent(event);
console.log("Workflow state reset");
}
/**
* Handles layout changes
* @param {Object} layoutConfig - Layout configuration
*/
function handleLayoutChange(layoutConfig) {
console.log("Layout change:", layoutConfig);
// Implement layout change handling logic
if (layoutConfig.collapseHeader) {
if (domElements.workflowHeader) {
domElements.workflowHeader.classList.add('collapsed');
}
}
if (layoutConfig.expandHeader) {
if (domElements.workflowHeader) {
domElements.workflowHeader.classList.remove('collapsed');
}
}
// Force layout adjustment
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 100);
}
// Export the initialization function
export {
initWorkflowModule,
WORKFLOW_STATES
};