diff --git a/templates/index.html b/templates/index.html index 569b55a..7a54645 100644 --- a/templates/index.html +++ b/templates/index.html @@ -227,10 +227,148 @@ 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; + } + @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; + } } @@ -243,6 +381,17 @@
+ +
📁

Drop files here or click to browse

@@ -341,9 +490,11 @@ actions.style.display = 'none'; currentFiles = []; - showInfo(`Processing ${files.length} file(s)...`); + const metadataSource = document.getElementById('metadataSource').value; + showInfo(`Processing ${files.length} file(s) with ${metadataSource} source...`); const formData = new FormData(); + formData.append('metadata_source', metadataSource); for (let file of files) { formData.append('files', file); } @@ -412,13 +563,25 @@
+ +
+ + +
`; fileList.appendChild(fileItem); + + // Initialize character counters + initCharCounters(index); }); } @@ -439,6 +602,186 @@ return html; } + function displayEditableMetadata(metadata, index) { + const title = metadata?.title || ''; + const subject = metadata?.subject || ''; + const keywords = metadata?.keywords || ''; + + return ` +
+ + + 0/200 +
+
+ + + 0/300 +
+
+ + + 0/500 +
+ `; + } + + 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; diff --git a/web_app.py b/web_app.py index dec759b..3086117 100644 --- a/web_app.py +++ b/web_app.py @@ -96,12 +96,15 @@ def upload_file(): if not files or files[0].filename == '': return jsonify({'error': 'No files selected'}), 400 + # Get metadata source choice (excel, manual, ai, import) + metadata_source = request.form.get('metadata_source', 'excel') + results = [] session_id = str(len(sessions) + 1) - sessions[session_id] = {'files': []} + sessions[session_id] = {'files': [], 'metadata_source': metadata_source} - # Get metadata lookup - lookup = get_metadata_lookup() + # Get metadata lookup (only if using Excel source) + lookup = get_metadata_lookup() if metadata_source == 'excel' else None for file in files: try: @@ -132,21 +135,52 @@ def upload_file(): # Read current metadata from file old_metadata = extractor.read_metadata(filepath) - # Lookup metadata from Excel by filename - excel_data = lookup.lookup_by_filename(filename) + # Generate metadata based on chosen source + excel_found = False + new_metadata = {'title': '', 'subject': '', 'keywords': ''} - if excel_data: - # Use Excel data for metadata + if metadata_source == 'excel' and lookup: + # Lookup metadata from Excel by filename + excel_data = lookup.lookup_by_filename(filename) + + if excel_data: + new_metadata = { + 'title': excel_data.get('title', ''), + 'subject': excel_data.get('description', ''), + 'keywords': '' + } + excel_found = True + else: + # No Excel data found - use filename as fallback + new_metadata = { + 'title': Path(filename).stem, + 'subject': f'No metadata found in Excel for {filename}', + 'keywords': '' + } + + elif metadata_source == 'manual': + # Return empty metadata for user to fill manually new_metadata = { - 'title': excel_data.get('title', ''), - 'subject': excel_data.get('description', ''), # External Description/Alt Text - 'keywords': '' # Not used from Excel + 'title': Path(filename).stem, # Suggest filename + 'subject': '', + 'keywords': '' } - else: - # No Excel data found - use filename as fallback + + elif metadata_source == 'ai': + # AI generation - will be implemented in Phase 2.3 + # For now, return placeholder new_metadata = { 'title': Path(filename).stem, - 'subject': f'No metadata found in Excel for {filename}', + 'subject': 'AI generation not yet implemented', + 'keywords': '' + } + + elif metadata_source == 'import': + # Import from file - will be implemented in Phase 2.4 + # For now, return placeholder + new_metadata = { + 'title': Path(filename).stem, + 'subject': 'Import feature not yet implemented', 'keywords': '' } @@ -157,7 +191,8 @@ def upload_file(): 'current_metadata': old_metadata, 'suggested_metadata': new_metadata, 'filepath': filepath, - 'excel_found': excel_data is not None + 'metadata_source': metadata_source, + 'excel_found': excel_found } results.append(file_info) @@ -240,6 +275,70 @@ def update_metadata(): except Exception as e: return jsonify({'error': str(e)}), 500 +@app.route('/update-manual', methods=['POST']) +def update_manual_metadata(): + """Update file with manually entered metadata.""" + data = request.json + session_id = data.get('session_id') + file_index = data.get('file_index') + + # Validate and sanitize metadata + custom_metadata = { + 'title': data.get('title', '').strip()[:200], + 'subject': data.get('subject', '').strip()[:300], + 'keywords': data.get('keywords', '').strip()[:500] + } + + # Validate session + if not session_id or session_id not in sessions: + return jsonify({'error': 'Invalid or expired session'}), 400 + + # Validate file index + if file_index is None or file_index >= len(sessions[session_id]['files']): + return jsonify({'error': 'Invalid file index'}), 400 + + try: + # Get file info from session + file_info = sessions[session_id]['files'][file_index] + filepath = file_info.get('filepath') + + if not filepath or not os.path.exists(filepath): + return jsonify({'error': 'File not found'}), 404 + + # Detect file type + file_type = FileDetector.detect_file_type(filepath) + + if file_type == FileType.UNSUPPORTED: + return jsonify({'error': 'Unsupported file type'}), 400 + + # Get updater for this file type + updater = updaters.get(file_type) + + if not updater: + return jsonify({'error': 'No updater available for this file type'}), 400 + + # Update metadata + success = updater.update_metadata(filepath, custom_metadata, backup=True) + + if not success: + return jsonify({'error': 'Failed to update metadata'}), 500 + + # Update session with new metadata + sessions[session_id]['files'][file_index]['suggested_metadata'] = custom_metadata + + # Verify update + verified = updater.verify_metadata(filepath, custom_metadata) + + return jsonify({ + 'status': 'success', + 'message': 'Metadata updated successfully', + 'verified': verified, + 'metadata': custom_metadata + }) + + except Exception as e: + return jsonify({'error': f'Error updating metadata: {str(e)}'}), 500 + @app.route('/download/') def download_file(filename): """Download processed file."""