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