Spaces:
Runtime error
Runtime error
Antigravity AI Agent commited on
Commit ·
5ed261a
1
Parent(s): 573f96d
UX: Remove 'Waiting for camera', icon toggle, native phone camera, fix VLM error
Browse files- frontend/camera.js +62 -7
- frontend/index.html +5 -5
- frontend/styles.css +5 -3
frontend/camera.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
| 41 |
$("#statusPill").classList.add("live");
|
| 42 |
$("#statusPill span").textContent = "Live · private preview";
|
| 43 |
$("#captureButton").disabled = false;
|
|
|
|
| 44 |
setStatus("Camera ready", "Position the document inside the frame and hold still. Capture starts automatically when the page is clear.");
|
| 45 |
requestAnimationFrame(monitor);
|
| 46 |
} catch (error) {
|
|
@@ -87,6 +88,7 @@
|
|
| 87 |
: !sharp ? "Move closer / focus"
|
| 88 |
: !still ? "Hold steady"
|
| 89 |
: stableFrames < 12 ? "Almost stable…" : "Frame stable";
|
|
|
|
| 90 |
|
| 91 |
if (stableFrames >= 12 && !processing && Date.now() - lastAutoScan > 8000) {
|
| 92 |
lastAutoScan = Date.now();
|
|
@@ -184,6 +186,7 @@
|
|
| 184 |
$("#statusPill").classList.add("live");
|
| 185 |
$("#statusPill span").textContent = "Uploaded · not stored";
|
| 186 |
$("#qualityPill").textContent = file.name;
|
|
|
|
| 187 |
$("#captureButton").disabled = true;
|
| 188 |
renderAnalysis(data, data.frame_width, data.frame_height);
|
| 189 |
} catch (error) {
|
|
@@ -266,13 +269,22 @@
|
|
| 266 |
|
| 267 |
async function analyzeVlm() {
|
| 268 |
$("#vlmButton").disabled = true;
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
}
|
| 277 |
|
| 278 |
function setStatus(title, message) {
|
|
@@ -292,7 +304,50 @@
|
|
| 292 |
window.FalconFeedback.init();
|
| 293 |
window.FalconInsights.init();
|
| 294 |
setDetails(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
$("#startCamera").onclick = start;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
$("#captureButton").onclick = analyze;
|
| 297 |
$("#vlmButton").onclick = analyzeVlm;
|
| 298 |
$("#detailsToggle").onclick = () => setDetails(!$("#scannerShell").classList.contains("details-open"));
|
|
|
|
| 41 |
$("#statusPill").classList.add("live");
|
| 42 |
$("#statusPill span").textContent = "Live · private preview";
|
| 43 |
$("#captureButton").disabled = false;
|
| 44 |
+
$("#qualityPill").hidden = false;
|
| 45 |
setStatus("Camera ready", "Position the document inside the frame and hold still. Capture starts automatically when the page is clear.");
|
| 46 |
requestAnimationFrame(monitor);
|
| 47 |
} catch (error) {
|
|
|
|
| 88 |
: !sharp ? "Move closer / focus"
|
| 89 |
: !still ? "Hold steady"
|
| 90 |
: stableFrames < 12 ? "Almost stable…" : "Frame stable";
|
| 91 |
+
$("#qualityPill").hidden = false;
|
| 92 |
|
| 93 |
if (stableFrames >= 12 && !processing && Date.now() - lastAutoScan > 8000) {
|
| 94 |
lastAutoScan = Date.now();
|
|
|
|
| 186 |
$("#statusPill").classList.add("live");
|
| 187 |
$("#statusPill span").textContent = "Uploaded · not stored";
|
| 188 |
$("#qualityPill").textContent = file.name;
|
| 189 |
+
$("#qualityPill").hidden = false;
|
| 190 |
$("#captureButton").disabled = true;
|
| 191 |
renderAnalysis(data, data.frame_width, data.frame_height);
|
| 192 |
} catch (error) {
|
|
|
|
| 269 |
|
| 270 |
async function analyzeVlm() {
|
| 271 |
$("#vlmButton").disabled = true;
|
| 272 |
+
try {
|
| 273 |
+
const response = await fetch("/analyze-document-vlm", {
|
| 274 |
+
method: "POST",
|
| 275 |
+
headers: { "Content-Type": "application/json" },
|
| 276 |
+
body: JSON.stringify({ user_requested: true }),
|
| 277 |
+
});
|
| 278 |
+
const data = await response.json();
|
| 279 |
+
if (!response.ok) {
|
| 280 |
+
throw new Error(data.detail || "Full document analysis failed");
|
| 281 |
+
}
|
| 282 |
+
toast(data.message || "Analysis complete");
|
| 283 |
+
} catch (error) {
|
| 284 |
+
toast(error.message || "Full document analysis is not available on this device.");
|
| 285 |
+
} finally {
|
| 286 |
+
setTimeout(() => { $("#vlmButton").disabled = false; }, 1200);
|
| 287 |
+
}
|
| 288 |
}
|
| 289 |
|
| 290 |
function setStatus(title, message) {
|
|
|
|
| 304 |
window.FalconFeedback.init();
|
| 305 |
window.FalconInsights.init();
|
| 306 |
setDetails(false);
|
| 307 |
+
|
| 308 |
+
// Detect touch/mobile device
|
| 309 |
+
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || ('ontouchstart' in window);
|
| 310 |
+
if (isMobile) {
|
| 311 |
+
$("#startCamera").style.display = "none";
|
| 312 |
+
$("#startCameraPhone").style.display = "";
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
$("#startCamera").onclick = start;
|
| 316 |
+
|
| 317 |
+
// Mobile: use native camera input for photo capture
|
| 318 |
+
$("#startCameraPhone").onclick = () => {
|
| 319 |
+
$("#phoneCameraInput").click();
|
| 320 |
+
};
|
| 321 |
+
$("#phoneCameraInput").onchange = async (event) => {
|
| 322 |
+
const file = event.target.files?.[0];
|
| 323 |
+
event.target.value = "";
|
| 324 |
+
if (!file) return;
|
| 325 |
+
// Normalize to JPEG for iOS compatibility (HEIC/HEIF may not be supported server-side)
|
| 326 |
+
let processFile = file;
|
| 327 |
+
const mimeType = file.type.toLowerCase();
|
| 328 |
+
if (mimeType === "image/heic" || mimeType === "image/heif" || !file.name.match(/\.(jpg|jpeg|png|webp)$/i)) {
|
| 329 |
+
try {
|
| 330 |
+
const objectUrl = URL.createObjectURL(file);
|
| 331 |
+
const img = await new Promise((res, rej) => {
|
| 332 |
+
const image = new Image();
|
| 333 |
+
image.onload = () => res(image);
|
| 334 |
+
image.onerror = rej;
|
| 335 |
+
image.src = objectUrl;
|
| 336 |
+
});
|
| 337 |
+
const cvs = document.createElement("canvas");
|
| 338 |
+
cvs.width = img.naturalWidth; cvs.height = img.naturalHeight;
|
| 339 |
+
cvs.getContext("2d").drawImage(img, 0, 0);
|
| 340 |
+
const blob = await new Promise(res => cvs.toBlob(res, "image/jpeg", 0.85));
|
| 341 |
+
URL.revokeObjectURL(objectUrl);
|
| 342 |
+
processFile = new File([blob], "camera-photo.jpg", { type: "image/jpeg" });
|
| 343 |
+
} catch (_) {
|
| 344 |
+
// fallback: try passing original
|
| 345 |
+
processFile = new File([file], "camera-photo.jpg", { type: "image/jpeg" });
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
handleUpload({ target: { files: [processFile], value: "" } });
|
| 349 |
+
};
|
| 350 |
+
|
| 351 |
$("#captureButton").onclick = analyze;
|
| 352 |
$("#vlmButton").onclick = analyzeVlm;
|
| 353 |
$("#detailsToggle").onclick = () => setDetails(!$("#scannerShell").classList.contains("details-open"));
|
frontend/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 6 |
<meta name="theme-color" content="#071a18">
|
| 7 |
<title>FalconScan — Customs terms, made clear</title>
|
| 8 |
-
<link rel="stylesheet" href="/static/styles.css?v=1.
|
| 9 |
</head>
|
| 10 |
<body>
|
| 11 |
<header>
|
|
@@ -39,13 +39,13 @@
|
|
| 39 |
<canvas id="captureCanvas" hidden></canvas>
|
| 40 |
<div id="overlay" class="overlay"></div>
|
| 41 |
<div class="corner tl"></div><div class="corner tr"></div><div class="corner bl"></div><div class="corner br"></div>
|
| 42 |
-
<div id="emptyState" class="empty-state"><div class="lens">â—Ž</div><strong>Camera is ready when you are</strong><span>Position a customs document inside the frame</span><button id="startCamera" class="primary">Start camera</button></div>
|
| 43 |
<div id="scanLine" class="scan-line"></div>
|
| 44 |
<div id="statusPill" class="status-pill"><i></i><span>Camera off</span></div>
|
| 45 |
-
<div id="qualityPill" class="quality-pill"
|
| 46 |
<div id="selectionHint" class="selection-hint" hidden>Select or highlight document text for a live business insight</div>
|
| 47 |
<button id="documentInfo" class="document-info" type="button" aria-label="Summarize this document" hidden><span aria-hidden="true">i</span></button>
|
| 48 |
-
<button id="detailsToggle" class="details-toggle" aria-expanded="false" aria-controls="scanDetails"><span>
|
| 49 |
</div>
|
| 50 |
<aside id="scanDetails" class="control-panel" aria-hidden="true">
|
| 51 |
<button id="detailsClose" class="details-close" aria-label="Close scan details">×</button>
|
|
@@ -69,6 +69,6 @@
|
|
| 69 |
<div id="adminModal" class="modal" aria-hidden="true"><div class="admin-card" role="dialog" aria-modal="true"><button class="close" data-close="adminModal">×</button><p class="eyebrow">KNOWLEDGE GOVERNANCE</p><h2>SME correction review</h2><p class="muted">Approve suggestions to make them the official definition.</p><div id="adminList" class="admin-list"></div></div></div>
|
| 70 |
<div id="insightModal" class="modal" aria-hidden="true"><div class="insight-card" role="dialog" aria-modal="true" aria-labelledby="insightTitle"><button id="insightClose" class="close" aria-label="Close insight">×</button><p class="eyebrow">LIVE DOCUMENT INSIGHT</p><h2 id="insightTitle">Selection insight</h2><blockquote id="insightSelection"></blockquote><section><small>SUMMARY</small><p id="insightSummary"></p></section><section><small>BUSINESS MEANING</small><p id="insightBusiness"></p></section><div id="insightTerms" class="insight-terms"></div><div class="definition-meta"><span id="insightSource"></span><span id="insightConfidence"></span></div></div></div>
|
| 71 |
<div id="toast" class="toast"></div>
|
| 72 |
-
<script src="/static/insights.js?v=1.
|
| 73 |
</body>
|
| 74 |
</html>
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 6 |
<meta name="theme-color" content="#071a18">
|
| 7 |
<title>FalconScan — Customs terms, made clear</title>
|
| 8 |
+
<link rel="stylesheet" href="/static/styles.css?v=1.2">
|
| 9 |
</head>
|
| 10 |
<body>
|
| 11 |
<header>
|
|
|
|
| 39 |
<canvas id="captureCanvas" hidden></canvas>
|
| 40 |
<div id="overlay" class="overlay"></div>
|
| 41 |
<div class="corner tl"></div><div class="corner tr"></div><div class="corner bl"></div><div class="corner br"></div>
|
| 42 |
+
<div id="emptyState" class="empty-state"><div class="lens">◎</div><strong>Camera is ready when you are</strong><span>Position a customs document inside the frame</span><button id="startCamera" class="primary">Start Camera</button><button id="startCameraPhone" class="primary phone-camera-btn" style="display:none">📷 Use Camera</button><input id="phoneCameraInput" type="file" accept="image/*" capture="environment" hidden></div>
|
| 43 |
<div id="scanLine" class="scan-line"></div>
|
| 44 |
<div id="statusPill" class="status-pill"><i></i><span>Camera off</span></div>
|
| 45 |
+
<div id="qualityPill" class="quality-pill" hidden></div>
|
| 46 |
<div id="selectionHint" class="selection-hint" hidden>Select or highlight document text for a live business insight</div>
|
| 47 |
<button id="documentInfo" class="document-info" type="button" aria-label="Summarize this document" hidden><span aria-hidden="true">i</span></button>
|
| 48 |
+
<button id="detailsToggle" class="details-toggle" aria-expanded="false" aria-controls="scanDetails"><span aria-hidden="true">⊹</span><i aria-hidden="true">⌃</i></button>
|
| 49 |
</div>
|
| 50 |
<aside id="scanDetails" class="control-panel" aria-hidden="true">
|
| 51 |
<button id="detailsClose" class="details-close" aria-label="Close scan details">×</button>
|
|
|
|
| 69 |
<div id="adminModal" class="modal" aria-hidden="true"><div class="admin-card" role="dialog" aria-modal="true"><button class="close" data-close="adminModal">×</button><p class="eyebrow">KNOWLEDGE GOVERNANCE</p><h2>SME correction review</h2><p class="muted">Approve suggestions to make them the official definition.</p><div id="adminList" class="admin-list"></div></div></div>
|
| 70 |
<div id="insightModal" class="modal" aria-hidden="true"><div class="insight-card" role="dialog" aria-modal="true" aria-labelledby="insightTitle"><button id="insightClose" class="close" aria-label="Close insight">×</button><p class="eyebrow">LIVE DOCUMENT INSIGHT</p><h2 id="insightTitle">Selection insight</h2><blockquote id="insightSelection"></blockquote><section><small>SUMMARY</small><p id="insightSummary"></p></section><section><small>BUSINESS MEANING</small><p id="insightBusiness"></p></section><div id="insightTerms" class="insight-terms"></div><div class="definition-meta"><span id="insightSource"></span><span id="insightConfidence"></span></div></div></div>
|
| 71 |
<div id="toast" class="toast"></div>
|
| 72 |
+
<script src="/static/insights.js?v=1.2" defer></script><script src="/static/overlay.js?v=1.2" defer></script><script src="/static/feedback.js?v=1.2" defer></script><script src="/static/camera.js?v=1.2" defer></script>
|
| 73 |
</body>
|
| 74 |
</html>
|
frontend/styles.css
CHANGED
|
@@ -264,6 +264,7 @@ main { width: 100%; max-width: 1320px; margin: auto; padding: 30px 14px calc(64p
|
|
| 264 |
}
|
| 265 |
.status-pill { left: 12px; }
|
| 266 |
.quality-pill { right: 12px; top: 58px; }
|
|
|
|
| 267 |
.status-pill i { display: inline-block; width: 7px; height: 7px; margin-right: 5px; background: #647570; border-radius: 50%; }
|
| 268 |
|
| 269 |
/* Status pill indicator pulsing animation */
|
|
@@ -280,11 +281,12 @@ main { width: 100%; max-width: 1320px; margin: auto; padding: 30px 14px calc(64p
|
|
| 280 |
/* Drawer Toggle Control */
|
| 281 |
.details-toggle {
|
| 282 |
position: absolute; z-index: 3; left: 50%; bottom: 14px; transform: translateX(-50%);
|
| 283 |
-
display: flex; align-items: center; gap:
|
| 284 |
-
border: 1px solid rgba(255, 255, 255, 0.16); padding:
|
| 285 |
background: rgba(14, 23, 21, 0.85); color: white; border-radius: 99px;
|
| 286 |
backdrop-filter: saturate(160%) blur(16px); box-shadow: 0 8px 30px rgba(0,0,0,.4);
|
| 287 |
-
font-size:
|
|
|
|
| 288 |
}
|
| 289 |
.details-toggle:hover {
|
| 290 |
background: rgba(22, 38, 34, 0.95);
|
|
|
|
| 264 |
}
|
| 265 |
.status-pill { left: 12px; }
|
| 266 |
.quality-pill { right: 12px; top: 58px; }
|
| 267 |
+
.quality-pill[hidden] { display: none; }
|
| 268 |
.status-pill i { display: inline-block; width: 7px; height: 7px; margin-right: 5px; background: #647570; border-radius: 50%; }
|
| 269 |
|
| 270 |
/* Status pill indicator pulsing animation */
|
|
|
|
| 281 |
/* Drawer Toggle Control */
|
| 282 |
.details-toggle {
|
| 283 |
position: absolute; z-index: 3; left: 50%; bottom: 14px; transform: translateX(-50%);
|
| 284 |
+
display: flex; align-items: center; gap: 6px; min-height: 36px;
|
| 285 |
+
border: 1px solid rgba(255, 255, 255, 0.16); padding: 7px 12px;
|
| 286 |
background: rgba(14, 23, 21, 0.85); color: white; border-radius: 99px;
|
| 287 |
backdrop-filter: saturate(160%) blur(16px); box-shadow: 0 8px 30px rgba(0,0,0,.4);
|
| 288 |
+
font-size: 14px; font-weight: 650; transition: border-color 0.2s, background 0.2s;
|
| 289 |
+
white-space: nowrap;
|
| 290 |
}
|
| 291 |
.details-toggle:hover {
|
| 292 |
background: rgba(22, 38, 34, 0.95);
|