VesperAI commited on
Commit
7ee60a0
·
1 Parent(s): ec4ffd4

addede a Production Branch

Browse files
Files changed (4) hide show
  1. Dockerfile +1 -1
  2. app.py +50 -0
  3. static/js/script.js +427 -8
  4. templates/index.html +19 -33
Dockerfile CHANGED
@@ -9,7 +9,7 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
9
  COPY . /code
10
 
11
  # Create necessary directories and set permissions for the entire app directory
12
- RUN mkdir -p /code/config /code/qdrant_data && \
13
  chown -R 1000:1000 /code
14
 
15
  ENV HF_HOME /code/.cache
 
9
  COPY . /code
10
 
11
  # Create necessary directories and set permissions for the entire app directory
12
+ RUN mkdir -p /code/config /code/qdrant_data /code/uploads && \
13
  chown -R 1000:1000 /code
14
 
15
  ENV HF_HOME /code/.cache
app.py CHANGED
@@ -1,4 +1,5 @@
1
  import os
 
2
  from pathlib import Path
3
  from typing import List, Optional
4
  import io
@@ -50,6 +51,55 @@ async def home(request: Request):
50
  }
51
  )
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  @app.post("/folders")
54
  async def add_folder(folder_path: str, background_tasks: BackgroundTasks):
55
  """Add a new folder to index"""
 
1
  import os
2
+ import uuid
3
  from pathlib import Path
4
  from typing import List, Optional
5
  import io
 
51
  }
52
  )
53
 
54
+ @app.post("/upload")
55
+ async def upload_images(
56
+ files: List[UploadFile] = File(...),
57
+ background_tasks: BackgroundTasks = None
58
+ ):
59
+ """Upload multiple images and index them"""
60
+ try:
61
+ # Create uploads directory if it doesn't exist
62
+ upload_dir = Path("uploads")
63
+ upload_dir.mkdir(exist_ok=True)
64
+
65
+ # Save uploaded files
66
+ saved_files = []
67
+ for file in files:
68
+ if file.content_type and file.content_type.startswith('image/'):
69
+ # Generate unique filename
70
+ file_extension = Path(file.filename).suffix
71
+ unique_filename = f"{uuid.uuid4()}{file_extension}"
72
+ file_path = upload_dir / unique_filename
73
+
74
+ # Save the file
75
+ contents = await file.read()
76
+ with open(file_path, "wb") as f:
77
+ f.write(contents)
78
+
79
+ saved_files.append(str(file_path))
80
+ else:
81
+ raise HTTPException(status_code=400, detail=f"File {file.filename} is not a valid image")
82
+
83
+ if saved_files:
84
+ # Add the upload folder to be indexed
85
+ folder_info = indexer.folder_manager.add_folder(str(upload_dir))
86
+
87
+ # Start indexing in the background
88
+ if background_tasks:
89
+ background_tasks.add_task(indexer.index_folder, str(upload_dir))
90
+
91
+ return {
92
+ "status": "success",
93
+ "message": f"Uploaded and indexing {len(saved_files)} images",
94
+ "folder_info": folder_info,
95
+ "uploaded_files": saved_files
96
+ }
97
+ else:
98
+ raise HTTPException(status_code=400, detail="No valid images were uploaded")
99
+
100
+ except Exception as e:
101
+ raise HTTPException(status_code=500, detail=str(e)) from e
102
+
103
  @app.post("/folders")
104
  async def add_folder(folder_path: str, background_tasks: BackgroundTasks):
105
  """Add a new folder to index"""
static/js/script.js CHANGED
@@ -1,13 +1,11 @@
1
  console.log('script.js loaded');
2
 
3
- let currentPath = null;
4
- let folderModal = null;
5
- let selectedFolder = null;
6
  let ws = null;
7
 
8
  // Initialize WebSocket connection
9
  function connectWebSocket() {
10
- ws = new WebSocket(`ws://${window.location.host}/ws`);
 
11
 
12
  ws.onopen = function () {
13
  console.log('WebSocket connected');
@@ -86,14 +84,14 @@ function observeLazyLoadImages() {
86
 
87
  if (fullSrc) {
88
  img.src = fullSrc;
89
- img.removeAttribute('data-src'); // Remove data-src to prevent re-processing
90
- img.classList.remove('lazy-load'); // Remove class to prevent re-observing
91
  }
92
- observer.unobserve(img); // Stop observing the image once loaded
93
  }
94
  });
