/** * Central coordination layer for workflow state management * Handles state transitions and provides methods for updating state */ // Workflow state with simplified status management let workflowState = { status: null, // null or 'running', 'completed', 'failed', 'stopped' workflowId: "", logs: [], chatMessages: [], lastPolledLogId: null, lastPolledMessageId: null, dataStats: { bytesSent: 0, bytesReceived: 0, tokensUsed: 0 }, pollFailCount: 0, // For tracking consecutive polling failures pollActive: false }; // User input state let userInputState = { promptText: "", // Current prompt text additionalFiles: [], // Additional files to send with prompt domElements: {} // DOM element references }; // Waiting animation state let waitingDotsInterval = null; let lastWaitingLogId = null; /** * Initializes the coordination module with a workflow state * @param {Object} initialState - Initial workflow state */ function initCoordination(initialState) { console.log("Initializing workflow coordination..."); // Initialize state with provided state or defaults if (initialState) { workflowState = { ...workflowState, ...initialState }; } document.addEventListener('removeAdditionalFile', function(event) { if (!event.detail || typeof event.detail.index !== 'number') { console.error("Missing index in removeAdditionalFile event"); return; } const index = event.detail.index; // Validate index is in range if (index >= 0 && index < userInputState.additionalFiles.length) { // Remove the file at the specified index userInputState.additionalFiles.splice(index, 1); // Update UI components const filesEvent = new CustomEvent('filesUpdated', { detail: { files: userInputState.additionalFiles } }); document.dispatchEvent(filesEvent); } }); console.log("Workflow coordination initialized with state:", workflowState); } /** * Updates workflow status and synchronizes UI * @param {string} newStatus - New status (null, 'running', 'completed', 'failed', 'stopped') * @param {Object} options - Additional options (message, workflowId, etc.) * @returns {Object} Updated workflow state */ function updateWorkflowStatus(newStatus, options = {}) { // Take a snapshot of the current state to detect race conditions const prevStatus = workflowState.status; const prevPollActive = workflowState.pollActive; // Validate state transition if (!isValidStateTransition(prevStatus, newStatus)) { console.warn(`Invalid state transition: ${prevStatus} → ${newStatus}`); return workflowState; } // Update workflow ID if provided if (options.workflowId) { workflowState.workflowId = options.workflowId; } // Log status change console.log(`Workflow status change: ${prevStatus} → ${newStatus}`, options.message ? `(${options.message})` : ''); // Set the new status - THIS MUST HAPPEN BEFORE MODIFYING pollActive workflowState.status = newStatus; // Reset poll fail count on valid status changes if (newStatus !== prevStatus) { workflowState.pollFailCount = 0; } if (newStatus === 'running' || newStatus === 'completed') { workflowState.pollActive = true; console.log(`Status changed to ${newStatus}, polling remains active`); } else { // For other states ('failed', 'stopped', null), disable polling workflowState.pollActive = false; console.log(`Status changed to ${newStatus}, polling deactivated`); } // Log if polling state changed to help debug race conditions if (prevPollActive !== workflowState.pollActive) { console.log(`Polling state changed: ${prevPollActive} → ${workflowState.pollActive}`); } // Add log entry if message provided if (options.message) { let logType = 'info'; if (newStatus === 'completed') logType = 'success'; if (newStatus === 'failed') logType = 'error'; if (newStatus === 'stopped') logType = 'warning'; addLogEntry(options.message, logType, null, options.agent || 'System'); } // Status-specific actions switch (newStatus) { case 'running': // Start/continue animation on log if specified if (options.logId) { startWaitingAnimation(options.logId); } else if (workflowState.logs.length > 0) { const recentLogs = workflowState.logs .filter(log => log.type === 'info' && !log.message.includes('completed')) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); if (recentLogs.length > 0) { startWaitingAnimation(recentLogs[0].id); } } break; case 'completed': case 'failed': case 'stopped': // Stop any animations stopWaitingAnimation(); // Add system message to chat if provided if (options.systemMessage) { addChatMessage({ type: 'system', content: options.systemMessage, timestamp: new Date().toISOString() }); } break; } // Trigger UI updates by dispatching an event const event = new CustomEvent('workflowStatusChanged', { detail: { status: newStatus, previousStatus: prevStatus, options: options } }); document.dispatchEvent(event); // Execute additional callback if provided if (typeof options.callback === 'function') { options.callback(); } return workflowState; } /** * Checks if a state transition is valid according to the state machine * @param {string} fromState - Current state * @param {string} toState - Target state * @returns {boolean} - Whether transition is valid */ function isValidStateTransition(fromState, toState) { // Define valid transitions const validTransitions = { 'null': ['running'], 'running': ['running', 'completed', 'failed', 'stopped'], 'completed': ['running', 'null'], 'failed': ['running', 'null'], 'stopped': ['running', 'null'] }; // Special case: Reset to null is always allowed if (toState === null) { return true; } // Check if current state has defined transitions if (!validTransitions[fromState]) { // Allow any transition if current state is unknown return true; } // Check if transition is valid return validTransitions[fromState].includes(toState); } /** * Shows the initial prompt view */ function showInitialPromptView() { const elements = userInputState.domElements; console.log("Showing initial prompt view"); // Change placeholder for initial prompts if (elements.userMessageInput) { elements.userMessageInput.placeholder = workflowState.workflowId ? "Continue the conversation..." : "Enter a new prompt..."; elements.userMessageInput.value = ""; // Clear existing text userInputState.promptText = ""; } // Change button text based on state if (elements.sendUserMessageBtn) { elements.sendUserMessageBtn.innerHTML = workflowState.workflowId ? ' Send' : ' Start'; } // Clear attached files userInputState.additionalFiles = []; if (elements.additionalFilesContainer) { elements.additionalFilesContainer.innerHTML = ''; } // Focus input field if (elements.userMessageInput) { elements.userMessageInput.focus(); } // Update UI elements const event = new CustomEvent('showInitialPromptView', { detail: { workflowId: workflowState.workflowId } }); document.dispatchEvent(event); } /** * Sets loading state for UI * @param {boolean} isLoading - Whether UI is in loading state */ function setLoadingState(isLoading) { const elements = userInputState.domElements; console.log(`Setting UI loading state: ${isLoading ? 'Active' : 'Inactive'}`); // User input field if (elements.userMessageInput) { elements.userMessageInput.disabled = isLoading; } // Send button if (elements.sendUserMessageBtn) { elements.sendUserMessageBtn.disabled = isLoading; if (isLoading) { elements.sendUserMessageBtn.innerHTML = ''; } else { // Different icons based on context elements.sendUserMessageBtn.innerHTML = workflowState.workflowId ? ' Send' : ' Start'; } } // File upload button if (elements.uploadAdditionalFileBtn) { elements.uploadAdditionalFileBtn.disabled = isLoading; } // Prompt selection if (elements.promptSelectMain) { elements.promptSelectMain.disabled = isLoading; } // Dispatch event for other components to respond const event = new CustomEvent('loadingStateChanged', { detail: { isLoading: isLoading } }); document.dispatchEvent(event); } /** * Starts waiting animation on a log entry * @param {string} logId - ID of the log entry */ function startWaitingAnimation(logId) { // Stop any existing animation stopWaitingAnimation(); // Mark log as waiting const log = workflowState.logs.find(l => l.id === logId); if (log) { // Reset waiting status for all logs workflowState.logs.forEach(l => l.waiting = false); // Set waiting status for this log log.waiting = true; lastWaitingLogId = logId; // Start animation waitingDotsInterval = setInterval(() => { updateWaitingDots(); }, 500); // Dispatch event for UI update const event = new CustomEvent('logsUpdated', { detail: { logs: workflowState.logs, waitingLogId: logId } }); document.dispatchEvent(event); // Start animation immediately updateWaitingDots(); } else { console.warn("Log entry not found for waiting animation:", logId); } } /** * Stops waiting animation */ function stopWaitingAnimation() { if (waitingDotsInterval) { clearInterval(waitingDotsInterval); waitingDotsInterval = null; } // Reset waiting status for all logs if (workflowState.logs) { workflowState.logs.forEach(log => { if (log.waiting) { log.waiting = false; } }); } // Clear waiting dots in DOM try { document.querySelectorAll('.waiting-dots').forEach(el => { el.textContent = ''; }); } catch (e) { console.error("Error clearing waiting dots:", e); } // Reset last waiting log ID lastWaitingLogId = null; // Dispatch event for UI update const event = new CustomEvent('logsUpdated', { detail: { logs: workflowState.logs, waitingLogId: null } }); document.dispatchEvent(event); } /** * Updates waiting dots animation */ function updateWaitingDots() { const waitingDotsElements = document.querySelectorAll('.waiting-dots'); waitingDotsElements.forEach(element => { // Get current dots count const currentText = element.textContent || ''; let dotsCount = currentText.length; // Increment and limit dots count dotsCount = (dotsCount + 1) % 4; element.textContent = '.'.repeat(dotsCount); }); // If no elements found but animation is running, force re-render if (waitingDotsElements.length === 0 && waitingDotsInterval) { // Find logs with waiting flag const waitingLogs = workflowState.logs ? workflowState.logs.filter(log => log.waiting) : []; if (waitingLogs.length > 0) { // Dispatch event for UI update const event = new CustomEvent('logsUpdated', { detail: { logs: workflowState.logs } }); document.dispatchEvent(event); } } } /** * Sets the active workflow * @param {string} workflowId - ID of the active workflow */ function setActiveWorkflow(workflowId) { if (!workflowId) { console.error("Invalid workflow ID"); return; } console.log(`Setting active workflow: ${workflowId}`); workflowState.workflowId = workflowId; workflowState.pollActive = true; // Set polling to active when workflow becomes active // Update global state for better accessibility if (window.globalState && window.globalState.mainView) { window.globalState.mainView.currentWorkflowId = workflowId; console.log("Workflow ID also set in globalState"); } // Update workflow status to running updateWorkflowStatus('running', { workflowId: workflowId, message: "Workflow started" }); } /** * Adds a log entry to the workflow * @param {string} message - Log message * @param {string} type - Log type ('info', 'warning', 'error', 'success') * @param {string|null} details - Additional details * @param {string} agentName - Name of the agent that generated the log * @param {number} progress - Progress value (0-100) * @returns {Object} The created log entry */ function addLogEntry(message, type = 'info', details = null, agentName = null, progress = null) { // Stop previous animation stopWaitingAnimation(); console.log(`Adding log entry: ${type} - ${message}${agentName ? ` [${agentName}]` : ''}`); const log = { id: `log_${Date.now()}`, message, type, details, agentName, timestamp: new Date().toISOString(), progress: progress, status: workflowState.status }; // Ensure logs array exists if (!workflowState.logs) { workflowState.logs = []; } // Special formatting for certain message types if (message.includes("Agent") && message.includes("selected")) { log.type = "info"; log.highlighted = true; } if (message.includes("Moderator") && message.includes("analyzing")) { log.type = "info"; log.highlighted = true; } if (message.includes("completed") || message.includes("finished")) { log.type = "success"; } workflowState.logs.push(log); // Dispatch event for UI update const event = new CustomEvent('logsUpdated', { detail: { logs: workflowState.logs, newLog: log } }); document.dispatchEvent(event); // If workflow is running, start waiting animation if (workflowState.status === 'running') { startWaitingAnimation(log.id); } return log; } /** * Adds a chat message to the workflow * @param {Object} message - Message to add * @returns {Object} The added message */ function addChatMessage(message) { // Ensure message has ID and other required properties const processedMessage = { ...message, id: message.id || `msg_${message.role || 'unknown'}_${Date.now()}`, documents: message.documents || [], timestamp: message.timestamp || new Date().toISOString() }; console.log(`Adding chat message (ID: ${processedMessage.id}, Role: ${processedMessage.role || 'unknown'}, Status: ${processedMessage.status || 'none'})`); // Ensure chat messages array exists if (!workflowState.chatMessages) { workflowState.chatMessages = []; } // Check for duplicates and update or add message const existingIndex = workflowState.chatMessages.findIndex(m => m.id === processedMessage.id); if (existingIndex === -1) { workflowState.chatMessages.push(processedMessage); } else { workflowState.chatMessages[existingIndex] = processedMessage; } // IMPORTANT: Handle message status according to state machine spec if (processedMessage.status === 'last') { console.log("Last message received, stopping polling without changing workflow status"); workflowState.pollActive = false; // Dispatch a workflow completion event const completionEvent = new CustomEvent('workflowCompleted', { detail: { status: workflowState.status, message: "All workflow messages received" } }); document.dispatchEvent(completionEvent); } // Dispatch event for UI update with immutable copies const event = new CustomEvent('chatMessagesUpdated', { detail: { chatMessages: [...workflowState.chatMessages], newMessage: {...processedMessage}, workflowId: workflowState.workflowId } }); document.dispatchEvent(event); return processedMessage; } /** * Clears all attached files */ function clearAttachedFiles() { userInputState.additionalFiles = []; // Dispatch event for UI update const event = new CustomEvent('filesUpdated', { detail: { files: userInputState.additionalFiles } }); document.dispatchEvent(event); // Clear file input const fileInput = userInputState.domElements.additionalFileInput; if (fileInput) { fileInput.value = ''; } console.log("Files cleared from input area"); } /** * Updates data statistics * @param {number} sentBytes - Sent bytes * @param {number} receivedBytes - Received bytes * @param {number} tokensUsed - Tokens used (for AI models) */ function updateDataStats(sentBytes, receivedBytes, tokensUsed = 0) { // Ensure valid numbers if (sentBytes && !isNaN(sentBytes)) { workflowState.dataStats.bytesSent += Math.max(0, parseInt(sentBytes, 10)); } if (receivedBytes && !isNaN(receivedBytes)) { workflowState.dataStats.bytesReceived += Math.max(0, parseInt(receivedBytes, 10)); } if (tokensUsed && !isNaN(tokensUsed)) { workflowState.dataStats.tokensUsed += Math.max(0, parseInt(tokensUsed, 10)); } // Dispatch event for UI update const event = new CustomEvent('dataStatsUpdated', { detail: { sentBytes: workflowState.dataStats.bytesSent, receivedBytes: workflowState.dataStats.bytesReceived, tokensUsed: workflowState.dataStats.tokensUsed } }); document.dispatchEvent(event); } /** * Gets current workflow state * @returns {Object} Workflow state */ function getWorkflowState() { // Return a copy to prevent unintended modifications return workflowState; } /** * Gets workflow status * @returns {string} Workflow status */ function getWorkflowStatus() { return workflowState.status; } /** * Resets workflow state */ function resetWorkflow() { // 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"); } // Export all functions and state objects export { initCoordination, userInputState, updateWorkflowStatus, showInitialPromptView, setLoadingState, startWaitingAnimation, stopWaitingAnimation, setActiveWorkflow, addLogEntry, addChatMessage, clearAttachedFiles, updateDataStats, getWorkflowState, getWorkflowStatus, resetWorkflow, isValidStateTransition };