672 lines
No EOL
20 KiB
JavaScript
672 lines
No EOL
20 KiB
JavaScript
/**
|
|
* 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 ?
|
|
'<i class="fas fa-paper-plane"></i> Send' :
|
|
'<i class="fas fa-play"></i> 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 = '<i class="fas fa-spinner fa-spin"></i>';
|
|
} else {
|
|
// Different icons based on context
|
|
elements.sendUserMessageBtn.innerHTML = workflowState.workflowId ?
|
|
'<i class="fas fa-paper-plane"></i> Send' :
|
|
'<i class="fas fa-play"></i> 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
|
|
}; |