/** * 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 };