// Document Ingestion Agent - Frontend JavaScript class DocumentAgent { constructor() { // Using relative API path - defaults to current host (localhost in development, production domain when deployed) // This is intentional to support both local development and production environments without code changes this.apiBase = '/api'; this.statsRefreshInterval = null; this.promptsCache = {}; // Pagination state this.currentPage = 1; this.totalPages = 1; this.pageSize = 25; this.totalResults = 0; this.currentSearchParams = {}; // CRUD mode state this.crudModeEnabled = false; // Store current Q&A data for re-rendering this.currentQAData = []; // Model loading state this.modelsLoaded = false; this.init(); } async init() { console.log('🔐 Starting authentication check...'); // Step 1: Check for JWT parameter in URL first const jwtProcessed = await this.handleJWTParameter(); console.log('🔍 JWT parameter processed:', jwtProcessed); // Step 2: Only check for existing cookie if no JWT parameter was processed let hasCookie = false; if (!jwtProcessed) { hasCookie = await this.checkForCookie(); console.log('🍪 Cookie found:', hasCookie); } // If no JWT parameter was processed and no cookie exists, redirect to login if (!jwtProcessed && !hasCookie) { console.log('❌ No authentication found, redirecting to login...'); this.showLoginRedirect(); return; } console.log('✅ Authentication successful, initializing app...'); // Continue with normal app initialization this.setupEventListeners(); this.checkAuthStatus(); this.loadStats(); this.loadSettings(); this.enableFormElements(); this.startStatsAutoRefresh(); } startStatsAutoRefresh() { // Refresh stats every 10 minutes this.statsRefreshInterval = setInterval(async () => { // Check JWT authentication before refreshing stats if (!(await this.validateJWTForRefresh())) { console.log('❌ JWT validation failed during stats refresh, stopping auto-refresh...'); this.stopStatsAutoRefresh(); return; } // Proceed with stats refresh if JWT is valid this.loadStats(); }, 600000); } stopStatsAutoRefresh() { if (this.statsRefreshInterval) { clearInterval(this.statsRefreshInterval); this.statsRefreshInterval = null; } } async checkForCookie() { console.log('🍪 Checking for n8n-auth cookie...'); console.log('🍪 All cookies:', document.cookie); // Check if the n8n-auth cookie exists const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); console.log(`🍪 Checking cookie: ${name} = ${value ? value.substring(0, 10) + '...' : 'empty'}`); if (name === 'n8n-auth' && value) { console.log('🍪 Found n8n-auth cookie, validating...'); // Validate the existing cookie with the backend const isValid = await this.validateExistingCookie(value); return isValid; } } console.log('🍪 No n8n-auth cookie found'); return false; } async validateExistingCookie(tokenValue) { try { console.log('Validating existing cookie token:', tokenValue); const response = await fetch('/api/auth/validate-jwt', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: tokenValue }) }); const result = await response.json(); if (result.success && result.valid) { console.log('Existing cookie token validated successfully'); return true; } else { console.log('Existing cookie token validation failed:', result); // Clear the invalid cookie document.cookie = 'n8n-auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; return false; } } catch (error) { console.error('Error validating existing cookie token:', error); // Clear the invalid cookie on error document.cookie = 'n8n-auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; return false; } } async validateJWTForRefresh() { try { console.log('🔐 Validating JWT for stats refresh...'); // Check if n8n-auth cookie exists const cookies = document.cookie.split(';'); let jwtToken = null; for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'n8n-auth' && value) { jwtToken = value; break; } } if (!jwtToken) { console.log('❌ No JWT token found during stats refresh, redirecting to login...'); this.showLoginRedirect(); return false; } // Validate the JWT token with the backend const response = await fetch('/api/auth/validate-jwt', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: jwtToken }) }); const result = await response.json(); if (result.success && result.valid) { console.log('✅ JWT validation successful for stats refresh'); return true; } else { console.log('❌ JWT validation failed during stats refresh:', result); // Clear the invalid cookie document.cookie = 'n8n-auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; this.showLoginRedirect(); return false; } } catch (error) { console.error('❌ Error validating JWT for stats refresh:', error); // Clear the invalid cookie on error document.cookie = 'n8n-auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; this.showLoginRedirect(); return false; } } async handleJWTParameter() { // Check for JWT token in URL parameters const urlParams = new URLSearchParams(window.location.search); const jwtToken = urlParams.get('jwt'); if (!jwtToken) { return false; // No JWT token found } console.log('JWT parameter found:', jwtToken); // Store the JWT token as n8n-auth cookie for 7 days this.setCookie('n8n-auth', jwtToken, 7); // Remove the JWT parameter from the URL to clean it up const newUrl = new URL(window.location); newUrl.searchParams.delete('jwt'); window.history.replaceState({}, '', newUrl); console.log('JWT token stored as n8n-auth cookie for 7 days'); // Validate the JWT token with the backend try { const response = await fetch('/api/auth/validate-jwt', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: jwtToken }) }); const result = await response.json(); if (result.success && result.valid) { console.log('JWT token validated successfully'); return true; // JWT was processed and validated } else { console.log('JWT token validation failed:', result); // Clear the invalid cookie document.cookie = 'n8n-auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; return false; // JWT was processed but invalid } } catch (error) { console.error('Error validating JWT token:', error); // Clear the invalid cookie on error document.cookie = 'n8n-auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; return false; // JWT was processed but validation failed } } setCookie(name, value, days) { // Set a cookie with the given name, value, and expiration days const expires = new Date(); expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`; } showLoginRedirect() { // Hide the main content and show login redirect message const mainContent = document.getElementById('main-content'); if (mainContent) { mainContent.style.display = 'none'; } // Create and show the login redirect message const loginMessage = document.createElement('div'); loginMessage.id = 'login-redirect-message'; loginMessage.innerHTML = `

Authentication Required

You are not logged into the five keys system. Please authenticate to access the Document Ingestion Agent.

