/** * 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 += `
[${logTimestamp}] ${agentHTML} ${shortMessage} ${progressHTML} ${logWaiting} ${isCollapsed ? '▾' : '▴'}
${WorkflowUtils.formatMarkdownLike(log.details)}
`; } else { logsHTML += `
[${logTimestamp}] ${agentHTML} ${logMessage} ${progressHTML} ${logWaiting}
`; } }); // 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}):
`; } // Status indicator for 'last' messages let statusIndicator = ''; if (message.status === 'last') { statusIndicator = '
Final Response
'; } messageDiv.innerHTML = `
${isUserMessage ? (window.globalState?.user?.fullName || 'You') : (message.agentName || 'Assistant')} ${new Date(message.timestamp || new Date()).toLocaleTimeString()} ${deleteButton}
${WorkflowUtils.formatMarkdownLike(formattedContent)}
${filesHTML} ${statusIndicator} ${message.role !== 'user' ? `
${isCollapsed ? 'Show more' : 'Show less'}
` : ''} `; } chatContainer.appendChild(messageDiv); }); // Set up toggle buttons setupMessageToggles(chatContainer); // Set up file actions setupFileActions(chatContainer, chatMessages, currentWorkflowId); // Set up message delete buttons setupMessageDeleteButtons(chatContainer, currentWorkflowId); // Auto-scroll to bottom if user was at bottom if (wasAtBottom) { chatContainer.scrollTop = chatContainer.scrollHeight; } } /** * Extracts file information from a document object * Prioritizes the numeric fileId for API operations * @param {Object} doc - Document object * @returns {Object|null} - Extracted file info or null if invalid */ function extractFileInfo(doc) { // Initialize with default values let fileInfo = { fileId: null, name: 'Unknown File', contentType: 'application/octet-stream', size: 0 }; try { // Extract numeric fileId if (doc.fileId !== undefined && doc.fileId !== null) { fileInfo.fileId = Number(doc.fileId); } // If no valid numeric ID found, return null if (fileInfo.fileId === null || isNaN(fileInfo.fileId)) { console.warn("No valid numeric fileId found in document:", doc); return null; } // Extract other properties based on available data if (doc.source && doc.source.type === 'file') { fileInfo.name = doc.source.name || 'Unknown File'; fileInfo.contentType = doc.source.content_type || doc.source.mimeType || 'application/octet-stream'; fileInfo.size = doc.source.size || 0; } else { // MODIFIED: Combine name and ext if both are available if (doc.name && doc.ext) { fileInfo.name = `${doc.name}.${doc.ext}`; } else { fileInfo.name = doc.name || 'Unknown File'; } fileInfo.contentType = doc.contentType || doc.content_type || doc.mimeType || 'application/octet-stream'; fileInfo.size = doc.size || 0; } return fileInfo; } catch (error) { console.error("Error extracting file info:", error); return null; } } /** * Sets up toggle buttons for expanding/collapsing messages * @param {HTMLElement} container - Container with messages */ function setupMessageToggles(container) { container.querySelectorAll('.toggle-content').forEach(toggle => { toggle.addEventListener('click', () => { const messageEl = toggle.closest('.chat-message'); if (messageEl) { messageEl.classList.toggle('collapsed'); toggle.textContent = messageEl.classList.contains('collapsed') ? 'Show more' : 'Show less'; } }); }); } /** * Sets up file actions (preview, delete) for message attachments * @param {HTMLElement} container - Container with messages * @param {Array} chatMessages - Array of chat messages * @param {string} currentWorkflowId - ID of the current workflow */ function setupFileActions(container, chatMessages, currentWorkflowId) { // File preview buttons container.querySelectorAll('.preview-file-btn').forEach(btn => { btn.addEventListener('click', async () => { // Get fileId attribute and ensure it's a number const fileIdAttr = btn.getAttribute('data-file-id'); const fileId = !isNaN(Number(fileIdAttr)) ? Number(fileIdAttr) : null; if (!fileId) { window.utils.showToast("Preview Error", "No valid numeric file ID for preview", "error"); return; } console.log(`Previewing file ID: ${fileId}`); // Find file info in messages let file = null; // Try to find in globalState first if (window.globalState?.mainView?.availableFiles) { file = window.globalState.mainView.availableFiles.find(f => (typeof f.fileId === 'number' && f.fileId === fileId) || (f.fileId !== undefined && Number(f.fileId) === fileId) ); } // If not found in globalState, search in messages if (!file) { // Search through all messages for (const message of chatMessages) { if (!message.documents) continue; for (const doc of message.documents) { // Extract file info const fileInfo = extractFileInfo(doc); if (fileInfo && fileInfo.fileId === fileId) { // Create a file object with the necessary properties for preview file = { ...fileInfo, contents: doc.contents || [], data: doc.data || null, base64Encoded: doc.base64Encoded || false }; break; } } if (file) break; } } if (file) { previewFile(file); // Enable download and copy buttons const downloadBtn = document.getElementById('download-file-btn'); const copyBtn = document.getElementById('copy-file-btn'); if (downloadBtn) downloadBtn.disabled = false; if (copyBtn) copyBtn.disabled = false; } else { window.utils.showToast("Preview Error", "File not found for preview", "error"); } }); }); // File delete buttons container.querySelectorAll('.delete-file-btn').forEach(btn => { btn.addEventListener('click', async () => { const fileIdAttr = btn.getAttribute('data-file-id'); const messageId = btn.getAttribute('data-message-id'); // Require numeric fileId const fileId = !isNaN(Number(fileIdAttr)) ? Number(fileIdAttr) : null; if (!fileId || !messageId || !currentWorkflowId) { window.utils.showToast("Delete Error", "Missing required information or invalid file ID", "error"); return; } try { // Find file details before deletion const message = chatMessages.find(msg => msg.id === messageId); if (!message) { throw new Error(`Message with ID ${messageId} not found`); } // Find the file to get details let fileName = "File"; if (message.documents) { for (const doc of message.documents) { const fileInfo = extractFileInfo(doc); if (fileInfo && fileInfo.fileId === fileId) { fileName = fileInfo.name || "File"; break; } } } // Call API to delete the file with numeric fileId console.log(`Deleting file from message: workflow=${currentWorkflowId}, message=${messageId}, file=${fileId}`); const success = await api.deleteFileFromMessage(currentWorkflowId, messageId, fileId); if (success) { window.utils.showToast("Success", `${fileName} was successfully deleted`, "success"); // Hide the file element in the UI const fileElement = btn.closest('.file-item'); if (fileElement) { fileElement.style.display = 'none'; } } else { window.utils.showToast("Delete Failed", "Could not delete the file", "error"); } } catch (error) { console.error(`Error deleting file: ${error.message}`); window.utils.showToast("Error", `Delete failed: ${error.message}`, "error"); } }); }); } /** * Sets up message delete buttons * @param {HTMLElement} container - Container with messages * @param {string} workflowId - ID of the current workflow */ function setupMessageDeleteButtons(container, workflowId) { container.querySelectorAll('.message-delete-btn').forEach(btn => { btn.addEventListener('click', async () => { const messageId = btn.getAttribute('data-message-id'); if (!messageId || !workflowId) return; if (confirm('Are you sure you want to delete this message?')) { try { console.log(`Deleting message: workflow=${workflowId}, message=${messageId}`); const success = await api.deleteWorkflowMessage(workflowId, messageId); if (success) { // Remove message from DOM const messageEl = btn.closest('.chat-message'); if (messageEl && messageEl.parentNode) { messageEl.parentNode.removeChild(messageEl); } window.utils.showToast("Success", "Message deleted", "success"); } else { window.utils.showToast("Delete Failed", "Could not delete the message", "error"); } } catch (error) { console.error(`Error deleting message: ${error.message}`); window.utils.showToast("Error", `Delete failed: ${error.message}`, "error"); } } }); }); } /** * Renders the additional files list * @param {Array} files - Array of file objects */ function renderAdditionalFiles(files) { const container = document.getElementById('additional-files-container'); if (!container) return; container.innerHTML = ''; if (!files || files.length === 0) return; files.forEach((file, index) => { const fileEl = document.createElement('div'); fileEl.className = 'additional-file-item'; // Add status class based on file parsing success if (file.hasOwnProperty('isExtracted')) { fileEl.classList.add(file.isExtracted ? 'parsing-success' : 'parsing-failed'); } fileEl.innerHTML = ` ${file.name} ${file.hasOwnProperty('isExtracted') && !file.isExtracted ? '⚠️' : ''} ${WorkflowUtils.formatFileSize(file.size)} `; container.appendChild(fileEl); }); // Add click handlers for remove buttons container.querySelectorAll('.remove-additional-file').forEach(btn => { btn.addEventListener('click', () => { const index = parseInt(btn.getAttribute('data-index'), 10); // Dispatch custom event for file removal const event = new CustomEvent('removeAdditionalFile', { detail: { index: index } }); document.dispatchEvent(event); }); }); } /** * Updates the visualization of prompt and files */ function updatePromptVisualization() { const userInputArea = document.getElementById('user-input-area'); if (!userInputArea) return; // Find or create the file preview area let filePreviewArea = document.getElementById('selected-files-preview'); if (!filePreviewArea) { filePreviewArea = document.createElement('div'); filePreviewArea.id = 'selected-files-preview'; filePreviewArea.className = 'selected-files-preview'; userInputArea.appendChild(filePreviewArea); } // Get attached files from custom event const filesEvent = new CustomEvent('getAttachedFiles', { detail: { files: [] } }); document.dispatchEvent(filesEvent); const files = filesEvent.detail.files || []; // If no files, hide the preview if (!files.length) { filePreviewArea.style.display = 'none'; filePreviewArea.innerHTML = ''; return; } // Show files filePreviewArea.style.display = 'block'; filePreviewArea.innerHTML = `
${files.length} ${files.length === 1 ? 'file' : 'files'} attached:
`; const filesList = filePreviewArea.querySelector('.files-preview-list'); files.forEach(file => { const item = document.createElement('li'); item.className = 'file-preview-item'; // Add status class based on file parsing success const extractionClass = file.hasOwnProperty('isExtracted') ? (file.isExtracted ? 'extracted' : 'extraction-failed') : ''; // IMPORTANT: Use fileId for API operations if available, otherwise fall back to id const fileIdForAPI = file.fileId !== undefined ? file.fileId : file.id; item.innerHTML = ` ${file.name} ${file.hasOwnProperty('isExtracted') && !file.isExtracted ? '⚠️' : ''} ${WorkflowUtils.formatFileSize(file.size)} `; filesList.appendChild(item); }); // FIX 2: Properly set up remove file event handlers with error messaging filePreviewArea.querySelectorAll('.remove-file-btn').forEach(btn => { btn.addEventListener('click', () => { const fileId = btn.getAttribute('data-file-id'); if (!fileId) { window.utils.showToast("Error", "No file ID found for removal", "error"); return; } try { // Dispatch custom event for file removal - log the event for debugging console.log(`Attempting to remove file with ID: ${fileId}`); const event = new CustomEvent('removeFileById', { detail: { fileId: fileId } }); document.dispatchEvent(event); // Show confirmation toast for better UX const fileItem = btn.closest('.file-preview-item'); const fileName = fileItem ? fileItem.querySelector('.file-name').textContent : 'File'; window.utils.showToast("Removing file", `Removing ${fileName}...`, "info"); } catch (error) { console.error("Error removing file:", error); window.utils.showToast("Error", `Failed to remove file: ${error.message}`, "error"); } }); }); } /** * Previews a file in the preview pane * @param {Object} file - File object to preview */ async function previewFile(file) { if (!file) { console.error("Cannot preview file: file is null"); return; } const previewContainer = document.getElementById('file-preview-container'); const previewContent = document.getElementById('file-preview-content'); if (!previewContainer || !previewContent) { console.error("Preview containers not found"); return; } // Show preview container previewContainer.style.display = 'flex'; // Get numeric fileId const fileId = typeof file.fileId === 'number' ? file.fileId : (file.fileId !== undefined && !isNaN(Number(file.fileId)) ? Number(file.fileId) : null); // Log file information console.log(`Previewing file: ${file.name || 'unnamed'} (ID: ${fileId || 'N/A'}, Type: ${file.contentType || file.mimeType || 'unknown'})`); // Update preview container header const previewHeader = previewContainer.querySelector('.file-preview-header'); if (previewHeader) { previewHeader.innerHTML = ` ${file.name || 'File Preview'} ${WorkflowUtils.formatFileSize(file.size)} `; } // Set current preview file for download/copy actions currentPreviewFile = file; // Show loading indicator previewContent.innerHTML = `
Loading file...
`; // Require numeric fileId if (!fileId) { previewContent.innerHTML = `

Preview Unavailable

This document doesn't have a valid numeric ID for preview.

According to the data model, only files with numeric IDs can be fetched from the API.

`; return; } try { // Call API with the numeric fileId const fileData = await api.previewFile(fileId); if (!fileData || !fileData.content) { throw new Error("No file data available"); } renderFilePreview(fileData, file, previewContent); } catch (error) { console.error("Error fetching file data:", error); previewContent.innerHTML = `

Preview Error

${error.message || "Could not load file preview"}

`; } } /** * Renders the file preview based on file type * @param {Object} fileData - File data from API * @param {Object} file - File metadata * @param {HTMLElement} previewContent - Container for preview */ function renderFilePreview(fileData, file, previewContent) { const mimeType = file.contentType || file.mimeType || ''; // Handle different file types if (mimeType.includes('image')) { // Image preview - always assume base64 for images from API try { const imageData = `data:${mimeType};base64,${fileData.content}`; previewContent.innerHTML = ` ${file.name} `; } catch (error) { console.error("Error rendering image preview:", error); window.utils.showToast(previewContent, "Error displaying image","error"); } } else if (mimeType.includes('text') || mimeType.includes('json') || mimeType.includes('csv') || mimeType.includes('markdown') || mimeType.includes('javascript') || mimeType.includes('html') || mimeType.includes('xml') || mimeType.includes('css')) { // Text preview - check base64Encoded flag try { // If we have the base64Encoded flag explicitly set to true, decode // Otherwise, assume it's already text since we're in the text formats branch const textContent = fileData.base64Encoded ? atob(fileData.content) : fileData.content; const formattedContent = WorkflowUtils.formatMarkdownLike(textContent); previewContent.innerHTML = `
${formattedContent}
`; } catch (error) { console.error("Error processing text content:", error); window.utils.showToast(previewContent, "Error decoding file content","error"); } } else if (mimeType.includes('pdf')) { // PDF preview try { const pdfData = `data:${mimeType};base64,${fileData.content}`; previewContent.innerHTML = ` `; } catch (error) { console.error("Error rendering PDF preview:", error); window.utils.showToast(previewContent, "Error displaying PDF","error"); } } else if (mimeType.includes('spreadsheet') || mimeType.includes('excel') || file.name.endsWith('.xlsx') || file.name.endsWith('.xls') || file.name.endsWith('.csv')) { // Excel/CSV preview try { const textContent = fileData.content ? atob(fileData.content) : 'No content available'; // For CSV or extracted text from Excel previewContent.innerHTML = `
Spreadsheet preview (limited formatting):
${textContent}
`; } catch (error) { console.error("Error processing spreadsheet content:", error); window.utils.showToast(previewContent, "Error rendering spreadsheet preview","error"); } } else { // Generic file info previewContent.innerHTML = `

${file.name}

Type: ${mimeType || 'Unknown'}

Size: ${WorkflowUtils.formatFileSize(file.size)}

No preview available for this file type

`; // Add direct download button event listener const downloadBtn = previewContent.querySelector('#download-file-direct'); if (downloadBtn) { downloadBtn.addEventListener('click', downloadPreviewFile); } } } /** * Downloads the currently previewed file */ async function downloadPreviewFile() { if (!currentPreviewFile) { window.utils.showToast("Error", "No file selected for download", "error"); return; } try { const file = currentPreviewFile; // Get numeric fileId const fileId = typeof file.fileId === 'number' ? file.fileId : (file.fileId !== undefined && !isNaN(Number(file.fileId)) ? Number(file.fileId) : null); if (!fileId) { window.utils.showToast("Error", "No valid numeric ID found for download", "error"); return; } console.log(`Downloading file: ${file.name} (ID: ${fileId})`); // Get file from API using the numeric fileId const response = await api.downloadFile(fileId); if (!response || !response.content) { throw new Error("No file data available for download"); } // Initiate download window.utils.data.initiateDownload( response.content, file.name || 'download', file.contentType || file.mimeType || 'application/octet-stream' ); } catch (error) { console.error("Error downloading file:", error); window.utils.showToast("Error", `Download failed: ${error.message}`, "error"); } } /** * Copies the content of the currently previewed file to clipboard */ async function copyPreviewFileContent() { if (!currentPreviewFile) { window.utils.showToast("Error", "No file selected for copying", "error"); return; } try { const file = currentPreviewFile; // Check if it's a text file - this is just for validation const mimeType = file.contentType || file.mimeType || ''; const isTextFile = mimeType.includes('text') || mimeType.includes('json') || mimeType.includes('csv') || mimeType.includes('markdown') || mimeType.includes('javascript') || mimeType.includes('html') || mimeType.includes('xml') || mimeType.includes('css'); if (!isTextFile) { window.utils.showToast("Warning", "Only text files can be copied to clipboard", "warning"); return; } // Only use the API - check for valid numeric fileId const fileId = file.fileId; if (!fileId || typeof fileId !== 'number') { window.utils.showToast("Error", "No valid database ID found for copy operation", "error"); return; } // Get file content from API console.log(`Fetching file content for copying (ID: ${fileId})`); const response = await api.previewFile(fileId); if (!response || !response.content) { throw new Error("No file content available"); } // Use base64Encoded flag to determine if we need to decode const textContent = response.base64Encoded ? atob(response.content) : response.content; // Copy to clipboard try { // Try using the modern Clipboard API if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(textContent); window.utils.showToast("Success", "Content copied to clipboard", "success"); return; } // Fallback method using a temporary textarea element const textarea = document.createElement('textarea'); textarea.value = textContent; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); // Try the deprecated execCommand as a fallback const successful = document.execCommand('copy'); document.body.removeChild(textarea); if (successful) { window.utils.showToast("Success", "Content copied to clipboard", "success"); } else { throw new Error("Copy operation failed"); } } catch (clipboardError) { console.error("Error copying to clipboard:", clipboardError); window.utils.showToast("Error", "Failed to copy to clipboard. Try manually selecting the text.", "error"); } } catch (error) { console.error("Error copying file content:", error); window.utils.showToast("Error", `Copy failed: ${error.message}`, "error"); } } /** * Updates data statistics display * @param {number} sentBytes - Bytes sent * @param {number} receivedBytes - Bytes received * @param {number} tokensUsed - Tokens used */ function updateDataStatistics(sentBytes, receivedBytes, tokensUsed = 0) { const statsEl = document.getElementById('data-statistics'); if (!statsEl) return; // Format sizes const formatSize = (bytes) => { if (!bytes || bytes < 0) return '0 B'; if (bytes < 1024) return `${bytes} B`; const kbytes = bytes / 1024; if (kbytes < 1000) return `${Math.round(kbytes)} kB`; const mbytes = kbytes / 1024; return `${Math.round(mbytes * 10) / 10} MB`; }; const sentFormatted = formatSize(sentBytes); const receivedFormatted = formatSize(receivedBytes); // Update display let statsHTML = ` ↑ ${sentFormatted} ↓ ${receivedFormatted} `; // Add tokens if available if (tokensUsed > 0) { statsHTML += `🔤 ${Math.round(tokensUsed).toLocaleString()}`; } statsEl.innerHTML = statsHTML; } /** * Initializes fixed layout components */ function initializeFixedLayout() { // Get main container const container = document.querySelector('.workflow-container'); if (!container) return; // Store initial heights const workflowHeader = document.querySelector('.workflow-header'); if (workflowHeader && !workflowHeader.getAttribute('data-default-height')) { const defaultHeight = window.getComputedStyle(workflowHeader).height; workflowHeader.setAttribute('data-default-height', defaultHeight); } // Set up resize handler window.addEventListener('resize', adjustLayoutOnResize); // Initial adjustment adjustLayoutOnResize(); } /** * Sets up header toggle functionality */ function setupHeaderToggle() { const toggleBtn = document.getElementById('toggle-header-btn'); const header = document.querySelector('.workflow-header'); if (!toggleBtn || !header) return; toggleBtn.addEventListener('click', () => { header.classList.toggle('collapsed'); // Update icon const icon = toggleBtn.querySelector('i'); if (icon) { if (header.classList.contains('collapsed')) { icon.className = 'fas fa-plus'; } else { icon.className = 'fas fa-minus'; } } // Adjust layout adjustLayoutOnResize(); }); } /** * Adjusts layout on window resize */ function adjustLayoutOnResize() { const container = document.querySelector('.workflow-container'); const header = document.querySelector('.workflow-header'); const chatSection = document.querySelector('.chat-section'); const footer = document.querySelector('.workflow-footer'); if (!container || !header || !chatSection || !footer) return; // Calculate available heights const windowHeight = window.innerHeight; const headerHeight = header.offsetHeight; const footerHeight = footer.offsetHeight; // Set chat section height const chatHeight = windowHeight - headerHeight - footerHeight; if (chatHeight > 100) { chatSection.style.height = `${chatHeight}px`; // Adjust chat messages container const messagesContainer = document.getElementById('agent-chat-messages'); if (messagesContainer) { messagesContainer.style.height = `${chatHeight - 20}px`; } } // Adjust file preview container const previewContainer = document.getElementById('file-preview-container'); if (previewContainer) { previewContainer.style.height = `${chatHeight}px`; // Adjust preview content const previewHeader = previewContainer.querySelector('.file-preview-header'); const previewContent = document.getElementById('file-preview-content'); if (previewHeader && previewContent) { const headerHeight = previewHeader.offsetHeight; previewContent.style.height = `${chatHeight - headerHeight - 20}px`; } } } /** * Checks if user has scrolled to the bottom of an element * @param {HTMLElement} element - Element to check * @returns {boolean} - Whether user is at bottom */ function isUserAtBottom(element) { if (!element) return false; const tolerance = 30; // Pixels from bottom to consider "at bottom" return element.scrollHeight - element.scrollTop - element.clientHeight <= tolerance; } // Export functions export { initUI, renderLogs, renderChatMessages, updateWorkflowButtonsState, updateDataStatistics, previewFile, renderAdditionalFiles, updatePromptVisualization };