solventum-image-metadata/templates/index.html
SamoilenkoVadym 03079080d8 Phase 2.4: Metadata import from external files (CSV, Excel, JSON)
Created comprehensive metadata_importer.py module:
- CSV import with multiple encoding support (UTF-8, Latin1, ISO-8859-1, CP1252)
- Excel import (.xlsx, .xls) with sheet selection
- JSON import (object and array formats)
- Intelligent column detection for filename, title, subject, keywords
- Fuzzy column matching (case-insensitive, multiple aliases)
- Metadata normalization to standard format
- Import validation with statistics
- File lookup by filename stem (case-insensitive)

Web interface enhancements:
- /import-metadata endpoint for file uploads
- Import section UI (appears when Import source selected)
- Real-time import statistics display (records, title/subject/keywords counts)
- Import session management with unique session IDs
- Visual feedback (active state, success/error messages)
- Validation: requires import file before processing with import source

Import workflow:
1. User selects "Import from File" metadata source
2. Import section appears with file chooser
3. User uploads CSV/Excel/JSON with metadata
4. System validates and shows statistics
5. User uploads files to process
6. System matches files to imported metadata by filename

Supported import formats:
- CSV: filename, title, subject/description, keywords columns
- Excel: Any sheet with filename and metadata columns
- JSON: {filename: {metadata}} or [{filename, metadata}] formats

Technical features:
- Pandas DataFrame parsing for CSV/Excel
- Flexible column name detection (10+ aliases per field)
- NaN/null value handling
- List/array keyword support
- Unicode filename support

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-25 15:39:27 +00:00

