Update tagger_ui/templates/index.html
Browse files
tagger_ui/templates/index.html
CHANGED
|
@@ -202,6 +202,36 @@
|
|
| 202 |
.tag-pill:hover { opacity: .8; }
|
| 203 |
.tag-pill .score { font-size: .66rem; opacity: .7; }
|
| 204 |
.tag-pill.hidden { display: none; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
</style>
|
| 206 |
</head>
|
| 207 |
<body>
|
|
@@ -239,12 +269,22 @@
|
|
| 239 |
|
| 240 |
<div id="results-area">
|
| 241 |
|
| 242 |
-
<!-- image
|
| 243 |
<div class="preview-wrap">
|
| 244 |
-
<div
|
| 245 |
<img id="preview-img" src="" alt="preview" />
|
| 246 |
<div class="img-meta" id="img-meta"></div>
|
| 247 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
</div>
|
| 249 |
|
| 250 |
<!-- global copy bar -->
|
|
@@ -298,6 +338,48 @@
|
|
| 298 |
if (el) el.value = Math.max(1, Math.min(99, parseInt(pct) || 1));
|
| 299 |
}
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
// ---- drag & drop ----
|
| 302 |
const dz = document.getElementById('drop-zone');
|
| 303 |
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag-over'); });
|
|
@@ -314,6 +396,8 @@
|
|
| 314 |
const url = document.getElementById('url-input').value.trim();
|
| 315 |
if (!url) return;
|
| 316 |
setPreview(url, url);
|
|
|
|
|
|
|
| 317 |
submitFetch(`/tag/url?max_size=${document.getElementById('maxsize-input').value}&url=${encodeURIComponent(url)}`,
|
| 318 |
{ method: 'POST' });
|
| 319 |
}
|
|
@@ -325,6 +409,8 @@
|
|
| 325 |
const reader = new FileReader();
|
| 326 |
reader.onload = e => setPreview(e.target.result, file.name);
|
| 327 |
reader.readAsDataURL(file);
|
|
|
|
|
|
|
| 328 |
submitFetch(`/tag/upload?max_size=${maxSize}`, { method: 'POST', body: fd });
|
| 329 |
}
|
| 330 |
|
|
|
|
| 202 |
.tag-pill:hover { opacity: .8; }
|
| 203 |
.tag-pill .score { font-size: .66rem; opacity: .7; }
|
| 204 |
.tag-pill.hidden { display: none; }
|
| 205 |
+
|
| 206 |
+
/* ---- PCA panel ---- */
|
| 207 |
+
.preview-wrap { flex-wrap: wrap; }
|
| 208 |
+
.preview-col { flex: 1 1 0; min-width: 0; }
|
| 209 |
+
.pca-col {
|
| 210 |
+
flex: 1 1 0; min-width: 0;
|
| 211 |
+
display: flex; flex-direction: column; gap: .5rem;
|
| 212 |
+
}
|
| 213 |
+
.pca-label {
|
| 214 |
+
font-size: .72rem; color: var(--muted); text-align: center;
|
| 215 |
+
letter-spacing: .04em; text-transform: uppercase;
|
| 216 |
+
}
|
| 217 |
+
#pca-img {
|
| 218 |
+
border-radius: var(--radius); width: 100%; max-height: 420px;
|
| 219 |
+
object-fit: contain; border: 1px solid var(--border);
|
| 220 |
+
display: block; image-rendering: pixelated;
|
| 221 |
+
}
|
| 222 |
+
#pca-spinner {
|
| 223 |
+
display: none; width: 18px; height: 18px; margin: auto;
|
| 224 |
+
border: 3px solid var(--border); border-top-color: var(--accent);
|
| 225 |
+
border-radius: 50%; animation: spin .7s linear infinite;
|
| 226 |
+
}
|
| 227 |
+
.pca-toggle {
|
| 228 |
+
background: var(--bg); border: 1px solid var(--border);
|
| 229 |
+
border-radius: 6px; color: var(--muted); cursor: pointer;
|
| 230 |
+
font-size: .75rem; padding: .3rem .7rem; align-self: center;
|
| 231 |
+
transition: border-color .15s, color .15s;
|
| 232 |
+
}
|
| 233 |
+
.pca-toggle:hover { border-color: var(--accent); color: var(--text); }
|
| 234 |
+
.pca-toggle.active { border-color: var(--accent); color: #a78bfa; }
|
| 235 |
</style>
|
| 236 |
</head>
|
| 237 |
<body>
|
|
|
|
| 269 |
|
| 270 |
<div id="results-area">
|
| 271 |
|
| 272 |
+
<!-- image + PCA side by side -->
|
| 273 |
<div class="preview-wrap">
|
| 274 |
+
<div class="preview-col">
|
| 275 |
<img id="preview-img" src="" alt="preview" />
|
| 276 |
<div class="img-meta" id="img-meta"></div>
|
| 277 |
</div>
|
| 278 |
+
<div class="pca-col" id="pca-col" style="display:none">
|
| 279 |
+
<div class="pca-label">PCA 路 patch features (R=PC1, G=PC2, B=PC3)</div>
|
| 280 |
+
<div id="pca-spinner"></div>
|
| 281 |
+
<img id="pca-img" src="" alt="PCA" style="display:none" />
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<!-- PCA toggle -->
|
| 286 |
+
<div style="display:flex;justify-content:flex-end;margin-bottom:.6rem">
|
| 287 |
+
<button class="pca-toggle" id="pca-toggle" onclick="togglePca()">Show PCA</button>
|
| 288 |
</div>
|
| 289 |
|
| 290 |
<!-- global copy bar -->
|
|
|
|
| 338 |
if (el) el.value = Math.max(1, Math.min(99, parseInt(pct) || 1));
|
| 339 |
}
|
| 340 |
|
| 341 |
+
// ---- PCA state ----
|
| 342 |
+
let _pcaEnabled = false;
|
| 343 |
+
let _lastPcaRequest = null; // { type: 'url'|'file', url?: string, file?: File }
|
| 344 |
+
|
| 345 |
+
function togglePca() {
|
| 346 |
+
_pcaEnabled = !_pcaEnabled;
|
| 347 |
+
const btn = document.getElementById('pca-toggle');
|
| 348 |
+
btn.textContent = _pcaEnabled ? 'Hide PCA' : 'Show PCA';
|
| 349 |
+
btn.classList.toggle('active', _pcaEnabled);
|
| 350 |
+
document.getElementById('pca-col').style.display = _pcaEnabled ? 'flex' : 'none';
|
| 351 |
+
if (_pcaEnabled && _lastPcaRequest) runPca(_lastPcaRequest);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
function runPca(req) {
|
| 355 |
+
const spinner = document.getElementById('pca-spinner');
|
| 356 |
+
const img = document.getElementById('pca-img');
|
| 357 |
+
spinner.style.display = 'block';
|
| 358 |
+
img.style.display = 'none';
|
| 359 |
+
|
| 360 |
+
const maxSize = document.getElementById('maxsize-input').value;
|
| 361 |
+
let fetchPromise;
|
| 362 |
+
if (req.type === 'url') {
|
| 363 |
+
fetchPromise = fetch(
|
| 364 |
+
`/pca/url?max_size=${maxSize}&url=${encodeURIComponent(req.url)}`,
|
| 365 |
+
{ method: 'POST' }
|
| 366 |
+
);
|
| 367 |
+
} else {
|
| 368 |
+
const fd = new FormData();
|
| 369 |
+
fd.append('file', req.file);
|
| 370 |
+
fetchPromise = fetch(`/pca/upload?max_size=${maxSize}`, { method: 'POST', body: fd });
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
fetchPromise
|
| 374 |
+
.then(r => r.ok ? r.blob() : Promise.reject('PCA failed'))
|
| 375 |
+
.then(blob => {
|
| 376 |
+
img.src = URL.createObjectURL(blob);
|
| 377 |
+
img.style.display = 'block';
|
| 378 |
+
})
|
| 379 |
+
.catch(() => { img.style.display = 'none'; })
|
| 380 |
+
.finally(() => { spinner.style.display = 'none'; });
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
// ---- drag & drop ----
|
| 384 |
const dz = document.getElementById('drop-zone');
|
| 385 |
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag-over'); });
|
|
|
|
| 396 |
const url = document.getElementById('url-input').value.trim();
|
| 397 |
if (!url) return;
|
| 398 |
setPreview(url, url);
|
| 399 |
+
_lastPcaRequest = { type: 'url', url };
|
| 400 |
+
if (_pcaEnabled) runPca(_lastPcaRequest);
|
| 401 |
submitFetch(`/tag/url?max_size=${document.getElementById('maxsize-input').value}&url=${encodeURIComponent(url)}`,
|
| 402 |
{ method: 'POST' });
|
| 403 |
}
|
|
|
|
| 409 |
const reader = new FileReader();
|
| 410 |
reader.onload = e => setPreview(e.target.result, file.name);
|
| 411 |
reader.readAsDataURL(file);
|
| 412 |
+
_lastPcaRequest = { type: 'file', file };
|
| 413 |
+
if (_pcaEnabled) runPca(_lastPcaRequest);
|
| 414 |
submitFetch(`/tag/upload?max_size=${maxSize}`, { method: 'POST', body: fd });
|
| 415 |
}
|
| 416 |
|