Go to Five Keys System
Redirecting automatically in:
10
`; document.body.appendChild(loginMessage); // Start countdown let countdown = 10; const timerElement = document.getElementById('countdown-timer'); const loginLink = document.getElementById('login-redirect-link'); const countdownInterval = setInterval(() => { countdown--; timerElement.textContent = countdown; if (countdown <= 0) { clearInterval(countdownInterval); window.location.href = 'https://aiwriter.fivekeysautomations.org/projects/'; } }, 1000); // Allow manual click to redirect immediately loginLink.addEventListener('click', (e) => { e.preventDefault(); clearInterval(countdownInterval); window.location.href = 'https://aiwriter.fivekeysautomations.org/projects/'; }); } enableFormElements() { // Enable Process Documents elements const processElements = [ 'sheetId', 'processBtn' ]; // Enable Q&A Management elements const qaManagementElements = [ 'qaSearchQuery', 'qaSearchYear', 'qaSearchDivision', 'qaSearchLimit', 'searchQABtn', 'loadAllQABtn', 'refreshQABtn' ]; // Enable Q&A Sheet elements const qaSheetElements = [ 'qaSheetId', 'answerColumns', 'targetFilePairs', 'qaSheetBtn', 'testQABtn' ]; // Enable all elements [...processElements, ...qaManagementElements, ...qaSheetElements].forEach(id => { const element = document.getElementById(id); if (element) { element.disabled = false; } }); console.log('Form elements enabled'); } setupEventListeners() { // Process documents form document.getElementById('processForm').addEventListener('submit', (e) => { e.preventDefault(); this.processDocuments(); }); // QA Management form const qaSearchForm = document.getElementById('qaSearchForm'); if (qaSearchForm) { qaSearchForm.addEventListener('submit', (e) => { e.preventDefault(); this.searchQAPairs(); }); } // Q&A Sheet form document.getElementById('qaSheetForm').addEventListener('submit', (e) => { e.preventDefault(); this.generateQAFromSheet(); }); // Test Q&A button document.getElementById('testQABtn').addEventListener('click', (e) => { e.preventDefault(); this.testQAGeneration(); }); // CRUD QA buttons document.querySelectorAll('.crud-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); this.crudQA(e.target.dataset.operation); }); }); // Auth button const authButton = document.getElementById('authButton'); if (authButton) { authButton.addEventListener('click', (e) => { e.preventDefault(); this.authenticate(); }); } // Disconnect Google button (in settings page) document.getElementById('disconnectGoogleButton').addEventListener('click', (e) => { e.preventDefault(); this.disconnectGoogle(); }); // Q&A Management buttons document.getElementById('searchQABtn').addEventListener('click', (e) => { e.preventDefault(); this.searchQAPairs(); }); document.getElementById('resetFiltersBtn').addEventListener('click', (e) => { e.preventDefault(); this.resetFilters(); }); // Add event listeners to filter inputs to enable reset button document.getElementById('qaSearchQuery').addEventListener('input', () => { this.updateResetButtonState(); }); document.getElementById('qaSearchYear').addEventListener('change', () => { this.updateResetButtonState(); }); document.getElementById('qaSearchDivision').addEventListener('change', () => { this.updateResetButtonState(); }); // CRUD interface toggle document.getElementById('crudInterfaceToggle').addEventListener('change', (e) => { this.crudModeEnabled = e.target.checked; // Refresh the current Q&A list to show/hide CRUD buttons this.refreshCurrentQAList(); }); // Pagination event listeners document.getElementById('qaFirstPageBtn').addEventListener('click', (e) => { e.preventDefault(); this.goToPage(1); }); document.getElementById('qaPrevPageBtn').addEventListener('click', (e) => { e.preventDefault(); this.goToPage(this.currentPage - 1); }); document.getElementById('qaNextPageBtn').addEventListener('click', (e) => { e.preventDefault(); this.goToPage(this.currentPage + 1); }); document.getElementById('qaLastPageBtn').addEventListener('click', (e) => { e.preventDefault(); this.goToPage(this.totalPages); }); // Top pagination event listeners (duplicate functionality) document.getElementById('qaFirstPageBtnTop').addEventListener('click', (e) => { e.preventDefault(); this.goToPage(1); }); document.getElementById('qaPrevPageBtnTop').addEventListener('click', (e) => { e.preventDefault(); this.goToPage(this.currentPage - 1); }); document.getElementById('qaNextPageBtnTop').addEventListener('click', (e) => { e.preventDefault(); this.goToPage(this.currentPage + 1); }); document.getElementById('qaLastPageBtnTop').addEventListener('click', (e) => { e.preventDefault(); this.goToPage(this.totalPages); }); document.getElementById('qaCurrentPageInput').addEventListener('change', (e) => { const page = parseInt(e.target.value); if (page >= 1 && page <= this.totalPages) { this.goToPage(page); } else { e.target.value = this.currentPage; // Reset to current page if invalid } }); document.getElementById('qaCurrentPageInputTop').addEventListener('change', (e) => { const page = parseInt(e.target.value); if (page >= 1 && page <= this.totalPages) { this.goToPage(page); } else { e.target.value = this.currentPage; // Reset to current page if invalid } }); // Delegated handlers for QA list action buttons (Edit / Unapprove / Delete) const qaListContainer = document.getElementById('qaPairsListManagement'); if (qaListContainer) { qaListContainer.addEventListener('click', (event) => { const actionButton = event.target.closest('button[data-action]'); if (!actionButton) return; const qaId = actionButton.getAttribute('data-id'); const action = actionButton.getAttribute('data-action'); if (!qaId || !action) return; if (action === 'edit') { this.editQAPair(qaId); } else if (action === 'delete') { this.deleteQAPair(qaId); } }); } } async apiCall(endpoint, method = 'GET', data = null) { try { const options = { method: method }; // NEVER set body for GET or HEAD requests if (method !== 'GET' && method !== 'HEAD' && data) { if (data instanceof FormData) { // Don't set Content-Type for FormData, let the browser set it options.body = data; } else { options.headers = { 'Content-Type': 'application/json', }; options.body = JSON.stringify(data); } } // Debug logging console.log('apiCall debug:', { endpoint, method, data, options }); const response = await fetch(`${this.apiBase}${endpoint}`, options); if (!response.ok) { let errorDetail = 'API call failed'; try { const errorResult = await response.json(); errorDetail = errorResult.detail || errorResult.error || errorDetail; } catch (parseError) { console.warn('Could not parse error response:', parseError); } // Handle authentication errors if (response.status === 401) { this.showError('Authentication required. Please authenticate with Google first.'); this.updateAuthStatus({ authenticated: false }); return { success: false, error: 'Authentication required' }; } // Handle validation errors if (response.status === 422) { this.showError(`Validation error: ${errorDetail}`); return { success: false, error: errorDetail }; } throw new Error(errorDetail); } // Parse the successful response try { // Handle empty/204 responses gracefully if (response.status === 204) { return { success: true }; } const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { const text = await response.text(); // If there's no body, still treat as success if (!text || text.trim().length === 0) { return { success: true }; } // Try to parse if server returned a non-JSON but non-empty body try { return JSON.parse(text); } catch (_) { return { success: true, data: text }; } } const result = await response.json(); return result; } catch (parseError) { console.error('Failed to parse response:', parseError); return { success: true }; } } catch (error) { console.error('API Error:', error); this.showError(error.message); throw error; } } showLoading(elementId) { const element = document.getElementById(elementId); if (element) { element.innerHTML = '
Processing...'; element.disabled = true; } } hideLoading(elementId) { const element = document.getElementById(elementId); if (element) { element.innerHTML = element.dataset.originalText || 'Submit'; element.disabled = false; } } showSuccess(message) { this.showMessage(message, 'success'); } showError(message) { this.showMessage(message, 'error'); } // Progress bar methods showProgress(percent, text) { const container = document.getElementById('progressContainer'); const progressFill = document.getElementById('progressFill'); const progressText = document.getElementById('progressText'); const progressPercent = document.getElementById('progressPercent'); if (container && progressFill && progressText && progressPercent) { container.style.display = 'block'; progressFill.style.width = percent + '%'; progressText.textContent = text; progressPercent.textContent = percent + '%'; } } updateProgress(percent, text) { const progressFill = document.getElementById('progressFill'); const progressText = document.getElementById('progressText'); const progressPercent = document.getElementById('progressPercent'); if (progressFill && progressText && progressPercent) { progressFill.style.width = percent + '%'; progressText.textContent = text; progressPercent.textContent = percent + '%'; } } hideProgress() { const container = document.getElementById('progressContainer'); if (container) { container.style.display = 'none'; } } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } showMessage(message, type) { const messageDiv = document.getElementById('message'); messageDiv.innerHTML = `
${message}
`; messageDiv.style.display = 'block'; setTimeout(() => { messageDiv.style.display = 'none'; }, 5000); } showResult(result, containerId) { const container = document.getElementById(containerId); container.innerHTML = `
${JSON.stringify(result, null, 2)}
`; container.style.display = 'block'; } updateSheetIdDisplay() { const sheetIdInput = document.getElementById('sheetId'); const sheetIdPlaceholder = document.getElementById('sheetIdPlaceholder'); const processBtn = document.getElementById('processBtn'); // Get Google Sheet ID from settings const googleSheetId = this.getSettingValue('GOOGLE_SHEET_ID'); if (googleSheetId && googleSheetId.trim()) { // Sheet ID is set - show input field with value sheetIdInput.style.display = 'block'; sheetIdPlaceholder.style.display = 'none'; sheetIdInput.value = googleSheetId; sheetIdInput.disabled = true; // Make it read-only since it comes from settings // Enable process button processBtn.disabled = false; processBtn.classList.remove('disabled'); } else { // Sheet ID is not set - show placeholder with link to settings sheetIdInput.style.display = 'none'; sheetIdPlaceholder.style.display = 'flex'; sheetIdInput.value = ''; // Disable process button processBtn.disabled = true; processBtn.classList.add('disabled'); } } getSettingValue(key) { // Get setting value from the global allSettings object if (typeof allSettings !== 'undefined' && allSettings[key]) { return allSettings[key].value || ''; } return ''; } async loadSettings() { try { const response = await fetch('/settings/?include_secrets=false'); const data = await response.json(); if (data.success) { allSettings = data.settings; // Update sheet ID display after settings are loaded this.updateSheetIdDisplay(); } else { console.error('Failed to load settings:', data.error); } } catch (error) { console.error('Error loading settings:', error); } } async processDocuments() { // Clear previous result log when starting new processing const processResultContainer = document.getElementById('processResult'); if (processResultContainer) { processResultContainer.innerHTML = ''; processResultContainer.style.display = 'none'; } const sheetId = this.getSettingValue('GOOGLE_SHEET_ID'); if (!sheetId || !sheetId.trim()) { this.showError('Please set the Google Sheet ID in the settings page first'); return; } // Always show column mapping modal - no direct processing try { this.showLoading('processBtn'); console.log('Getting sheet headers for sheet:', sheetId); const headersResult = await this.apiCall('/get-sheet-headers', 'POST', { sheet_id: sheetId }); console.log('Headers result:', headersResult); if (headersResult && headersResult.success) { console.log('Showing column mapping modal with headers:', headersResult.headers); if (headersResult.headers && headersResult.headers.length > 0) { this.showColumnMappingModal(headersResult.headers, sheetId); } else { this.showError('No columns found in the sheet. Please check your Google Sheet.'); } } else { this.showError('Failed to get sheet headers: ' + (headersResult?.error || 'Unknown error')); } } catch (error) { console.error('Error getting sheet headers:', error); this.showError('Failed to get sheet headers: ' + error.message); } finally { this.hideLoading('processBtn'); } } showColumnMappingModal(headers, sheetId) { console.log('Showing column mapping modal with sheetId:', sheetId, 'headers:', headers); console.log('Headers type:', typeof headers, 'Headers length:', headers?.length); // Populate the submission columns dropdown const submissionSelect = document.getElementById('submissionColumnsMapping'); const yearSelect = document.getElementById('yearColumnMapping'); const divisionSelect = document.getElementById('divisionColumnMapping'); const proposalSourceSelect = document.getElementById('proposalSourceColumnsMapping'); // Clear existing options submissionSelect.innerHTML = ''; yearSelect.innerHTML = ''; divisionSelect.innerHTML = ''; proposalSourceSelect.innerHTML = ''; // Add header options to all selects headers.forEach((header, index) => { if (header && header.trim()) { console.log(`Adding header ${index}: "${header}"`); // Add to submission columns (multiple selection) const submissionOption = document.createElement('option'); submissionOption.value = header; submissionOption.textContent = header; submissionSelect.appendChild(submissionOption); // Add to year column (single selection) const yearOption = document.createElement('option'); yearOption.value = header; yearOption.textContent = header; yearSelect.appendChild(yearOption); // Add to division column (single selection) const divisionOption = document.createElement('option'); divisionOption.value = header; divisionOption.textContent = header; divisionSelect.appendChild(divisionOption); // Add to proposal source columns (multiple selection) const proposalSourceOption = document.createElement('option'); proposalSourceOption.value = header; proposalSourceOption.textContent = header; proposalSourceSelect.appendChild(proposalSourceOption); } else { console.log(`Skipping empty header ${index}: "${header}"`); } }); // Auto-select columns with "Year" and "Division" in their names const lowerHeaders = headers.map(h => h ? h.toLowerCase() : ''); // Find and select year column const yearIndex = lowerHeaders.findIndex(h => h.includes('year')); if (yearIndex !== -1) { const yearHeader = headers[yearIndex]; yearSelect.value = yearHeader; console.log(`Auto-selected year column: "${yearHeader}"`); } // Find and select division column const divisionIndex = lowerHeaders.findIndex(h => h.includes('division')); if (divisionIndex !== -1) { const divisionHeader = headers[divisionIndex]; divisionSelect.value = divisionHeader; console.log(`Auto-selected division column: "${divisionHeader}"`); } console.log('Final submission select options:', Array.from(submissionSelect.options).map(o => ({ value: o.value, text: o.textContent }))); // Store the sheet ID for later use this.pendingSheetId = sheetId; console.log('Set pendingSheetId to:', this.pendingSheetId); // Show the modal with animation const modal = document.getElementById('columnMappingModal'); if (modal) { console.log('Showing column mapping modal'); console.log('Modal element found:', modal); console.log('Modal current display:', modal.style.display); modal.style.display = 'flex'; modal.style.zIndex = '9999'; // Trigger animation setTimeout(() => { modal.classList.add('show'); console.log('Modal animation triggered'); }, 10); // Add a visual indicator that modal is shown console.log('Modal should now be visible. Please select your columns and click "Process Documents" in the modal.'); // Force focus to the modal setTimeout(() => { modal.focus(); console.log('Modal focused'); }, 100); } else { console.error('Column mapping modal not found!'); this.showError('Column mapping modal not found. Please refresh the page and try again.'); } } closeColumnMappingModal() { const modal = document.getElementById('columnMappingModal'); if (modal) { modal.classList.remove('show'); // Hide modal after animation completes setTimeout(() => { modal.style.display = 'none'; }, 300); } // Don't clear pendingSheetId here - it might be needed for processing } cancelColumnMappingModal() { this.closeColumnMappingModal(); // Clear pendingSheetId when user cancels the modal this.pendingSheetId = null; } async processWithColumnMapping() { const submissionSelect = document.getElementById('submissionColumnsMapping'); const yearSelect = document.getElementById('yearColumnMapping'); const divisionSelect = document.getElementById('divisionColumnMapping'); const proposalSourceSelect = document.getElementById('proposalSourceColumnsMapping'); const startRow = document.getElementById('startRowMapping').value; const totalEntries = document.getElementById('totalEntriesMapping').value; const saveToSupabase = document.getElementById('saveToSupabaseMapping').checked; // Get selected submission columns from multi-select const submissionColumns = Array.from(submissionSelect.selectedOptions) .map(option => option.value) .filter(value => value !== ''); // Remove empty values // Get selected proposal source columns from multi-select const proposalSourceColumns = Array.from(proposalSourceSelect.selectedOptions) .map(option => option.value) .filter(value => value !== ''); // Remove empty values console.log('Selected submission columns:', submissionColumns); console.log('Selected proposal source columns:', proposalSourceColumns); console.log('Available options:', Array.from(submissionSelect.options).map(o => o.value)); const yearColumn = yearSelect.value; const divisionColumn = divisionSelect.value; console.log('Form values:', { submissionColumns, proposalSourceColumns, yearColumn, divisionColumn, startRow, totalEntries, saveToSupabase, pendingSheetId: this.pendingSheetId }); if (submissionColumns.length === 0) { this.showError('Please select at least one submission document column'); return; } if (!yearColumn) { this.showError('Please select a year column'); return; } if (!divisionColumn) { this.showError('Please select a division column'); return; } if (!this.pendingSheetId) { this.showError('No sheet ID found'); return; } this.closeColumnMappingModal(); this.showLoading('processBtn'); this.showProgress(0, 'Starting document processing...'); try { // Process documents with column mapping and progress tracking const requestData = { sheet_id: this.pendingSheetId, submission_columns: submissionColumns, proposal_source_columns: proposalSourceColumns, year_column: yearColumn, division_column: divisionColumn, start_row: startRow ? parseInt(startRow) : 1, total_entries: totalEntries ? parseInt(totalEntries) : null }; console.log('Sending request data:', requestData); console.log('VERIFICATION: Using these columns:', { submission_columns: requestData.submission_columns, proposal_source_columns: requestData.proposal_source_columns, year_column: requestData.year_column, division_column: requestData.division_column }); // Start processing with progress updates console.log('About to start processing with data:', requestData); const result = await this.processDocumentsWithProgress(requestData); console.log('Processing completed with result:', result); console.log('Result type:', typeof result); console.log('Result success:', result?.success); console.log('Result total_documents:', result?.total_documents); this.hideLoading('processBtn'); this.hideProgress(); if (result && (result.success || result.status === 'completed_with_errors')) { console.log('Processing completed, checking document count...'); if (result.total_documents === 0) { console.log('No documents found, showing error message'); this.showError(result.message || 'No documents found to process. Please check that your Google Sheet contains valid document links in the specified columns.'); } else { console.log('Documents found, showing result message'); if (result.status === 'completed_with_errors') { this.showError(`Processing completed with ${result.errors?.length || 0} errors. Processed ${result.processed_documents} out of ${result.total_documents} documents.`); } else { this.showSuccess(`Documents processed successfully! Processed ${result.processed_documents} out of ${result.total_documents} documents.`); } this.showResult(result, 'processResult'); } await this.loadStats(); } else { console.error('Processing failed with result:', result); this.showError('Processing failed: ' + (result?.error || 'Unknown error')); } } catch (error) { this.hideLoading('processBtn'); this.hideProgress(); console.error('Document processing error:', error); console.error('Error stack:', error.stack); this.showError('Failed to process documents: ' + (error.message || 'Unknown error occurred')); } finally { // Clear the pending sheet ID after processing is complete this.pendingSheetId = null; } } async processDocumentsWithProgress(requestData) { // Start the processing job console.log('Starting document processing with data:', requestData); const startResult = await this.apiCall('/process-documents-start', 'POST', requestData); console.log('Start result received:', startResult); if (!startResult || !startResult.success) { console.error('Start result failed:', startResult); throw new Error(startResult?.error || 'Failed to start document processing'); } const jobId = startResult.job_id; let progress = 0; let status = 'starting'; // Poll for progress updates while (status !== 'completed' && status !== 'completed_with_errors' && status !== 'failed') { await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds try { const progressResult = await this.apiCall(`/process-documents-status?job_id=${jobId}`, 'GET'); if (progressResult && progressResult.success) { progress = progressResult.progress || 0; status = progressResult.status || 'processing'; // Update progress bar this.updateProgress(progress, progressResult.message || 'Processing documents...'); if (status === 'completed' || status === 'completed_with_errors') { // Return the full progress result, not just the result field return progressResult; } else if (status === 'failed') { const errorMessage = progressResult.message || progressResult.error || 'Processing failed'; console.error('Processing failed:', errorMessage); throw new Error(errorMessage); } } else if (progressResult && !progressResult.success) { throw new Error(progressResult.error || 'Progress check failed'); } } catch (error) { console.error('Error checking progress:', error); // Continue polling even if there's an error, but limit retries await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds before retrying (configurable via FRONTEND_POLLING_DELAY) } } throw new Error('Processing timed out'); } showQASheetSection(sheetId, answerColumns = []) { // Show the Q&A sheet section const qaSheetSection = document.getElementById('qaSheetSection'); if (qaSheetSection) { qaSheetSection.style.display = 'block'; // Pre-fill the form fields const qaSheetIdInput = document.getElementById('qaSheetId'); const answerColumnsInput = document.getElementById('answerColumns'); if (qaSheetIdInput) qaSheetIdInput.value = sheetId; if (answerColumnsInput) answerColumnsInput.value = Array.isArray(answerColumns) ? answerColumns.join(', ') : answerColumns; } } async generateQAFromSheet() { const sheetId = document.getElementById('qaSheetId').value; const answerColumns = document.getElementById('answerColumns').value; if (!sheetId || !answerColumns) { this.showError('Please fill in all required fields (Sheet ID and Answer Document Columns)'); return; } // Validate target file pairs if specified const targetFilePairs = document.getElementById('targetFilePairs').value; if (targetFilePairs) { const targetNumber = parseInt(targetFilePairs); if (isNaN(targetNumber) || targetNumber < 1) { this.showError('Target file pairs must be a positive number'); return; } // TODO: Temporarily disabled validation to debug repeated API calls // The validation will be re-enabled once we identify the root cause console.log('Target file pairs validation temporarily disabled for debugging'); } this.showLoading('qaSheetBtn'); try { const requestData = { sheet_id: sheetId, answer_columns: answerColumns.split(',').map(col => col.trim()), debug_mode: false, // Always false since we removed the checkbox target_file_pairs: targetFilePairs ? parseInt(targetFilePairs) : null }; const result = await this.apiCall('/process-simple-qa', 'POST', requestData); if (result && result.success) { let successMessage = `Q&A generation completed! Generated ${result.qa_pairs?.length || 0} Q&A pairs from ${result.files_processed || 0} answer documents.`; if (result.target_file_pairs) { if (result.target_reached) { successMessage += ` 🎯 Target of ${result.target_file_pairs} answer documents reached!`; } else { successMessage += ` 🎯 Target: ${result.target_file_pairs} answer documents (processed ${result.files_processed})`; } } this.showSuccess(successMessage); this.showResult(result, 'qaSheetResult'); } else { this.showError('Failed to generate Q&A from sheet: ' + result.error); } } catch (error) { this.showError('Failed to generate Q&A from sheet: ' + error.message); } finally { this.hideLoading('qaSheetBtn'); } } async testQAGeneration() { this.showLoading('testQABtn'); try { const result = await this.apiCall('/test-qa-generation', 'POST', {}); if (result && result.success) { this.showSuccess(`Test completed! Generated ${result.qa_pairs?.length || 0} Q&A pairs from ${result.files_processed || 0} file pairs.`); this.showResult(result, 'qaSheetResult'); } else { this.showError('Test failed: ' + result.error); } } catch (error) { this.showError('Test failed: ' + error.message); } finally { this.hideLoading('testQABtn'); } } async crudQA(operation) { this.showLoading(`crud${operation}Btn`); try { const result = await this.apiCall('/workflow/crud-qa', 'POST', { operation }); this.showSuccess(`${operation} operation completed successfully!`); this.showResult(result, 'crudResult'); } catch (error) { this.showError(`Failed to perform ${operation} operation: ` + error.message); } finally { this.hideLoading(`crud${operation}Btn`); } } async loadStats() { try { const result = await this.apiCall('/stats'); // Update stats cards with real data if (result && result.success) { // Calculate totals from processed + pending for consistency const totalDocs = (result.processed_documents || 0) + (result.pending_documents || 0); const totalQAPairs = (result.processed_qa_pairs || 0) + (result.pending_qa_pairs || 0); document.getElementById('totalDocs').textContent = `${totalDocs} Total Documents`; document.getElementById('totalQA').textContent = `${totalQAPairs} Total Q&A Pairs`; document.getElementById('status').textContent = result.system_status || 'Unknown'; // Update detailed stats with color coding const processedDocs = result.processed_documents || 0; const pendingDocs = result.pending_documents || 0; const processedQA = result.processed_qa_pairs || 0; const pendingQA = result.pending_qa_pairs || 0; document.getElementById('processedDocs').textContent = `${processedDocs} Processed`; document.getElementById('pendingDocs').textContent = `${pendingDocs} Pending`; document.getElementById('processedQA').textContent = `${processedQA} Processed`; document.getElementById('pendingQA').textContent = `${pendingQA} Pending`; // Apply conditional color coding for pending items const pendingDocsElement = document.getElementById('pendingDocs'); const pendingQAElement = document.getElementById('pendingQA'); if (pendingDocs === 0) { pendingDocsElement.classList.add('zero-pending'); } else { pendingDocsElement.classList.remove('zero-pending'); } if (pendingQA === 0) { pendingQAElement.classList.add('zero-pending'); } else { pendingQAElement.classList.remove('zero-pending'); } // Update status card styling based on system status const statusCard = document.getElementById('status').parentElement; statusCard.className = 'stat-card system-status-card'; // Reset classes but keep system-status-card switch (result.system_status) { case 'Processing': statusCard.classList.add('status-processing'); break; case 'Issues Found': statusCard.classList.add('status-error'); break; case 'Ready': statusCard.classList.add('status-ready'); break; case 'Error': statusCard.classList.add('status-error'); break; default: statusCard.classList.add('status-idle'); } } } catch (error) { console.error('Failed to load stats:', error); // Show error state in stats document.getElementById('totalDocs').textContent = 'Error Total Documents'; document.getElementById('totalQA').textContent = 'Error Total Q&A Pairs'; document.getElementById('status').textContent = 'Error'; document.getElementById('processedDocs').textContent = 'Error Processed'; document.getElementById('pendingDocs').textContent = 'Error Pending'; document.getElementById('processedQA').textContent = 'Error Processed'; document.getElementById('pendingQA').textContent = 'Error Pending'; // Remove zero-pending class on error document.getElementById('pendingDocs').classList.remove('zero-pending'); document.getElementById('pendingQA').classList.remove('zero-pending'); } } async checkAuthStatus() { try { const result = await this.apiCall('/oauth/status'); this.updateAuthStatus(result); } catch (error) { console.error('Failed to check auth status:', error); this.updateAuthStatus({ authenticated: false, error: error.message }); } } updateAuthStatus(status) { const indicator = document.getElementById('authIndicator'); const authButton = document.getElementById('authButton'); const disconnectButton = document.getElementById('disconnectGoogleButton'); const oauthStatusInfo = document.getElementById('oauthStatusInfo'); // Update sheet ID display based on settings this.updateSheetIdDisplay(); // Update reset button state this.updateResetButtonState(); // Get all form elements that should be disabled/enabled const formElements = [ 'sheetId', 'qaSheetId', 'answerColumns', 'targetFilePairs', 'qaSearchQuery', 'qaSearchYear', 'qaSearchDivision', 'qaSearchLimit', 'searchQABtn', 'resetFiltersBtn' ]; const buttons = [ 'processBtn', 'qaSheetBtn', 'testQABtn', 'crudcreateBtn', 'crudreadBtn', 'crudupdateBtn', 'cruddeleteBtn', 'searchQABtn', 'resetFiltersBtn' ]; // Hide Q&A sheet section if not authenticated const qaSheetSection = document.getElementById('qaSheetSection'); if (qaSheetSection && !status.authenticated) { qaSheetSection.style.display = 'none'; } if (status.authenticated) { if (indicator) { indicator.innerHTML = ' Authenticated with Google'; indicator.className = 'auth-indicator authenticated'; } if (authButton) { authButton.style.display = 'none'; } // Show disconnect button in settings if (disconnectButton) { disconnectButton.style.display = 'inline-block'; } // Update OAuth status info in settings if (oauthStatusInfo && status.user_info) { const userInfo = status.user_info; if (userInfo.email) { oauthStatusInfo.innerHTML = ` Connected as: ${userInfo.email} ${userInfo.name ? `
Name: ${userInfo.name}` : ''} `; } else { oauthStatusInfo.innerHTML = ' Connected to Google Drive'; } } // Enable all form elements formElements.forEach(id => { const element = document.getElementById(id); if (element) element.disabled = false; }); buttons.forEach(id => { const element = document.getElementById(id); if (element) element.disabled = false; }); // Load filter options only when authenticated this.loadFilterOptions(); } else { if (indicator) { indicator.innerHTML = ' Not authenticated'; indicator.className = 'auth-indicator not-authenticated'; } if (authButton) { authButton.style.display = 'inline-block'; } // Hide disconnect button in settings if (disconnectButton) { disconnectButton.style.display = 'none'; } // Update OAuth status info in settings if (oauthStatusInfo) { oauthStatusInfo.innerHTML = ' Not connected to Google Drive'; } // Disable all form elements formElements.forEach(id => { const element = document.getElementById(id); if (element) element.disabled = true; }); buttons.forEach(id => { const element = document.getElementById(id); if (element) element.disabled = true; }); } } authenticate() { window.location.href = '/auth'; } async disconnectGoogle() { if (!confirm('Are you sure you want to disconnect from Google? This will revoke your access and you will need to re-authenticate.')) { return; } try { this.showLoading('disconnectGoogleButton'); const response = await fetch(`${this.apiBase}/oauth/disconnect`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); const result = await response.json(); if (result.success) { this.showSuccess('Successfully disconnected from Google'); // Refresh auth status to update UI await this.checkAuthStatus(); } else { this.showError('Failed to disconnect: ' + (result.error || 'Unknown error')); } } catch (error) { this.showError('Failed to disconnect: ' + error.message); } finally { this.hideLoading('disconnectGoogleButton'); } } // Q&A Management Methods async searchQAPairs(page = 1) { const query = document.getElementById('qaSearchQuery').value.trim(); const year = document.getElementById('qaSearchYear').value; const division = document.getElementById('qaSearchDivision').value; const limit = parseInt(document.getElementById('qaSearchLimit').value); // Update pagination state this.currentPage = page; this.pageSize = limit; this.currentSearchParams = { query, year, division }; this.showLoading('searchQABtn'); try { const offset = (page - 1) * limit; const data = { query: query || null, year: year || null, division: division || null, limit, offset }; const result = await this.apiCall('/crud/qa/search', 'POST', data); if (result && result.success) { const qaPairs = result.qa_pairs || []; this.displayQAPairsList(qaPairs, 'qaPairsListManagement'); // Update pagination info with total count from API this.updatePaginationInfo(qaPairs.length, limit, result.total_count); this.showSuccess(`Found ${qaPairs.length} Q&A pairs`); } else { this.showError('Failed to search Q&A pairs: ' + result.error); } } catch (error) { this.showError('Failed to search Q&A pairs: ' + error.message); } finally { this.hideLoading('searchQABtn'); } } async loadAllQAPairs(page = 1) { const limit = parseInt(document.getElementById('qaSearchLimit').value); // Update pagination state this.currentPage = page; this.pageSize = limit; this.currentSearchParams = {}; // Clear search params for "load all" this.showLoading('loadAllQABtn'); try { const offset = (page - 1) * limit; const result = await this.apiCall(`/crud/qa/list?limit=${limit}&offset=${offset}`, 'GET'); if (result && result.success) { const qaPairs = result.qa_pairs || []; this.displayQAPairsList(qaPairs, 'qaPairsListManagement'); // Update pagination info with total count from API this.updatePaginationInfo(qaPairs.length, limit, result.total_count); this.showSuccess(`Loaded ${qaPairs.length} Q&A pairs`); } else { this.showError('Failed to load Q&A pairs: ' + result.error); } } catch (error) { this.showError('Failed to load Q&A pairs: ' + error.message); } finally { this.hideLoading('loadAllQABtn'); } } async loadAllQA() { // Alias for loadAllQAPairs for compatibility await this.loadAllQAPairs(); // Ensure reset button is in correct state (disabled by default when no filters) this.updateResetButtonState(); } displayQAPairsList(qaPairs, containerId = 'qaPairsList') { const container = document.getElementById(containerId); // Store current Q&A data for re-rendering when CRUD mode changes this.currentQAData = qaPairs || []; if (!qaPairs || qaPairs.length === 0) { container.innerHTML = '

No Q&A pairs found.

'; return; } let html = '
'; qaPairs.forEach((qa, index) => { const createdDate = new Date(qa.created_at).toLocaleDateString(); const updatedDate = new Date(qa.updated_at).toLocaleDateString(); // Calculate the global question number based on current page and position const globalQuestionNumber = ((this.currentPage - 1) * this.pageSize) + index + 1; html += `
${globalQuestionNumber}
Q: ${qa.question}
A: ${qa.answer}
`; }); html += '
'; container.innerHTML = html; } // Pagination helper methods updatePaginationInfo(resultsCount, pageSize, totalCount = null) { // Show pagination controls if we have results const paginationContainer = document.getElementById('qaPaginationContainer'); const paginationContainerTop = document.getElementById('qaPaginationContainerTop'); if (resultsCount > 0) { paginationContainer.style.display = 'flex'; paginationContainerTop.style.display = 'flex'; // Use total count from API if available, otherwise estimate if (totalCount !== null) { this.totalResults = totalCount; this.totalPages = Math.ceil(totalCount / pageSize); } else { // Fallback estimation (if API doesn't provide total count) if (resultsCount === pageSize) { this.totalPages = Math.max(this.currentPage + 1, this.totalPages); } else { this.totalPages = this.currentPage; } } // Update pagination controls (bottom) document.getElementById('qaTotalPages').textContent = this.totalPages; document.getElementById('qaCurrentPageInput').value = this.currentPage; // Update pagination controls (top) document.getElementById('qaTotalPagesTop').textContent = this.totalPages; document.getElementById('qaCurrentPageInputTop').value = this.currentPage; // Enable/disable navigation buttons (bottom) document.getElementById('qaFirstPageBtn').disabled = this.currentPage <= 1; document.getElementById('qaPrevPageBtn').disabled = this.currentPage <= 1; document.getElementById('qaNextPageBtn').disabled = this.currentPage >= this.totalPages; document.getElementById('qaLastPageBtn').disabled = this.currentPage >= this.totalPages; // Enable/disable navigation buttons (top) document.getElementById('qaFirstPageBtnTop').disabled = this.currentPage <= 1; document.getElementById('qaPrevPageBtnTop').disabled = this.currentPage <= 1; document.getElementById('qaNextPageBtnTop').disabled = this.currentPage >= this.totalPages; document.getElementById('qaLastPageBtnTop').disabled = this.currentPage >= this.totalPages; // Update results summary this.updateResultsSummary(resultsCount, totalCount); } else { paginationContainer.style.display = 'none'; paginationContainerTop.style.display = 'none'; this.hideResultsSummary(); } } updateResultsSummary(resultsCount, totalCount) { const summaryContainer = document.getElementById('qaResultsSummary'); const resultsText = document.getElementById('qaResultsText'); if (summaryContainer && resultsText) { const startNumber = ((this.currentPage - 1) * this.pageSize) + 1; const endNumber = startNumber + resultsCount - 1; let summaryText = `Showing questions ${startNumber}-${endNumber}`; if (totalCount !== null) { summaryText += ` of ${totalCount} total approved Q&A pairs`; } else { summaryText += ` (page ${this.currentPage} of ${this.totalPages})`; } resultsText.textContent = summaryText; summaryContainer.style.display = 'block'; } } hideResultsSummary() { const summaryContainer = document.getElementById('qaResultsSummary'); if (summaryContainer) { summaryContainer.style.display = 'none'; } } updateResetButtonState() { const resetBtn = document.getElementById('resetFiltersBtn'); const query = document.getElementById('qaSearchQuery').value.trim(); const year = document.getElementById('qaSearchYear').value; const division = document.getElementById('qaSearchDivision').value; // Check if any filters are active const hasActiveFilters = query || year || division; if (hasActiveFilters) { // Enable button and make it green resetBtn.disabled = false; resetBtn.classList.remove('btn-secondary'); resetBtn.classList.add('btn-success'); } else { // Disable button and make it gray resetBtn.disabled = true; resetBtn.classList.remove('btn-success'); resetBtn.classList.add('btn-secondary'); } } async resetFilters() { // Clear all filter inputs document.getElementById('qaSearchQuery').value = ''; document.getElementById('qaSearchYear').value = ''; document.getElementById('qaSearchDivision').value = ''; // Reset pagination state this.currentPage = 1; this.currentSearchParams = {}; // Update button state (should be disabled after reset) this.updateResetButtonState(); // Load all Q&A pairs without filters await this.loadAllQAPairs(1); } refreshCurrentQAList() { // Re-render the current Q&A list to show/hide CRUD buttons based on mode if (this.currentQAData && this.currentQAData.length > 0) { this.displayQAPairsList(this.currentQAData, 'qaPairsListManagement'); } } goToPage(page) { if (page < 1 || page > this.totalPages) return; // Check if we have search params or should load all if (Object.keys(this.currentSearchParams).length > 0 && (this.currentSearchParams.query || this.currentSearchParams.year || this.currentSearchParams.division)) { this.searchQAPairs(page); } else { this.loadAllQAPairs(page); } } async editQAPair(qaId) { // Find the QA pair data from the current list const qaPair = this.currentQAData?.find(qa => qa.id === qaId); if (!qaPair) { this.showError('QA pair not found'); return; } // Show edit modal this.showEditModal(qaPair); } showEditModal(qaPair) { // Create edit modal const modal = document.createElement('div'); modal.className = 'qa-edit-modal'; modal.innerHTML = `