1064 lines
36 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oliver Metadata Tool</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 { font-size: 28px; margin-bottom: 10px; }
.header p { opacity: 0.9; font-size: 14px; }
.content { padding: 40px; }
.upload-section {
background: #f8f9ff;
border-radius: 10px;
padding: 20px;
margin-bottom: 30px;
}
.upload-area {
border: 3px dashed #667eea;
border-radius: 10px;
padding: 60px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: white;
margin-bottom: 20px;
}
.upload-area:hover { background: #f0f2ff; border-color: #764ba2; }
.upload-area.dragover { background: #e8ebff; transform: scale(1.02); }
#fileInput { display: none; }
.upload-icon { font-size: 48px; margin-bottom: 15px; }
.output-dir-section {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: 8px;
}
.output-dir-section label {
font-weight: 600;
color: #495057;
min-width: 120px;
}
#outputDir {
flex: 1;
padding: 10px;
border: 2px solid #dee2e6;
border-radius: 5px;
font-size: 14px;
}
.output-dir-hint {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: transform 0.2s;
margin: 5px;
}
.btn:hover { transform: translateY(-2px); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-small {
padding: 8px 20px;
font-size: 14px;
}
.progress-bar {
width: 100%;
height: 30px;
background: #e9ecef;
border-radius: 15px;
overflow: hidden;
margin: 20px 0;
display: none;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 14px;
}
.file-list {
margin-top: 30px;
display: none;
}
.file-item {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid #667eea;
}
.file-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.file-name {
font-weight: 600;
font-size: 16px;
color: #495057;
}
.file-type {
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
}
.metadata-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.metadata-box {
background: white;
border-radius: 8px;
padding: 15px;
}
.metadata-box h4 {
color: #667eea;
margin-bottom: 10px;
font-size: 14px;
}
.metadata-item {
display: flex;
flex-direction: column;
padding: 8px 0;
border-bottom: 1px solid #dee2e6;
}
.metadata-item:last-child { border-bottom: none; }
.metadata-label { font-weight: 600; color: #495057; font-size: 12px; margin-bottom: 4px; }
.metadata-value { color: #6c757d; font-size: 13px; }
.alert {
padding: 15px;
border-radius: 8px;
margin: 15px 0;
display: none;
}
.alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.alert-info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
.actions {
text-align: center;
margin-top: 20px;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.footer {
text-align: center;
padding: 20px;
color: #6c757d;
font-size: 12px;
border-top: 1px solid #dee2e6;
}
/* Metadata Source Selector */
.metadata-source-selector {
background: white;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
}
.metadata-source-selector label {
font-weight: 600;
color: #495057;
min-width: 140px;
}
.source-select {
flex: 1;
padding: 10px;
border: 2px solid #667eea;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
background: white;
}
.source-info {
font-size: 12px;
color: #6c757d;
margin-left: 10px;
}
/* Editable Metadata Fields */
.editable-field {
width: 100%;
padding: 8px;
border: 2px solid #dee2e6;
border-radius: 5px;
font-size: 13px;
font-family: inherit;
transition: border-color 0.3s;
}
.editable-field:focus {
outline: none;
border-color: #667eea;
}
.editable-field.invalid {
border-color: #dc3545;
}
textarea.editable-field {
min-height: 60px;
resize: vertical;
}
.char-count {
font-size: 11px;
color: #6c757d;
margin-top: 4px;
display: block;
}
.char-count.warning {
color: #ffc107;
}
.char-count.danger {
color: #dc3545;
}
.metadata-field {
margin-bottom: 15px;
}
.metadata-field label {
display: block;
font-weight: 600;
color: #495057;
font-size: 12px;
margin-bottom: 5px;
}
/* File Action Buttons */
.file-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-save {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
border: none;
padding: 8px 20px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: transform 0.2s;
}
.btn-save:hover {
transform: translateY(-2px);
}
.btn-save:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-reset {
background: #6c757d;
color: white;
border: none;
padding: 8px 20px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: transform 0.2s;
}
.btn-reset:hover {
transform: translateY(-2px);
background: #5a6268;
}
/* Import Metadata Section */
.import-section {
background: white;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
border: 2px dashed #dee2e6;
}
.import-section.active {
border-color: #28a745;
background: #f0fff4;
}
.btn-import {
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
color: white;
border: none;
padding: 8px 20px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: transform 0.2s;
}
.btn-import:hover {
transform: translateY(-2px);
}
.import-stats {
font-size: 12px;
color: #28a745;
margin-top: 10px;
padding: 8px;
background: white;
border-radius: 5px;
}
@media (max-width: 768px) {
.metadata-comparison {
grid-template-columns: 1fr;
}
.metadata-source-selector {
flex-direction: column;
align-items: flex-start;
}
.metadata-source-selector label {
min-width: auto;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 Oliver Metadata Tool</h1>
<p>Universal metadata creation and management for all file types</p>
</div>
<div class="content">
<div class="upload-section">
<div class="metadata-source-selector">
<label for="metadataSource">Metadata Source:</label>
<select id="metadataSource" class="source-select" onchange="handleSourceChange()">
<option value="excel" selected>📊 Excel Lookup (Fastest)</option>
<option value="manual">✏️ Manual Entry</option>
<option value="import">📂 Import from File</option>
<option value="ai">🤖 AI Generation (Slower)</option>
</select>
<span class="source-info"> Choose how to generate metadata</span>
</div>
<div class="import-section" id="importSection" style="display: none;">
<h4 style="margin-bottom: 10px; color: #495057;">📂 Import Metadata File</h4>
<p style="font-size: 13px; color: #6c757d; margin-bottom: 10px;">
Upload a CSV, Excel, or JSON file with metadata (columns: filename, title, subject, keywords)
</p>
<input type="file" id="importFileInput" accept=".csv,.xlsx,.xls,.json" style="display: none;">
<button class="btn-import" onclick="document.getElementById('importFileInput').click()">
📤 Choose Import File
</button>
<div id="importStats" class="import-stats" style="display: none;"></div>
</div>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📁</div>
<h3>Drop files here or click to browse</h3>
<p style="color: #6c757d; margin-top: 10px;">Supported: PDF, JPG, PNG, DOCX, XLSX, PPTX, MP4, MOV</p>
<p style="color: #667eea; margin-top: 5px; font-weight: 600;">Multiple files supported!</p>
<input type="file" id="fileInput" accept=".pdf,.jpg,.jpeg,.png,.gif,.docx,.xlsx,.pptx,.mp4,.mov,.avi" multiple>
</div>
<div class="output-dir-section">
<label for="outputDir">Save to folder:</label>
<input type="text" id="outputDir" placeholder="Leave empty to save in original location" />
</div>
<div class="output-dir-hint">
💡 Tip: Paste the folder path where you want to save processed files (e.g., /Users/YourName/Desktop/ProcessedFiles)
</div>
</div>
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill">0%</div>
</div>
<div class="spinner" id="spinner"></div>
<div class="alert alert-error" id="errorAlert"></div>
<div class="alert alert-success" id="successAlert"></div>
<div class="alert alert-info" id="infoAlert"></div>
<div class="file-list" id="fileList"></div>
<div class="actions" id="actions" style="display: none;">
<button class="btn" id="updateAllBtn" onclick="updateAllFiles()">
Update All Files
</button>
<button class="btn" onclick="resetForm()">
Process More Files
</button>
</div>
</div>
<div class="footer">
Oliver Metadata Tool v3.0 | Multiple metadata sources | Excel • AI • Manual • Import
</div>
</div>
<script>
let currentFiles = [];
let sessionId = null;
let importSessionId = null;
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');
if (source === 'import') {
importSection.style.display = 'block';
} else {
importSection.style.display = 'none';
}
}
async function handleImportFile(e) {
const file = e.target.files[0];
if (!file) return;
hideAlerts();
showInfo(`Importing metadata from ${file.name}...`);
const formData = new FormData();
formData.append('import_file', file);
try {
const response = await fetch('/import-metadata', {
method: 'POST',
body: formData
});
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');
showSuccess(`${data.message}`);
} catch (error) {
showError(`Import failed: ${error.message}`);
}
}
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 import source
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.`);
} 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('/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
hideSpinner();
hideProgress();
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';
} catch (error) {
hideSpinner();
hideProgress();
showError('Error processing files: ' + error.message);
}
}
function displayFiles(files) {
fileList.innerHTML = '';
fileList.style.display = 'block';
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-name">📄 ${file.filename}</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>
<button class="btn-reset" onclick="resetMetadata(${index})">
🔄 Reset
</button>
</div>
`;
fileList.appendChild(fileItem);
// Initialize character counters
initCharCounters(index);
});
}
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 || '';
// Don't show internal metadata fields in the form
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}" />
<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}">${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}" />
<span class="char-count" id="keywords-count-${index}">0/500</span>
</div>
`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function initCharCounters(index) {
const fields = ['title', 'subject', 'keywords'];
const limits = { title: 200, subject: 300, keywords: 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();
try {
const response = await fetch('/update-manual', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
file_index: index,
title: title,
subject: subject,
keywords: keywords
})
});
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 };
// 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;
}
}
function resetMetadata(index) {
const file = currentFiles[index];
if (!file || file.error) return;
// Reset to original suggested metadata
const original = file.suggested_metadata;
document.getElementById(`title-${index}`).value = original?.title || '';
document.getElementById(`subject-${index}`).value = original?.subject || '';
document.getElementById(`keywords-${index}`).value = original?.keywords || '';
// Update character counters
initCharCounters(index);
// Remove any success messages
const fileItem = document.getElementById(`file-${index}`);
if (fileItem) {
const successMsg = fileItem.querySelector('.save-success');
if (successMsg) successMsg.remove();
fileItem.style.borderLeftColor = '#667eea';
}
}
async function updateAllFiles() {
if (currentFiles.length === 0) return;
const outputDir = document.getElementById('outputDir').value.trim();
const updateBtn = document.getElementById('updateAllBtn');
updateBtn.disabled = true;
hideAlerts();
showProgress(0);
showInfo(`Updating ${currentFiles.length} file(s)...`);
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < currentFiles.length; i++) {
const file = currentFiles[i];
if (file.error) {
errorCount++;
continue;
}
const progress = ((i + 1) / currentFiles.length) * 100;
showProgress(progress);
try {
const response = await fetch('/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filepath: file.filepath,
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';
const successDiv = document.createElement('div');
successDiv.className = 'alert alert-success';
successDiv.style.display = 'block';
successDiv.textContent = data.verified ?
`✅ Updated and verified! Saved to: ${data.output_path}` :
`✅ Updated! Saved to: ${data.output_path}`;
fileItem.appendChild(successDiv);
}
}
} catch (error) {
errorCount++;
console.error('Error updating file:', error);
}
}
hideProgress();
updateBtn.disabled = false;
if (successCount > 0 && errorCount === 0) {
showSuccess(`✅ All ${successCount} file(s) updated successfully!`);
} else if (successCount > 0 && errorCount > 0) {
showInfo(`⚠️ Updated ${successCount} file(s), ${errorCount} failed.`);
} else {
showError(`❌ Failed to update files. Check individual file errors above.`);
}
}
function resetForm() {
fileInput.value = '';
fileList.style.display = 'none';
actions.style.display = 'none';
hideAlerts();
hideProgress();
currentFiles = [];
sessionId = null;
}
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';
}
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';
}
</script>
</body>
</html>