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
Files changed (3) hide show
  1. frontend/camera.js +62 -7
  2. frontend/index.html +5 -5
  3. 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
- const data = await fetch("/analyze-document-vlm", {
270
- method: "POST",
271
- headers: { "Content-Type": "application/json" },
272
- body: JSON.stringify({ user_requested: true }),
273
- }).then((response) => response.json());
274
- toast(data.message);
275
- setTimeout(() => { $("#vlmButton").disabled = false; }, 1200);
 
 
 
 
 
 
 
 
 
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.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">Waiting for camera</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>Scan</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,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.1" defer></script><script src="/static/overlay.js?v=1.1" defer></script><script src="/static/feedback.js?v=1.1" defer></script><script src="/static/camera.js?v=1.1" defer></script>
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: 8px; min-height: 42px;
284
- border: 1px solid rgba(255, 255, 255, 0.16); padding: 8px 11px 8px 14px;
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: 11px; font-weight: 650; transition: border-color 0.2s, background 0.2s;
 
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);