95
  }, {
96
- rootMargin: '0px 0px 200px 0px' // Load images 200px before they enter viewport
97
  });
98
 
99
  lazyLoadImages.forEach(img => {
@@ -101,6 +99,427 @@ function observeLazyLoadImages() {
101
  });
102
  }
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  // Initialize folder browser
105
  async function initFolderBrowser() {
106
  folderModal = new bootstrap.Modal(document.getElementById('folderBrowserModal'));
 
1
  console.log('script.js loaded');
2
 
 
 
 
3
  let ws = null;
4
 
5
  // Initialize WebSocket connection
6
  function connectWebSocket() {
7
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
8
+ ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
9
 
10
  ws.onopen = function () {
11
  console.log('WebSocket connected');
 
84
 
85
  if (fullSrc) {
86
  img.src = fullSrc;
87
+ img.removeAttribute('data-src');
88
+ img.classList.remove('lazy-load');
89
  }
90
+ observer.unobserve(img);
91
  }
92
  });
93
  }, {
94
+ rootMargin: '0px 0px 200px 0px'
95
  });
96
 
97
  lazyLoadImages.forEach(img => {
 
99
  });
100
  }
101
 
102
+ // Open file upload dialog
103
+ function openFileUpload() {
104
+ document.getElementById('multipleImageUpload').click();
105
+ }
106
+
107
+ // Upload images
108
+ async function uploadImages(event) {
109
+ const files = event.target.files;
110
+ if (!files || files.length === 0) return;
111
+
112
+ // Validate file types
113
+ const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
114
+ const validFiles = Array.from(files).filter(file =>
115
+ allowedTypes.includes(file.type.toLowerCase())
116
+ );
117
+
118
+ if (validFiles.length === 0) {
119
+ alert('Please select valid image files (JPEG, PNG, GIF, WebP)');
120
+ return;
121
+ }
122
+
123
+ if (validFiles.length !== files.length) {
124
+ const skipped = files.length - validFiles.length;
125
+ alert(`${skipped} file(s) were skipped as they are not valid image files`);
126
+ }
127
+
128
+ try {
129
+ // Show upload progress
130
+ showUploadProgress(validFiles.length);
131
+
132
+ const formData = new FormData();
133
+ validFiles.forEach(file => {
134
+ formData.append('files', file);
135
+ });
136
+
137
+ const response = await fetch('/upload', {
138
+ method: 'POST',
139
+ body: formData
140
+ });
141
+
142
+ if (response.ok) {
143
+ const result = await response.json();
144
+ hideUploadProgress();
145
+
146
+ // Show success message
147
+ showNotification('success', `Successfully uploaded ${validFiles.length} image(s)!`);
148
+
149
+ // Reload folders and images
150
+ await loadIndexedFolders();
151
+ } else {
152
+ const error = await response.json();
153
+ hideUploadProgress();
154
+ showNotification('error', `Upload failed: ${error.detail || 'Unknown error'}`);
155
+ }
156
+ } catch (error) {
157
+ console.error('Upload error:', error);
158
+ hideUploadProgress();
159
+ showNotification('error', 'Upload failed. Please try again.');
160
+ }
161
+
162
+ // Reset file input
163
+ event.target.value = '';
164
+ }
165
+
166
+ // Show upload progress
167
+ function showUploadProgress(fileCount) {
168
+ const progressDiv = document.getElementById('uploadProgress');
169
+ const statusText = document.getElementById('uploadStatus');
170
+
171
+ progressDiv.style.display = 'block';
172
+ statusText.textContent = `Uploading ${fileCount} image(s)...`;
173
+
174
+ // Simulate progress
175
+ const progressBar = document.getElementById('uploadProgressBar');
176
+ let progress = 0;
177
+
178
+ const interval = setInterval(() => {
179
+ progress += 5;
180
+ progressBar.style.width = `${progress}%`;
181
+
182
+ if (progress >= 90) {
183
+ statusText.textContent = 'Processing and indexing images...';
184
+ clearInterval(interval);
185
+ }
186
+ }, 100);
187
+
188
+ progressDiv.dataset.intervalId = interval;
189
+ }
190
+
191
+ // Hide upload progress
192
+ function hideUploadProgress() {
193
+ const progressDiv = document.getElementById('uploadProgress');
194
+ const progressBar = document.getElementById('uploadProgressBar');
195
+
196
+ if (progressDiv.dataset.intervalId) {
197
+ clearInterval(progressDiv.dataset.intervalId);
198
+ delete progressDiv.dataset.intervalId;
199
+ }
200
+
201
+ progressBar.style.width = '100%';
202
+
203
+ setTimeout(() => {
204
+ progressDiv.style.display = 'none';
205
+ progressBar.style.width = '0%';
206
+ }, 1000);
207
+ }
208
+
209
+ // Show notification
210
+ function showNotification(type, message) {
211
+ const notification = document.createElement('div');
212
+ notification.className = `alert alert-${type === 'success' ? 'success' : 'danger'} alert-dismissible fade show position-fixed`;
213
+ notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 400px;';
214
+
215
+ notification.innerHTML = `
216
+ ${message}
217
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
218
+ `;
219
+
220
+ document.body.appendChild(notification);
221
+
222
+ setTimeout(() => {
223
+ if (notification.parentNode) {
224
+ notification.remove();
225
+ }
226
+ }, 5000);
227
+ }
228
+
229
+ // Load indexed folders
230
+ async function loadIndexedFolders() {
231
+ try {
232
+ const response = await fetch('/folders');
233
+ const folders = await response.json();
234
+
235
+ const folderList = document.getElementById('folderList');
236
+ folderList.innerHTML = '';
237
+
238
+ if (folders.length === 0) {
239
+ folderList.innerHTML = `
240
+ <div class="text-center p-4 text-muted">
241
+ <i class="bi bi-folder-x fs-2 d-block mb-2"></i>
242
+ <small>No folders indexed yet</small>
243
+ </div>
244
+ `;
245
+ return;
246
+ }
247
+
248
+ folders.forEach(folder => {
249
+ const escapedPath = folder.path.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
250
+ const folderCard = document.createElement('div');
251
+ folderCard.className = `folder-item-card ${!folder.is_valid ? 'invalid' : ''}`;
252
+ folderCard.innerHTML = `
253
+ <div class="d-flex justify-content-between align-items-start p-3">
254
+ <div class="flex-grow-1 me-2">
255
+ <div class="d-flex align-items-center mb-1">
256
+ <i class="bi bi-folder-fill me-2 ${folder.is_valid ? 'text-primary' : 'text-danger'}"></i>
257
+ <span class="fw-semibold ${!folder.is_valid ? 'text-danger' : 'text-dark'}" style="font-size: 0.9rem;">
258
+ ${folder.path.split(/[\\/]/).pop()}
259
+ </span>
260
+ </div>
261
+ <div class="text-muted small" style="word-break: break-all; line-height: 1.3;">
262
+ ${folder.path}
263
+ </div>
264
+ ${!folder.is_valid ? '<small class="text-danger"><i class="bi bi-exclamation-triangle me-1"></i>Path not accessible</small>' : ''}
265
+ </div>
266
+ <button class="btn btn-outline-danger btn-sm" onclick="removeFolder('${escapedPath}')" title="Remove folder">
267
+ <i class="bi bi-trash"></i>
268
+ </button>
269
+ </div>
270
+ `;
271
+ folderList.appendChild(folderCard);
272
+ });
273
+
274
+ // Load images from all folders
275
+ await loadImages();
276
+ } catch (error) {
277
+ console.error('Error loading folders:', error);
278
+ }
279
+ }
280
+
281
+ // Remove folder
282
+ async function removeFolder(path) {
283
+ if (confirm('Are you sure you want to remove this folder?')) {
284
+ try {
285
+ const encodedPath = encodeURIComponent(path).replace(/%5C/g, '\\');
286
+ const response = await fetch(`/folders/${encodedPath}`, {
287
+ method: 'DELETE'
288
+ });
289
+
290
+ if (response.ok) {
291
+ await loadIndexedFolders();
292
+ } else {
293
+ const error = await response.text();
294
+ alert(`Error removing folder: ${error}`);
295
+ }
296
+ } catch (error) {
297
+ console.error('Error removing folder:', error);
298
+ alert('Error removing folder. Please try again.');
299
+ }
300
+ }
301
+ }
302
+
303
+ // Load images
304
+ async function loadImages(folder = null) {
305
+ try {
306
+ const url = folder ? `/images?folder=${encodeURIComponent(folder)}` : '/images';
307
+ const response = await fetch(url);
308
+ const images = await response.json();
309
+
310
+ const imageGrid = document.getElementById('imageGrid');
311
+ imageGrid.innerHTML = '';
312
+
313
+ if (images.length === 0) {
314
+ imageGrid.innerHTML = `
315
+ <div class="col-12">
316
+ <div class="text-center p-5">
317
+ <i class="bi bi-images fs-1 text-muted d-block mb-3"></i>
318
+ <h5 class="text-muted mb-2">No images found</h5>
319
+ <p class="text-muted">Upload some images to start building your visual search database</p>
320
+ </div>
321
+ </div>
322
+ `;
323
+ return;
324
+ }
325
+
326
+ images.forEach(image => {
327
+ const card = document.createElement('div');
328
+ card.className = 'image-card';
329
+ card.innerHTML = `
330
+ <div class="image-wrapper">
331
+ <img class="lazy-load"
332
+ src="/thumbnail/${image.id}"
333
+ data-src="/image/${image.id}"
334
+ alt="${image.filename || image.path}"
335
+ loading="lazy">
336
+ </div>
337
+ <div class="image-info">
338
+ <span class="filename" title="${image.filename || image.path}">${image.filename || image.path}</span>
339
+ <span class="file-size">${formatFileSize(image.file_size)}</span>
340
+ </div>
341
+ `;
342
+ imageGrid.appendChild(card);
343
+ });
344
+ observeLazyLoadImages();
345
+ } catch (error) {
346
+ console.error('Error loading images:', error);
347
+ const imageGrid = document.getElementById('imageGrid');
348
+ imageGrid.innerHTML = '<div class="col-12"><div class="error text-center p-4">Error loading images. Please try again.</div></div>';
349
+ }
350
+ }
351
+
352
+ // Utility function to format file sizes
353
+ function formatFileSize(bytes) {
354
+ if (bytes === 0) return '0 Bytes';
355
+ const k = 1024;
356
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
357
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
358
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
359
+ }
360
+
361
+ // Search images
362
+ async function searchImages(event) {
363
+ event.preventDefault();
364
+ const query = document.getElementById('searchInput').value;
365
+ if (!query) return;
366
+
367
+ try {
368
+ const searchUrl = `/search/text?query=${encodeURIComponent(query)}`;
369
+ const response = await fetch(searchUrl);
370
+ const results = await response.json();
371
+
372
+ displaySearchResults(results);
373
+ } catch (error) {
374
+ console.error('Error searching images:', error);
375
+ const imageGrid = document.getElementById('imageGrid');
376
+ imageGrid.innerHTML = `
377
+ <div class="col-12">
378
+ <div class="error text-center p-5">
379
+ <i class="bi bi-exclamation-triangle fs-1 text-danger d-block mb-3"></i>
380
+ <h5 class="text-danger mb-2">Search Error</h5>
381
+ <p class="text-muted">An error occurred while searching. Please try again.</p>
382
+ </div>
383
+ </div>
384
+ `;
385
+ }
386
+ }
387
+
388
+ // Search by image
389
+ async function searchByImage(event) {
390
+ const file = event.target.files[0];
391
+ if (!file) return;
392
+
393
+ const formData = new FormData();
394
+ formData.append('file', file);
395
+
396
+ try {
397
+ const searchUrl = '/search/image';
398
+ const response = await fetch(searchUrl, {
399
+ method: 'POST',
400
+ body: formData
401
+ });
402
+ const results = await response.json();
403
+
404
+ displaySearchResults(results);
405
+ event.target.value = '';
406
+ } catch (error) {
407
+ console.error('Error searching by image:', error);
408
+ const imageGrid = document.getElementById('imageGrid');
409
+ imageGrid.innerHTML = `
410
+ <div class="col-12">
411
+ <div class="error text-center p-5">
412
+ <i class="bi bi-exclamation-triangle fs-1 text-danger d-block mb-3"></i>
413
+ <h5 class="text-danger mb-2">Image Search Error</h5>
414
+ <p class="text-muted">An error occurred while processing your image. Please try again.</p>
415
+ </div>
416
+ </div>
417
+ `;
418
+ }
419
+ }
420
+
421
+ // Search by URL
422
+ async function searchByUrl(event) {
423
+ event.preventDefault();
424
+ const url = document.getElementById('urlInput').value;
425
+ if (!url) return;
426
+
427
+ try {
428
+ const imageGrid = document.getElementById('imageGrid');
429
+ imageGrid.innerHTML = `
430
+ <div class="col-12">
431
+ <div class="loading text-center p-5">
432
+ <div class="spinner-border text-primary mb-3" role="status">
433
+ <span class="visually-hidden">Loading...</span>
434
+ </div>
435
+ <h5 class="text-primary mb-2">Downloading and analyzing image...</h5>
436
+ <p class="text-muted">This may take a few moments</p>
437
+ </div>
438
+ </div>
439
+ `;
440
+
441
+ const searchUrl = `/search/url?url=${encodeURIComponent(url)}`;
442
+ const response = await fetch(searchUrl);
443
+ const results = await response.json();
444
+
445
+ displaySearchResults(results);
446
+ document.getElementById('urlInput').value = '';
447
+ toggleUrlSearch();
448
+ } catch (error) {
449
+ console.error('Error searching by URL:', error);
450
+ const imageGrid = document.getElementById('imageGrid');
451
+ imageGrid.innerHTML = `
452
+ <div class="col-12">
453
+ <div class="error text-center p-5">
454
+ <i class="bi bi-exclamation-triangle fs-1 text-danger d-block mb-3"></i>
455
+ <h5 class="text-danger mb-2">Error processing URL</h5>
456
+ <p class="text-muted">Please check the URL and try again. Make sure it points to a valid image.</p>
457
+ </div>
458
+ </div>
459
+ `;
460
+ }
461
+ }
462
+
463
+ // Display search results
464
+ function displaySearchResults(results) {
465
+ const imageGrid = document.getElementById('imageGrid');
466
+ imageGrid.innerHTML = '';
467
+
468
+ if (results.length === 0) {
469
+ imageGrid.innerHTML = `
470
+ <div class="col-12">
471
+ <div class="no-results text-center p-5">
472
+ <i class="bi bi-search fs-1 text-muted d-block mb-3"></i>
473
+ <h5 class="text-muted mb-2">No similar images found</h5>
474
+ <p class="text-muted">Try adjusting your search terms or uploading a different image</p>
475
+ </div>
476
+ </div>
477
+ `;
478
+ return;
479
+ }
480
+
481
+ results.forEach(result => {
482
+ const card = document.createElement('div');
483
+ card.className = 'image-card';
484
+ card.innerHTML = `
485
+ <div class="image-wrapper">
486
+ <img class="lazy-load"
487
+ src="/thumbnail/${result.id}"
488
+ data-src="/image/${result.id}"
489
+ alt="${result.filename || result.path}"
490
+ loading="lazy">
491
+ <div class="similarity-score">${result.similarity}%</div>
492
+ </div>
493
+ <div class="image-info">
494
+ <span class="filename" title="${result.filename || result.path}">${result.filename || result.path}</span>
495
+ <span class="file-size">${formatFileSize(result.file_size)}</span>
496
+ </div>
497
+ `;
498
+ imageGrid.appendChild(card);
499
+ });
500
+ observeLazyLoadImages();
501
+ }
502
+
503
+ // Toggle URL search form visibility
504
+ function toggleUrlSearch() {
505
+ const urlForm = document.getElementById('urlSearchForm');
506
+ const isVisible = urlForm.style.display !== 'none';
507
+
508
+ if (isVisible) {
509
+ urlForm.style.display = 'none';
510
+ document.getElementById('urlInput').value = '';
511
+ } else {
512
+ urlForm.style.display = 'flex';
513
+ document.getElementById('urlInput').focus();
514
+ }
515
+ }
516
+
517
+ // Initialize
518
+ document.addEventListener('DOMContentLoaded', () => {
519
+ connectWebSocket();
520
+ loadIndexedFolders();
521
+ });
522
+
523
  // Initialize folder browser