Edit Q&A Pair

`; document.body.appendChild(modal); // Add form submit handler const form = modal.querySelector('#editQAForm'); form.addEventListener('submit', (e) => { e.preventDefault(); this.saveEditQA(modal); }); // Add escape key handler const escapeHandler = (e) => { if (e.key === 'Escape') { modal.remove(); document.removeEventListener('keydown', escapeHandler); } }; document.addEventListener('keydown', escapeHandler); modal._escapeHandler = escapeHandler; } async saveEditQA(modal) { const qaId = modal.querySelector('#editQAId').value; const documentId = modal.querySelector('#editDocumentId').value; const question = modal.querySelector('#editQuestion').value.trim(); const answer = modal.querySelector('#editAnswer').value.trim(); if (!question || !answer) { this.showError('Please fill in both question and answer'); return; } const saveBtn = modal.querySelector('#saveEditBtn'); const originalText = saveBtn.innerHTML; saveBtn.disabled = true; saveBtn.innerHTML = ' Saving...'; try { const data = { id: qaId, question: question, answer: answer, document_id: documentId }; const result = await this.apiCall('/crud/qa/update', 'PUT', data); if (result.success) { this.showSuccess('Q&A pair updated successfully!'); // Update the local display this.updateQAPairInDisplay(qaId, question, answer); // Close modal modal.remove(); // Refresh current list if we're in QA management if (this.currentQAData) { this.refreshCurrentQAList(); } } else { this.showError('Failed to update Q&A pair: ' + (result.error || 'Unknown error')); saveBtn.disabled = false; saveBtn.innerHTML = originalText; } } catch (error) { this.showError('Failed to update Q&A pair: ' + error.message); saveBtn.disabled = false; saveBtn.innerHTML = originalText; } } updateQAPairInDisplay(qaId, question, answer) { // Update in QA management list const qaItem = document.querySelector(`[data-qa-id="${qaId}"]`); if (qaItem) { const questionElement = qaItem.querySelector('.qa-question'); const answerElement = qaItem.querySelector('.qa-answer'); if (questionElement) { questionElement.innerHTML = `Q: ${question}`; } if (answerElement) { answerElement.innerHTML = `A: ${answer}`; } } // Update in QA review modal if it's open const reviewModal = document.querySelector('.qa-review-modal'); if (reviewModal) { const reviewItem = reviewModal.querySelector(`[data-qa-id="${qaId}"]`); if (reviewItem) { const questionElement = reviewItem.querySelector('.qa-question'); const answerElement = reviewItem.querySelector('.qa-answer'); if (questionElement) { questionElement.innerHTML = `Q: ${question}`; } if (answerElement) { answerElement.innerHTML = `A: ${answer}`; } } } // Update the current review data if (currentQAReviewData) { const qaPair = currentQAReviewData.find(qa => qa.id === qaId); if (qaPair) { qaPair.question = question; qaPair.answer = answer; } } // Update the current QA data in DocumentAgent if (window.documentAgentApp && window.documentAgentApp.currentQAData) { const qaPair = window.documentAgentApp.currentQAData.find(qa => qa.id === qaId); if (qaPair) { qaPair.question = question; qaPair.answer = answer; } } } async deleteQAPair(qaId) { if (!confirm('Are you sure you want to delete this Q&A pair? This action cannot be undone.')) { return; } try { const result = await this.apiCall(`/crud/qa/delete/${qaId}`, 'DELETE'); if (result && result.success) { this.showSuccess('Q&A pair deleted successfully!'); // Refresh the current page instead of resetting to first page this.goToPage(this.currentPage); // Update stats await this.loadStats(); } else { this.showError('Failed to delete Q&A pair: ' + result.error); } } catch (error) { this.showError('Failed to delete Q&A pair: ' + error.message); } } async loadFilterOptions() { try { const result = await this.apiCall('/crud/qa/filter-options', 'GET'); if (result && result.success) { // Populate year filter const yearSelect = document.getElementById('qaSearchYear'); if (yearSelect) { yearSelect.innerHTML = ''; if (result.years) { result.years.forEach(year => { const option = document.createElement('option'); option.value = year; option.textContent = year; yearSelect.appendChild(option); }); } } // Populate division filter const divisionSelect = document.getElementById('qaSearchDivision'); if (divisionSelect) { divisionSelect.innerHTML = ''; if (result.divisions) { result.divisions.forEach(division => { const option = document.createElement('option'); option.value = division; option.textContent = division; divisionSelect.appendChild(option); }); } } } } catch (error) { console.error('Failed to load filter options:', error); // Don't show error to user for filter options - it's not critical } } // Prompts Tab Logic async loadPromptsTab() { try { // Match exact behavior of /prompts page document.addEventListener('DOMContentLoaded', () => {}); // no-op await this.reloadPromptsData(); // Load OpenAI models when prompts tab is accessed await this.loadOpenAIModels(); } catch (e) { this.showPromptError('Failed to load prompts: ' + e.message); } finally { this.showPromptsLoading(false); } } async reloadPromptsData() { this.showPromptsLoading(true); // Clear cache to ensure fresh data from database this.promptsCache = {}; const promptsResp = await this.apiCall('/prompts/', 'GET'); if (promptsResp && promptsResp.success) { this.promptsCache = promptsResp.prompts || {}; console.log('Prompts loaded from API:', Object.keys(this.promptsCache)); console.log('Number of prompts:', Object.keys(this.promptsCache).length); this.updatePromptStats(); this.renderPromptGrid(Object.values(this.promptsCache)); } this.showPromptsLoading(false); } updatePromptStats() { // Stats cards removed - no longer needed } async loadOpenAIModels() { if (this.modelsLoaded) { return; // Already loaded } try { const response = await this.apiCall('/openai-models'); if (response.success && response.models) { this.populateModelDropdown(response.models); this.modelsLoaded = true; if (response.fallback) { console.info('Using fallback OpenAI models (API unavailable)'); } else { console.info('Loaded OpenAI models from API'); } } else { console.warn('Failed to load OpenAI models, using fallback'); this.populateModelDropdown([]); this.modelsLoaded = true; } } catch (error) { console.error('Failed to load OpenAI models:', error); if (error.message && error.message.includes('OpenAI API key not configured')) { console.warn('OpenAI API key not configured. Please set it in the Settings tab.'); } this.populateModelDropdown([]); this.modelsLoaded = true; } } populateModelDropdown(models) { const modelSelect = document.getElementById('model'); if (!modelSelect) return; // Clear existing options modelSelect.innerHTML = ''; // Add models from API or fallback if (models && models.length > 0) { models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.name || model.id; modelSelect.appendChild(option); }); } else { // Fallback models const fallbackModels = [ { id: 'gpt-5-mini', name: 'GPT-5 Mini' }, { id: 'gpt-4o-mini', name: 'GPT-4o Mini' }, { id: 'gpt-4o', name: 'GPT-4o' }, { id: 'gpt-4-turbo', name: 'GPT-4 Turbo' }, { id: 'gpt-4', name: 'GPT-4' }, { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' }, { id: 'o1-preview', name: 'O1 Preview' }, { id: 'o1-mini', name: 'O1 Mini' } ]; fallbackModels.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.name; modelSelect.appendChild(option); }); } // Add change event listener for validation modelSelect.addEventListener('change', () => { this.validateModelSelection(); }); } validateModelSelection() { const modelSelect = document.getElementById('model'); if (!modelSelect) return; // Remove any existing warning const existingWarning = document.getElementById('model-warning'); if (existingWarning) { existingWarning.remove(); } // Reset styling modelSelect.classList.remove('invalid-model'); } _parseResponseFormat(responseFormatText) { if (!responseFormatText || responseFormatText.trim() === '') { return null; } try { // Try to parse as JSON const parsed = JSON.parse(responseFormatText); return parsed; } catch (error) { console.warn('Invalid JSON in response format, treating as null:', error); return null; } } showPromptsLoading(show) { const el = document.getElementById('promptsLoadingIndicator'); if (el) el.style.display = show ? 'block' : 'none'; } showPromptError(message) { const el = document.getElementById('promptErrorMessage'); if (el) { el.textContent = message; el.style.display = 'block'; setTimeout(() => (el.style.display = 'none'), 4000); } } showPromptSuccess(message) { const el = document.getElementById('promptSuccessMessage'); if (el) { el.textContent = message; el.style.display = 'block'; setTimeout(() => (el.style.display = 'none'), 2500); } } renderPromptGrid(prompts) { const grid = document.getElementById('promptGrid'); if (!grid) return; grid.innerHTML = ''; (prompts || []).forEach(p => { const card = document.createElement('div'); card.className = 'prompt-card'; // Display both prompt and system_message in the preview let combinedContent = ''; if (p.prompt) { combinedContent += `Prompt: ${p.prompt}`; } if (p.system_message) { if (combinedContent) { combinedContent += '\n\n'; // Add a separator if prompt already exists } combinedContent += `System Message: ${p.system_message}`; } const preview = combinedContent.length > 200 ? combinedContent.slice(0, 200) + '...' : combinedContent; // Check if this is an enhanced two-pass flow prompt (only if they exist in the database) // For now, no enhanced prompts should exist - this will be updated when the feature is implemented const isEnhancedPrompt = false; const enhancedBadge = isEnhancedPrompt ? 'ENHANCED' : ''; card.innerHTML = `

