Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Visual Product Search</title> | |
<link rel="icon" href="/static/image.png" type="image/png"> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css"> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script src="/static/js/script.js"></script> | |
<style> | |
:root { | |
--primary-color: #6366f1; | |
--primary-dark: #4f46e5; | |
--primary-light: #8b5cf6; | |
--secondary-color: #f8fafc; | |
--accent-color: #06b6d4; | |
--text-primary: #1e293b; | |
--text-secondary: #64748b; | |
--border-color: #e2e8f0; | |
--success-color: #10b981; | |
--warning-color: #f59e0b; | |
--danger-color: #ef4444; | |
--gradient-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
--card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
--card-shadow-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
} | |
* { | |
font-family: 'Inter', sans-serif; | |
} | |
body { | |
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); | |
min-height: 100vh; | |
} | |
.navbar { | |
background: var(--gradient-bg) ; | |
backdrop-filter: blur(10px); | |
border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
padding: 1rem 0; | |
} | |
.navbar-brand { | |
font-weight: 700; | |
font-size: 1.5rem; | |
color: white ; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.brand-icon { | |
width: 32px; | |
height: 32px; | |
background: rgba(255, 255, 255, 0.2); | |
border-radius: 8px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.search-container { | |
max-width: 600px; | |
margin: 0 auto; | |
} | |
.search-form { | |
background: rgba(255, 255, 255, 0.95); | |
backdrop-filter: blur(10px); | |
border-radius: 20px; | |
padding: 8px; | |
box-shadow: var(--card-shadow); | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
} | |
.search-input { | |
border: none; | |
background: transparent; | |
padding: 12px 20px; | |
font-weight: 500; | |
} | |
.search-input:focus { | |
outline: none; | |
box-shadow: none; | |
} | |
.search-btn { | |
border-radius: 16px; | |
padding: 12px 24px; | |
font-weight: 600; | |
background: var(--primary-color); | |
border: none; | |
transition: all 0.3s ease; | |
} | |
.search-btn:hover { | |
background: var(--primary-dark); | |
transform: translateY(-1px); | |
} | |
.action-btn { | |
border-radius: 16px; | |
padding: 12px 16px; | |
border: 1px solid rgba(255, 255, 255, 0.5); | |
background: rgba(255, 255, 255, 0.9); | |
color: var(--primary-color); | |
font-weight: 600; | |
transition: all 0.3s ease; | |
min-width: 48px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.action-btn:hover { | |
background: rgba(255, 255, 255, 1); | |
color: var(--primary-dark); | |
transform: translateY(-1px); | |
border-color: rgba(255, 255, 255, 0.7); | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
.action-btn i { | |
font-size: 1.1rem; | |
} | |
.url-search-form { | |
background: rgba(255, 255, 255, 0.95); | |
backdrop-filter: blur(10px); | |
border-radius: 20px; | |
padding: 8px; | |
box-shadow: var(--card-shadow); | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
} | |
.main-container { | |
padding-top: 2rem; | |
} | |
.sidebar { | |
background: white; | |
border-radius: 20px; | |
padding: 1.5rem; | |
box-shadow: var(--card-shadow); | |
border: 1px solid var(--border-color); | |
height: fit-content; | |
position: sticky; | |
top: 2rem; | |
} | |
.sidebar-title { | |
font-weight: 600; | |
color: var(--text-primary); | |
margin-bottom: 1rem; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.folder-list { | |
border: none; | |
} | |
.folder-item-card { | |
border: 1px solid var(--border-color); | |
border-radius: 12px; | |
margin-bottom: 8px; | |
transition: all 0.3s ease; | |
background: white; | |
} | |
.folder-item-card:hover { | |
transform: translateY(-2px); | |
box-shadow: var(--card-shadow-hover); | |
border-color: var(--primary-color); | |
} | |
.folder-item-card.invalid { | |
border-color: var(--danger-color); | |
background: #fef2f2; | |
} | |
.add-folder-btn { | |
background: var(--primary-color); | |
color: white; | |
border: none; | |
border-radius: 12px; | |
padding: 12px 20px; | |
font-weight: 600; | |
width: 100%; | |
margin-bottom: 1rem; | |
transition: all 0.3s ease; | |
} | |
.add-folder-btn:hover { | |
background: var(--primary-dark); | |
transform: translateY(-1px); | |
color: white; | |
} | |
.content-area { | |
background: white; | |
border-radius: 20px; | |
padding: 2rem; | |
box-shadow: var(--card-shadow); | |
border: 1px solid var(--border-color); | |
min-height: 70vh; | |
} | |
.image-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
gap: 1.5rem; | |
padding: 1rem 0; | |
} | |
.image-card { | |
position: relative; | |
background: white; | |
border-radius: 16px; | |
overflow: hidden; | |
box-shadow: var(--card-shadow); | |
border: 1px solid var(--border-color); | |
transition: all 0.3s ease; | |
} | |
.image-card:hover { | |
transform: translateY(-4px); | |
box-shadow: var(--card-shadow-hover); | |
} | |
.image-wrapper { | |
aspect-ratio: 1; | |
overflow: hidden; | |
position: relative; | |
} | |
.image-card img { | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
transition: transform 0.3s ease; | |
} | |
.image-card:hover img { | |
transform: scale(1.05); | |
} | |
.similarity-score { | |
position: absolute; | |
top: 12px; | |
right: 12px; | |
background: var(--primary-color); | |
color: white; | |
padding: 6px 12px; | |
border-radius: 20px; | |
font-size: 0.875rem; | |
font-weight: 600; | |
backdrop-filter: blur(10px); | |
} | |
.image-info { | |
padding: 1rem; | |
background: white; | |
} | |
.filename { | |
display: block; | |
font-weight: 600; | |
color: var(--text-primary); | |
margin-bottom: 0.5rem; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
} | |
.file-size { | |
font-size: 0.875rem; | |
color: var(--text-secondary); | |
font-weight: 500; | |
} | |
.status-card { | |
background: white; | |
border-radius: 16px; | |
padding: 1.5rem; | |
box-shadow: var(--card-shadow); | |
border: 1px solid var(--border-color); | |
margin-bottom: 1.5rem; | |
} | |
.progress { | |
height: 8px; | |
border-radius: 20px; | |
background: #f1f5f9; | |
overflow: hidden; | |
} | |
.progress-bar { | |
background: var(--gradient-bg); | |
border-radius: 20px; | |
transition: width 0.3s ease; | |
} | |
.no-results, .error, .loading { | |
text-align: center; | |
padding: 3rem; | |
color: var(--text-secondary); | |
font-weight: 500; | |
} | |
.error { | |
color: var(--danger-color); | |
} | |
.loading { | |
color: var(--primary-color); | |
} | |
.folder-browser { | |
max-height: 400px; | |
overflow-y: auto; | |
border-radius: 12px; | |
border: 1px solid var(--border-color); | |
} | |
.folder-item { | |
cursor: pointer; | |
padding: 12px 16px; | |
border-radius: 8px; | |
margin: 4px; | |
transition: all 0.2s ease; | |
display: flex; | |
align-items: center; | |
gap: 12px; | |
} | |
.folder-item:hover { | |
background: var(--secondary-color); | |
color: var(--primary-color); | |
} | |
.modal-content { | |
border-radius: 20px; | |
border: none; | |
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
} | |
.modal-header { | |
border-bottom: 1px solid var(--border-color); | |
padding: 1.5rem; | |
} | |
.breadcrumb { | |
background: var(--secondary-color); | |
border-radius: 12px; | |
padding: 12px 16px; | |
} | |
.btn-primary { | |
background: var(--primary-color); | |
border: none; | |
border-radius: 12px; | |
padding: 10px 20px; | |
font-weight: 600; | |
} | |
.btn-primary:hover { | |
background: var(--primary-dark); | |
} | |
.btn-secondary { | |
background: var(--text-secondary); | |
border: none; | |
border-radius: 12px; | |
padding: 10px 20px; | |
font-weight: 600; | |
} | |
.btn-outline-danger { | |
border-color: var(--danger-color); | |
color: var(--danger-color); | |
border-radius: 8px; | |
font-weight: 600; | |
} | |
.btn-outline-danger:hover { | |
background: var(--danger-color); | |
border-color: var(--danger-color); | |
} | |
@media (max-width: 768px) { | |
.search-container { | |
padding: 0 1rem; | |
} | |
.main-container { | |
padding-top: 1rem; | |
} | |
.image-grid { | |
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
gap: 1rem; | |
} | |
.sidebar { | |
margin-bottom: 1.5rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<nav class="navbar navbar-expand-lg" aria-label="Main navigation"> | |
<div class="container-fluid px-4"> | |
<a class="navbar-brand" href="#"> | |
<div class="brand-icon"> | |
<i class="bi bi-search"></i> | |
</div> | |
Visual Product Search | |
</a> | |
<div class="search-container"> | |
<form class="search-form d-flex" onsubmit="searchImages(event)"> | |
<input class="form-control search-input" type="search" id="searchInput" placeholder="Search products and images..."> | |
<button class="btn btn-primary search-btn" type="submit"> | |
<i class="bi bi-search me-1"></i>Search | |
</button> | |
<label class="btn action-btn ms-2" for="imageUpload" title="Search by image"> | |
<i class="bi bi-image"></i> | |
</label> | |
<input type="file" id="imageUpload" style="display: none" accept="image/*" onchange="searchByImage(event)"> | |
<button class="btn action-btn ms-2" type="button" onclick="toggleUrlSearch()" title="Search by URL"> | |
<i class="bi bi-link-45deg"></i> | |
</button> | |
</form> | |
<!-- URL Search Form (initially hidden) --> | |
<form class="url-search-form d-flex mt-3" id="urlSearchForm" style="display: none;" onsubmit="searchByUrl(event)"> | |
<input class="form-control search-input" type="url" id="urlInput" placeholder="Enter image URL..." required> | |
<button class="btn btn-primary search-btn" type="submit"> | |
<i class="bi bi-link me-1"></i>Search URL | |
</button> | |
<button class="btn action-btn ms-2" type="button" onclick="toggleUrlSearch()"> | |
<i class="bi bi-x"></i> | |
</button> | |
</form> | |
</div> | |
</div> | |
</nav> | |
<!-- Indexing Progress --> | |
<div class="container main-container" id="indexingStatus" style="display: none;"> | |
<div class="status-card"> | |
<h6 class="mb-3"> | |
<i class="bi bi-gear-fill me-2 text-primary"></i> | |
Indexing Progress | |
</h6> | |
<div class="progress mb-3"> | |
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0%"></div> | |
</div> | |
<p class="mb-0 text-muted" id="indexingDetails"></p> | |
</div> | |
</div> | |
<!-- Main Content --> | |
<div class="container main-container"> | |
<div class="row g-4"> | |
<div class="col-lg-3"> | |
<div class="sidebar"> | |
<button class="add-folder-btn" onclick="openFileUpload()"> | |
<i class="bi bi-cloud-upload me-2"></i>Upload Images | |
</button> | |
<!-- Hidden file input for multiple image uploads --> | |
<input type="file" id="multipleImageUpload" style="display: none" | |
accept="image/*" multiple onchange="uploadImages(event)"> | |
<div class="sidebar-title"> | |
<i class="bi bi-folder2-open"></i> | |
Indexed Folders | |
</div> | |
<div id="folderList"> | |
<!-- Folders will be listed here --> | |
</div> | |
<!-- Upload Progress (initially hidden) --> | |
<div id="uploadProgress" style="display: none;" class="mt-3"> | |
<div class="sidebar-title"> | |
<i class="bi bi-upload"></i> | |
Upload Progress | |
</div> | |
<div class="progress mb-2"> | |
<div class="progress-bar progress-bar-striped progress-bar-animated" | |
id="uploadProgressBar" style="width: 0%"></div> | |
</div> | |
<small class="text-muted" id="uploadStatus">Preparing upload...</small> | |
</div> | |
</div> | |
</div> | |
<div class="col-lg-9"> | |
<div class="content-area"> | |
<div class="image-grid" id="imageGrid"> | |
<!-- Images will be displayed here --> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> |