This commit completes Phase 3 implementation with advanced batch processing
and metadata template system.
Changes:
- Added batch file selection with checkboxes
- Implemented select all/deselect all functionality
- Updated batch processing to handle only selected files
- Added CSV export for processing results
- Created template_manager.py with variable substitution system
- Added template endpoints (list, save, load, delete, apply, preview)
- Integrated template UI with modal dialog for creation
- Template variables: {filename}, {date}, {datetime}, {user}, {year}, {month}, {day}
Phase 3 Status: Complete
Next Phase: Phase 4 (Authentication + SSO) for v3.1 release
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1725 lines
60 KiB
HTML
1725 lines
60 KiB
HTML
<!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;
|
||
}
|
||
|
||
.batch-toolbar {
|
||
background: #f8f9ff;
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 15px;
|
||
}
|
||
|
||
.batch-toolbar-left {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.batch-toolbar-right {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.btn-toolbar {
|
||
background: #6c757d;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.btn-toolbar:hover {
|
||
transform: translateY(-2px);
|
||
background: #5a6268;
|
||
}
|
||
|
||
.btn-export {
|
||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||
}
|
||
|
||
.btn-export:hover {
|
||
background: linear-gradient(135deg, #218838 0%, #1fa589 100%);
|
||
}
|
||
|
||
.selection-count {
|
||
font-size: 13px;
|
||
color: #495057;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.file-item {
|
||
background: #f8f9fa;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
border-left: 4px solid #667eea;
|
||
}
|
||
|
||
.file-item.selected {
|
||
background: #e8f4f8;
|
||
border-left-color: #28a745;
|
||
}
|
||
|
||
.file-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.file-header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.file-checkbox {
|
||
width: 20px;
|
||
height: 20px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* Template Section */
|
||
.template-section {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
border: 2px dashed #dee2e6;
|
||
}
|
||
|
||
.template-section.active {
|
||
border-color: #667eea;
|
||
background: #f8f9ff;
|
||
}
|
||
|
||
.template-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.template-select {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
padding: 8px;
|
||
border: 2px solid #667eea;
|
||
border-radius: 5px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-template {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.btn-template:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-template:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.template-preview {
|
||
margin-top: 10px;
|
||
padding: 10px;
|
||
background: white;
|
||
border-radius: 5px;
|
||
font-size: 12px;
|
||
color: #495057;
|
||
display: none;
|
||
}
|
||
|
||
.template-preview-item {
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.template-preview-label {
|
||
font-weight: 600;
|
||
color: #667eea;
|
||
}
|
||
|
||
/* Modal Styles */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
margin: 5% auto;
|
||
padding: 30px;
|
||
border-radius: 15px;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
color: #667eea;
|
||
margin: 0;
|
||
}
|
||
|
||
.close-modal {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
color: #aaa;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.close-modal:hover {
|
||
color: #000;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
margin-bottom: 5px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 2px solid #dee2e6;
|
||
border-radius: 5px;
|
||
font-size: 13px;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.form-group textarea {
|
||
min-height: 60px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.form-group small {
|
||
font-size: 11px;
|
||
color: #6c757d;
|
||
margin-top: 3px;
|
||
display: block;
|
||
}
|
||
|
||
.variable-hint {
|
||
background: #f8f9ff;
|
||
padding: 8px;
|
||
border-radius: 5px;
|
||
font-size: 11px;
|
||
color: #667eea;
|
||
margin-top: 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="template-section" id="templateSection">
|
||
<h4 style="margin-bottom: 10px; color: #495057;">📋 Metadata Templates</h4>
|
||
<p style="font-size: 13px; color: #6c757d; margin-bottom: 10px;">
|
||
Use templates with variables like {filename}, {date}, {user} for quick metadata generation
|
||
</p>
|
||
<div class="template-controls">
|
||
<select id="templateSelect" class="template-select">
|
||
<option value="">Select a template...</option>
|
||
</select>
|
||
<button class="btn-template" onclick="applyTemplate()" id="applyTemplateBtn" disabled>
|
||
✓ Apply Template
|
||
</button>
|
||
<button class="btn-template" onclick="showCreateTemplateModal()">
|
||
➕ Create New
|
||
</button>
|
||
<button class="btn-template" onclick="manageTemplates()">
|
||
⚙️ Manage
|
||
</button>
|
||
</div>
|
||
<div id="templatePreview" class="template-preview"></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 class="batch-toolbar" id="batchToolbar" style="display: none;">
|
||
<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>
|
||
</div>
|
||
|
||
<div class="actions" id="actions" style="display: none;">
|
||
<button class="btn" id="updateAllBtn" onclick="updateAllFiles()">
|
||
Update Selected 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 • Templates
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Template Modal -->
|
||
<div id="createTemplateModal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>Create Metadata Template</h3>
|
||
<span class="close-modal" onclick="closeCreateTemplateModal()">×</span>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="templateName">Template Name *</label>
|
||
<input type="text" id="templateName" placeholder="e.g., Product Brochure Template" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="templateDescription">Description</label>
|
||
<input type="text" id="templateDescription" placeholder="Optional description of this template">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="templateTitle">Title Template *</label>
|
||
<input type="text" id="templateTitle" placeholder="e.g., {filename} - Product Guide">
|
||
<div class="variable-hint">
|
||
Available variables: {filename}, {date}, {datetime}, {user}, {year}, {month}, {day}
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="templateSubject">Subject Template *</label>
|
||
<textarea id="templateSubject" placeholder="e.g., Product information guide for {filename}"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="templateKeywords">Keywords Template *</label>
|
||
<input type="text" id="templateKeywords" placeholder="e.g., product, guide, {year}">
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn" onclick="saveNewTemplate()">💾 Save Template</button>
|
||
<button class="btn" style="background: #6c757d;" onclick="closeCreateTemplateModal()">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
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');
|
||
|
||
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) {
|
||
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>
|
||
<button class="btn-reset" onclick="resetMetadata(${index})">
|
||
🔄 Reset
|
||
</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`;
|
||
}
|
||
}
|
||
|
||
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 (selectedFiles.size === 0) {
|
||
showError('Please select at least one file to update');
|
||
return;
|
||
}
|
||
|
||
const outputDir = document.getElementById('outputDir').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('/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.`);
|
||
}
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
// Template Management Functions
|
||
async function loadTemplateList() {
|
||
try {
|
||
const response = await fetch('/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('/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('/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('/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(`/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(`/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';
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|