${p.name} ${enhancedBadge}

${p.description || ''}
${preview}
`; grid.appendChild(card); }); } openEditPrompt(id) { const p = this.promptsCache[id]; if (!p) return; // Debug logging console.log('🔍 Opening edit prompt for:', id); console.log('🔍 Prompt data:', p); console.log('🔍 Response format:', p.response_format); document.getElementById('modalTitle').textContent = 'Edit Prompt'; document.getElementById('promptId').value = p.id; document.getElementById('promptId').disabled = true; document.getElementById('promptName').value = p.name || ''; document.getElementById('promptDescription').value = p.description || ''; document.getElementById('promptText').value = p.prompt || ''; document.getElementById('systemMessage').value = p.system_message || ''; document.getElementById('responseFormat').value = p.response_format ? JSON.stringify(p.response_format, null, 2) : ''; document.getElementById('temperature').value = p.temperature ?? 1.0; // Set model value and validate const modelSelect = document.getElementById('model'); const currentModel = p.model || 'gpt-4o-mini'; modelSelect.value = currentModel; // Check if current model is in the available options const availableModels = Array.from(modelSelect.options).map(option => option.value); if (!availableModels.includes(currentModel)) { // Model not found in current options, mark as invalid modelSelect.classList.add('invalid-model'); // Add a warning message let warningMsg = document.getElementById('model-warning'); if (!warningMsg) { warningMsg = document.createElement('div'); warningMsg.id = 'model-warning'; modelSelect.parentNode.appendChild(warningMsg); } warningMsg.textContent = `Model "${currentModel}" is not available in your OpenAI account`; } else { // Model is valid, reset styling modelSelect.classList.remove('invalid-model'); // Remove warning message const warningMsg = document.getElementById('model-warning'); if (warningMsg) { warningMsg.remove(); } } document.getElementById('promptModal').style.display = 'block'; document.getElementById('promptModal').classList.add('show'); this._editingPromptId = id; } closeModal() { const modal = document.getElementById('promptModal'); if (modal) { modal.style.display = 'none'; modal.classList.remove('show'); } } async savePrompt() { const form = document.getElementById('promptForm'); const fd = new FormData(form); const body = { id: fd.get('id'), name: fd.get('name'), description: fd.get('description'), prompt: fd.get('prompt'), system_message: fd.get('system_message'), response_format: this._parseResponseFormat(fd.get('response_format')), temperature: parseFloat(fd.get('temperature') || '1.0'), model: fd.get('model') || 'gpt-4o-mini' }; try { let resp; if (this._editingPromptId) { resp = await this.apiCall(`/prompts/${this._editingPromptId}`, 'PUT', body); } else { resp = await this.apiCall('/prompts/', 'POST', body); } if (resp && resp.success) { this.showPromptSuccess('Prompt saved'); this.closeModal(); await this.reloadPromptsData(); } else { this.showPromptError(resp?.message || 'Failed to save prompt'); } } catch (e) { this.showPromptError('Error saving prompt: ' + e.message); } } } // Global functions for modal handling function closeColumnMappingModal() { const app = window.documentAgentApp; if (app) { app.closeColumnMappingModal(); } } function processWithColumnMapping() { const app = window.documentAgentApp; if (app) { app.processWithColumnMapping(); } } function cancelColumnMappingModal() { const app = window.documentAgentApp; if (app) { app.cancelColumnMappingModal(); } } // JWT Validation for Navigation async function validateJWTForNavigation() { try { console.log('🔐 Validating JWT for navigation...'); // Check if n8n-auth cookie exists const cookies = document.cookie.split(';'); let jwtToken = null; for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'n8n-auth' && value) { jwtToken = value; break; } } if (!jwtToken) { console.log('❌ No JWT token found, redirecting to login...'); if (window.documentAgentApp) { window.documentAgentApp.showLoginRedirect(); } else { // Fallback if app not available window.location.href = '/auth-required'; } return false; } // Validate the JWT token with the backend const response = await fetch('/api/auth/validate-jwt', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: jwtToken }) }); const result = await response.json(); if (result.success && result.valid) { console.log('✅ JWT validation successful for navigation'); return true; } else { console.log('❌ JWT validation failed for navigation:', result); // Clear the invalid cookie document.cookie = 'n8n-auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; if (window.documentAgentApp) { window.documentAgentApp.showLoginRedirect(); } else { // Fallback if app not available window.location.href = '/auth-required'; } return false; } } catch (error) { console.error('❌ Error validating JWT for navigation:', error); // Clear the invalid cookie on error document.cookie = 'n8n-auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; if (window.documentAgentApp) { window.documentAgentApp.showLoginRedirect(); } else { // Fallback if app not available window.location.href = '/auth-required'; } return false; } } // Tab Navigation Functions async function showTab(tabName) { // Check JWT authentication before allowing tab navigation if (!(await validateJWTForNavigation())) { console.log('❌ JWT validation failed, redirecting to login...'); return; // Stop navigation if JWT is invalid } // Hide all content sections const mainContent = document.getElementById('main-content'); const qaReviewContent = document.getElementById('qa-review-content'); const qaManagementContent = document.getElementById('qa-management-content'); const promptsContent = document.getElementById('prompts-content'); const settingsContent = document.getElementById('settings-content'); // Remove active class from all tabs document.querySelectorAll('.nav-tab').forEach(tab => { tab.classList.remove('active'); }); // Hide all content sections first mainContent.style.display = 'none'; qaReviewContent.style.display = 'none'; qaManagementContent.style.display = 'none'; if (promptsContent) promptsContent.style.display = 'none'; if (settingsContent) settingsContent.style.display = 'none'; // Show selected content and activate tab if (tabName === 'main') { mainContent.style.display = 'grid'; document.querySelector('.nav-tab[onclick="showTab(\'main\')"]').classList.add('active'); } else if (tabName === 'qa-review') { qaReviewContent.style.display = 'block'; document.querySelector('.nav-tab[onclick="showTab(\'qa-review\')"]').classList.add('active'); loadDocumentsForReview(); } else if (tabName === 'qa-management') { qaManagementContent.style.display = 'grid'; document.querySelector('.nav-tab[onclick="showTab(\'qa-management\')"]').classList.add('active'); // Load QA pairs for management and enable vector search if (window.documentAgentApp) { window.documentAgentApp.loadAllQA(); window.documentAgentApp.loadFilterOptions(); } } else if (tabName === 'prompts') { if (promptsContent) { promptsContent.style.display = 'block'; document.querySelector('.nav-tab[onclick="showTab(\'prompts\')"]').classList.add('active'); if (window.documentAgentApp) { window.documentAgentApp.loadPromptsTab(); } } } else if (tabName === 'settings') { if (settingsContent) { settingsContent.style.display = 'block'; document.querySelector('.nav-tab[onclick="showTab(\'settings\')"]').classList.add('active'); // Load settings when tab is opened loadSettings(); } } } // QA Review Functions async function loadDocumentsForReview() { try { const response = await fetch('/api/qa-review/documents'); const data = await response.json(); if (data.success) { // Cache all documents to avoid repeated API calls allDocuments = data.documents; displayDocumentsForReview(allDocuments); } else { if (window.documentAgentApp) { window.documentAgentApp.showError('Error loading documents: ' + (data.message || 'Unknown error')); } else { console.error('Error loading documents:', data.message); } } } catch (error) { console.error('Error loading documents:', error); if (window.documentAgentApp) { window.documentAgentApp.showError('Error loading documents: ' + error.message); } } } function toggleCompletedDocuments() { showCompletedDocuments = !showCompletedDocuments; const toggleBtn = document.getElementById('toggleCompletedBtn'); if (showCompletedDocuments) { // Show all documents (including completed) if (toggleBtn) { toggleBtn.innerHTML = ' Show Pending Documents'; toggleBtn.classList.add('active'); } } else { // Show only pending documents (hide completed) if (toggleBtn) { toggleBtn.innerHTML = ' Show All Documents'; toggleBtn.classList.remove('active'); } } // Re-render the documents list with current filter (no API call needed) displayDocumentsForReview(allDocuments); } // Function to refresh the cached documents (call this after making changes) async function refreshCachedDocuments() { try { const response = await fetch('/api/qa-review/documents'); const data = await response.json(); if (data.success) { // Update the cache allDocuments = data.documents; // Re-render with current filter state displayDocumentsForReview(allDocuments); } } catch (error) { console.error('Error refreshing cached documents:', error); } } function displayDocumentsForReview(documents) { const container = document.getElementById('documentsList'); if (!documents || documents.length === 0) { container.innerHTML = '

No documents with QA pairs found.

'; return; } // Filter documents based on showCompletedDocuments state (local filtering only) const filteredDocuments = showCompletedDocuments ? documents : documents.filter(doc => !doc.processed); const html = filteredDocuments.map(doc => { const isProcessed = doc.processed; const canMarkProcessed = doc.pending_review === 0 && !isProcessed; return `

