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 @@
+
+
+
+ âšī¸ Choose how to generate metadata
+
+
đ
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 @@
-
⨠AI-Generated Metadata
- ${displayMetadata(file.suggested_metadata)}
+ âī¸ Edit Metadata
+ ${displayEditableMetadata(file.suggested_metadata, index)}
+
+
+
+
+
`;
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."""