DocUA commited on
Commit
fc7f010
·
1 Parent(s): 5a3f318

feat(ui): preview sample documents before ingesting them

Browse files

Кнопка «Load demo card» спершу показує сирі документи (і синтетичний бланк
для фото-шляху), і лише потім вантажить їх у картку — щоб витяг можна було
перевірити, а не приймати на віру.

- GET /samples, GET /samples/image
- модалка перегляду (UA/EN)

.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.jpg filter=lfs diff=lfs merge=lfs -text
2
+ livemedcard/static/sample_lab_report.jpg filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
livemedcard/api.py CHANGED
@@ -264,6 +264,22 @@ def ingest_image(req: ImageIngestRequest, state: _State = Depends(_get_state)):
264
 
265
 
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  @app.post("/ingest/samples")
268
  def ingest_samples(lang: str = "uk", state: _State = Depends(_get_state)):
269
  """Демо-документи мовою `lang` (`uk`|`en`) — набори дзеркальні за значеннями."""
 
264
 
265
 
266
 
267
+ @app.get("/samples")
268
+ def samples(lang: str = "uk"):
269
+ """Демо-документи як вхідні дані — БЕЗ завантаження в картку.
270
+
271
+ Дає журі побачити сирий документ до того, як картка з нього щось витягне:
272
+ спершу вхід, потім результат. Сесія не потрібна — нічого не змінюємо.
273
+ """
274
+ return {"documents": load_sample_docs(lang)}
275
+
276
+
277
+ @app.get("/samples/image")
278
+ def sample_image() -> FileResponse:
279
+ """Синтетичний бланк лабораторії — фото-шлях (OCR) для того самого порівняння."""
280
+ return FileResponse(_STATIC / "sample_lab_report.jpg", media_type="image/jpeg")
281
+
282
+
283
  @app.post("/ingest/samples")
284
  def ingest_samples(lang: str = "uk", state: _State = Depends(_get_state)):
285
  """Демо-документи мовою `lang` (`uk`|`en`) — набори дзеркальні за значеннями."""
livemedcard/static/index.html CHANGED
@@ -345,6 +345,16 @@
345
  .gwrap.sel .node.on text{fill:var(--ink);font-weight:700}
346
  .drawer .body{padding:14px 18px 20px;overflow:auto;white-space:pre-wrap;
347
  font-size:13.5px;line-height:1.75;color:var(--muted)}
 
 
 
 
 
 
 
 
 
 
348
  /* ── cloud consent (photo leaves the device — say so before it does) ── */
