DocUA commited on
Commit
7a195fe
·
1 Parent(s): 8ead42b

feat(ui): progress indicator while the cloud OCR reads a photo

Browse files
Files changed (1) hide show
  1. livemedcard/static/index.html +26 -2
livemedcard/static/index.html CHANGED
@@ -354,6 +354,14 @@
354
  .sphoto{border:1px dashed var(--line);border-radius:10px;padding:12px 14px;margin-top:14px}
355
  .sphoto img{width:100%;max-width:420px;border-radius:8px;border:1px solid var(--line);display:block;margin:9px 0}
356
  .sfoot{display:flex;gap:9px;justify-content:flex-end;padding:12px 18px;border-top:1px solid var(--line)}
 
 
 
 
 
 
 
 
357
  a#samples-photo-dl{display:inline-flex;align-items:center;text-decoration:none;cursor:pointer;
358
  font:inherit;font-weight:550;border:1px solid var(--line);border-radius:10px;padding:9px 15px;
359
  background:transparent;color:var(--ink);transition:.14s}
@@ -379,6 +387,7 @@
379
  .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
380
 
381
  @media (prefers-reduced-motion:reduce){
 
382
  *,*::before,*::after{animation-duration:.001s!important;animation-iteration-count:1!important;
383
  transition-duration:.001s!important}
384
  .spark path.line{stroke-dashoffset:0}
@@ -444,6 +453,7 @@
444
  </select>
445
  <button class="primary" id="add-submit">Process document</button>
446
  </div>
 
447
  </div>
448
 
449
  <div class="story" id="story" role="note">
@@ -533,6 +543,7 @@
533
  <button class="ghost" id="samples-photo-btn"></button>
534
  <a class="ghost" id="samples-photo-dl" href="/samples/image" download="livemedcard_sample_lab_report.jpg"></a>
535
  </div>
 
536
  </div>
537
  </div>
538
  <div class="sfoot">
@@ -636,6 +647,7 @@ const I18N = {
636
  toastExport:'FHIR bundle downloaded', toastNeedInput:'Enter text or choose a photo',
637
  ocrReading:'Reading (OCR)…', processing:'Processing…',
638
  toastOcrWait:'Reading the photo on the cloud OCR lab — up to ~90 s',
 
639
  toastEscalated:'Processed · escalated to human', toastAuto:'Processed · auto',
640
  toastNoFacts:'Document saved, but no measurements were extracted',
641
  errorPrefix:'Error: ', selected:name=>`Selected: ${name}`,
@@ -740,6 +752,7 @@ const I18N = {
740
  toastExport:'FHIR Bundle завантажено', toastNeedInput:'Введіть текст або виберіть фото',
741
  ocrReading:'Зчитування (OCR)…', processing:'Опрацювання…',
742
  toastOcrWait:'Фото читає хмарний OCR-лаб — до ~90 с',
 
743
  toastEscalated:'Оброблено · ескалація до людини', toastAuto:'Оброблено · авто',
744
  toastNoFacts:'Документ збережено, але жодного показника не витягнуто',
745
  errorPrefix:'Помилка: ', selected:name=>`Вибрано: ${name}`,
@@ -1260,6 +1273,7 @@ $('#samples-load').onclick=async()=>{
1260
  $('#samples-photo-btn').onclick=async()=>{
1261
  const t=I18N[LANG], b=$('#samples-photo-btn'), orig=b.textContent;
1262
  b.disabled=true; b.textContent=t.ocrReading; toast(t.toastOcrWait);
 
1263
  try{
1264
  const blob=await (await fetch('/samples/image')).blob();
1265
  const base64=await new Promise((res,rej)=>{
@@ -1270,7 +1284,7 @@ $('#samples-photo-btn').onclick=async()=>{
1270
  const got=(rep.extraction&&rep.extraction.observations||[]).length;
1271
  toast(got?(rep.decision.escalate?t.toastEscalated:t.toastAuto):t.toastNoFacts);
1272
  }catch(e){toast(t.errorPrefix+e.message);}
1273
- finally{b.disabled=false;b.textContent=orig;}
1274
  };
1275
  $('#btn-reset').onclick=async()=>{await api('/reset',{method:'POST'});$('#log').innerHTML='';await refresh();toast(I18N[LANG].toastReset);};
1276
  $('#btn-export').onclick=async()=>{
@@ -1283,6 +1297,14 @@ $('#btn-export').onclick=async()=>{
1283
  };
1284
  $('#btn-add').onclick=()=>$('#add-form').classList.toggle('show');
1285
 
 
 
 
 
 
 
 
 
1286
  /* Згода на відправку фото в хмару. Питаємо ПЕРЕД тим, як файл по��ине пристрій —
1287
  згода постфактум згодою не є. Локальний бекенд нічого не питає. */
1288
  function askCloudConsent(){
@@ -1322,11 +1344,13 @@ $('#add-submit').onclick=async()=>{
1322
  const text=$('#add-text').value.trim(), file=fileInput.files[0];
1323
  if(!text&&!file){toast(t.toastNeedInput);return;}
1324
  const btn=$('#add-submit'), orig=btn.textContent; btn.disabled=true;
 
1325
  try{
1326
  let rep;
1327
  if(file){
1328
  if(!await askCloudConsent()){ btn.disabled=false; btn.textContent=orig; return; }
1329
  btn.textContent=t.ocrReading;
 
1330
  // Хмарний OCR — це ~70–90 с. Без попередження мовчазне очікування читається
1331
  // як зависання (і саме так виглядає для журі, що клікне демо самостійно).
1332
  toast(t.toastOcrWait);
@@ -1347,7 +1371,7 @@ $('#add-submit').onclick=async()=>{
1347
  const got=(rep.extraction&&rep.extraction.observations||[]).length;
1348
  toast(got?(rep.decision.escalate?t.toastEscalated:t.toastAuto):t.toastNoFacts);
1349
  }catch(e){toast(t.errorPrefix+e.message);}
1350
- finally{btn.disabled=false;btn.textContent=orig;}
1351
  };
1352
 
1353
  $('#ask').onclick=()=>ask();
 
354
  .sphoto{border:1px dashed var(--line);border-radius:10px;padding:12px 14px;margin-top:14px}
355
  .sphoto img{width:100%;max-width:420px;border-radius:8px;border:1px solid var(--line);display:block;margin:9px 0}
356
  .sfoot{display:flex;gap:9px;justify-content:flex-end;padding:12px 18px;border-top:1px solid var(--line)}
357
+ .ocrp{display:none;margin-top:11px}
358
+ .ocrp.on{display:block}
359
+ .ocrp .track{height:4px;border-radius:99px;background:var(--line-soft);overflow:hidden}
360
+ .ocrp .track i{display:block;height:100%;width:38%;border-radius:99px;background:var(--accent);
361
+ animation:ocrslide 1.5s ease-in-out infinite}
362
+ @keyframes ocrslide{0%{transform:translateX(-100%)}100%{transform:translateX(360%)}}
363
+ .ocrp .lbl{margin-top:7px;font-family:var(--mono);font-size:11.5px;color:var(--muted)}
364
+ .ocrp .lbl b{color:var(--accent);font-weight:600;font-variant-numeric:tabular-nums}
365
  a#samples-photo-dl{display:inline-flex;align-items:center;text-decoration:none;cursor:pointer;
366
  font:inherit;font-weight:550;border:1px solid var(--line);border-radius:10px;padding:9px 15px;
367
  background:transparent;color:var(--ink);transition:.14s}
 
387
  .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
388
 
389
  @media (prefers-reduced-motion:reduce){
390
+ .ocrp .track i{animation:none;width:100%}
391
  *,*::before,*::after{animation-duration:.001s!important;animation-iteration-count:1!important;
392
  transition-duration:.001s!important}
393
  .spark path.line{stroke-dashoffset:0}
 
453
  </select>
454
  <button class="primary" id="add-submit">Process document</button>
455
  </div>
456
+ <div class="ocrp" id="add-ocrp"><div class="track"><i></i></div><div class="lbl"></div></div>
457
  </div>
458
 
459
  <div class="story" id="story" role="note">
 
543
  <button class="ghost" id="samples-photo-btn"></button>
544
  <a class="ghost" id="samples-photo-dl" href="/samples/image" download="livemedcard_sample_lab_report.jpg"></a>
545
  </div>
546
+ <div class="ocrp" id="samples-ocrp"><div class="track"><i></i></div><div class="lbl"></div></div>
547
  </div>
548
  </div>
549
  <div class="sfoot">
 
647
  toastExport:'FHIR bundle downloaded', toastNeedInput:'Enter text or choose a photo',
648
  ocrReading:'Reading (OCR)…', processing:'Processing…',
649
  toastOcrWait:'Reading the photo on the cloud OCR lab — up to ~90 s',
650
+ ocrProgress:sec=>`Reading on the cloud OCR lab — <b>${sec} s</b> · usually 40–90 s`,
651
  toastEscalated:'Processed · escalated to human', toastAuto:'Processed · auto',
652
  toastNoFacts:'Document saved, but no measurements were extracted',
653
  errorPrefix:'Error: ', selected:name=>`Selected: ${name}`,
 
752
  toastExport:'FHIR Bundle завантажено', toastNeedInput:'Введіть текст або виберіть фото',
753
  ocrReading:'Зчитування (OCR)…', processing:'Опрацювання…',
754
  toastOcrWait:'Фото читає хмарний OCR-лаб — до ~90 с',
755
+ ocrProgress:sec=>`Читає хмарний OCR-лаб — <b>${sec} с</b> · зазвичай 40–90 с`,
756
  toastEscalated:'Оброблено · ескалація до людини', toastAuto:'Оброблено · авто',
757
  toastNoFacts:'Документ збережено, але жодного показника не витягнуто',
758
  errorPrefix:'Помилка: ', selected:name=>`Вибрано: ${name}`,
 
1273
  $('#samples-photo-btn').onclick=async()=>{
1274
  const t=I18N[LANG], b=$('#samples-photo-btn'), orig=b.textContent;
1275
  b.disabled=true; b.textContent=t.ocrReading; toast(t.toastOcrWait);
1276
+ const stop=startOcrProgress('#samples-ocrp');
1277
  try{
1278
  const blob=await (await fetch('/samples/image')).blob();
1279
  const base64=await new Promise((res,rej)=>{
 
1284
  const got=(rep.extraction&&rep.extraction.observations||[]).length;
1285
  toast(got?(rep.decision.escalate?t.toastEscalated:t.toastAuto):t.toastNoFacts);
1286
  }catch(e){toast(t.errorPrefix+e.message);}
1287
+ finally{stop();b.disabled=false;b.textContent=orig;}
1288
  };
1289
  $('#btn-reset').onclick=async()=>{await api('/reset',{method:'POST'});$('#log').innerHTML='';await refresh();toast(I18N[LANG].toastReset);};
1290
  $('#btn-export').onclick=async()=>{
 
1297
  };
1298
  $('#btn-add').onclick=()=>$('#add-form').classList.toggle('show');
1299
 
1300
+ function startOcrProgress(id){
1301
+ const box=$(id), lbl=box.querySelector('.lbl'), t0=Date.now();
1302
+ const tick=()=>{lbl.innerHTML=I18N[LANG].ocrProgress(Math.round((Date.now()-t0)/1000));};
1303
+ tick(); box.classList.add('on');
1304
+ const iv=setInterval(tick,1000);
1305
+ return ()=>{clearInterval(iv); box.classList.remove('on'); lbl.textContent='';};
1306
+ }
1307
+
1308
  /* Згода на відправку фото в хмару. Питаємо ПЕРЕД тим, як файл по��ине пристрій —
1309
  згода постфактум згодою не є. Локальний бекенд нічого не питає. */
1310
  function askCloudConsent(){
 
1344
  const text=$('#add-text').value.trim(), file=fileInput.files[0];
1345
  if(!text&&!file){toast(t.toastNeedInput);return;}
1346
  const btn=$('#add-submit'), orig=btn.textContent; btn.disabled=true;
1347
+ let stopOcr=null;
1348
  try{
1349
  let rep;
1350
  if(file){
1351
  if(!await askCloudConsent()){ btn.disabled=false; btn.textContent=orig; return; }
1352
  btn.textContent=t.ocrReading;
1353
+ stopOcr=startOcrProgress('#add-ocrp');
1354
  // Хмарний OCR — це ~70–90 с. Без попередження мовчазне очікування читається
1355
  // як зависання (і саме так виглядає для журі, що клікне демо самостійно).
1356
  toast(t.toastOcrWait);
 
1371
  const got=(rep.extraction&&rep.extraction.observations||[]).length;
1372
  toast(got?(rep.decision.escalate?t.toastEscalated:t.toastAuto):t.toastNoFacts);
1373
  }catch(e){toast(t.errorPrefix+e.message);}
1374
+ finally{if(stopOcr)stopOcr();btn.disabled=false;btn.textContent=orig;}
1375
  };
1376
 
1377
  $('#ask').onclick=()=>ask();