multimodal / static /index.html
yugbirla's picture
Upload index.html
1d979ec verified
Raw
History Blame Contribute Delete
26.2 kB
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enterprise AI Search</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
darkbg: '#121212',
sidebar: '#18181b',
card: '#27272a',
accent: '#ff2d55'
}
}
}
}
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.log-scroll::-webkit-scrollbar { width: 6px; }
.log-scroll::-webkit-scrollbar-track { background: #121212; }
.log-scroll::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 10px; }
.truncate-1-line {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body class="bg-darkbg text-gray-200 flex h-screen overflow-hidden font-sans">
<!-- GPT-STYLE SMART COLLAPSIBLE SIDEBAR -->
<!-- Added: ID 'app-sidebar', w-64, overflow-hidden, and smooth width transitions -->
<aside id="app-sidebar" class="w-64 bg-sidebar flex flex-col border-r border-gray-800 flex-shrink-0 h-screen transition-all duration-300 overflow-hidden">
<!-- TOP: Branding & Sidebar Toggle -->
<div class="p-4 flex items-center justify-between">
<div class="flex items-center gap-3 font-bold text-xl tracking-wider text-white px-1 whitespace-nowrap">
<i class="fa-solid fa-layer-group text-accent"></i>
<span>Multimodal AI</span>
</div>
<!-- Added: The Collapse Button -->
<button onclick="toggleSidebar()" class="text-gray-400 hover:text-white hover:bg-gray-800 w-8 h-8 rounded-md flex items-center justify-center transition flex-shrink-0" title="Close sidebar">
<i class="fa-solid fa-bars-staggered"></i>
</button>
</div>
<div class="px-4 py-2">
<button onclick="resetHome()" class="w-full bg-accent hover:bg-pink-600 text-white py-2.5 rounded-lg font-semibold flex items-center gap-3 px-4 transition shadow-lg shadow-accent/20 whitespace-nowrap">
<i class="fa-solid fa-plus text-sm"></i> New Search
</button>
</div>
<!-- MIDDLE: Scrollable Search History -->
<div class="flex-1 overflow-y-auto no-scrollbar px-3 py-2 mt-2">
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 px-3 whitespace-nowrap">Recent Searches</div>
<div id="searchHistoryList" class="space-y-1 w-full">
<div class="text-gray-600 text-xs text-center mt-4">No recent searches</div>
</div>
</div>
<!-- BOTTOM: Fixed Tools & Settings -->
<div class="p-3 border-t border-gray-800 space-y-1 bg-sidebar w-full whitespace-nowrap">
<a href="#" onclick="resetHome()" class="flex items-center gap-3 px-3 py-2.5 hover:bg-gray-800 rounded-lg text-gray-300 transition text-sm font-medium"><i class="fa-solid fa-house w-4"></i> Home</a>
<a href="#" onclick="document.getElementById('imageInput').click()" class="flex items-center gap-3 px-3 py-2.5 hover:bg-gray-800 rounded-lg text-gray-300 transition text-sm font-medium"><i class="fa-solid fa-camera w-4"></i> Visual Search</a>
<a href="#" onclick="triggerDashboard()" class="flex items-center gap-3 px-3 py-2.5 hover:bg-gray-800 rounded-lg text-gray-300 transition text-sm font-medium"><i class="fa-solid fa-shield-halved w-4"></i> Admin Panel</a>
</div>
</aside>
<!-- MAIN APP CANVAS -->
<main class="flex-1 flex flex-col h-full relative overflow-hidden">
<!-- HEADER -->
<header class="p-4 border-b border-gray-800 flex items-center bg-darkbg z-10 gap-4">
<!-- Added: Hamburger Menu (Shows only when sidebar is closed) -->
<button id="headerMenuBtn" onclick="toggleSidebar()" class="hidden text-gray-400 hover:text-white hover:bg-gray-800 w-10 h-10 rounded-md flex items-center justify-center transition flex-shrink-0" title="Open sidebar">
<i class="fa-solid fa-bars text-lg"></i>
</button>
<div class="relative w-full max-w-3xl mx-auto flex-1">
<i class="fa-solid fa-magnifying-glass absolute left-5 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
<input type="text" id="searchInput" autocomplete="off" spellcheck="false" placeholder="Search 80,000 products... (e.g., a ceramic coffee mug)" class="w-full bg-card text-white rounded-full pl-12 pr-28 py-3.5 focus:outline-none focus:ring-2 focus:ring-gray-600 transition shadow-lg text-sm border border-gray-700/50">
<input type="file" id="imageInput" accept="image/*" class="hidden">
<button onclick="document.getElementById('imageInput').click()" class="absolute right-28 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition px-2"><i class="fa-solid fa-camera text-lg"></i></button>
<button onclick="performSearch()" class="absolute right-2 top-1/2 transform -translate-y-1/2 bg-gray-700 hover:bg-gray-600 px-6 py-2 rounded-full text-sm font-medium transition text-white">Search</button>
</div>
<!-- Invisible spacer to keep the search bar perfectly centered -->
<div id="headerSpacer" class="w-10 hidden flex-shrink-0"></div>
</header>
<div class="flex-1 overflow-y-auto p-8 relative">
<!-- HOME SCREEN -->
<div id="homeState" class="flex flex-col items-center justify-center mt-10 text-center animate-fade-in">
<div class="w-20 h-20 bg-card rounded-2xl flex items-center justify-center mb-6 shadow-lg border border-gray-700">
<i class="fa-solid fa-wand-magic-sparkles text-3xl text-accent"></i>
</div>
<h1 class="text-3xl font-bold text-white mb-3">What are you looking for?</h1>
<p class="text-gray-400 max-w-lg mx-auto mb-8">Type a complex semantic query, apply filters, or upload a photo to test the multimodal vision engine.</p>
</div>
<!-- LOADING SPINNER -->
<div id="loadingIndicator" class="hidden flex-col items-center justify-center mt-20 text-gray-400">
<i class="fa-solid fa-circle-notch fa-spin text-4xl mb-4 text-accent"></i>
<p id="loadingText" class="text-lg font-medium">AI is scanning the database...</p>
</div>
<!-- ADMIN DASHBOARD -->
<div id="dashboardState" class="hidden flex-col h-full max-w-7xl mx-auto">
<div class="flex items-center justify-between border-b border-gray-800 pb-4 mb-6">
<h2 class="text-2xl font-bold text-white flex items-center gap-2"><i class="fa-solid fa-shield-halved text-accent"></i> Secure Admin Analytics Panel</h2>
<button onclick="resetHome()" class="text-xs bg-gray-800 hover:bg-gray-700 px-3 py-1.5 rounded-md text-gray-400 font-medium transition">Exit Admin Mode</button>
</div>
<div class="flex flex-col lg:flex-row gap-6 h-[calc(100vh-200px)]">
<div class="flex-1 overflow-y-auto no-scrollbar space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-card border border-gray-800 p-6 rounded-xl shadow-lg">
<div class="flex justify-between items-start mb-4">
<div class="p-3 bg-blue-500/10 rounded-lg text-blue-400 text-xl"><i class="fa-solid fa-database"></i></div>
<span id="qdrantStatus" class="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded bg-green-500/10 text-green-400 border border-green-500/20">UNKNOWN</span>
</div>
<p class="text-gray-400 text-xs uppercase font-semibold tracking-wider mb-1">Qdrant Vectors</p>
<h3 id="qdrantCount" class="text-3xl font-bold text-white tracking-tight">0</h3>
</div>
<div class="bg-card border border-gray-800 p-6 rounded-xl shadow-lg">
<div class="flex justify-between items-start mb-4">
<div class="p-3 bg-purple-500/10 rounded-lg text-purple-400 text-xl"><i class="fa-solid fa-stopwatch"></i></div>
<span class="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded bg-gray-700 text-gray-300 border border-gray-600">LIFETIME</span>
</div>
<p class="text-gray-400 text-xs uppercase font-semibold tracking-wider mb-1">Avg Search Latency</p>
<h3 id="avgLatency" class="text-3xl font-bold text-white tracking-tight">0.00 ms</h3>
</div>
<div class="bg-card border border-gray-800 p-6 rounded-xl shadow-lg">
<div class="flex justify-between items-start mb-4">
<div class="p-3 bg-pink-500/10 rounded-lg text-accent text-xl"><i class="fa-solid fa-bolt"></i></div>
<span id="redisStatus" class="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded bg-green-500/10 text-green-400 border border-green-500/20">UNKNOWN</span>
</div>
<p class="text-gray-400 text-xs uppercase font-semibold tracking-wider mb-1">Redis Cache Hits</p>
<h3 id="redisHits" class="text-3xl font-bold text-white tracking-tight">0</h3>
</div>
<div class="bg-card border border-gray-800 p-6 rounded-xl shadow-lg">
<div class="flex justify-between items-start mb-4">
<div class="p-3 bg-amber-500/10 rounded-lg text-amber-400 text-xl"><i class="fa-solid fa-gauge-high"></i></div>
<span class="text-[10px] text-gray-400 font-semibold uppercase tracking-wider">Sub-ms Tier</span>
</div>
<p class="text-gray-400 text-xs uppercase font-semibold tracking-wider mb-1">Redis Cache Misses</p>
<h3 id="redisMisses" class="text-3xl font-bold text-white tracking-tight">0</h3>
</div>
</div>
</div>
<div class="w-full lg:w-96 bg-card border border-gray-800 rounded-xl flex flex-col shadow-lg overflow-hidden flex-shrink-0">
<div class="p-4 bg-gray-900 border-b border-gray-800 flex justify-between items-center">
<span class="text-xs font-bold text-gray-400 uppercase tracking-widest"><i class="fa-solid fa-terminal mr-2"></i> Live Event Logs</span>
<span class="flex h-2 w-2 relative">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-accent"></span>
</span>
</div>
<div id="logContainer" class="flex-1 p-4 overflow-y-auto log-scroll space-y-3">
<div class="text-gray-500 text-center text-sm mt-10">Awaiting system events...</div>
</div>
</div>
</div>
</div>
<!-- SEARCH RESULTS GRID -->
<div id="resultsGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 pb-20 w-full hidden"></div>
</div>
</main>
<script>
const searchInput = document.getElementById('searchInput');
const resultsGrid = document.getElementById('resultsGrid');
const homeState = document.getElementById('homeState');
const dashboardState = document.getElementById('dashboardState');
const loadingIndicator = document.getElementById('loadingIndicator');
const loadingText = document.getElementById('loadingText');
const imageInput = document.getElementById('imageInput');
let currentImageFile = null;
// --- NEW: SIDEBAR TOGGLE LOGIC ---
function toggleSidebar() {
const sidebar = document.getElementById('app-sidebar');
const headerBtn = document.getElementById('headerMenuBtn');
const headerSpacer = document.getElementById('headerSpacer');
if (sidebar.classList.contains('w-64')) {
// Collapse the sidebar to 0 width
sidebar.classList.remove('w-64');
sidebar.classList.add('w-0', 'border-transparent');
// Show hamburger menu in the main header
headerBtn.classList.remove('hidden');
headerSpacer.classList.remove('hidden');
} else {
// Expand the sidebar back to normal
sidebar.classList.add('w-64');
sidebar.classList.remove('w-0', 'border-transparent');
// Hide the hamburger menu
headerBtn.classList.add('hidden');
headerSpacer.classList.add('hidden');
}
}
// --- LOCAL STORAGE HISTORY ENGINE ---
function loadHistory() {
const history = JSON.parse(localStorage.getItem('search_history') || '[]');
const listEl = document.getElementById('searchHistoryList');
if (history.length === 0) {
listEl.innerHTML = '<div class="text-gray-600 text-xs text-center mt-4">No recent searches</div>';
return;
}
listEl.innerHTML = history.map(item => `
<button onclick="triggerPastSearch('${item.query.replace(/'/g, "\\'")}')" class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-gray-800 text-gray-300 flex items-center gap-3 transition group">
<i class="fa-regular ${item.type === 'image' ? 'fa-image' : 'fa-message'} text-gray-500 group-hover:text-accent transition flex-shrink-0"></i>
<span class="text-sm truncate-1-line">${item.query}</span>
</button>
`).join('');
}
function saveToHistory(query, type) {
let history = JSON.parse(localStorage.getItem('search_history') || '[]');
history = history.filter(item => item.query !== query);
history.unshift({ query, type });
if (history.length > 15) history.pop();
localStorage.setItem('search_history', JSON.stringify(history));
loadHistory();
}
function triggerPastSearch(query) {
resetHome();
searchInput.value = query;
executeUnifiedSearch();
}
// Initialize history on page load
window.addEventListener('DOMContentLoaded', () => {
loadHistory();
const params = new URLSearchParams(window.location.search);
if (params.get('admin') === 'true') {
triggerDashboard();
}
});
// --- INPUT EVENT LISTENERS ---
searchInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') executeUnifiedSearch();
});
imageInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
currentImageFile = file;
searchInput.placeholder = `📸 Image attached. Add text to modify it`;
searchInput.value = '';
searchInput.focus();
setTimeout(() => {
if(searchInput.value === '') executeUnifiedSearch();
}, 500);
});
// --- APP NAVIGATION ---
function resetHome() {
window.history.replaceState({}, document.title, window.location.pathname);
searchInput.value = '';
searchInput.placeholder = "Search 80,000 products... (e.g., a ceramic coffee mug)";
currentImageFile = null;
imageInput.value = '';
resultsGrid.innerHTML = '';
resultsGrid.classList.add('hidden');
loadingIndicator.classList.add('hidden');
dashboardState.classList.add('hidden');
homeState.classList.remove('hidden');
}
function triggerDashboard() {
showDashboard();
}
async function showDashboard() {
homeState.classList.add('hidden');
resultsGrid.classList.add('hidden');
dashboardState.classList.add('hidden');
loadingIndicator.classList.remove('hidden');
loadingText.innerText = "Authenticating secure handshake...";
const adminKey = prompt("Enter Admin Security Token:");
if (!adminKey) { resetHome(); return; }
try {
const response = await fetch('/api/stats', {
headers: { 'x-admin-token': adminKey }
});
if (response.status === 403) {
showError("Access Denied: Invalid Security Header.");
return;
}
const data = await response.json();
if (data.status === "success") {
document.getElementById('qdrantCount').innerText = data.qdrant.points_count.toLocaleString();
document.getElementById('qdrantStatus').innerText = data.qdrant.status.toUpperCase();
document.getElementById('redisStatus').innerText = data.redis.status;
document.getElementById('redisHits').innerText = data.redis.hits.toLocaleString();
document.getElementById('redisMisses').innerText = data.redis.misses.toLocaleString();
document.getElementById('avgLatency').innerText = `${data.performance.avg_latency_ms} ms`;
const logContainer = document.getElementById('logContainer');
if (data.performance.recent_logs.length > 0) {
logContainer.innerHTML = data.performance.recent_logs.map(log => `
<div class="bg-darkbg p-3 rounded-lg border border-gray-800 text-xs font-mono shadow-inner">
<div class="flex justify-between items-center mb-1.5">
<span class="text-accent font-bold">${log.type}</span>
<span class="text-gray-500">${log.time}</span>
</div>
<div class="text-gray-300 truncate" title="${log.details}">${log.details}</div>
${log.latency_ms > 0 ? `<div class="text-gray-500 mt-1"><i class="fa-solid fa-stopwatch text-[10px] mr-1"></i>${log.latency_ms} ms</div>` : ''}
</div>
`).join('');
} else {
logContainer.innerHTML = '<div class="text-gray-500 text-center text-sm mt-10">No recent system activity recorded.</div>';
}
loadingIndicator.classList.add('hidden');
dashboardState.classList.remove('hidden');
dashboardState.style.display = 'flex';
}
} catch (error) {
showError("Could not capture cloud resource statistics.");
}
}
// --- CORE SEARCH EXECUTION ---
async function executeUnifiedSearch() {
const textQuery = searchInput.value.trim();
if (!textQuery && !currentImageFile) return;
if (textQuery) {
saveToHistory(textQuery, currentImageFile ? 'hybrid' : 'text');
} else if (currentImageFile) {
saveToHistory(`📸 Visual Search: ${currentImageFile.name}`, 'image');
}
homeState.classList.add('hidden');
dashboardState.classList.add('hidden');
resultsGrid.innerHTML = '';
resultsGrid.classList.add('hidden');
loadingIndicator.classList.remove('hidden');
try {
let response, data;
if (currentImageFile && textQuery) {
loadingText.innerText = "VLM is composing your search...";
const formData = new FormData();
formData.append("file", currentImageFile);
formData.append("query", textQuery);
formData.append("top_k", 6);
response = await fetch('/api/search/composed', { method: 'POST', body: formData });
data = await response.json();
} else if (currentImageFile && !textQuery) {
loadingText.innerText = "Analyzing visual embeddings...";
const formData = new FormData();
formData.append("file", currentImageFile);
formData.append("top_k", 6);
response = await fetch('/api/search/image', { method: 'POST', body: formData });
data = await response.json();
} else {
loadingText.innerText = "Querying semantic vectors...";
response = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: textQuery, top_k: 6 })
});
data = await response.json();
}
processResults(data);
} catch (error) {
showError("Error connecting to AI backend.");
}
}
function performSearch() { executeUnifiedSearch(); }
function processResults(data) {
loadingIndicator.classList.add('hidden');
resultsGrid.classList.remove('hidden');
if (data.results && data.results.length > 0) {
renderCards(data.results);
} else {
resultsGrid.innerHTML = '<div class="w-full text-center mt-10 text-gray-500 col-span-full">No products found matching your criteria.</div>';
}
}
function renderCards(results) {
resultsGrid.innerHTML = results.map(item => `
<div class="mb-6 bg-card rounded-xl overflow-hidden hover:ring-2 hover:ring-gray-500 transition duration-300 shadow-lg group cursor-pointer border border-gray-800 flex flex-col justify-between">
<div class="relative bg-gray-900">
<img src="${item.image_url}" alt="Product" class="w-full h-64 object-cover opacity-90 group-hover:opacity-100 transition duration-300">
<div class="absolute top-3 right-3 bg-black/80 px-2.5 py-1 rounded-md text-xs font-bold text-accent backdrop-blur-sm shadow-md border border-gray-700/50">Match: ${(item.hybrid_sort_score * 100).toFixed(1)}%</div>
<span>Match: ${(item.hybrid_sort_score * 100).toFixed(1)}%</span>
</div>
</div>
<div class="p-4 flex-1 flex flex-col justify-between">
<div>
<p class="text-[10px] text-gray-400 mb-1.5 uppercase tracking-widest font-semibold">${item.category || 'Product'}</p>
<h3 class="font-medium text-gray-200 text-sm mb-3 leading-snug line-clamp-2">${item.title}</h3>
</div>
<div class="flex justify-between items-center pt-2">
<span class="text-lg font-bold text-white">$${item.price || 'N/A'}</span>
<button class="text-gray-500 hover:text-accent transition"><i class="fa-solid fa-bookmark"></i></button>
</div>
</div>
</div>
`).join('');
}
function showError(msg) {
loadingIndicator.classList.add('hidden');
dashboardState.classList.add('hidden');
resultsGrid.classList.remove('hidden');
resultsGrid.innerHTML = `<div class="w-full text-center mt-10 text-red-400 font-medium">${msg}</div>`;
}
</script>
</body>
</html>