349
  .consent{position:fixed;left:50%;top:50%;transform:translate(-50%,-46%) scale(.98);
350
  width:min(470px,92vw);background:var(--panel);border:1px solid var(--line);border-radius:14px;
@@ -502,6 +512,28 @@
502
  </div>
503
  </aside>
504
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  <div class="drawer-back" id="help-back"></div>
506
  <aside class="help" id="help" role="dialog" aria-modal="true" aria-labelledby="help-title">
507
  <div class="head">
@@ -587,6 +619,13 @@ const I18N = {
587
  escalate:'escalated to human (L4)', auto:'automatic processing',
588
  sevLabel:{critical:'critical',major:'high',moderate:'moderate',info:'info'},
589
  toastSamples:'Demo card loaded', toastReset:'Card reset',
 
 
 
 
 
 
 
590
  toastExport:'FHIR bundle downloaded', toastNeedInput:'Enter text or choose a photo',
591
  ocrReading:'Reading (OCR)…', processing:'Processing…',
592
  toastOcrWait:'Reading the photo on the cloud OCR lab — up to ~90 s',
@@ -684,6 +723,13 @@ const I18N = {
684
  escalate:'ескалація до людини (L4)', auto:'автоматична обробка',
685
  sevLabel:{critical:'критичний',major:'високий',moderate:'помірний',info:'інфо'},
686
  toastSamples:'Демо-картку завантажено', toastReset:'Картку скинуто',
 
 
 
 
 
 
 
687
  toastExport:'FHIR Bundle завантажено', toastNeedInput:'Введіть текст або виберіть фото',
688
  ocrReading:'Зчитування (OCR)…', processing:'Опрацювання…',
689
  toastOcrWait:'Фото читає хмарний OCR-лаб — до ~90 с',
@@ -1045,7 +1091,7 @@ async function openDoc(docId, span){
1045
  function closeDoc(){$('#drawer').classList.remove('show');$('#drawer-back').classList.remove('show');}
1046
  $('#drawer-close').onclick=closeDoc;
1047
  $('#drawer-back').onclick=closeDoc;
1048
- addEventListener('keydown',e=>{if(e.key==='Escape'){closeDoc();closeHelp();closeGraph();}});
1049
 
1050
  // ── довідка ───────────────────────────────────────────────
1051
  function renderHelp(){
@@ -1159,10 +1205,63 @@ async function ask(q){
1159
  }catch(e){toast(I18N[LANG].errorPrefix+e.message);}
1160
  }
1161
 
1162
- // Демо-документи — мовою інтерфейсу (набори дзеркальні: ті самі значення, ті самі сигнали).
1163
- $('#btn-samples').onclick=async()=>{
1164
- await api(`/ingest/samples?lang=${LANG==='ua'?'uk':'en'}`,{method:'POST'});
1165
- await refresh();toast(I18N[LANG].toastSamples);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1166
  };
1167
  $('#btn-reset').onclick=async()=>{await api('/reset',{method:'POST'});$('#log').innerHTML='';await refresh();toast(I18N[LANG].toastReset);};
1168
  $('#btn-export').onclick=async()=>{
 
345
  .gwrap.sel .node.on text{fill:var(--ink);font-weight:700}
346
  .drawer .body{padding:14px 18px 20px;overflow:auto;white-space:pre-wrap;
347
  font-size:13.5px;line-height:1.75;color:var(--muted)}
348
+ /* ── зразки документів: спершу вхід, потім результат ── */
349
+ .sdoc{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px;background:var(--panel-2)}
350
+ .sdoc .sh{display:flex;gap:9px;align-items:baseline;margin-bottom:7px}
351
+ .sdoc .sk{font-size:12px;font-weight:600;color:var(--ink)}
352
+ .sdoc .sd{font-family:var(--mono);font-size:11px;color:var(--muted)}
353
+ .sdoc .st{font-family:var(--mono);font-size:12px;line-height:1.65;color:var(--muted);white-space:pre-wrap;word-break:break-word;margin:0}
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
+
358
  /* ── cloud consent (photo leaves the device — say so before it does) ── */
359
  .consent{position:fixed;left:50%;top:50%;transform:translate(-50%,-46%) scale(.98);
360
  width:min(470px,92vw);background:var(--panel);border:1px solid var(--line);border-radius:14px;
 
512
  </div>
513
  </aside>
514
 
515
+ <div class="drawer-back" id="samples-back"></div>
516
+ <aside class="help" id="samplesm" role="dialog" aria-modal="true" aria-labelledby="samples-title">
517
+ <div class="head">
518
+ <h3 id="samples-title"></h3>
519
+ <button class="close" id="samples-close" aria-label="Close">×</button>
520
+ </div>
521
+ <div class="body">
522
+ <p class="gcap" id="samples-cap"></p>
523
+ <div id="samples-list"></div>
524
+ <div class="sphoto">
525
+ <div class="sk" id="samples-photo-head"></div>
526
+ <p class="st" id="samples-photo-cap" style="margin:6px 0 0"></p>
527
+ <img id="samples-photo" alt="">
528
+ <button class="ghost" id="samples-photo-btn"></button>
529
+ </div>
530
+ </div>
531
+ <div class="sfoot">
532
+ <button class="ghost" id="samples-cancel"></button>
533
+ <button class="primary" id="samples-load"></button>
534
+ </div>
535
+ </aside>
536
+
537
  <div class="drawer-back" id="help-back"></div>
538
  <aside class="help" id="help" role="dialog" aria-modal="true" aria-labelledby="help-title">
539
  <div class="head">
 
619
  escalate:'escalated to human (L4)', auto:'automatic processing',
620
  sevLabel:{critical:'critical',major:'high',moderate:'moderate',info:'info'},
621
  toastSamples:'Demo card loaded', toastReset:'Card reset',
622
+ samplesTitle:'Sample documents — what goes in',
623
+ samplesCap:'These are the raw inputs, exactly as the card receives them. Read them first, then load them and check what was extracted — the timeline, the medications, the safety signals. Nothing here is hidden from you.',
624
+ samplesHint:'Note the two dangerous medications sit in <b>different</b> documents, two visits apart. That combination is what the deterministic layer catches.',
625
+ samplesLoad:'Load into the card', samplesCancel:'Cancel',
626
+ samplesPhotoHead:'Or a photo of a lab form',
627
+ samplesPhotoCap:'Same comparison, harder path: this synthetic form is read by a real OCR model, not by a script with the answers.',
628
+ samplesPhotoBtn:'Read this photo (OCR)',
629
  toastExport:'FHIR bundle downloaded', toastNeedInput:'Enter text or choose a photo',
630
  ocrReading:'Reading (OCR)…', processing:'Processing…',
631
  toastOcrWait:'Reading the photo on the cloud OCR lab — up to ~90 s',
 
723
  escalate:'ескалація до людини (L4)', auto:'автоматична обробка',
724
  sevLabel:{critical:'критичний',major:'високий',moderate:'помірний',info:'інфо'},
725
  toastSamples:'Демо-картку завантажено', toastReset:'Картку скинуто',
726
+ samplesTitle:'Зразки документів — що подається на вхід',
727
+ samplesCap:'Це сирі документи, рівно в тому вигляді, в якому їх отримує картка. Спершу прочитайте їх, потім завантажте — і перевірте, що з них витягнуто: таймлайн, ліки, сигнали безпеки. Нічого не приховано.',
728
+ samplesHint:'Зверніть увагу: два небезпечні препарати лежать у <b>різних</b> документах, між ними — рік. Саме таку комбінацію й ловить детермінований шар.',
729
+ samplesLoad:'Завантажити в картку', samplesCancel:'Скасувати',
730
+ samplesPhotoHead:'Або фото лабораторного бланка',
731
+ samplesPhotoCap:'Те саме порівняння, складніший шлях: цей синтетичний бланк читає справжня OCR-модель, а н�� скрипт із готовими відповідями.',
732
+ samplesPhotoBtn:'Прочитати це фото (OCR)',
733
  toastExport:'FHIR Bundle завантажено', toastNeedInput:'Введіть текст або виберіть фото',
734
  ocrReading:'Зчитування (OCR)…', processing:'Опрацювання…',
735
  toastOcrWait:'Фото читає хмарний OCR-лаб — до ~90 с',
 
1091
  function closeDoc(){$('#drawer').classList.remove('show');$('#drawer-back').classList.remove('show');}
1092
  $('#drawer-close').onclick=closeDoc;
1093
  $('#drawer-back').onclick=closeDoc;
1094
+ addEventListener('keydown',e=>{if(e.key==='Escape'){closeDoc();closeHelp();closeGraph();closeSamples();}});
1095
 
1096
  // ── довідка ───────────────────────────────────────────────
1097
  function renderHelp(){
 
1205
  }catch(e){toast(I18N[LANG].errorPrefix+e.message);}
1206
  }
1207
 
1208
+ /* Демо-документи — мовою інтерфейсу (набори дзеркальні: ті самі значення, ті самі
1209
+ сигнали). Спершу показуємо ВХІД, і лише потім вантажимо: інакше витягнутим фактам
1210
+ немає з чим порівнювати, і журі лишається тільки вірити нам на слово. */
1211
+ const kindLabels=()=>{const t=I18N[LANG];
1212
+ return {discharge_summary:t.optDischarge,lab_report:t.optLab,prescription:t.optRx,note:t.optNote};};
1213
+
1214
+ async function openSamples(){
1215
+ const t=I18N[LANG];
1216
+ $('#samples-title').textContent=t.samplesTitle;
1217
+ $('#samples-cap').innerHTML=t.samplesCap+' '+t.samplesHint;
1218
+ $('#samples-load').textContent=t.samplesLoad;
1219
+ $('#samples-cancel').textContent=t.samplesCancel;
1220
+ $('#samples-photo-head').textContent=t.samplesPhotoHead;
1221
+ $('#samples-photo-cap').textContent=t.samplesPhotoCap;
1222
+ $('#samples-photo-btn').textContent=t.samplesPhotoBtn;
1223
+ $('#samples-photo').src='/samples/image';
1224
+ try{
1225
+ const d=await api('/samples?lang='+(LANG==='ua'?'uk':'en'));
1226
+ const K=kindLabels();
1227
+ $('#samples-list').innerHTML=d.documents.map(doc=>`<div class="sdoc">
1228
+ <div class="sh"><span class="sk">${K[doc.kind]||doc.kind}</span><span class="sd">${escHtml((doc.received_at||'').slice(0,10))}</span></div>
1229
+ <p class="st">${escHtml(doc.text)}</p></div>`).join('');
1230
+ }catch(e){toast(t.errorPrefix+e.message);return;}
1231
+ $('#samplesm').classList.add('show');$('#samples-back').classList.add('show');
1232
+ $('#samples-close').focus();
1233
+ }
1234
+ function closeSamples(){$('#samplesm').classList.remove('show');$('#samples-back').classList.remove('show');}
1235
+ $('#btn-samples').onclick=openSamples;
1236
+ $('#samples-close').onclick=closeSamples;
1237
+ $('#samples-cancel').onclick=closeSamples;
1238
+ $('#samples-back').onclick=closeSamples;
1239
+
1240
+ $('#samples-load').onclick=async()=>{
1241
+ const t=I18N[LANG], b=$('#samples-load'), orig=b.textContent; b.disabled=true;
1242
+ try{
1243
+ await api(`/ingest/samples?lang=${LANG==='ua'?'uk':'en'}`,{method:'POST'});
1244
+ closeSamples(); await refresh(); toast(t.toastSamples);
1245
+ }catch(e){toast(t.errorPrefix+e.message);}
1246
+ finally{b.disabled=false;b.textContent=orig;}
1247
+ };
1248
+
1249
+ // Той самий бланк, що в кліпі: журі бачить його очима, потім віддає справжньому OCR.
1250
+ $('#samples-photo-btn').onclick=async()=>{
1251
+ const t=I18N[LANG], b=$('#samples-photo-btn'), orig=b.textContent;
1252
+ if(!await askCloudConsent()) return;
1253
+ b.disabled=true; b.textContent=t.ocrReading; toast(t.toastOcrWait);
1254
+ try{
1255
+ const blob=await (await fetch('/samples/image')).blob();
1256
+ const base64=await new Promise((res,rej)=>{
1257
+ const rd=new FileReader(); rd.onload=()=>res(rd.result); rd.onerror=rej; rd.readAsDataURL(blob);});
1258
+ const rep=await api('/ingest/image',{method:'POST',headers:{'Content-Type':'application/json'},
1259
+ body:JSON.stringify({image_base64:base64,kind:'lab_report'})});
1260
+ closeSamples(); await refresh();
1261
+ const got=(rep.extraction&&rep.extraction.observations||[]).length;
1262
+ toast(got?(rep.decision.escalate?t.toastEscalated:t.toastAuto):t.toastNoFacts);
1263
+ }catch(e){toast(t.errorPrefix+e.message);}
1264
+ finally{b.disabled=false;b.textContent=orig;}
1265
  };
1266
  $('#btn-reset').onclick=async()=>{await api('/reset',{method:'POST'});$('#log').innerHTML='';await refresh();toast(I18N[LANG].toastReset);};
1267
  $('#btn-export').onclick=async()=>{
livemedcard/static/sample_lab_report.jpg ADDED

Git LFS Details

  • SHA256: cdf2847c0778f8be20050343284c9f33f3a37242b4a68521d9dec15b827a28a8
  • Pointer size: 131 Bytes
  • Size of remote file: 317 kB