/**
* Responsible for all UI rendering and updates in the workflow module
* Purely renders UI based on state, contains no business logic
*/
import api from '../shared/apiCalls.js';
import * as WorkflowUtils from './workflowUtils.js';
// DOM elements
let domElements = {};
// Current preview file
let currentPreviewFile = null;
/**
* Initializes the UI components
* @param {Object} workflowState - Current workflow state
* @param {Object} callbackFunctions - Callback functions for UI events
*/
function initUI(workflowState, callbackFunctions) {
console.log("Initializing workflow UI...");
// Store callback functions
const callbacks = {
onLayoutChange: callbackFunctions.onLayoutChange || (() => {}),
onResetWorkflow: callbackFunctions.onResetWorkflow || (() => {}),
onStopWorkflow: callbackFunctions.onStopWorkflow || (() => {})
};
// Set up event listeners for the custom events from coordination layer
document.addEventListener('workflowStatusChanged', (event) => {
updateWorkflowButtonsState(event.detail);
});
document.addEventListener('logsUpdated', (event) => {
renderLogs(event.detail.logs, event.detail.waitingLogId);
});
document.addEventListener('chatMessagesUpdated', (event) => {
renderChatMessages(event.detail.chatMessages, event.detail.workflowId);
});
document.addEventListener('dataStatsUpdated', (event) => {
updateDataStatistics(event.detail.sentBytes, event.detail.receivedBytes, event.detail.tokensUsed);
});
document.addEventListener('filesUpdated', (event) => {
renderAdditionalFiles(event.detail.files);
});
// Add listener for workflow completion
document.addEventListener('workflowCompleted', (event) => {
showWorkflowCompletionIndicator(event.detail);
});
// Initialize buttons
setupUIButtons(callbacks);
// Initialize layout controls
initializeFixedLayout();
setupHeaderToggle();
// Initial renders
renderLogs(workflowState.logs || []);
renderChatMessages(workflowState.chatMessages || [], workflowState.workflowId);
updateDataStatistics(
workflowState.dataStats ? workflowState.dataStats.bytesSent : 0,
workflowState.dataStats ? workflowState.dataStats.bytesReceived : 0,
workflowState.dataStats ? workflowState.dataStats.tokensUsed : 0
);
updateWorkflowButtonsState({
status: workflowState.status,
workflowId: workflowState.workflowId
});
console.log("Workflow UI successfully initialized");
}
/**
* Sets up UI buttons and their event handlers
* @param {Object} callbacks - Callback functions
*/
function setupUIButtons(callbacks) {
// Reset button
const resetBtn = document.getElementById('reset-btn');
if (resetBtn) {
resetBtn.addEventListener('click', callbacks.onResetWorkflow);
}
// Stop button
const stopBtn = document.getElementById('stop-workflow-btn');
if (stopBtn) {
stopBtn.addEventListener('click', callbacks.onStopWorkflow);
}
// File preview controls
const downloadBtn = document.getElementById('download-file-btn');
if (downloadBtn) {
downloadBtn.addEventListener('click', downloadPreviewFile);
}
const copyBtn = document.getElementById('copy-file-btn');
if (copyBtn) {
copyBtn.addEventListener('click', copyPreviewFileContent);
}
const closePreviewBtn = document.getElementById('close-file-preview-btn');
if (closePreviewBtn) {
closePreviewBtn.addEventListener('click', () => {
const container = document.getElementById('file-preview-container');
if (container) {
container.style.display = 'none';
}
});
}
}
/**
* Updates the status of workflow buttons based on current state
* @param {Object} stateInfo - Object containing state information
*/
function updateWorkflowButtonsState(stateInfo) {
// Extract status (supports both direct status and event detail format)
const status = stateInfo.status || (stateInfo.detail ? stateInfo.detail.status : null);
const workflowId = stateInfo.workflowId || (stateInfo.detail ? stateInfo.detail.workflowId : null);
if (status === undefined) return;
console.log(`Updating workflow buttons for status: ${status}`);
const stopBtn = document.getElementById('stop-workflow-btn');
const resetBtn = document.getElementById('reset-btn');
const sendBtn = document.getElementById('send-user-message-btn');
const userInput = document.getElementById('user-message-input');
// Update stop button
if (stopBtn) {
if (status === 'running') {
stopBtn.style.display = 'inline-block';
stopBtn.disabled = false;
stopBtn.innerHTML = ' Stop';
} else {
stopBtn.style.display = 'none';
}
}
// Update reset button
if (resetBtn) {
resetBtn.disabled = status === 'running';
}
// Update send button and input field
const isInputEnabled = status !== 'running';
if (sendBtn) {
sendBtn.disabled = !isInputEnabled;
// Only update text if not in loading state (no spinner)
if (!sendBtn.querySelector('.fa-spinner')) {
const isInitial = !workflowId && (status === null);
sendBtn.innerHTML = isInitial ?
' Start' :
' Send';
}
}
// Update input field
if (userInput) {
userInput.disabled = !isInputEnabled;
// Update placeholder based on state
if (status === 'completed') {
userInput.placeholder = "Workflow completed. Enter a new prompt to continue...";
} else if (status === 'failed') {
userInput.placeholder = "Workflow failed. Enter a new prompt to retry...";
} else if (status === 'stopped') {
userInput.placeholder = "Workflow stopped. Enter a new prompt to resume...";
} else if (!workflowId) {
userInput.placeholder = "Enter a prompt to start...";
} else {
userInput.placeholder = "Enter your message...";
}
}
// Update workflow status indicator
updateWorkflowStatusIndicator(status);
}
/**
* Updates the workflow status indicator in the UI
* @param {string} status - Workflow status
*/
function updateWorkflowStatusIndicator(status) {
// Find or create status indicator
let statusIndicator = document.getElementById('workflow-status-indicator');
if (!statusIndicator) {
statusIndicator = document.createElement('div');
statusIndicator.id = 'workflow-status-indicator';
// Insert after header or as first child of workflow container
const header = document.querySelector('.workflow-header');
const container = document.querySelector('.workflow-container');
if (header && header.parentNode) {
header.parentNode.insertBefore(statusIndicator, header.nextSibling);
} else if (container) {
container.insertBefore(statusIndicator, container.firstChild);
}
}
// Set status classes and text
statusIndicator.className = `status-indicator status-${status || 'null'}`;
let statusText = '';
let statusIcon = '';
switch (status) {
case 'running':
statusText = 'Workflow Running';
statusIcon = '';
break;
case 'completed':
statusText = 'Workflow Completed';
statusIcon = '';
break;
case 'failed':
statusText = 'Workflow Failed';
statusIcon = '';
break;
case 'stopped':
statusText = 'Workflow Stopped';
statusIcon = '';
break;
default:
statusText = 'Ready to Start';
statusIcon = '';
}
statusIndicator.innerHTML = `${statusIcon} ${statusText}`;
// Hide indicator if status is null
if (status === null) {
statusIndicator.style.display = 'none';
} else {
statusIndicator.style.display = 'flex';
}
}
/**
* Shows a workflow completion indicator
* @param {Object} detail - Completion details
*/
function showWorkflowCompletionIndicator(detail) {
const chatContainer = document.getElementById('agent-chat-messages');
if (!chatContainer) return;
// Create completion indicator
const completionIndicator = document.createElement('div');
completionIndicator.className = 'workflow-completion-indicator';
let message = detail.message || 'Workflow completed';
let className = 'completion-success';
let icon = 'check-circle';
if (detail.status === 'failed') {
message = detail.message || 'Workflow failed';
className = 'completion-error';
icon = 'exclamation-circle';
} else if (detail.status === 'stopped') {
message = detail.message || 'Workflow stopped';
className = 'completion-warning';
icon = 'stop-circle';
}
completionIndicator.classList.add(className);
completionIndicator.innerHTML = `
${message}
`;
// Add to chat
chatContainer.appendChild(completionIndicator);
// Scroll to bottom
chatContainer.scrollTop = chatContainer.scrollHeight;
// Remove after delay
setTimeout(() => {
completionIndicator.classList.add('fade-out');
setTimeout(() => {
if (completionIndicator.parentNode) {
completionIndicator.parentNode.removeChild(completionIndicator);
}
}, 500);
}, 5000);
}
/**
* Renders logs in the UI
* @param {Array} logs - Array of log entries
* @param {string} waitingLogId - ID of the log that should show a waiting animation
*/
function renderLogs(logs, waitingLogId = null) {
const logContainer = document.getElementById('execution-log');
if (!logContainer) return;
// Check if user has scrolled to bottom
const isAtBottom = isUserAtBottom(logContainer);
// If no logs, show empty state
if (!logs || logs.length === 0) {
logContainer.innerHTML = '
No logs yet. Workflow execution logs will appear here.
';
return;
}
// Build logs HTML
let logsHTML = '';
// Deduplicate logs by grouping those with the same message and agent
const uniqueLogs = [];
const seenMessageAgentPairs = new Map();
// Process logs backwards (newest first) to keep the latest one in each group
for (let i = logs.length - 1; i >= 0; i--) {
const log = logs[i];
if (!log) continue;
const key = `${log.message}|${log.agentName || ''}`;
// If we haven't seen this message+agent combo or this is a special log type, add it
if (!seenMessageAgentPairs.has(key) ||
log.type === 'error' ||
log.type === 'warning' ||
log.highlighted) {
seenMessageAgentPairs.set(key, uniqueLogs.length);
uniqueLogs.unshift(log); // Add to beginning to maintain original order
} else {
// If we already have this message, update the existing one with any new info
const existingIndex = seenMessageAgentPairs.get(key);
// Keep the newest ID (for polling reference)
uniqueLogs[existingIndex].id = log.id;
// Update progress if present
if (log.progress !== undefined && log.progress !== null) {
uniqueLogs[existingIndex].progress = log.progress;
}
// Update details if present
if (log.details && !uniqueLogs[existingIndex].details) {
uniqueLogs[existingIndex].details = log.details;
}
// Update waiting status if needed
if (log.waiting) {
uniqueLogs[existingIndex].waiting = true;
}
}
}
// Find the latest log with progress information and mark it
let latestProgressLog = null;
for (let i = uniqueLogs.length - 1; i >= 0; i--) {
const log = uniqueLogs[i];
if (log && log.progress !== undefined && log.progress !== null) {
latestProgressLog = log;
break;
}
}
// Now render the deduplicated logs
uniqueLogs.forEach((log, index) => {
if (!log) return; // Skip invalid logs
const logId = log.id || `log_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
const logType = log.type || 'info';
const logMessage = log.message || 'No message';
const logTimestamp = log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
// FIX 1: Only show waiting animation on the last log entry
// All older logs should have no animation
const isLastLog = index === uniqueLogs.length - 1;
const shouldShowWaiting = isLastLog && (waitingLogId === null || waitingLogId === logId);
const logWaiting = shouldShowWaiting ? '' : '';
// Extract agent name and format it
let agentHTML = '';
if (log.agentName) {
// Agent-specific colors
const agentClass = `agent-${log.agentName.toLowerCase().replace(/\s+/g, '-')}`;
agentHTML = `[${log.agentName}]`;
}
// Extra classes for styling
const highlightClass = log.highlighted ? 'highlighted' : '';
// FIX 1: Progress indicator - only show active progress for the latest log
// All previous logs should be either at 100% or have no progress bar
let progressHTML = '';
if (log.progress !== undefined && log.progress !== null) {
// For the latest log, show actual progress
// For all older logs, force to 100%
const progressPercent = isLastLog ?
Math.min(100, Math.max(0, parseInt(log.progress, 10))) : 100;
progressHTML = `
${progressPercent}%
`;
}
// Collapsible details
if (log.details) {
// Format the log message for display - truncate if too long
const shortMessage = truncateLogMessage(logMessage, 60);
const isCollapsed = !log.expanded; // Default to collapsed unless explicitly expanded
logsHTML += `
`;
}
});
// Batch DOM updates using requestAnimationFrame to reduce reflow violations
requestAnimationFrame(() => {
logContainer.innerHTML = logsHTML;
// Add toggle listener for collapsible details
logContainer.querySelectorAll('.log-toggle').forEach(toggle => {
toggle.addEventListener('click', () => {
const logEntry = toggle.closest('.log-entry');
const content = logEntry.querySelector('.log-content');
const isVisible = content.style.display !== 'none';
// Toggle display
content.style.display = isVisible ? 'none' : 'block';
toggle.textContent = isVisible ? '▾' : '▴';
// Update full message when expanded
if (!isVisible) {
const logMessage = logEntry.querySelector('.log-message');
const fullMessage = logMessage.getAttribute('title');
if (fullMessage) {
logMessage.textContent = fullMessage;
}
} else {
// Revert to truncated when collapsed
const logMessage = logEntry.querySelector('.log-message');
const fullMessage = logMessage.getAttribute('title');
if (fullMessage) {
logMessage.textContent = truncateLogMessage(fullMessage, 60);
}
}
// Store expanded state for this log ID
const logId = logEntry.getAttribute('data-log-id');
if (logId) {
const log = logs.find(l => l.id === logId);
if (log) {
log.expanded = !isVisible;
}
}
});
});
// Auto-scroll if user was at bottom - using requestAnimationFrame to avoid forced reflow
if (isAtBottom) {
requestAnimationFrame(() => {
logContainer.scrollTop = logContainer.scrollHeight;
});
}
});
}
/**
* Truncates a log message to a specified length
* @param {string} message - The message to truncate
* @param {number} maxLength - Maximum length before truncation
* @returns {string} - Truncated message
*/
function truncateLogMessage(message, maxLength = 60) {
if (!message || message.length <= maxLength) {
return message;
}
return message.substring(0, maxLength) + '...';
}
/**
* Renders chat messages in the UI
* @param {Array} chatMessages - Array of chat messages
* @param {string} currentWorkflowId - ID of the current workflow
*/
function renderChatMessages(chatMessages, currentWorkflowId) {
const chatContainer = document.getElementById('agent-chat-messages');
const emptyStateContainer = document.getElementById('empty-chat-state');
if (!chatContainer) return;
// If no messages, show empty state
if (!chatMessages || chatMessages.length === 0) {
if (emptyStateContainer) {
emptyStateContainer.style.display = 'flex';
}
chatContainer.style.display = 'none';
return;
}
// Show chat container, hide empty state
if (emptyStateContainer) {
emptyStateContainer.style.display = 'none';
}
chatContainer.style.display = 'block';
// Remember scroll position
const wasAtBottom = isUserAtBottom(chatContainer);
// Clear container
chatContainer.innerHTML = '';
// Render each message
chatMessages.forEach((message, index) => {
if (!message) return; // Skip invalid messages
const messageDiv = document.createElement('div');
if (message.type === 'system') {
// System message
messageDiv.className = 'chat-message system-message';
messageDiv.innerHTML = `
${message.content || 'No message'}
`;
} else {
// Standard message (user or assistant)
const isLastMessage = index === chatMessages.length - 1;
const isCollapsed = !isLastMessage && message.role !== 'user'; // Only last message expanded by default, user messages always expanded
const isUserMessage = message.role === 'user';
// Apply classes
messageDiv.className = `chat-message agent-message ${isCollapsed ? 'collapsed' : ''} ${isUserMessage ? 'user' : 'assistant'}`;
// Check for message status
if (message.status) {
messageDiv.setAttribute('data-status', message.status);
// Handle "last" message status - completion indicator
if (message.status === 'last') {
messageDiv.classList.add('last-message');
}
}
// Format content
let formattedContent = message.content || '';
// Delete button for messages
let deleteButton = '';
if (currentWorkflowId && message.id) {
deleteButton = `
`;
}
// File attachments
let filesHTML = '';
if (message.documents && message.documents.length > 0) {
filesHTML = `
Attached Files (${message.documents.length}):
`;
message.documents.forEach(doc => {
// Extract file information from the document object
const fileInfo = extractFileInfo(doc);
if (fileInfo) {
const fileIcon = WorkflowUtils.getFileTypeIcon({mimeType: fileInfo.contentType});
// IMPORTANT: Use fileId for data-file-id if available, otherwise fall back to id
const fileIdForAPI = fileInfo.fileId || fileInfo.id;
filesHTML += `
${fileInfo.name}
`;
}
});
filesHTML += `
`;
}
// Status indicator for 'last' messages
let statusIndicator = '';
if (message.status === 'last') {
statusIndicator = '