face-intel / ui /static /index.html
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
23.8 kB
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Face Intel — multi-provider face intelligence analysis console" />
<meta name="color-scheme" content="light dark" />
<title>Face Intel · Intelligence Console</title>
<link rel="stylesheet" href="styles.css" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230e7490'/%3E%3Ccircle cx='16' cy='13' r='5' fill='none' stroke='%23fff' stroke-width='2'/%3E%3Cpath d='M6 27c1.5-5 5-7 10-7s8.5 2 10 7' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<div class="app-shell">
<!-- ============== Sidebar / Nav ============== -->
<aside class="sidebar" id="sidebar" aria-label="Primary navigation">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 32 32" width="28" height="28">
<rect width="32" height="32" rx="7" fill="currentColor" />
<circle cx="16" cy="13" r="5" fill="none" stroke="#fff" stroke-width="2" />
<path d="M6 27c1.5-5 5-7 10-7s8.5 2 10 7" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" />
</svg>
</div>
<div class="brand-text">
<span class="brand-name">Face Intel</span>
<span class="brand-sub">Intelligence Console</span>
</div>
</div>
<nav class="nav" role="tablist" aria-label="Sections">
<button class="nav-item is-active" data-view="analyze" role="tab" aria-selected="true" aria-controls="view-analyze">
<svg class="nav-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 3a9 9 0 1 0 9 9 9 9 0 0 0-9-9m0 16a7 7 0 1 1 7-7 7 7 0 0 1-7 7m1-11h-2v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
<span class="nav-label">Analyze</span>
</button>
<button class="nav-item" data-view="report" role="tab" aria-selected="false" aria-controls="view-report">
<svg class="nav-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zm4 18H6V4h7v5h5z"/></svg>
<span class="nav-label">Report</span>
</button>
<button class="nav-item" data-view="providers" role="tab" aria-selected="false" aria-controls="view-providers">
<svg class="nav-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M4 6h16v2H4zm0 5h16v2H4zm0 5h10v2H4z"/></svg>
<span class="nav-label">Providers</span>
</button>
<button class="nav-item" data-view="stats" role="tab" aria-selected="false" aria-controls="view-stats">
<svg class="nav-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M5 9.2h3V19H5zm5.6-4.2h2.8v14h-2.8zm5.6 8H19V19h-2.8z"/></svg>
<span class="nav-label">Metrics</span>
</button>
<button class="nav-item" data-view="jobs" role="tab" aria-selected="false" aria-controls="view-jobs">
<svg class="nav-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M20 6h-4V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2H4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2M10 4h4v2h-4z"/></svg>
<span class="nav-label">Jobs</span>
</button>
<button class="nav-item" data-view="health" role="tab" aria-selected="false" aria-controls="view-health">
<svg class="nav-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 21s-7-4.5-9.5-9C1 8.5 3 5 6.5 5c2 0 3.5 1 5.5 3 2-2 3.5-3 5.5-3C21 5 23 8.5 21.5 12 19 16.5 12 21 12 21"/></svg>
<span class="nav-label">Health</span>
</button>
</nav>
<div class="sidebar-footer">
<button class="ghost-btn" id="theme-toggle" aria-label="Toggle dark mode" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 7a5 5 0 1 0 5 5 5 5 0 0 0-5-5m0-5h2v3h-2zM4.9 4.9 6.3 6.3 4.9 7.8 3.5 6.3zM2 11h3v2H2zm11 9h-2v-3h2zm6.1-2.1-1.4-1.4 1.4-1.5 1.4 1.5zM19 11h3v2h-3zM6.3 17.7 4.9 19.1l-1.4-1.4 1.4-1.5zM17.7 6.3l1.4-1.4 1.4 1.4-1.4 1.5z"/></svg>
<svg class="icon-moon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 3a9 9 0 1 0 9 9 7 7 0 0 1-9-9"/></svg>
<span class="theme-label">Theme</span>
</button>
</div>
</aside>
<!-- ============== Main content ============== -->
<main class="main" id="main">
<header class="topbar">
<button class="icon-btn menu-toggle" id="menu-toggle" aria-label="Toggle navigation menu" aria-expanded="false">
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path fill="currentColor" d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</button>
<h1 class="topbar-title" id="view-title">Analyze</h1>
<div class="topbar-actions">
<span class="connection-pill" id="connection-pill" title="Backend connection status">
<span class="dot" aria-hidden="true"></span>
<span class="pill-text">Connecting…</span>
</span>
</div>
</header>
<div class="content" id="content">
<!-- ============== View: Analyze ============== -->
<section class="view is-active" id="view-analyze" role="tabpanel" aria-labelledby="view-title">
<div class="grid-2">
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">Image source</h2>
<span class="muted small">JPEG or PNG · up to 20 MB</span>
</div>
<div class="panel-body">
<div id="dropzone" class="dropzone" role="button" tabindex="0"
aria-label="Drop an image here or press Enter to choose a file">
<input type="file" id="file-input" accept="image/jpeg,image/png" hidden />
<svg viewBox="0 0 24 24" width="40" height="40" aria-hidden="true"><path fill="currentColor" d="M19 13v6H5v-6H3v8h18v-8zM6 9.4l1.4 1.4L11 7.2V17h2V7.2l3.6 3.6L18 9.4 12 3.4z"/></svg>
<p class="dropzone-title">Drop image here</p>
<p class="dropzone-sub">or <span class="link">browse files</span></p>
</div>
<div class="preview-area" id="preview-area" hidden>
<div class="preview-frame">
<img id="preview-img" alt="Selected preview" />
</div>
<div class="preview-meta">
<div class="meta-row"><span class="meta-key">File</span><span class="meta-val" id="meta-name"></span></div>
<div class="meta-row"><span class="meta-key">Type</span><span class="meta-val" id="meta-type"></span></div>
<div class="meta-row"><span class="meta-key">Size</span><span class="meta-val" id="meta-size"></span></div>
<div class="meta-row"><span class="meta-key">Dimensions</span><span class="meta-val" id="meta-dims"></span></div>
<button class="ghost-btn sm" id="clear-preview">Remove</button>
</div>
</div>
<div class="url-input-group">
<label for="image-url" class="field-label">Or provide an image URL</label>
<div class="input-row">
<input type="url" id="image-url" class="text-input"
placeholder="https://example.com/photo.jpg" autocomplete="off" />
<button class="btn secondary" id="load-url-btn" type="button">Load URL</button>
</div>
<p class="muted small">URL-fetched images are sent to the API as <code>image_url</code>.</p>
</div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">Run analysis</h2>
<span class="muted small" id="action-source-hint">No image selected</span>
</div>
<div class="panel-body">
<div class="action-grid" id="action-grid">
<button class="action-btn" data-action="detect">
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M3 5v14h18V5zm16 12H5V7h14zM7 9h10v2H7zm0 4h7v2H7z"/></svg>
</span>
<span class="action-text">
<span class="action-title">Detect Faces</span>
<span class="action-desc">Multi-provider face detection</span>
</span>
</button>
<button class="action-btn" data-action="recognize">
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5m0 2c-3.3 0-7 1.7-7 5v3h14v-3c0-3.3-3.7-5-7-5"/></svg>
</span>
<span class="action-text">
<span class="action-title">Recognize</span>
<span class="action-desc">Match against reference gallery</span>
</span>
</button>
<button class="action-btn" data-action="reverse">
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.8l-.3-.3a6.5 6.5 0 1 0-.7.7l.3.3v.8l5 5 1.5-1.5zM10 14a4 4 0 1 1 4-4 4 4 0 0 1-4 4"/></svg>
</span>
<span class="action-text">
<span class="action-title">Reverse Search</span>
<span class="action-desc">Find this image across the web</span>
</span>
</button>
<button class="action-btn" data-action="image_analysis">
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M21 19V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2M8.9 13.6l2.1 2.7 3-3.9 4 5H5z"/></svg>
</span>
<span class="action-text">
<span class="action-title">Image Analysis</span>
<span class="action-desc">Quality, brightness, dominant colors</span>
</span>
</button>
<button class="action-btn" data-action="metadata">
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M4 4h16v4H4zm0 6h16v10H4zm2 2v6h12v-6zm2 1h8v2H8z"/></svg>
</span>
<span class="action-text">
<span class="action-title">Metadata</span>
<span class="action-desc">EXIF, camera, GPS extraction</span>
</span>
</button>
<button class="action-btn" data-action="forensics">
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 1 3 5v6c0 5.5 3.8 10.7 9 12 5.2-1.3 9-6.5 9-12V5zm-1 6h2v2h-2zm0 4h2v6h-2z"/></svg>
</span>
<span class="action-text">
<span class="action-title">Forensics</span>
<span class="action-desc">Integrity, duplicates, manipulation</span>
</span>
</button>
<button class="action-btn primary-action" data-action="full_pipeline">
<span class="action-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M13 2 3 14h7l-1 8 10-12h-7zM12 4l-1 2h5l-6 7h5l-1 5 6-7h-5z"/></svg>
</span>
<span class="action-text">
<span class="action-title">Full Pipeline</span>
<span class="action-desc">Run every capability (creates a job)</span>
</span>
</button>
</div>
<div class="last-run" id="last-run" hidden>
<div class="last-run-row">
<span class="last-run-label">Last action</span>
<span class="last-run-value" id="last-run-name"></span>
</div>
<div class="last-run-row">
<span class="last-run-label">Elapsed</span>
<span class="last-run-value" id="last-run-elapsed"></span>
</div>
<div class="last-run-row">
<span class="last-run-label">Status</span>
<span class="last-run-value" id="last-run-status"></span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============== View: Report ============== -->
<section class="view" id="view-report" role="tabpanel" aria-labelledby="view-title" hidden>
<div id="report-empty" class="empty-state">
<svg viewBox="0 0 24 24" width="48" height="48" aria-hidden="true"><path fill="currentColor" d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zm4 18H6V4h7v5h5z"/></svg>
<h3>No report yet</h3>
<p>Run an analysis from the <strong>Analyze</strong> tab, or pick a job from the <strong>Jobs</strong> tab to view its report here.</p>
</div>
<div id="report-content" hidden></div>
</section>
<!-- ============== View: Providers ============== -->
<section class="view" id="view-providers" role="tabpanel" aria-labelledby="view-title" hidden>
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">Registered providers</h2>
<button class="btn secondary sm" id="refresh-providers">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M17.6 6.4A8 8 0 1 0 20 12h-2a6 6 0 1 1-1.8-4.2L13 11h7V4z"/></svg>
Refresh
</button>
</div>
<div class="panel-body">
<div id="providers-errors" class="callout warn" hidden></div>
<div class="table-wrap">
<table class="data-table" id="providers-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Capability</th>
<th scope="col">Status</th>
<th scope="col">Description</th>
</tr>
</thead>
<tbody id="providers-tbody"></tbody>
</table>
</div>
<p id="providers-empty" class="empty-row" hidden>No providers registered.</p>
</div>
</div>
</section>
<!-- ============== View: Stats ============== -->
<section class="view" id="view-stats" role="tabpanel" aria-labelledby="view-title" hidden>
<div class="stack">
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">Provider metrics</h2>
<button class="btn secondary sm" id="refresh-stats">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M17.6 6.4A8 8 0 1 0 20 12h-2a6 6 0 1 1-1.8-4.2L13 11h7V4z"/></svg>
Refresh
</button>
</div>
<div class="panel-body">
<div class="table-wrap">
<table class="data-table" id="metrics-table">
<thead>
<tr>
<th scope="col">Provider</th>
<th scope="col" class="num">Invocations</th>
<th scope="col" class="num">Successes</th>
<th scope="col" class="num">Failures</th>
<th scope="col" class="num">Retries</th>
<th scope="col" class="num">Avg ms</th>
<th scope="col" class="num">p95 ms</th>
<th scope="col" class="num">Success rate</th>
</tr>
</thead>
<tbody id="metrics-tbody"></tbody>
</table>
</div>
<p id="metrics-empty" class="empty-row" hidden>No provider metrics recorded yet.</p>
</div>
</div>
<div class="grid-2">
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">Cache</h2>
<button class="btn danger sm" id="clear-cache-btn">Clear cache</button>
</div>
<div class="panel-body">
<div class="stat-grid" id="cache-stats">
<div class="stat-cell"><span class="stat-label">Entries</span><span class="stat-value" data-key="entries"></span></div>
<div class="stat-cell"><span class="stat-label">Max</span><span class="stat-value" data-key="max_entries"></span></div>
<div class="stat-cell"><span class="stat-label">Hits</span><span class="stat-value" data-key="hits"></span></div>
<div class="stat-cell"><span class="stat-label">Misses</span><span class="stat-value" data-key="misses"></span></div>
<div class="stat-cell"><span class="stat-label">Hit ratio</span><span class="stat-value" data-key="hit_ratio"></span></div>
<div class="stat-cell"><span class="stat-label">Evictions</span><span class="stat-value" data-key="evictions"></span></div>
<div class="stat-cell"><span class="stat-label">TTL (s)</span><span class="stat-value" data-key="ttl_seconds"></span></div>
</div>
<div class="hit-ratio-bar" id="hit-ratio-bar" aria-hidden="true">
<div class="hit-ratio-fill" id="hit-ratio-fill"></div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">Counters</h2>
</div>
<div class="panel-body">
<div class="counter-list" id="counter-list"></div>
<p id="counters-empty" class="empty-row" hidden>No counters recorded.</p>
</div>
</div>
</div>
</div>
</section>
<!-- ============== View: Jobs ============== -->
<section class="view" id="view-jobs" role="tabpanel" aria-labelledby="view-title" hidden>
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">Recent jobs</h2>
<button class="btn secondary sm" id="refresh-jobs">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M17.6 6.4A8 8 0 1 0 20 12h-2a6 6 0 1 1-1.8-4.2L13 11h7V4z"/></svg>
Refresh
</button>
</div>
<div class="panel-body">
<div class="table-wrap">
<table class="data-table" id="jobs-table">
<thead>
<tr>
<th scope="col">Job ID</th>
<th scope="col">Kind</th>
<th scope="col">Status</th>
<th scope="col">Created</th>
<th scope="col">Error</th>
<th scope="col" class="col-actions">Actions</th>
</tr>
</thead>
<tbody id="jobs-tbody"></tbody>
</table>
</div>
<p id="jobs-empty" class="empty-row" hidden>No jobs recorded yet. Run an action to create one.</p>
</div>
</div>
</section>
<!-- ============== View: Health ============== -->
<section class="view" id="view-health" role="tabpanel" aria-labelledby="view-title" hidden>
<div class="stack">
<div class="panel" id="health-banner-card">
<div class="panel-body">
<div class="health-banner" id="health-banner">
<div class="health-banner-icon" id="health-banner-icon" aria-hidden="true"></div>
<div class="health-banner-text">
<div class="health-banner-title" id="health-banner-title">Checking system…</div>
<div class="health-banner-sub" id="health-banner-sub"></div>
</div>
<div class="health-banner-meta" id="health-banner-meta"></div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<h2 class="panel-title">Per-provider health</h2>
<button class="btn secondary sm" id="refresh-health">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M17.6 6.4A8 8 0 1 0 20 12h-2a6 6 0 1 1-1.8-4.2L13 11h7V4z"/></svg>
Refresh
</button>
</div>
<div class="panel-body">
<div class="table-wrap">
<table class="data-table" id="health-table">
<thead>
<tr>
<th scope="col">Provider</th>
<th scope="col">Healthy</th>
<th scope="col" class="num">Failures</th>
<th scope="col" class="num">Avg latency</th>
<th scope="col">Circuit</th>
<th scope="col">Last success</th>
<th scope="col">Last failure</th>
</tr>
</thead>
<tbody id="health-tbody"></tbody>
</table>
</div>
<p id="health-empty" class="empty-row" hidden>No provider health data available.</p>
</div>
</div>
</div>
</section>
</div>
</main>
</div>
<!-- ============== Loading overlay ============== -->
<div class="loading-overlay" id="loading-overlay" hidden role="status" aria-live="polite">
<div class="loading-card">
<div class="spinner" aria-hidden="true"></div>
<div class="loading-text" id="loading-text">Working…</div>
</div>
</div>
<!-- ============== Toast container ============== -->
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
<script src="app.js" defer></script>
</body>
</html>