`;
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 = '