524
  async function initFolderBrowser() {
525
  folderModal = new bootstrap.Modal(document.getElementById('folderBrowserModal'));
templates/index.html CHANGED
@@ -472,10 +472,14 @@
472
  <div class="row g-4">
473
  <div class="col-lg-3">
474
  <div class="sidebar">
475
- <button class="add-folder-btn" onclick="openFolderBrowser()">
476
- <i class="bi bi-folder-plus me-2"></i>Add Folder
477
  </button>
478
 
 
 
 
 
479
  <div class="sidebar-title">
480
  <i class="bi bi-folder2-open"></i>
481
  Indexed Folders
@@ -483,6 +487,19 @@
483
  <div id="folderList">
484
  <!-- Folders will be listed here -->
485
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  </div>
487
  </div>
488
 
@@ -495,36 +512,5 @@
495
  </div>
496
  </div>
497
  </div>
498
-
499
- <!-- Folder Browser Modal -->
500
- <div class="modal fade" id="folderBrowserModal" tabindex="-1">
501
- <div class="modal-dialog modal-lg">
502
- <div class="modal-content">
503
- <div class="modal-header">
504
- <h5 class="modal-title">
505
- <i class="bi bi-folder2-open me-2 text-primary"></i>
506
- Choose Folder to Index
507
- </h5>
508
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
509
- </div>
510
- <div class="modal-body">
511
- <nav aria-label="breadcrumb">
512
- <ol class="breadcrumb" id="folderBreadcrumb">
513
- <li class="breadcrumb-item active">Root</li>
514
- </ol>
515
- </nav>
516
- <div class="folder-browser" id="folderBrowser">
517
- <!-- Folder contents will be displayed here -->
518
- </div>
519
- </div>
520
- <div class="modal-footer">
521
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
522
- <button type="button" class="btn btn-primary" onclick="addSelectedFolder()">
523
- <i class="bi bi-plus-circle me-1"></i>Add Folder
524
- </button>
525
- </div>
526
- </div>
527
- </div>
528
- </div>
529
  </body>
530
  </html>
 
472
  <div class="row g-4">
473
  <div class="col-lg-3">
474
  <div class="sidebar">
475
+ <button class="add-folder-btn" onclick="openFileUpload()">
476
+ <i class="bi bi-cloud-upload me-2"></i>Upload Images
477
  </button>
478
 
479
+ <!-- Hidden file input for multiple image uploads -->
480
+ <input type="file" id="multipleImageUpload" style="display: none"
481
+ accept="image/*" multiple onchange="uploadImages(event)">
482
+
483
  <div class="sidebar-title">
484
  <i class="bi bi-folder2-open"></i>
485
  Indexed Folders
 
487
  <div id="folderList">
488
  <!-- Folders will be listed here -->
489
  </div>
490
+
491
+ <!-- Upload Progress (initially hidden) -->
492
+ <div id="uploadProgress" style="display: none;" class="mt-3">
493
+ <div class="sidebar-title">
494
+ <i class="bi bi-upload"></i>
495
+ Upload Progress
496
+ </div>
497
+ <div class="progress mb-2">
498
+ <div class="progress-bar progress-bar-striped progress-bar-animated"
499
+ id="uploadProgressBar" style="width: 0%"></div>
500
+ </div>
501
+ <small class="text-muted" id="uploadStatus">Preparing upload...</small>
502
+ </div>
503
  </div>
504
  </div>
505
 
 
512
  </div>
513
  </div>
514
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  </body>
516
  </html>