${doc.title || doc.file_id || 'Unknown Document'} ${isProcessed ? '✓ Processed' : ''}

Year: ${doc.year || 'N/A'} Division: ${doc.division || 'N/A'} File ID: ${doc.file_id}
${doc.total_qa_pairs}
Total
${doc.pending_review}
Pending
${doc.approved_qa_pairs}
Approved
`; }).join(''); container.innerHTML = html; } async function openQAReviewModal(documentId, documentName, isProcessed = false) { try { const response = await fetch(`/api/qa-review/document/${documentId}/qa-pairs`); const data = await response.json(); if (data.success) { showQAReviewModal(documentName, data.qa_pairs, documentId, isProcessed); } else { if (window.documentAgentApp) { window.documentAgentApp.showError('Error loading QA pairs: ' + (data.message || 'Unknown error')); } else { console.error('Error loading QA pairs:', data.message); } } } catch (error) { console.error('Error loading QA pairs:', error); if (window.documentAgentApp) { window.documentAgentApp.showError('Error loading QA pairs: ' + error.message); } } } // Global state for pending filter let showOnlyPending = false; // Global state for completed documents filter let showCompletedDocuments = false; let allDocuments = []; // Cache all documents to avoid repeated API calls // Global state for QA review modal let currentQAReviewData = null; function showQAReviewModal(documentName, qaPairs, documentId, isProcessed = false) { // Store current QA data for editing currentQAReviewData = qaPairs; // Automatically set showOnlyPending state based on document completion // For partially complete documents: show only pending questions // For fully complete documents: show all questions showOnlyPending = !isProcessed; const modal = document.createElement('div'); modal.className = 'qa-review-modal'; modal.innerHTML = `

