- Upload returns immediately for AI source, processes in background
- New GET /session/{id}/files endpoint for polling AI progress
- Frontend polls every 3s, updates UI as files complete
- Shows progress: "X of Y files done..."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1529 lines
63 KiB
JavaScript
1529 lines
63 KiB
JavaScript
let currentFiles = [];
|
||
let sessionId = null;
|
||
let importSessionId = null;
|
||
let selectedFiles = new Set();
|
||
|
||
const uploadArea = document.getElementById('uploadArea');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const spinner = document.getElementById('spinner');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressFill = document.getElementById('progressFill');
|
||
const fileList = document.getElementById('fileList');
|
||
const actions = document.getElementById('actions');
|
||
const errorAlert = document.getElementById('errorAlert');
|
||
const successAlert = document.getElementById('successAlert');
|
||
const infoAlert = document.getElementById('infoAlert');
|
||
|
||
// Click to upload
|
||
uploadArea.addEventListener('click', () => fileInput.click());
|
||
|
||
// File selection
|
||
fileInput.addEventListener('change', handleFileSelect);
|
||
|
||
// Drag and drop
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
handleFiles(files);
|
||
}
|
||
});
|
||
|
||
// Import file input
|
||
const importFileInput = document.getElementById('importFileInput');
|
||
importFileInput.addEventListener('change', handleImportFile);
|
||
|
||
function handleSourceChange() {
|
||
const source = document.getElementById('metadataSource').value;
|
||
const importSection = document.getElementById('importSection');
|
||
|
||
// Show/hide import section
|
||
if (source === 'import') {
|
||
importSection.style.display = 'block';
|
||
} else {
|
||
importSection.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
let currentImportData = null;
|
||
|
||
async function handleImportFile(e) {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
|
||
hideAlerts();
|
||
showInfo(`Uploading import file: ${file.name}...`);
|
||
|
||
const formData = new FormData();
|
||
formData.append('import_file', file);
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + '/import-metadata', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.error) {
|
||
showError(data.error);
|
||
return;
|
||
}
|
||
|
||
// Store import data and show mapping modal
|
||
currentImportData = data;
|
||
showImportMappingModal(data);
|
||
|
||
} catch (error) {
|
||
showError(`Import upload failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
function showImportMappingModal(data) {
|
||
const modal = document.getElementById('importMappingModal');
|
||
const content = document.getElementById('importMappingContent');
|
||
|
||
// Build sheet selector for Excel files
|
||
let sheetsHTML = '';
|
||
if (data.sheets && data.sheets.length > 0) {
|
||
sheetsHTML = '<div class="form-group">';
|
||
sheetsHTML += '<label for="importSheetSelect">Select Sheet *</label>';
|
||
sheetsHTML += '<select id="importSheetSelect" class="form-control" onchange="updateImportSheetPreview()">';
|
||
data.sheets.forEach(sheet => {
|
||
sheetsHTML += `<option value="${sheet}">${sheet}</option>`;
|
||
});
|
||
sheetsHTML += '</select></div>';
|
||
}
|
||
|
||
content.innerHTML = `
|
||
<div class="form-group">
|
||
<label>File: <strong>${data.filename}</strong></label>
|
||
</div>
|
||
|
||
${sheetsHTML}
|
||
|
||
<div class="form-group">
|
||
<label>Column Mappings *</label>
|
||
<div style="background: #f8f9ff; padding: 15px; border-radius: 8px; margin-top: 10px;">
|
||
<div class="form-group">
|
||
<label for="importFilenameColumn">Filename Column *</label>
|
||
<select id="importFilenameColumn" class="form-control">
|
||
<option value="">-- Select Column --</option>
|
||
${data.columns.map(col => `<option value="${col}">${col}</option>`).join('')}
|
||
</select>
|
||
<small style="color: #6b7280;">Column containing filenames (with or without extension)</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="importTitleColumn">Title Column</label>
|
||
<select id="importTitleColumn" class="form-control">
|
||
<option value="">-- Select Column (Optional) --</option>
|
||
${data.columns.map(col => `<option value="${col}">${col}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="importSubjectColumn">Subject/Description Column</label>
|
||
<select id="importSubjectColumn" class="form-control">
|
||
<option value="">-- Select Column (Optional) --</option>
|
||
${data.columns.map(col => `<option value="${col}">${col}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="importKeywordsColumn">Keywords Column</label>
|
||
<select id="importKeywordsColumn" class="form-control">
|
||
<option value="">-- Select Column (Optional) --</option>
|
||
${data.columns.map(col => `<option value="${col}">${col}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Preview (First 3 rows)</label>
|
||
<div id="importPreviewTable" style="overflow-x: auto; margin-top: 10px;">
|
||
${buildImportPreviewTable(data)}
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button onclick="confirmImportMapping()" class="btn btn-primary" style="flex: 1;">
|
||
✅ Confirm Mapping
|
||
</button>
|
||
<button onclick="closeImportMappingModal()" class="btn btn-secondary" style="flex: 1;">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// Auto-select likely columns
|
||
autoSelectImportColumns(data.columns);
|
||
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
async function updateImportSheetPreview() {
|
||
const sheetName = document.getElementById('importSheetSelect').value;
|
||
if (!currentImportData || !sheetName) return;
|
||
|
||
try {
|
||
showInfo('Loading sheet preview...');
|
||
|
||
const response = await fetch(BASE_PATH + '/preview-excel-sheet', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
excel_session_id: currentImportData.import_session_id,
|
||
sheet_name: sheetName
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.error) {
|
||
showError(data.error);
|
||
return;
|
||
}
|
||
|
||
// Update column dropdowns
|
||
const columns = data.columns;
|
||
['importFilenameColumn', 'importTitleColumn', 'importSubjectColumn', 'importKeywordsColumn'].forEach(id => {
|
||
const select = document.getElementById(id);
|
||
const currentValue = select.value;
|
||
select.innerHTML = id === 'importFilenameColumn'
|
||
? '<option value="">-- Select Column --</option>'
|
||
: '<option value="">-- Select Column (Optional) --</option>';
|
||
columns.forEach(col => {
|
||
select.innerHTML += `<option value="${col}">${col}</option>`;
|
||
});
|
||
// Try to restore selection
|
||
if (columns.includes(currentValue)) {
|
||
select.value = currentValue;
|
||
}
|
||
});
|
||
|
||
// Update preview table
|
||
document.getElementById('importPreviewTable').innerHTML = buildImportPreviewTable(data);
|
||
|
||
// Auto-select columns again
|
||
autoSelectImportColumns(columns);
|
||
|
||
hideAlerts();
|
||
|
||
} catch (error) {
|
||
showError(`Failed to load sheet preview: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
function autoSelectImportColumns(columns) {
|
||
// Try to auto-detect filename column
|
||
const filenameCandidates = ['filename', 'file name', 'file', 'name', 'path'];
|
||
for (const col of columns) {
|
||
if (filenameCandidates.some(c => col.toLowerCase().includes(c))) {
|
||
document.getElementById('importFilenameColumn').value = col;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Try to auto-detect title column
|
||
const titleCandidates = ['title', 'heading', 'name'];
|
||
for (const col of columns) {
|
||
if (titleCandidates.some(c => col.toLowerCase() === c)) {
|
||
document.getElementById('importTitleColumn').value = col;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Try to auto-detect subject column
|
||
const subjectCandidates = ['description', 'desc', 'subject', 'summary'];
|
||
for (const col of columns) {
|
||
if (subjectCandidates.some(c => col.toLowerCase().includes(c))) {
|
||
document.getElementById('importSubjectColumn').value = col;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Try to auto-detect keywords column
|
||
const keywordsCandidates = ['keywords', 'keyword', 'tags', 'tag'];
|
||
for (const col of columns) {
|
||
if (keywordsCandidates.some(c => col.toLowerCase().includes(c))) {
|
||
document.getElementById('importKeywordsColumn').value = col;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
function buildImportPreviewTable(data) {
|
||
if (!data || !data.sample_data || data.sample_data.length === 0) {
|
||
return '<p style="color: #6b7280;">No preview data available</p>';
|
||
}
|
||
|
||
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 13px;">';
|
||
|
||
// Header
|
||
html += '<thead><tr style="background: var(--primary-gold); color: var(--dark-secondary);">';
|
||
data.columns.forEach(col => {
|
||
html += `<th style="padding: 8px; border: 1px solid #dee2e6; font-weight: 600;">${col}</th>`;
|
||
});
|
||
html += '</tr></thead>';
|
||
|
||
// Rows
|
||
html += '<tbody>';
|
||
data.sample_data.slice(0, 3).forEach((row, idx) => {
|
||
html += `<tr style="background: ${idx % 2 === 0 ? '#fff' : '#f8f9ff'};">`;
|
||
data.columns.forEach(col => {
|
||
const value = row[col] || '';
|
||
html += `<td style="padding: 8px; border: 1px solid #dee2e6;">${value}</td>`;
|
||
});
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
|
||
return html;
|
||
}
|
||
|
||
async function confirmImportMapping() {
|
||
const filenameColumn = document.getElementById('importFilenameColumn').value;
|
||
const titleColumn = document.getElementById('importTitleColumn').value;
|
||
const subjectColumn = document.getElementById('importSubjectColumn').value;
|
||
const keywordsColumn = document.getElementById('importKeywordsColumn').value;
|
||
|
||
if (!filenameColumn) {
|
||
showError('Please select a filename column');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showInfo('Configuring import mapping...');
|
||
|
||
const response = await fetch(BASE_PATH + '/configure-import-mapping', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
import_session_id: currentImportData.import_session_id,
|
||
column_mapping: {
|
||
filename: filenameColumn,
|
||
title: titleColumn,
|
||
subject: subjectColumn,
|
||
keywords: keywordsColumn
|
||
}
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.error) {
|
||
showError(data.error);
|
||
return;
|
||
}
|
||
|
||
// Store import session ID
|
||
importSessionId = data.import_session_id;
|
||
|
||
// Display stats
|
||
const importStats = document.getElementById('importStats');
|
||
const stats = data.stats;
|
||
importStats.innerHTML = `
|
||
✅ ${data.message}<br>
|
||
<small>
|
||
Title: ${stats.with_title}/${stats.total_records} •
|
||
Subject: ${stats.with_subject}/${stats.total_records} •
|
||
Keywords: ${stats.with_keywords}/${stats.total_records}
|
||
</small>
|
||
`;
|
||
importStats.style.display = 'block';
|
||
|
||
// Mark import section as active
|
||
document.getElementById('importSection').classList.add('active');
|
||
|
||
closeImportMappingModal();
|
||
showSuccess(`✅ ${data.message}`);
|
||
|
||
} catch (error) {
|
||
showError(`Import configuration failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
function closeImportMappingModal() {
|
||
const modal = document.getElementById('importMappingModal');
|
||
modal.style.display = 'none';
|
||
currentImportData = null;
|
||
}
|
||
|
||
function handleFileSelect(e) {
|
||
const files = e.target.files;
|
||
if (files.length > 0) {
|
||
handleFiles(files);
|
||
}
|
||
}
|
||
|
||
async function handleFiles(files) {
|
||
hideAlerts();
|
||
showSpinner();
|
||
showProgress(0);
|
||
fileList.style.display = 'none';
|
||
actions.style.display = 'none';
|
||
currentFiles = [];
|
||
|
||
const metadataSource = document.getElementById('metadataSource').value;
|
||
|
||
// Validate metadata sources
|
||
if (metadataSource === 'import' && !importSessionId) {
|
||
showError('Please import a metadata file first using the "Choose Import File" button');
|
||
hideSpinner();
|
||
return;
|
||
}
|
||
|
||
// Show specific message for AI processing
|
||
if (metadataSource === 'ai') {
|
||
showInfo(`🤖 Generating AI metadata for ${files.length} file(s)... This may take 10-30 seconds per file.`);
|
||
// Start animated progress for AI
|
||
startProgressAnimation();
|
||
} else {
|
||
showInfo(`Processing ${files.length} file(s) with ${metadataSource} source...`);
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('metadata_source', metadataSource);
|
||
if (importSessionId) {
|
||
formData.append('import_session_id', importSessionId);
|
||
}
|
||
for (let file of files) {
|
||
formData.append('files', file);
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + '/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
hideSpinner();
|
||
hideProgress();
|
||
stopProgressAnimation();
|
||
|
||
if (data.error) {
|
||
showError(data.error);
|
||
return;
|
||
}
|
||
|
||
sessionId = data.session_id;
|
||
currentFiles = data.files;
|
||
displayFiles(data.files);
|
||
showSuccess(`Successfully analyzed ${data.files.length} file(s)!`);
|
||
actions.style.display = 'block';
|
||
|
||
// If AI is processing in background, start polling for results
|
||
if (data.ai_processing) {
|
||
showInfo('Generating AI metadata for ' + data.files.length + ' file(s)... This may take 10-30 seconds per file.');
|
||
pollAiResults(data.session_id);
|
||
}
|
||
|
||
} catch (error) {
|
||
hideSpinner();
|
||
hideProgress();
|
||
stopProgressAnimation();
|
||
showError('Error processing files: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function pollAiResults(sid) {
|
||
const poll = async () => {
|
||
try {
|
||
const resp = await fetch(BASE_PATH + '/session/' + sid + '/files');
|
||
const data = await resp.json();
|
||
if (!data.success) return;
|
||
|
||
// Update files and UI with latest data
|
||
currentFiles = data.files;
|
||
displayFiles(data.files);
|
||
|
||
const status = data.ai_status;
|
||
if (status.done) {
|
||
if (status.error > 0) {
|
||
showInfo(`AI completed: ${status.complete} succeeded, ${status.error} failed.`);
|
||
} else {
|
||
showSuccess(`AI metadata generated for ${status.complete} file(s)!`);
|
||
}
|
||
return; // Stop polling
|
||
}
|
||
|
||
// Show progress
|
||
showInfo(`AI processing: ${status.complete} of ${status.complete + status.pending} files done...`);
|
||
|
||
// Poll again in 3 seconds
|
||
setTimeout(poll, 3000);
|
||
} catch (err) {
|
||
console.error('Polling error:', err);
|
||
setTimeout(poll, 5000); // Retry after 5s on error
|
||
}
|
||
};
|
||
// Start first poll after 3 seconds
|
||
setTimeout(poll, 3000);
|
||
}
|
||
|
||
function displayFiles(files) {
|
||
const batchToolbar = `
|
||
<div class="batch-toolbar" id="batchToolbar">
|
||
<div class="batch-toolbar-left">
|
||
<button class="btn-toolbar" onclick="selectAllFiles()">✓ Select All</button>
|
||
<button class="btn-toolbar" onclick="deselectAllFiles()">✗ Deselect All</button>
|
||
<span class="selection-count" id="selectionCount">0 selected</span>
|
||
</div>
|
||
<div class="batch-toolbar-right">
|
||
<button class="btn-toolbar btn-export" onclick="exportResults()">📊 Export Results</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
fileList.innerHTML = batchToolbar;
|
||
fileList.style.display = 'block';
|
||
|
||
// Reset selected files
|
||
selectedFiles.clear();
|
||
|
||
files.forEach((file, index) => {
|
||
if (file.error) {
|
||
const errorItem = document.createElement('div');
|
||
errorItem.className = 'file-item';
|
||
errorItem.style.borderLeftColor = '#dc3545';
|
||
errorItem.innerHTML = `
|
||
<div class="file-header">
|
||
<div class="file-name">❌ ${file.filename}</div>
|
||
</div>
|
||
<div class="alert alert-error" style="display: block;">${file.error}</div>
|
||
`;
|
||
fileList.appendChild(errorItem);
|
||
return;
|
||
}
|
||
|
||
const fileItem = document.createElement('div');
|
||
fileItem.className = 'file-item';
|
||
fileItem.id = `file-${index}`;
|
||
|
||
// Build AI info section if available
|
||
let aiInfoHtml = '';
|
||
if (file.suggested_metadata._tokens_used) {
|
||
aiInfoHtml = `<div style="font-size: 11px; color: #6c757d; margin-top: 5px;">
|
||
✓ AI generated (${file.suggested_metadata._tokens_used} tokens used)
|
||
</div>`;
|
||
}
|
||
if (file.suggested_metadata._ai_error) {
|
||
aiInfoHtml = `<div class="alert alert-error" style="display: block; margin-top: 5px; font-size: 12px;">
|
||
⚠️ AI Error: ${file.suggested_metadata._ai_error}
|
||
</div>`;
|
||
}
|
||
|
||
fileItem.innerHTML = `
|
||
<div class="file-header">
|
||
<div class="file-header-left">
|
||
<input type="checkbox"
|
||
class="file-checkbox"
|
||
id="checkbox-${index}"
|
||
onchange="toggleFileSelection(${index})"
|
||
checked />
|
||
<div class="file-name">📄 ${file.filename}</div>
|
||
</div>
|
||
<div class="file-type">${file.file_type}</div>
|
||
</div>
|
||
|
||
<div class="metadata-comparison">
|
||
<div class="metadata-box">
|
||
<h4>📋 Current Metadata</h4>
|
||
${displayMetadata(file.current_metadata)}
|
||
</div>
|
||
|
||
<div class="metadata-box">
|
||
<h4>✏️ Edit Metadata</h4>
|
||
${displayEditableMetadata(file.suggested_metadata, index)}
|
||
${aiInfoHtml}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="file-actions">
|
||
<button class="btn-save" onclick="saveMetadata(${index})" id="saveBtn-${index}">
|
||
💾 Save Changes
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
fileList.appendChild(fileItem);
|
||
|
||
// Initialize character counters
|
||
initCharCounters(index);
|
||
|
||
// Select by default
|
||
selectedFiles.add(index);
|
||
fileItem.classList.add('selected');
|
||
});
|
||
|
||
updateSelectionCount();
|
||
}
|
||
|
||
function toggleFileSelection(index) {
|
||
const checkbox = document.getElementById(`checkbox-${index}`);
|
||
const fileItem = document.getElementById(`file-${index}`);
|
||
|
||
if (checkbox.checked) {
|
||
selectedFiles.add(index);
|
||
fileItem.classList.add('selected');
|
||
} else {
|
||
selectedFiles.delete(index);
|
||
fileItem.classList.remove('selected');
|
||
}
|
||
|
||
updateSelectionCount();
|
||
}
|
||
|
||
function selectAllFiles() {
|
||
selectedFiles.clear();
|
||
currentFiles.forEach((file, index) => {
|
||
if (!file.error) {
|
||
selectedFiles.add(index);
|
||
const checkbox = document.getElementById(`checkbox-${index}`);
|
||
const fileItem = document.getElementById(`file-${index}`);
|
||
if (checkbox) checkbox.checked = true;
|
||
if (fileItem) fileItem.classList.add('selected');
|
||
}
|
||
});
|
||
updateSelectionCount();
|
||
}
|
||
|
||
function deselectAllFiles() {
|
||
selectedFiles.clear();
|
||
currentFiles.forEach((file, index) => {
|
||
if (!file.error) {
|
||
const checkbox = document.getElementById(`checkbox-${index}`);
|
||
const fileItem = document.getElementById(`file-${index}`);
|
||
if (checkbox) checkbox.checked = false;
|
||
if (fileItem) fileItem.classList.remove('selected');
|
||
}
|
||
});
|
||
updateSelectionCount();
|
||
}
|
||
|
||
function updateSelectionCount() {
|
||
const countElement = document.getElementById('selectionCount');
|
||
if (countElement) {
|
||
countElement.textContent = `${selectedFiles.size} selected`;
|
||
}
|
||
|
||
// Update download button text if it exists
|
||
const downloadBtn = document.getElementById('download-selected-btn');
|
||
if (downloadBtn) {
|
||
downloadBtn.innerHTML = `📦 Download Selected Files (${selectedFiles.size}) as ZIP`;
|
||
}
|
||
}
|
||
|
||
function displayMetadata(metadata) {
|
||
if (!metadata || Object.keys(metadata).length === 0) {
|
||
return '<p style="color: #6c757d; font-size: 13px;">(empty)</p>';
|
||
}
|
||
|
||
let html = '';
|
||
for (const [key, value] of Object.entries(metadata)) {
|
||
html += `
|
||
<div class="metadata-item">
|
||
<span class="metadata-label">${key}:</span>
|
||
<span class="metadata-value">${value || '(empty)'}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function displayEditableMetadata(metadata, index) {
|
||
// Filter out internal fields (starting with _)
|
||
const title = metadata?.title || '';
|
||
const subject = metadata?.subject || '';
|
||
const keywords = metadata?.keywords || '';
|
||
const author = metadata?.author || metadata?.creator || '';
|
||
const copyright = metadata?.copyright || '';
|
||
const comments = metadata?.comments || '';
|
||
|
||
return `
|
||
<div class="metadata-field">
|
||
<label for="title-${index}">Title:</label>
|
||
<input type="text"
|
||
id="title-${index}"
|
||
class="editable-field"
|
||
value="${escapeHtml(title)}"
|
||
maxlength="200"
|
||
data-field="title"
|
||
data-index="${index}"
|
||
placeholder="e.g., Product Brochure 2026" />
|
||
<span class="char-count" id="title-count-${index}">0/200</span>
|
||
</div>
|
||
<div class="metadata-field">
|
||
<label for="subject-${index}">Description/Subject:</label>
|
||
<textarea id="subject-${index}"
|
||
class="editable-field"
|
||
maxlength="300"
|
||
data-field="subject"
|
||
data-index="${index}"
|
||
placeholder="e.g., Marketing brochure for product line">${escapeHtml(subject)}</textarea>
|
||
<span class="char-count" id="subject-count-${index}">0/300</span>
|
||
</div>
|
||
<div class="metadata-field">
|
||
<label for="keywords-${index}">Keywords:</label>
|
||
<input type="text"
|
||
id="keywords-${index}"
|
||
class="editable-field"
|
||
value="${escapeHtml(keywords)}"
|
||
maxlength="500"
|
||
data-field="keywords"
|
||
data-index="${index}"
|
||
placeholder="e.g., product, marketing, 2026" />
|
||
<span class="char-count" id="keywords-count-${index}">0/500</span>
|
||
</div>
|
||
<div class="metadata-field">
|
||
<label for="author-${index}">Author/Creator:</label>
|
||
<input type="text"
|
||
id="author-${index}"
|
||
class="editable-field"
|
||
value="${escapeHtml(author)}"
|
||
maxlength="100"
|
||
data-field="author"
|
||
data-index="${index}"
|
||
placeholder="e.g., John Smith" />
|
||
<span class="char-count" id="author-count-${index}">0/100</span>
|
||
</div>
|
||
<div class="metadata-field">
|
||
<label for="copyright-${index}">Copyright:</label>
|
||
<input type="text"
|
||
id="copyright-${index}"
|
||
class="editable-field"
|
||
value="${escapeHtml(copyright)}"
|
||
maxlength="150"
|
||
data-field="copyright"
|
||
data-index="${index}"
|
||
placeholder="e.g., Copyright 2026 Company Name" />
|
||
<span class="char-count" id="copyright-count-${index}">0/150</span>
|
||
</div>
|
||
<div class="metadata-field">
|
||
<label for="comments-${index}">Comments/Notes:</label>
|
||
<textarea id="comments-${index}"
|
||
class="editable-field"
|
||
maxlength="500"
|
||
data-field="comments"
|
||
data-index="${index}"
|
||
placeholder="e.g., Internal notes about this file">${escapeHtml(comments)}</textarea>
|
||
<span class="char-count" id="comments-count-${index}">0/500</span>
|
||
</div>
|
||
<div id="custom-fields-${index}" style="margin-top: 10px;">
|
||
<!-- Custom fields will be added here -->
|
||
</div>
|
||
<button class="btn-small" onclick="addCustomField(${index})" type="button" style="margin-top: 10px; background: #17a2b8;">
|
||
➕ Add Custom Field
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function initCharCounters(index) {
|
||
const fields = ['title', 'subject', 'keywords', 'author', 'copyright', 'comments'];
|
||
const limits = {
|
||
title: 200,
|
||
subject: 300,
|
||
keywords: 500,
|
||
author: 100,
|
||
copyright: 150,
|
||
comments: 500
|
||
};
|
||
|
||
fields.forEach(field => {
|
||
const input = document.getElementById(`${field}-${index}`);
|
||
const counter = document.getElementById(`${field}-count-${index}`);
|
||
|
||
if (input && counter) {
|
||
// Initial count
|
||
updateCharCount(input, counter, limits[field]);
|
||
|
||
// Listen for changes
|
||
input.addEventListener('input', () => {
|
||
updateCharCount(input, counter, limits[field]);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateCharCount(input, counter, limit) {
|
||
const length = input.value.length;
|
||
counter.textContent = `${length}/${limit}`;
|
||
|
||
// Remove all classes first
|
||
counter.classList.remove('warning', 'danger');
|
||
input.classList.remove('invalid');
|
||
|
||
// Add warning/danger classes
|
||
if (length >= limit) {
|
||
counter.classList.add('danger');
|
||
input.classList.add('invalid');
|
||
} else if (length >= limit * 0.9) {
|
||
counter.classList.add('warning');
|
||
}
|
||
}
|
||
|
||
async function saveMetadata(index) {
|
||
const file = currentFiles[index];
|
||
if (!file || file.error) return;
|
||
|
||
const saveBtn = document.getElementById(`saveBtn-${index}`);
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = '💾 Saving...';
|
||
|
||
// Get edited metadata
|
||
const title = document.getElementById(`title-${index}`).value.trim();
|
||
const subject = document.getElementById(`subject-${index}`).value.trim();
|
||
const keywords = document.getElementById(`keywords-${index}`).value.trim();
|
||
const author = document.getElementById(`author-${index}`).value.trim();
|
||
const copyright = document.getElementById(`copyright-${index}`).value.trim();
|
||
const comments = document.getElementById(`comments-${index}`).value.trim();
|
||
|
||
// Get custom fields
|
||
const customFields = getCustomFields(index);
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + '/update-manual', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
session_id: sessionId,
|
||
file_index: index,
|
||
title: title,
|
||
subject: subject,
|
||
keywords: keywords,
|
||
author: author,
|
||
copyright: copyright,
|
||
comments: comments,
|
||
custom_fields: customFields
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.error) {
|
||
showError(`Failed to update ${file.filename}: ${data.error}`);
|
||
saveBtn.textContent = '💾 Save Changes';
|
||
saveBtn.disabled = false;
|
||
return;
|
||
}
|
||
|
||
// Update the file in currentFiles
|
||
currentFiles[index].suggested_metadata = {
|
||
title, subject, keywords, author, copyright, comments
|
||
};
|
||
|
||
// Show success indicator
|
||
const fileItem = document.getElementById(`file-${index}`);
|
||
if (fileItem) {
|
||
fileItem.style.borderLeftColor = '#28a745';
|
||
|
||
// Remove old success message if exists
|
||
const oldSuccess = fileItem.querySelector('.save-success');
|
||
if (oldSuccess) oldSuccess.remove();
|
||
|
||
// Add success message
|
||
const successDiv = document.createElement('div');
|
||
successDiv.className = 'alert alert-success save-success';
|
||
successDiv.style.display = 'block';
|
||
successDiv.style.marginTop = '10px';
|
||
successDiv.textContent = `✅ Metadata saved successfully!`;
|
||
fileItem.appendChild(successDiv);
|
||
|
||
// Remove success message after 3 seconds
|
||
setTimeout(() => {
|
||
successDiv.remove();
|
||
fileItem.style.borderLeftColor = '#667eea';
|
||
}, 3000);
|
||
}
|
||
|
||
saveBtn.textContent = '✅ Saved!';
|
||
setTimeout(() => {
|
||
saveBtn.textContent = '💾 Save Changes';
|
||
saveBtn.disabled = false;
|
||
}, 2000);
|
||
|
||
} catch (error) {
|
||
showError(`Error saving metadata: ${error.message}`);
|
||
saveBtn.textContent = '💾 Save Changes';
|
||
saveBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
|
||
async function updateAllFiles() {
|
||
if (selectedFiles.size === 0) {
|
||
showError('Please select at least one file to update');
|
||
return;
|
||
}
|
||
|
||
const outputDirEl = document.getElementById('outputDir');
|
||
const outputDir = outputDirEl ? outputDirEl.value.trim() : '';
|
||
const updateBtn = document.getElementById('updateAllBtn');
|
||
updateBtn.disabled = true;
|
||
hideAlerts();
|
||
showProgress(0);
|
||
|
||
showInfo(`Updating ${selectedFiles.size} selected file(s)...`);
|
||
|
||
let successCount = 0;
|
||
let errorCount = 0;
|
||
|
||
const selectedIndices = Array.from(selectedFiles);
|
||
|
||
for (let idx = 0; idx < selectedIndices.length; idx++) {
|
||
const i = selectedIndices[idx];
|
||
const file = currentFiles[i];
|
||
|
||
if (file.error) {
|
||
errorCount++;
|
||
continue;
|
||
}
|
||
|
||
const progress = ((idx + 1) / selectedIndices.length) * 100;
|
||
showProgress(progress);
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + '/update', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
filepath: file.filepath,
|
||
session_id: sessionId,
|
||
file_index: i,
|
||
output_dir: outputDir
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.error) {
|
||
errorCount++;
|
||
const fileItem = document.getElementById(`file-${i}`);
|
||
if (fileItem) {
|
||
fileItem.style.borderLeftColor = '#dc3545';
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.className = 'alert alert-error';
|
||
errorDiv.style.display = 'block';
|
||
errorDiv.textContent = `Error: ${data.error}`;
|
||
fileItem.appendChild(errorDiv);
|
||
}
|
||
} else {
|
||
successCount++;
|
||
const fileItem = document.getElementById(`file-${i}`);
|
||
if (fileItem) {
|
||
fileItem.style.borderLeftColor = '#28a745';
|
||
|
||
// Create success message with download button
|
||
const successDiv = document.createElement('div');
|
||
successDiv.className = 'alert alert-success';
|
||
successDiv.style.display = 'flex';
|
||
successDiv.style.alignItems = 'center';
|
||
successDiv.style.justifyContent = 'space-between';
|
||
successDiv.style.gap = '15px';
|
||
|
||
const messageSpan = document.createElement('span');
|
||
messageSpan.textContent = data.verified ?
|
||
`✅ Updated and verified!` :
|
||
`✅ Updated!`;
|
||
|
||
const downloadBtn = document.createElement('a');
|
||
downloadBtn.href = `${BASE_PATH}/download/${file.filename}`;
|
||
downloadBtn.className = 'btn';
|
||
downloadBtn.style.padding = '8px 16px';
|
||
downloadBtn.style.fontSize = '14px';
|
||
downloadBtn.style.backgroundColor = '#28a745';
|
||
downloadBtn.style.color = 'white';
|
||
downloadBtn.style.textDecoration = 'none';
|
||
downloadBtn.style.borderRadius = '4px';
|
||
downloadBtn.style.display = 'inline-block';
|
||
downloadBtn.download = file.filename;
|
||
downloadBtn.textContent = '⬇️ Download';
|
||
|
||
successDiv.appendChild(messageSpan);
|
||
successDiv.appendChild(downloadBtn);
|
||
fileItem.appendChild(successDiv);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
errorCount++;
|
||
console.error('Error updating file:', error);
|
||
}
|
||
}
|
||
|
||
hideProgress();
|
||
updateBtn.disabled = false;
|
||
|
||
// Show final message and Download All button
|
||
if (successCount > 0 && errorCount === 0) {
|
||
showSuccess(`✅ All ${successCount} file(s) updated successfully!`);
|
||
showDownloadAllButton();
|
||
} else if (successCount > 0 && errorCount > 0) {
|
||
showInfo(`⚠️ Updated ${successCount} file(s), ${errorCount} failed.`);
|
||
showDownloadAllButton();
|
||
} else {
|
||
showError(`❌ Failed to update files. Check individual file errors above.`);
|
||
}
|
||
}
|
||
|
||
function showDownloadAllButton() {
|
||
// Remove existing Download All button if present
|
||
const existingBtn = document.getElementById('download-all-btn');
|
||
if (existingBtn) {
|
||
existingBtn.remove();
|
||
}
|
||
|
||
// Create Download All button container
|
||
const btnContainer = document.createElement('div');
|
||
btnContainer.id = 'download-all-btn';
|
||
btnContainer.style.marginTop = '30px';
|
||
btnContainer.style.marginBottom = '20px';
|
||
btnContainer.style.textAlign = 'center';
|
||
btnContainer.style.padding = '20px';
|
||
btnContainer.style.backgroundColor = '#f8f9fa';
|
||
btnContainer.style.borderRadius = '8px';
|
||
btnContainer.style.border = '2px solid #007bff';
|
||
|
||
const downloadAllBtn = document.createElement('button');
|
||
downloadAllBtn.id = 'download-selected-btn';
|
||
downloadAllBtn.className = 'btn';
|
||
downloadAllBtn.style.padding = '15px 30px';
|
||
downloadAllBtn.style.fontSize = '18px';
|
||
downloadAllBtn.style.fontWeight = 'bold';
|
||
downloadAllBtn.style.backgroundColor = '#007bff';
|
||
downloadAllBtn.style.color = 'white';
|
||
downloadAllBtn.style.border = 'none';
|
||
downloadAllBtn.style.borderRadius = '8px';
|
||
downloadAllBtn.style.cursor = 'pointer';
|
||
downloadAllBtn.style.boxShadow = '0 2px 4px rgba(0,123,255,0.3)';
|
||
downloadAllBtn.innerHTML = `📦 Download Selected Files (${selectedFiles.size}) as ZIP`;
|
||
downloadAllBtn.onmouseover = function() { this.style.backgroundColor = '#0056b3'; };
|
||
downloadAllBtn.onmouseout = function() { this.style.backgroundColor = '#007bff'; };
|
||
downloadAllBtn.onclick = downloadSelectedFiles;
|
||
|
||
btnContainer.appendChild(downloadAllBtn);
|
||
|
||
// Insert after the file list
|
||
const fileList = document.getElementById('fileList');
|
||
if (fileList && fileList.parentNode) {
|
||
fileList.parentNode.insertBefore(btnContainer, fileList.nextSibling);
|
||
}
|
||
}
|
||
|
||
async function downloadSelectedFiles() {
|
||
if (!sessionId) {
|
||
showError('No active session');
|
||
return;
|
||
}
|
||
|
||
if (selectedFiles.size === 0) {
|
||
showError('Please select at least one file to download');
|
||
return;
|
||
}
|
||
|
||
const selectedIndices = Array.from(selectedFiles);
|
||
|
||
try {
|
||
showInfo('📦 Preparing ZIP archive for download...');
|
||
|
||
// Send POST request with selected file indices
|
||
const response = await fetch(BASE_PATH + '/download-selected', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
session_id: sessionId,
|
||
file_indices: selectedIndices
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.error || 'Failed to create ZIP archive');
|
||
}
|
||
|
||
// Get the blob from response
|
||
const blob = await response.blob();
|
||
|
||
// Create download link
|
||
const url = window.URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = `oliver_metadata_files_${new Date().getTime()}.zip`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(url);
|
||
|
||
showSuccess(`✅ Downloaded ${selectedFiles.size} file(s) as ZIP archive`);
|
||
} catch (error) {
|
||
showError(`Error: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async function exportResults() {
|
||
if (currentFiles.length === 0) {
|
||
showError('No files to export');
|
||
return;
|
||
}
|
||
|
||
// Create CSV content
|
||
let csvContent = 'Filename,Title,Subject,Keywords,Status\n';
|
||
|
||
currentFiles.forEach((file, index) => {
|
||
if (file.error) {
|
||
csvContent += `"${file.filename}","","","","Error: ${file.error}"\n`;
|
||
} else {
|
||
const title = file.suggested_metadata?.title || '';
|
||
const subject = file.suggested_metadata?.subject || '';
|
||
const keywords = file.suggested_metadata?.keywords || '';
|
||
const status = selectedFiles.has(index) ? 'Selected' : 'Not selected';
|
||
|
||
// Escape quotes in CSV
|
||
const escapeCsv = (str) => `"${String(str).replace(/"/g, '""')}"`;
|
||
|
||
csvContent += `${escapeCsv(file.filename)},${escapeCsv(title)},${escapeCsv(subject)},${escapeCsv(keywords)},${escapeCsv(status)}\n`;
|
||
}
|
||
});
|
||
|
||
// Create download
|
||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||
link.setAttribute('href', url);
|
||
link.setAttribute('download', `oliver_metadata_export_${timestamp}.csv`);
|
||
link.style.visibility = 'hidden';
|
||
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
showSuccess('✅ Results exported to CSV');
|
||
}
|
||
|
||
function resetForm() {
|
||
fileInput.value = '';
|
||
fileList.style.display = 'none';
|
||
actions.style.display = 'none';
|
||
hideAlerts();
|
||
hideProgress();
|
||
currentFiles = [];
|
||
sessionId = null;
|
||
selectedFiles.clear();
|
||
}
|
||
|
||
function showSpinner() {
|
||
spinner.style.display = 'block';
|
||
}
|
||
|
||
function hideSpinner() {
|
||
spinner.style.display = 'none';
|
||
}
|
||
|
||
function showProgress(percent) {
|
||
progressBar.style.display = 'block';
|
||
progressFill.style.width = percent + '%';
|
||
progressFill.textContent = Math.round(percent) + '%';
|
||
}
|
||
|
||
function hideProgress() {
|
||
progressBar.style.display = 'none';
|
||
}
|
||
|
||
// Animated progress for long-running operations like AI generation
|
||
let progressAnimationInterval = null;
|
||
let animatedProgress = 0;
|
||
|
||
function startProgressAnimation() {
|
||
animatedProgress = 0;
|
||
showProgress(0);
|
||
|
||
progressAnimationInterval = setInterval(() => {
|
||
// Slow down as we approach 90%
|
||
if (animatedProgress < 30) {
|
||
animatedProgress += 2;
|
||
} else if (animatedProgress < 60) {
|
||
animatedProgress += 1;
|
||
} else if (animatedProgress < 90) {
|
||
animatedProgress += 0.5;
|
||
}
|
||
|
||
showProgress(animatedProgress);
|
||
}, 500);
|
||
}
|
||
|
||
function stopProgressAnimation() {
|
||
if (progressAnimationInterval) {
|
||
clearInterval(progressAnimationInterval);
|
||
progressAnimationInterval = null;
|
||
}
|
||
animatedProgress = 0;
|
||
}
|
||
|
||
function showError(message) {
|
||
errorAlert.textContent = message;
|
||
errorAlert.style.display = 'block';
|
||
}
|
||
|
||
function showSuccess(message) {
|
||
successAlert.textContent = message;
|
||
successAlert.style.display = 'block';
|
||
}
|
||
|
||
function showInfo(message) {
|
||
infoAlert.textContent = message;
|
||
infoAlert.style.display = 'block';
|
||
}
|
||
|
||
function hideAlerts() {
|
||
errorAlert.style.display = 'none';
|
||
successAlert.style.display = 'none';
|
||
infoAlert.style.display = 'none';
|
||
}
|
||
|
||
// Custom Fields Management
|
||
let customFieldCounters = {}; // Track number of custom fields per file
|
||
|
||
function addCustomField(index) {
|
||
if (!customFieldCounters[index]) {
|
||
customFieldCounters[index] = 0;
|
||
}
|
||
|
||
const fieldId = customFieldCounters[index]++;
|
||
const container = document.getElementById(`custom-fields-${index}`);
|
||
|
||
const customFieldDiv = document.createElement('div');
|
||
customFieldDiv.className = 'metadata-field';
|
||
customFieldDiv.id = `custom-field-container-${index}-${fieldId}`;
|
||
customFieldDiv.style.border = '1px dashed #17a2b8';
|
||
customFieldDiv.style.padding = '10px';
|
||
customFieldDiv.style.borderRadius = '5px';
|
||
customFieldDiv.style.marginTop = '10px';
|
||
customFieldDiv.style.background = '#f0f9ff';
|
||
|
||
customFieldDiv.innerHTML = `
|
||
<div style="display: flex; gap: 10px; align-items: flex-start;">
|
||
<div style="flex: 1;">
|
||
<label style="display: block; font-weight: 600; color: #495057; font-size: 12px; margin-bottom: 5px;">
|
||
Field Name:
|
||
</label>
|
||
<input type="text"
|
||
id="custom-field-name-${index}-${fieldId}"
|
||
class="editable-field"
|
||
placeholder="e.g., Department, Project, Location"
|
||
style="margin-bottom: 10px;" />
|
||
|
||
<label style="display: block; font-weight: 600; color: #495057; font-size: 12px; margin-bottom: 5px;">
|
||
Field Value:
|
||
</label>
|
||
<input type="text"
|
||
id="custom-field-value-${index}-${fieldId}"
|
||
class="editable-field"
|
||
placeholder="Enter value"
|
||
maxlength="200" />
|
||
<span class="char-count" id="custom-field-count-${index}-${fieldId}">0/200</span>
|
||
</div>
|
||
<button onclick="removeCustomField(${index}, ${fieldId})"
|
||
style="background: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer; margin-top: 25px;"
|
||
type="button">
|
||
✕
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
container.appendChild(customFieldDiv);
|
||
|
||
// Initialize character counter for value field
|
||
const valueInput = document.getElementById(`custom-field-value-${index}-${fieldId}`);
|
||
const counter = document.getElementById(`custom-field-count-${index}-${fieldId}`);
|
||
if (valueInput && counter) {
|
||
valueInput.addEventListener('input', () => {
|
||
updateCharCount(valueInput, counter, 200);
|
||
});
|
||
updateCharCount(valueInput, counter, 200);
|
||
}
|
||
}
|
||
|
||
function removeCustomField(index, fieldId) {
|
||
const container = document.getElementById(`custom-field-container-${index}-${fieldId}`);
|
||
if (container) {
|
||
container.remove();
|
||
}
|
||
}
|
||
|
||
function getCustomFields(index) {
|
||
const customFields = {};
|
||
const container = document.getElementById(`custom-fields-${index}`);
|
||
|
||
if (container) {
|
||
const fieldContainers = container.querySelectorAll('[id^="custom-field-container-"]');
|
||
fieldContainers.forEach(fieldContainer => {
|
||
const match = fieldContainer.id.match(/custom-field-container-(\d+)-(\d+)/);
|
||
if (match && match[1] === String(index)) {
|
||
const fieldId = match[2];
|
||
const nameInput = document.getElementById(`custom-field-name-${index}-${fieldId}`);
|
||
const valueInput = document.getElementById(`custom-field-value-${index}-${fieldId}`);
|
||
|
||
if (nameInput && valueInput && nameInput.value.trim()) {
|
||
customFields[nameInput.value.trim()] = valueInput.value.trim();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
return customFields;
|
||
}
|
||
|
||
// Template Management Functions
|
||
async function loadTemplateList() {
|
||
try {
|
||
const response = await fetch(BASE_PATH + '/templates/list');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
const templateSelect = document.getElementById('templateSelect');
|
||
templateSelect.innerHTML = '<option value="">Select a template...</option>';
|
||
|
||
data.templates.forEach(template => {
|
||
const option = document.createElement('option');
|
||
option.value = template.name;
|
||
option.textContent = template.name;
|
||
if (template.description) {
|
||
option.title = template.description;
|
||
}
|
||
templateSelect.appendChild(option);
|
||
});
|
||
|
||
// Enable/disable apply button
|
||
document.getElementById('applyTemplateBtn').disabled = !data.templates.length;
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load templates:', error);
|
||
}
|
||
}
|
||
|
||
// Load templates on page load
|
||
loadTemplateList();
|
||
|
||
function showCreateTemplateModal() {
|
||
document.getElementById('createTemplateModal').style.display = 'block';
|
||
}
|
||
|
||
function closeCreateTemplateModal() {
|
||
document.getElementById('createTemplateModal').style.display = 'none';
|
||
// Clear form
|
||
document.getElementById('templateName').value = '';
|
||
document.getElementById('templateDescription').value = '';
|
||
document.getElementById('templateTitle').value = '';
|
||
document.getElementById('templateSubject').value = '';
|
||
document.getElementById('templateKeywords').value = '';
|
||
}
|
||
|
||
async function saveNewTemplate() {
|
||
const name = document.getElementById('templateName').value.trim();
|
||
const description = document.getElementById('templateDescription').value.trim();
|
||
const title = document.getElementById('templateTitle').value.trim();
|
||
const subject = document.getElementById('templateSubject').value.trim();
|
||
const keywords = document.getElementById('templateKeywords').value.trim();
|
||
|
||
if (!name || !title || !subject || !keywords) {
|
||
showError('Please fill in all required fields (Name, Title, Subject, Keywords)');
|
||
return;
|
||
}
|
||
|
||
hideAlerts();
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + '/templates/save', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, description, title, subject, keywords })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showSuccess(`Template "${name}" created successfully!`);
|
||
closeCreateTemplateModal();
|
||
loadTemplateList();
|
||
} else {
|
||
showError(data.error || 'Failed to save template');
|
||
}
|
||
} catch (error) {
|
||
showError(`Failed to save template: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async function applyTemplate() {
|
||
const templateName = document.getElementById('templateSelect').value;
|
||
|
||
if (!templateName) {
|
||
showError('Please select a template');
|
||
return;
|
||
}
|
||
|
||
if (selectedFiles.size === 0) {
|
||
showError('Please select at least one file to apply template');
|
||
return;
|
||
}
|
||
|
||
if (!sessionId) {
|
||
showError('No active session. Please upload files first.');
|
||
return;
|
||
}
|
||
|
||
hideAlerts();
|
||
showInfo(`Applying template "${templateName}" to ${selectedFiles.size} file(s)...`);
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + '/templates/apply', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
template_name: templateName,
|
||
file_indices: Array.from(selectedFiles),
|
||
session_id: sessionId,
|
||
custom_vars: {}
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
// Update UI with new metadata
|
||
data.results.forEach(result => {
|
||
const index = result.file_index;
|
||
document.getElementById(`title-${index}`).value = result.metadata.title;
|
||
document.getElementById(`subject-${index}`).value = result.metadata.subject;
|
||
document.getElementById(`keywords-${index}`).value = result.metadata.keywords;
|
||
|
||
// Update character counters
|
||
initCharCounters(index);
|
||
|
||
// Update currentFiles
|
||
currentFiles[index].suggested_metadata = result.metadata;
|
||
});
|
||
|
||
showSuccess(`✅ Template applied to ${data.results.length} file(s)`);
|
||
} else {
|
||
showError(data.error || 'Failed to apply template');
|
||
}
|
||
} catch (error) {
|
||
showError(`Failed to apply template: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async function manageTemplates() {
|
||
try {
|
||
const response = await fetch(BASE_PATH + '/templates/list');
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
showError('Failed to load templates');
|
||
return;
|
||
}
|
||
|
||
if (data.templates.length === 0) {
|
||
showInfo('No templates available. Create a new template to get started!');
|
||
return;
|
||
}
|
||
|
||
let message = 'Available Templates:\n\n';
|
||
data.templates.forEach((template, index) => {
|
||
message += `${index + 1}. ${template.name}\n`;
|
||
if (template.description) {
|
||
message += ` ${template.description}\n`;
|
||
}
|
||
message += ` Created: ${new Date(template.created_at).toLocaleDateString()}\n`;
|
||
message += ` Variables: ${template.variables_used.join(', ') || 'None'}\n\n`;
|
||
});
|
||
|
||
const templateName = prompt(message + '\nEnter template name to delete (or Cancel):');
|
||
|
||
if (templateName) {
|
||
const confirmDelete = confirm(`Are you sure you want to delete template "${templateName}"?`);
|
||
if (confirmDelete) {
|
||
await deleteTemplate(templateName);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
showError(`Failed to manage templates: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async function deleteTemplate(name) {
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}/templates/delete/${encodeURIComponent(name)}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showSuccess(`Template "${name}" deleted successfully`);
|
||
loadTemplateList();
|
||
} else {
|
||
showError(data.error || 'Failed to delete template');
|
||
}
|
||
} catch (error) {
|
||
showError(`Failed to delete template: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Preview template when selected
|
||
document.getElementById('templateSelect').addEventListener('change', async function() {
|
||
const templateName = this.value;
|
||
const previewDiv = document.getElementById('templatePreview');
|
||
|
||
if (!templateName) {
|
||
previewDiv.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}/templates/load/${encodeURIComponent(templateName)}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
const template = data.template;
|
||
previewDiv.innerHTML = `
|
||
<div class="template-preview-item">
|
||
<span class="template-preview-label">Title:</span> ${template.title}
|
||
</div>
|
||
<div class="template-preview-item">
|
||
<span class="template-preview-label">Subject:</span> ${template.subject}
|
||
</div>
|
||
<div class="template-preview-item">
|
||
<span class="template-preview-label">Keywords:</span> ${template.keywords}
|
||
</div>
|
||
`;
|
||
previewDiv.style.display = 'block';
|
||
} else {
|
||
previewDiv.style.display = 'none';
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to preview template:', error);
|
||
previewDiv.style.display = 'none';
|
||
}
|
||
});
|