Review QA Pairs - ${documentName}

${qaPairs.map(qa => `
${qa.question}
${qa.answer}
`).join('')}
`; // Add escape key listener const handleEscape = (event) => { if (event.key === 'Escape') { closeQAReviewModal(); } }; // Store the handler so we can remove it later modal._escapeHandler = handleEscape; document.addEventListener('keydown', handleEscape); document.body.appendChild(modal); // Apply initial filtering based on showOnlyPending state (now for all documents) const container = document.getElementById('qaPairsContainer'); const qaPairElements = container.querySelectorAll('.qa-pair-review'); if (showOnlyPending) { // Show only pending (unreviewed) questions qaPairElements.forEach(qaPair => { const isReviewed = qaPair.classList.contains('approved') || qaPair.classList.contains('discarded'); if (isReviewed) { qaPair.style.display = 'none'; } else { qaPair.style.display = 'block'; } }); // Add active class to button to show it's in "show only pending" mode const toggleBtn = document.getElementById('togglePendingBtn'); if (toggleBtn) { toggleBtn.classList.add('active'); } } else { // Show all questions qaPairElements.forEach(qaPair => { qaPair.style.display = 'block'; }); // Remove active class from button const toggleBtn = document.getElementById('togglePendingBtn'); if (toggleBtn) { toggleBtn.classList.remove('active'); } } // Add event listeners for radio buttons (now available for all documents) const radioButtons = modal.querySelectorAll('.qa-radio'); radioButtons.forEach(radio => { radio.addEventListener('change', updateQAReviewProgress); }); updateQAReviewProgress(); } function togglePendingQuestions() { showOnlyPending = !showOnlyPending; const toggleBtn = document.getElementById('togglePendingBtn'); const container = document.getElementById('qaPairsContainer'); if (!container) return; const qaPairElements = container.querySelectorAll('.qa-pair-review'); if (showOnlyPending) { // Show only pending (unreviewed) questions qaPairElements.forEach(qaPair => { const isReviewed = qaPair.classList.contains('approved') || qaPair.classList.contains('discarded'); if (isReviewed) { qaPair.style.display = 'none'; } else { qaPair.style.display = 'block'; } }); // Update button text and icon if (toggleBtn) { toggleBtn.innerHTML = ' Show All Questions'; toggleBtn.classList.add('active'); } } else { // Show all questions qaPairElements.forEach(qaPair => { qaPair.style.display = 'block'; }); // Update button text and icon if (toggleBtn) { toggleBtn.innerHTML = ' Only Show Pending Questions'; toggleBtn.classList.remove('active'); } } // Update progress display updateQAReviewProgress(); } function editQAPairInReview(qaId) { // Find the QA pair data from the current review data const qaPair = currentQAReviewData?.find(qa => qa.id === qaId); if (!qaPair) { if (window.documentAgentApp) { window.documentAgentApp.showError('QA pair not found'); } return; } // Show edit modal using the same function as QA management if (window.documentAgentApp) { window.documentAgentApp.showEditModal(qaPair); } } function closeQAReviewModal() { const modal = document.querySelector('.qa-review-modal'); if (modal) { // Remove the escape key listener if (modal._escapeHandler) { document.removeEventListener('keydown', modal._escapeHandler); } modal.remove(); } // Reset filter state showOnlyPending = false; currentQAReviewData = null; } function showAddQuestionModal() { return new Promise((resolve) => { // Create modal overlay const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; `; // Create modal content const modal = document.createElement('div'); modal.className = 'add-question-modal'; modal.style.cssText = ` background: white; border-radius: 8px; padding: 24px; width: 90%; max-width: 600px; max-height: 80vh; overflow-y: auto; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); `; modal.innerHTML = ` `; overlay.appendChild(modal); document.body.appendChild(overlay); // Focus on the question textarea const questionTextarea = modal.querySelector('#newQuestion'); questionTextarea.focus(); // Handle events let closeModal = () => { document.body.removeChild(overlay); }; const saveQuestion = () => { const question = questionTextarea.value.trim(); const answer = modal.querySelector('#newAnswer').value.trim(); if (!question || !answer) { alert('Please enter both a question and an answer.'); return; } closeModal(); resolve({ question, answer }); }; // Handle escape key const handleEscape = (e) => { if (e.key === 'Escape') { closeModal(); resolve(null); } }; // Update closeModal to include cleanup const originalCloseModal = closeModal; closeModal = () => { document.removeEventListener('keydown', handleEscape); originalCloseModal(); }; // Event listeners modal.querySelector('.modal-close').addEventListener('click', () => { closeModal(); resolve(null); }); modal.querySelector('#cancelAddQuestion').addEventListener('click', () => { closeModal(); resolve(null); }); modal.querySelector('#saveAddQuestion').addEventListener('click', saveQuestion); document.addEventListener('keydown', handleEscape); // Handle Enter key in textareas (Ctrl+Enter to save) const handleKeyDown = (e) => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); saveQuestion(); } }; questionTextarea.addEventListener('keydown', handleKeyDown); modal.querySelector('#newAnswer').addEventListener('keydown', handleKeyDown); // Handle overlay click to close overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeModal(); resolve(null); } }); }); } async function addQuestionToDocument(documentId, documentName) { // Show modal dialog for adding new question const result = await showAddQuestionModal(); if (!result) { return; // User cancelled } const { question, answer } = result; try { // Call the API to add the new question with auto-approval const response = await fetch('/api/qa-review/add-question', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ document_id: documentId, question: question.trim(), answer: answer.trim(), auto_approve: true }) }); const result = await response.json(); if (result.success) { // Show success message if (window.documentAgentApp) { window.documentAgentApp.showSuccess('New question added and auto-approved successfully!'); } // Refresh the document list to show updated counts await refreshCachedDocuments(); // Refresh header stats to show updated counts if (window.documentAgentApp) { window.documentAgentApp.loadStats(); } } else { throw new Error(result.error || 'Failed to add question'); } } catch (error) { console.error('Error adding new question:', error); if (window.documentAgentApp) { window.documentAgentApp.showError('Failed to add question: ' + error.message); } else { alert('Failed to add question: ' + error.message); } } } async function approveQAPair(qaId) { try { const response = await fetch(`/api/qa-review/qa-pair/${qaId}/approve`, { method: 'POST' }); const data = await response.json(); if (data.success) { const qaElement = document.getElementById(`qa-${qaId}`); qaElement.classList.add('approved'); qaElement.querySelector('.btn-approve').disabled = true; qaElement.querySelector('.btn-discard').disabled = true; updateQAReviewProgress(); if (window.documentAgentApp) { window.documentAgentApp.showSuccess('QA pair approved successfully'); } } else { if (window.documentAgentApp) { window.documentAgentApp.showError('Error approving QA pair: ' + (data.message || 'Unknown error')); } else { console.error('Error approving QA pair:', data.message); } } } catch (error) { console.error('Error approving QA pair:', error); if (window.documentAgentApp) { window.documentAgentApp.showError('Error approving QA pair: ' + error.message); } } } async function discardQAPair(qaId) { try { const response = await fetch(`/api/qa-review/qa-pair/${qaId}/discard`, { method: 'POST' }); const data = await response.json(); if (data.success) { const qaElement = document.getElementById(`qa-${qaId}`); qaElement.classList.add('discarded'); qaElement.querySelector('.btn-approve').disabled = true; qaElement.querySelector('.btn-discard').disabled = true; updateQAReviewProgress(); if (window.documentAgentApp) { window.documentAgentApp.showSuccess('QA pair discarded successfully'); } } else { if (window.documentAgentApp) { window.documentAgentApp.showError('Error discarding QA pair: ' + (data.message || 'Unknown error')); } else { console.error('Error discarding QA pair:', data.message); } } } catch (error) { console.error('Error discarding QA pair:', error); if (window.documentAgentApp) { window.documentAgentApp.showError('Error discarding QA pair: ' + error.message); } } } async function saveQABatch(documentId) { const container = document.getElementById('qaPairsContainer'); if (!container) return; const qaPairElements = container.querySelectorAll('.qa-pair-review'); const batchData = []; // Collect all QA pair decisions (including changes to already reviewed items) qaPairElements.forEach(qaPair => { const qaId = qaPair.getAttribute('data-qa-id'); const approveRadio = document.getElementById(`approve-${qaId}`); const discardRadio = document.getElementById(`discard-${qaId}`); // Check if this item is already reviewed const isAlreadyReviewed = qaPair.classList.contains('approved') || qaPair.classList.contains('discarded'); const wasApproved = qaPair.classList.contains('approved'); if (approveRadio && approveRadio.checked) { // Add to batch if it's a new selection or a change from discarded to approved // For new questions (not already reviewed), always include them if (!isAlreadyReviewed || !wasApproved) { batchData.push({ id: qaId, action: 'approve' }); } } else if (discardRadio && discardRadio.checked) { // Add to batch if it's a new selection or a change from approved to discarded // For new questions (not already reviewed), always include them if (!isAlreadyReviewed || wasApproved) { batchData.push({ id: qaId, action: 'discard' }); } } }); if (batchData.length === 0) { if (window.documentAgentApp) { window.documentAgentApp.showError('No QA pairs selected for processing'); } return; } // Disable save button and show loading state const saveBtn = document.getElementById('saveQABtn'); if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = ' Saving...'; } try { const response = await fetch('/api/qa-review/batch-update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ documentId: documentId, qaPairs: batchData }) }); const data = await response.json(); if (data.success) { if (window.documentAgentApp) { window.documentAgentApp.showSuccess(`Successfully saved ${batchData.length} QA pairs`); } // Update UI to show processed state only for the saved QA pairs qaPairElements.forEach(qaPair => { const qaId = qaPair.getAttribute('data-qa-id'); const approveRadio = document.getElementById(`approve-${qaId}`); const discardRadio = document.getElementById(`discard-${qaId}`); // Only update UI for QA pairs that were actually saved if (batchData.some(item => item.id === qaId)) { if (approveRadio && approveRadio.checked) { qaPair.classList.add('approved'); qaPair.classList.remove('discarded', 'selected-approve', 'selected-discard'); } else if (discardRadio && discardRadio.checked) { qaPair.classList.add('discarded'); qaPair.classList.remove('approved', 'selected-approve', 'selected-discard'); } // Keep radio buttons enabled so they can be changed again // Don't disable them - allow further changes } }); // Update progress (don't hide save button - allow more saves) updateQAReviewProgress(); // Check if all QA pairs are now reviewed const allReviewed = container.querySelectorAll('.qa-pair-review').length === container.querySelectorAll('.qa-pair-review.approved, .qa-pair-review.discarded').length; // Close modal and refresh document list after any save setTimeout(() => { closeQAReviewModal(); // Refresh the document list to show updated counts refreshCachedDocuments(); // Refresh header stats to show updated counts if (window.documentAgentApp) { window.documentAgentApp.loadStats(); } }, 1500); if (allReviewed) { // Only mark as processed if ALL are reviewed await markDocumentProcessed(documentId); if (window.documentAgentApp) { window.documentAgentApp.showSuccess('All QA pairs processed and document marked as completed!'); } } } else { if (window.documentAgentApp) { window.documentAgentApp.showError('Error processing QA pairs: ' + (data.message || 'Unknown error')); } // Re-enable save button on error if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = ' Save Selected'; } } } catch (error) { console.error('Error processing QA pairs:', error); if (window.documentAgentApp) { window.documentAgentApp.showError('Error processing QA pairs: ' + error.message); } // Re-enable save button on error if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = ' Save Selected'; } } } async function markDocumentProcessed(documentId) { try { const response = await fetch(`/api/qa-review/document/${documentId}/mark-processed`, { method: 'POST' }); const data = await response.json(); if (data.success) { if (window.documentAgentApp) { window.documentAgentApp.showSuccess('Document marked as processed successfully'); } closeQAReviewModal(); refreshCachedDocuments(); // Refresh the documents list } else { if (window.documentAgentApp) { window.documentAgentApp.showError('Error marking document as processed: ' + (data.message || 'Unknown error')); } else { console.error('Error marking document as processed:', data.message); } } } catch (error) { console.error('Error marking document as processed:', error); if (window.documentAgentApp) { window.documentAgentApp.showError('Error marking document as processed: ' + error.message); } } } function updateQAReviewProgress() { const container = document.getElementById('qaPairsContainer'); if (!container) return; const qaPairElements = container.querySelectorAll('.qa-pair-review'); const progress = document.getElementById('qaReviewProgress'); const saveBtn = document.getElementById('saveQABtn'); const markProcessedBtn = document.getElementById('markProcessedBtn'); let reviewedCount = 0; // Count how many QA pairs have been selected (either approve or discard) qaPairElements.forEach(qaPair => { // Skip hidden items when counting if (qaPair.style.display === 'none') return; const qaId = qaPair.getAttribute('data-qa-id'); const approveRadio = document.getElementById(`approve-${qaId}`); const discardRadio = document.getElementById(`discard-${qaId}`); // Check if this QA pair is already reviewed (has approved/discarded class) const isAlreadyReviewed = qaPair.classList.contains('approved') || qaPair.classList.contains('discarded'); if (approveRadio && approveRadio.checked) { reviewedCount++; qaPair.classList.add('selected-approve'); qaPair.classList.remove('selected-discard'); } else if (discardRadio && discardRadio.checked) { reviewedCount++; qaPair.classList.add('selected-discard'); qaPair.classList.remove('selected-approve'); } else if (isAlreadyReviewed) { // Count already reviewed items reviewedCount++; } else { qaPair.classList.remove('selected-approve', 'selected-discard'); } }); if (progress) { const savedCount = container.querySelectorAll('.qa-pair-review.approved, .qa-pair-review.discarded').length; const pendingCount = qaPairElements.length - savedCount; if (savedCount > 0) { progress.textContent = `${reviewedCount} of ${qaPairElements.length} reviewed (${savedCount} saved, ${pendingCount} pending)`; } else { progress.textContent = `${reviewedCount} of ${qaPairElements.length} reviewed`; } } // Enable Save button when any NEW QA pairs are selected OR when already reviewed items are changed if (saveBtn) { const newSelections = container.querySelectorAll('.qa-pair-review.selected-approve, .qa-pair-review.selected-discard').length; // Also check for changes to already reviewed items let hasChanges = false; qaPairElements.forEach(qaPair => { const qaId = qaPair.getAttribute('data-qa-id'); const approveRadio = document.getElementById(`approve-${qaId}`); const discardRadio = document.getElementById(`discard-${qaId}`); const isAlreadyReviewed = qaPair.classList.contains('approved') || qaPair.classList.contains('discarded'); const wasApproved = qaPair.classList.contains('approved'); if (isAlreadyReviewed) { // Check if the current selection differs from the saved state if ((approveRadio && approveRadio.checked && !wasApproved) || (discardRadio && discardRadio.checked && wasApproved)) { hasChanges = true; } } }); const shouldBeDisabled = newSelections === 0 && !hasChanges; saveBtn.disabled = shouldBeDisabled; // FORCE the button to be visible - this is critical! saveBtn.style.display = 'inline-flex'; saveBtn.style.visibility = 'visible'; saveBtn.style.opacity = '1'; saveBtn.style.position = 'relative'; saveBtn.style.zIndex = '9999'; console.log(`Save button: reviewedCount=${reviewedCount}, total=${qaPairElements.length}, disabled=${shouldBeDisabled}, visible=${saveBtn.style.display}, offsetParent=${saveBtn.offsetParent !== null}`); // Emergency backup: Force button visibility every time setTimeout(() => { if (saveBtn) { saveBtn.style.display = 'inline-flex'; saveBtn.style.visibility = 'visible'; saveBtn.style.opacity = '1'; console.log('Emergency: Forced Save button visibility'); } }, 100); } else { console.log('Save button not found in DOM'); } // Mark Processed button visibility is controlled by saveQABatch function // Don't hide it here - it should only show after successful save } // Event listeners for QA Review // Initialize the app when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.documentAgentApp = new DocumentAgent(); window.app = window.documentAgentApp; // Alias for easier access }); // Expose small helpers for inline handlers (matching prompt_manager.html API) function closeModal(){ if(window.documentAgentApp) window.documentAgentApp.closeModal(); } function savePrompt(){ if(window.documentAgentApp) window.documentAgentApp.savePrompt(); } // Settings Management Functions let allSettings = {}; let currentEditKey = null; // Define allowed settings const ALLOWED_SETTINGS = { "GOOGLE_CLIENT_ID": { category: "oauth", description: "Google OAuth Client ID", is_secret: false }, "GOOGLE_CLIENT_SECRET": { category: "oauth", description: "Google OAuth Client Secret", is_secret: true }, "GOOGLE_REDIRECT_URI": { category: "oauth", description: "Google OAuth Redirect URI", is_secret: false }, "OPENAI_API_KEY": { category: "api", description: "OpenAI API Key for AI processing", is_secret: true }, "JWT_SECRET": { category: "api", description: "JWT Secret for token validation", is_secret: true }, "GOOGLE_SHEET_ID": { category: "integration", description: "Google Sheet ID for ingestion", is_secret: false }, "ENHANCED_TWO_PASS_FLOW": { category: "general", description: "Enable enhanced two-pass flow for question extraction and answer generation", is_secret: false, input_type: "checkbox" }, "MAIN_APP_BASE_URL": { category: "general", description: "Base URL where the application is accessible", is_secret: false } }; // Show messages for settings function showSettingsMessage(message, type = 'info') { const messageDiv = document.getElementById('settings-message'); messageDiv.innerHTML = `
${message}
`; // Auto-hide after 5 seconds setTimeout(() => { messageDiv.innerHTML = ''; }, 5000); } // Load all settings async function loadSettings() { try { const response = await fetch('/settings/?include_secrets=false'); const data = await response.json(); if (data.success) { allSettings = data.settings; renderSettings(); // Update sheet ID display after settings are loaded if (window.app && typeof window.app.updateSheetIdDisplay === 'function') { window.app.updateSheetIdDisplay(); } } else { showSettingsMessage('Failed to load settings', 'error'); } } catch (error) { console.error('Error loading settings:', error); showSettingsMessage('Error loading settings: ' + error.message, 'error'); } } // Render settings by category function renderSettings() { const categories = { oauth: document.getElementById('oauth-settings'), api: document.getElementById('api-settings'), integration: document.getElementById('integration-settings'), general: document.getElementById('general-settings') }; // Clear all categories Object.values(categories).forEach(container => { if (container) container.innerHTML = ''; }); // Group settings by category const settingsByCategory = { oauth: [], api: [], integration: [], general: [] }; Object.entries(allSettings).forEach(([key, setting]) => { const category = setting.category || 'general'; if (settingsByCategory[category]) { // Merge metadata from ALLOWED_SETTINGS so flags like `readonly` and `is_secret` are preserved const cfg = ALLOWED_SETTINGS[key] || {}; settingsByCategory[category].push({ key, ...setting, ...cfg }); } }); // Render each category, but also show empty slots for allowed settings Object.entries(settingsByCategory).forEach(([category, settings]) => { const container = categories[category]; if (!container) return; // Get all allowed settings for this category const allowedForCategory = Object.entries(ALLOWED_SETTINGS) .filter(([key, config]) => config.category === category) .map(([key]) => key); // Create a map of existing settings const existingKeys = new Set(settings.map(s => s.key)); // Render existing settings settings.forEach(setting => { container.innerHTML += renderSettingItem(setting); }); // Render empty slots for allowed settings that don't exist yet allowedForCategory.forEach(key => { if (!existingKeys.has(key)) { const config = ALLOWED_SETTINGS[key]; container.innerHTML += renderSettingItem({ key: key, value: '', category: config.category, description: config.description, is_secret: config.is_secret, isEmpty: true }); } }); }); } // Render a single setting item function renderSettingItem(setting) { const isSecret = setting.is_secret; const isReadonly = setting.readonly || false; const inputType = setting.input_type || (isSecret ? 'password' : 'text'); const isEmpty = setting.isEmpty || !setting.value; const valueDisplay = isEmpty ? '' : setting.value; const isCheckbox = inputType === 'checkbox'; const isChecked = isCheckbox && (valueDisplay === 'true' || valueDisplay === true); return `
${setting.key} ${isSecret ? 'SECRET' : ''} ${isEmpty ? 'NOT SET' : ''}
${setting.description ? `
${setting.description}
` : ''}
${isCheckbox ? ` ` : ` `} ${isReadonly ? ` Read-only` : ''}
${!isEmpty ? `` : ''}
`; } // Auto-save setting when user finishes editing (onblur) async function autoSaveSetting(key) { const inputElement = document.getElementById(`value-${key}`); const savingIndicator = document.getElementById(`saving-${key}`); // Handle checkbox vs text input differently const config = ALLOWED_SETTINGS[key]; const isCheckbox = config && config.input_type === 'checkbox'; const newValue = isCheckbox ? inputElement.checked.toString() : inputElement.value.trim(); // Get the current value from allSettings const currentValue = allSettings[key]?.value || ''; // Only save if value changed if (newValue === currentValue) { return; } try { // Get setting config const config = ALLOWED_SETTINGS[key]; if (!config) { showSettingsMessage(`Setting '${key}' is not allowed`, 'error'); return; } const payload = { key, value: newValue, category: config.category, description: config.description, is_secret: config.is_secret }; // Check if setting exists const exists = allSettings[key]; let response; if (exists) { // Update existing response = await fetch(`/settings/${encodeURIComponent(key)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } else { // Create new response = await fetch('/settings/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } const data = await response.json(); if (data.success) { // Show saved indicator briefly if (savingIndicator) { savingIndicator.style.display = 'inline'; setTimeout(() => { savingIndicator.style.display = 'none'; }, 2000); } // Update local cache await loadSettings(); } else { showSettingsMessage(data.detail || 'Failed to save setting', 'error'); // Revert to old value inputElement.value = currentValue; } } catch (error) { console.error('Error saving setting:', error); showSettingsMessage('Error saving setting: ' + error.message, 'error'); // Revert to old value inputElement.value = currentValue; } } // Clear setting value async function clearSetting(key) { if (!confirm(`Are you sure you want to clear the value for "${key}"?`)) { return; } try { const response = await fetch(`/settings/${encodeURIComponent(key)}`, { method: 'DELETE' }); if (response.ok) { showSettingsMessage(`Setting "${key}" cleared successfully`, 'success'); await loadSettings(); } else { const data = await response.json(); showSettingsMessage(data.detail || 'Failed to clear setting', 'error'); } } catch (error) { console.error('Error clearing setting:', error); showSettingsMessage('Error clearing setting: ' + error.message, 'error'); } }