r0mant1c Codex commited on
Commit
ae97f35
·
1 Parent(s): 08dcb5b

Add local model bundle and report UI refinements

Browse files

Co-authored-by: Codex <chatgpt-codex-connector[bot]@users.noreply.github.com>

Files changed (3) hide show
  1. .gitattributes +1 -0
  2. app.py +225 -107
  3. src/extraction/factory.py +4 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* 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
 
 
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
36
+ *.gguf filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -19,7 +19,6 @@ _API_MODE = os.getenv("EXTRACTOR_BACKEND", "auto").strip().lower() == "api"
19
 
20
  def extract_lab_values(
21
  uploaded_file: str | None,
22
- api_key_override: str,
23
  ) -> tuple[str, str, Any]:
24
  if not uploaded_file:
25
  return (
@@ -28,9 +27,7 @@ def extract_lab_values(
28
  gr.update(visible=True),
29
  )
30
 
31
- extractor = build_extractor(
32
- api_key=(api_key_override or "").strip() or None,
33
- )
34
 
35
  try:
36
  result = extractor.extract(uploaded_file)
@@ -200,6 +197,7 @@ def ideal_document_example_html() -> str:
200
  "value": "14.2",
201
  "unit": "g/dL",
202
  "status": "ideal",
 
203
  "summary": "Oxygen-carrying protein in red blood cells.",
204
  "importance": "Helps show whether your blood can transport oxygen efficiently and can point toward anemia or dehydration patterns.",
205
  "improve": "Maintain iron-rich foods, B12, folate, and steady protein intake. Review heavy fatigue or shortness of breath with a clinician.",
@@ -209,6 +207,7 @@ def ideal_document_example_html() -> str:
209
  "value": "78",
210
  "unit": "ng/mL",
211
  "status": "ideal",
 
212
  "summary": "Stored iron available for future red blood cell production.",
213
  "importance": "Low ferritin can appear before hemoglobin drops and may affect energy, hair shedding, endurance, and recovery.",
214
  "improve": "Pair iron foods with vitamin C, avoid tea or coffee directly around iron-heavy meals, and confirm supplementation needs clinically.",
@@ -218,6 +217,7 @@ def ideal_document_example_html() -> str:
218
  "value": "68",
219
  "unit": "mg/dL",
220
  "status": "ideal",
 
221
  "summary": "Protective cholesterol involved in reverse cholesterol transport.",
222
  "importance": "Higher HDL in context can reflect stronger cardiometabolic resilience, especially alongside healthy triglycerides.",
223
  "improve": "Prioritize aerobic training, olive oil, nuts, fatty fish, fiber, and sleep consistency.",
@@ -227,6 +227,7 @@ def ideal_document_example_html() -> str:
227
  "value": "34",
228
  "unit": "ng/mL",
229
  "status": "normal",
 
230
  "summary": "Fat-soluble hormone-like vitamin linked to bone, immune, and muscle function.",
231
  "importance": "A normal value may still be worth optimizing depending on season, symptoms, diet, and sun exposure.",
232
  "improve": "Use safe sun exposure, vitamin D-rich foods, and discuss dose and recheck timing before supplementing heavily.",
@@ -236,6 +237,7 @@ def ideal_document_example_html() -> str:
236
  "value": "92",
237
  "unit": "mg/dL",
238
  "status": "normal",
 
239
  "summary": "Blood sugar level after an overnight fast.",
240
  "importance": "Useful for spotting glucose regulation trends, especially when viewed with HbA1c, insulin, and triglycerides.",
241
  "improve": "Walk after meals, lift weights, increase fiber, reduce liquid sugar, and keep sleep timing stable.",
@@ -245,13 +247,15 @@ def ideal_document_example_html() -> str:
245
  "value": "184",
246
  "unit": "mg/dL",
247
  "status": "bad",
 
248
  "summary": "Circulating blood fats strongly affected by diet, alcohol, insulin sensitivity, and recent intake.",
249
  "importance": "High triglycerides can signal cardiometabolic stress and should be interpreted with HDL, glucose, liver markers, and context.",
250
  "improve": "Reduce refined carbohydrates and alcohol, add omega-3 rich fish, build regular zone-2 cardio, and recheck fasting values.",
251
  },
252
  ]
253
 
254
- cards = "\n".join(_ideal_marker_card(test) for test in tests)
 
255
 
256
  return f"""
257
  <section class="bte-ideal-doc">
@@ -263,31 +267,37 @@ def ideal_document_example_html() -> str:
263
  </div>
264
  </header>
265
 
 
 
 
 
 
266
  <div class="bte-ideal-stats">
267
- <div class="bte-ideal-stat bte-ideal-stat--total">
268
  <span>6</span>
269
  <strong>Total tests</strong>
270
- <small>Detected and explained</small>
271
- </div>
272
- <div class="bte-ideal-stat bte-ideal-stat--ideal">
273
  <span>3</span>
274
  <strong>Ideal</strong>
275
- <small>Green, optimized markers</small>
276
- </div>
277
- <div class="bte-ideal-stat bte-ideal-stat--normal">
278
  <span>2</span>
279
  <strong>Normal</strong>
280
- <small>Blue, within range</small>
281
- </div>
282
- <div class="bte-ideal-stat bte-ideal-stat--bad">
283
  <span>1</span>
284
  <strong>Bad</strong>
285
- <small>Light red, needs attention</small>
286
- </div>
287
  </div>
288
 
289
  <div class="bte-ideal-grid">
290
- {cards}
 
 
 
 
 
291
  </div>
292
  </section>
293
  """
@@ -295,14 +305,28 @@ def ideal_document_example_html() -> str:
295
 
296
  def _ideal_marker_card(test: dict[str, str]) -> str:
297
  status = test["status"]
 
 
 
 
298
  return f"""
299
  <article class="bte-ideal-marker bte-ideal-marker--{escape(status)}">
300
  <div class="bte-ideal-marker-head">
301
- <div>
302
- <span class="bte-ideal-status">{escape(status.title())}</span>
303
  <h3>{escape(test["marker"])}</h3>
 
 
 
 
 
 
 
 
 
 
 
 
304
  </div>
305
- <strong>{escape(test["value"])} <small>{escape(test["unit"])}</small></strong>
306
  </div>
307
  <div class="bte-ideal-marker-body">
308
  <div>
@@ -679,40 +703,6 @@ gradio-app,
679
  box-shadow: none !important;
680
  }
681
 
682
- .bte-api-key-panel {
683
- width: 100%;
684
- max-width: 560px;
685
- justify-self: end;
686
- margin: 0;
687
- border: 1px solid rgba(255, 255, 255, 0.58);
688
- border-radius: 18px;
689
- padding: 12px;
690
- background: rgba(255, 255, 255, 0.12);
691
- box-shadow: none;
692
- }
693
-
694
- .bte-api-key-panel > div,
695
- .bte-api-key-panel > div > div {
696
- background: transparent !important;
697
- }
698
-
699
- .bte-api-key-panel .block {
700
- border: 0 !important;
701
- background: transparent !important;
702
- }
703
-
704
- .bte-api-key-panel input {
705
- min-height: 44px !important;
706
- border-radius: 12px !important;
707
- }
708
-
709
- .bte-title .bte-api-key-panel label,
710
- .bte-title .bte-api-key-panel .label-wrap,
711
- .bte-title .bte-api-key-panel .label-wrap span {
712
- color: #ffffff !important;
713
- -webkit-text-fill-color: #ffffff !important;
714
- }
715
-
716
  .bte-hero-grid {
717
  width: var(--bte-rail) !important;
718
  max-width: var(--bte-rail) !important;
@@ -1844,13 +1834,10 @@ button.bte-action *,
1844
  max-width: 100% !important;
1845
  margin-left: auto !important;
1846
  margin-right: auto !important;
1847
- margin-top: 18px;
1848
- border: 1px solid var(--bte-line);
1849
- border-radius: var(--bte-radius);
1850
- padding: 22px;
1851
- background: var(--bte-surface);
1852
- box-shadow: var(--bte-shadow);
1853
- overflow: hidden;
1854
  }
1855
 
1856
  .bte-ideal-hero {
@@ -1858,12 +1845,14 @@ button.bte-action *,
1858
  align-items: center;
1859
  justify-content: space-between;
1860
  gap: 22px;
1861
- padding: 24px;
1862
- border-radius: 20px;
 
1863
  color: #ffffff;
1864
  background:
1865
  linear-gradient(120deg, rgba(18, 128, 92, 0.98) 0%, rgba(37, 99, 235, 0.95) 58%, rgba(191, 52, 52, 0.82) 100%),
1866
  #12805c;
 
1867
  }
1868
 
1869
  .bte-ideal-hero .bte-kicker,
@@ -1888,19 +1877,32 @@ button.bte-action *,
1888
  display: grid;
1889
  grid-template-columns: repeat(4, minmax(0, 1fr));
1890
  gap: 12px;
1891
- margin: 16px 0;
 
 
 
 
 
 
 
 
1892
  }
1893
 
1894
  .bte-ideal-stat {
 
 
1895
  padding: 16px;
1896
- border: 1px solid var(--bte-line);
1897
  border-radius: 18px;
1898
- background: var(--bte-surface);
 
 
 
 
1899
  }
1900
 
1901
  .bte-ideal-stat span,
1902
- .bte-ideal-stat strong,
1903
- .bte-ideal-stat small {
1904
  display: block;
1905
  }
1906
 
@@ -1911,17 +1913,26 @@ button.bte-action *,
1911
  margin-bottom: 8px;
1912
  }
1913
 
1914
- .bte-ideal-stat small {
1915
- color: var(--bte-muted);
1916
- }
1917
-
1918
  .bte-ideal-stat--total span {
1919
  color: var(--bte-ink);
1920
  }
1921
 
 
 
 
 
 
 
 
 
 
 
 
 
1922
  .bte-ideal-stat--ideal {
 
 
1923
  border-color: rgba(18, 128, 92, 0.22);
1924
- background: var(--bte-green-soft);
1925
  }
1926
 
1927
  .bte-ideal-stat--ideal span {
@@ -1929,8 +1940,9 @@ button.bte-action *,
1929
  }
1930
 
1931
  .bte-ideal-stat--normal {
 
 
1932
  border-color: rgba(37, 99, 235, 0.22);
1933
- background: var(--bte-blue-soft);
1934
  }
1935
 
1936
  .bte-ideal-stat--normal span {
@@ -1938,8 +1950,9 @@ button.bte-action *,
1938
  }
1939
 
1940
  .bte-ideal-stat--bad {
 
 
1941
  border-color: rgba(191, 52, 52, 0.2);
1942
- background: var(--bte-red-soft);
1943
  }
1944
 
1945
  .bte-ideal-stat--bad span {
@@ -1949,49 +1962,91 @@ button.bte-action *,
1949
  .bte-ideal-grid {
1950
  display: grid;
1951
  grid-template-columns: repeat(2, minmax(0, 1fr));
1952
- gap: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
1953
  }
1954
 
1955
  .bte-ideal-marker {
 
 
1956
  border: 1px solid var(--bte-line);
1957
- border-radius: 18px;
1958
- padding: 16px;
1959
  background: var(--bte-surface);
 
 
 
 
 
 
 
 
 
1960
  }
1961
 
1962
  .bte-ideal-marker--ideal {
 
 
1963
  border-color: rgba(18, 128, 92, 0.22);
1964
  }
1965
 
1966
  .bte-ideal-marker--normal {
 
 
1967
  border-color: rgba(37, 99, 235, 0.22);
1968
  }
1969
 
1970
  .bte-ideal-marker--bad {
 
 
1971
  border-color: rgba(191, 52, 52, 0.2);
1972
  background: linear-gradient(180deg, #fff8f8, #ffffff);
1973
  }
1974
 
1975
  .bte-ideal-marker-head {
1976
- display: flex;
1977
- justify-content: space-between;
1978
- gap: 16px;
1979
- align-items: start;
 
 
 
 
 
 
 
1980
  padding-bottom: 12px;
1981
- border-bottom: 1px solid var(--bte-line);
1982
  }
1983
 
1984
  .bte-ideal-marker-head h3 {
1985
- margin: 7px 0 0 !important;
1986
  color: var(--bte-ink) !important;
1987
  font-size: 22px !important;
1988
  line-height: 1.1 !important;
1989
  }
1990
 
1991
- .bte-ideal-marker-head strong {
1992
- color: var(--bte-ink);
1993
- font-size: 26px;
1994
- white-space: nowrap;
 
 
 
 
 
1995
  }
1996
 
1997
  .bte-ideal-marker-head small {
@@ -2023,10 +2078,87 @@ button.bte-action *,
2023
  background: var(--bte-red-soft);
2024
  }
2025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2026
  .bte-ideal-marker-body {
2027
  display: grid;
2028
  gap: 12px;
 
 
 
 
 
 
 
 
 
 
 
2029
  padding-top: 14px;
 
 
2030
  }
2031
 
2032
  .bte-ideal-marker-body div {
@@ -2101,8 +2233,8 @@ button.bte-action *,
2101
  .bte-title > *,
2102
  .bte-title *,
2103
  .bte-hero-grid > *,
2104
- .bte-api-key-panel,
2105
- .bte-api-key-panel * {
2106
  min-width: 0 !important;
2107
  max-width: 100% !important;
2108
  overflow-wrap: anywhere;
@@ -2158,12 +2290,6 @@ button.bte-action *,
2158
  min-width: 0 !important;
2159
  }
2160
 
2161
- .bte-api-key-panel {
2162
- margin: 0;
2163
- max-width: 100%;
2164
- justify-self: stretch;
2165
- }
2166
-
2167
  .bte-step-heading {
2168
  min-height: auto;
2169
  align-items: start;
@@ -2313,7 +2439,7 @@ with gr.Blocks(title="Blood Test Explainer") as demo:
2313
  "border:0 !important;box-shadow:none !important;padding:0 !important;}</style>"
2314
  )
2315
  with gr.Row(equal_height=True, elem_classes=["bte-title"]):
2316
- with gr.Column(scale=6, min_width=420, elem_classes=["bte-title-copy"]):
2317
  gr.HTML(
2318
  """
2319
  <div>
@@ -2323,14 +2449,6 @@ with gr.Blocks(title="Blood Test Explainer") as demo:
2323
  </div>
2324
  """
2325
  )
2326
- with gr.Column(scale=4, min_width=360, visible=_API_MODE):
2327
- with gr.Group(elem_classes=["bte-api-key-panel"]):
2328
- api_key_override = gr.Textbox(
2329
- label="OpenBMB API key",
2330
- type="password",
2331
- placeholder="Paste your OpenBMB API key",
2332
- interactive=True,
2333
- )
2334
 
2335
  gr.HTML(
2336
  """
@@ -2399,7 +2517,7 @@ with gr.Blocks(title="Blood Test Explainer") as demo:
2399
  show_progress="hidden",
2400
  ).then(
2401
  extract_lab_values,
2402
- inputs=[uploaded, api_key_override],
2403
  outputs=[status, report, report_panel],
2404
  scroll_to_output=True,
2405
  show_progress="hidden",
 
19
 
20
  def extract_lab_values(
21
  uploaded_file: str | None,
 
22
  ) -> tuple[str, str, Any]:
23
  if not uploaded_file:
24
  return (
 
27
  gr.update(visible=True),
28
  )
29
 
30
+ extractor = build_extractor()
 
 
31
 
32
  try:
33
  result = extractor.extract(uploaded_file)
 
197
  "value": "14.2",
198
  "unit": "g/dL",
199
  "status": "ideal",
200
+ "range_position": "76",
201
  "summary": "Oxygen-carrying protein in red blood cells.",
202
  "importance": "Helps show whether your blood can transport oxygen efficiently and can point toward anemia or dehydration patterns.",
203
  "improve": "Maintain iron-rich foods, B12, folate, and steady protein intake. Review heavy fatigue or shortness of breath with a clinician.",
 
207
  "value": "78",
208
  "unit": "ng/mL",
209
  "status": "ideal",
210
+ "range_position": "72",
211
  "summary": "Stored iron available for future red blood cell production.",
212
  "importance": "Low ferritin can appear before hemoglobin drops and may affect energy, hair shedding, endurance, and recovery.",
213
  "improve": "Pair iron foods with vitamin C, avoid tea or coffee directly around iron-heavy meals, and confirm supplementation needs clinically.",
 
217
  "value": "68",
218
  "unit": "mg/dL",
219
  "status": "ideal",
220
+ "range_position": "80",
221
  "summary": "Protective cholesterol involved in reverse cholesterol transport.",
222
  "importance": "Higher HDL in context can reflect stronger cardiometabolic resilience, especially alongside healthy triglycerides.",
223
  "improve": "Prioritize aerobic training, olive oil, nuts, fatty fish, fiber, and sleep consistency.",
 
227
  "value": "34",
228
  "unit": "ng/mL",
229
  "status": "normal",
230
+ "range_position": "53",
231
  "summary": "Fat-soluble hormone-like vitamin linked to bone, immune, and muscle function.",
232
  "importance": "A normal value may still be worth optimizing depending on season, symptoms, diet, and sun exposure.",
233
  "improve": "Use safe sun exposure, vitamin D-rich foods, and discuss dose and recheck timing before supplementing heavily.",
 
237
  "value": "92",
238
  "unit": "mg/dL",
239
  "status": "normal",
240
+ "range_position": "58",
241
  "summary": "Blood sugar level after an overnight fast.",
242
  "importance": "Useful for spotting glucose regulation trends, especially when viewed with HbA1c, insulin, and triglycerides.",
243
  "improve": "Walk after meals, lift weights, increase fiber, reduce liquid sugar, and keep sleep timing stable.",
 
247
  "value": "184",
248
  "unit": "mg/dL",
249
  "status": "bad",
250
+ "range_position": "88",
251
  "summary": "Circulating blood fats strongly affected by diet, alcohol, insulin sensitivity, and recent intake.",
252
  "importance": "High triglycerides can signal cardiometabolic stress and should be interpreted with HDL, glucose, liver markers, and context.",
253
  "improve": "Reduce refined carbohydrates and alcohol, add omega-3 rich fish, build regular zone-2 cardio, and recheck fasting values.",
254
  },
255
  ]
256
 
257
+ left_cards = "\n".join(_ideal_marker_card(test) for index, test in enumerate(tests) if index % 2 == 0)
258
+ right_cards = "\n".join(_ideal_marker_card(test) for index, test in enumerate(tests) if index % 2 == 1)
259
 
260
  return f"""
261
  <section class="bte-ideal-doc">
 
267
  </div>
268
  </header>
269
 
270
+ <input class="bte-ideal-filter" type="radio" name="bte-ideal-filter" id="bte-filter-total" checked>
271
+ <input class="bte-ideal-filter" type="radio" name="bte-ideal-filter" id="bte-filter-ideal">
272
+ <input class="bte-ideal-filter" type="radio" name="bte-ideal-filter" id="bte-filter-normal">
273
+ <input class="bte-ideal-filter" type="radio" name="bte-ideal-filter" id="bte-filter-bad">
274
+
275
  <div class="bte-ideal-stats">
276
+ <label class="bte-ideal-stat bte-ideal-stat--total" for="bte-filter-total">
277
  <span>6</span>
278
  <strong>Total tests</strong>
279
+ </label>
280
+ <label class="bte-ideal-stat bte-ideal-stat--ideal" for="bte-filter-ideal">
 
281
  <span>3</span>
282
  <strong>Ideal</strong>
283
+ </label>
284
+ <label class="bte-ideal-stat bte-ideal-stat--normal" for="bte-filter-normal">
 
285
  <span>2</span>
286
  <strong>Normal</strong>
287
+ </label>
288
+ <label class="bte-ideal-stat bte-ideal-stat--bad" for="bte-filter-bad">
 
289
  <span>1</span>
290
  <strong>Bad</strong>
291
+ </label>
 
292
  </div>
293
 
294
  <div class="bte-ideal-grid">
295
+ <div class="bte-ideal-column">
296
+ {left_cards}
297
+ </div>
298
+ <div class="bte-ideal-column">
299
+ {right_cards}
300
+ </div>
301
  </div>
302
  </section>
303
  """
 
305
 
306
  def _ideal_marker_card(test: dict[str, str]) -> str:
307
  status = test["status"]
308
+ range_position_value = test.get("range_position", "50")
309
+ range_position = escape(range_position_value)
310
+ range_labels = test.get("range_labels", ("Bad", "Normal", "Good"))
311
+ low_label, mid_label, high_label = (escape(label) for label in range_labels)
312
  return f"""
313
  <article class="bte-ideal-marker bte-ideal-marker--{escape(status)}">
314
  <div class="bte-ideal-marker-head">
315
+ <div class="bte-ideal-title-line">
 
316
  <h3>{escape(test["marker"])}</h3>
317
+ <span class="bte-ideal-status">{escape(status.title())}</span>
318
+ </div>
319
+ <div class="bte-range-scale" style="--value-position: {range_position}%; --value-position-number: {range_position}">
320
+ <div class="bte-range-value">
321
+ <strong>{escape(test["value"])}</strong>
322
+ <small>{escape(test["unit"])}</small>
323
+ </div>
324
+ <div class="bte-range-track" aria-hidden="true">
325
+ <span>{low_label}</span>
326
+ <span>{mid_label}</span>
327
+ <span>{high_label}</span>
328
+ </div>
329
  </div>
 
330
  </div>
331
  <div class="bte-ideal-marker-body">
332
  <div>
 
703
  box-shadow: none !important;
704
  }
705
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
  .bte-hero-grid {
707
  width: var(--bte-rail) !important;
708
  max-width: var(--bte-rail) !important;
 
1834
  max-width: 100% !important;
1835
  margin-left: auto !important;
1836
  margin-right: auto !important;
1837
+ margin-top: 34px;
1838
+ display: grid;
1839
+ gap: 16px;
1840
+ background: transparent;
 
 
 
1841
  }
1842
 
1843
  .bte-ideal-hero {
 
1845
  align-items: center;
1846
  justify-content: space-between;
1847
  gap: 22px;
1848
+ padding: 28px;
1849
+ border: 1px solid rgba(255, 255, 255, 0.42);
1850
+ border-radius: var(--bte-radius);
1851
  color: #ffffff;
1852
  background:
1853
  linear-gradient(120deg, rgba(18, 128, 92, 0.98) 0%, rgba(37, 99, 235, 0.95) 58%, rgba(191, 52, 52, 0.82) 100%),
1854
  #12805c;
1855
+ box-shadow: var(--bte-shadow-strong);
1856
  }
1857
 
1858
  .bte-ideal-hero .bte-kicker,
 
1877
  display: grid;
1878
  grid-template-columns: repeat(4, minmax(0, 1fr));
1879
  gap: 12px;
1880
+ margin: 0;
1881
+ }
1882
+
1883
+ .bte-ideal-filter {
1884
+ position: absolute;
1885
+ inline-size: 1px;
1886
+ block-size: 1px;
1887
+ opacity: 0;
1888
+ pointer-events: none;
1889
  }
1890
 
1891
  .bte-ideal-stat {
1892
+ --bte-stat-bg: var(--bte-surface);
1893
+ --bte-stat-ring: linear-gradient(120deg, var(--bte-green), var(--bte-blue), var(--bte-red));
1894
  padding: 16px;
1895
+ border: 2px solid var(--bte-line);
1896
  border-radius: 18px;
1897
+ background: var(--bte-stat-bg);
1898
+ box-shadow: var(--bte-shadow);
1899
+ overflow: hidden;
1900
+ cursor: pointer;
1901
+ transition: background 220ms ease, box-shadow 220ms ease, border-color 220ms ease, filter 220ms ease;
1902
  }
1903
 
1904
  .bte-ideal-stat span,
1905
+ .bte-ideal-stat strong {
 
1906
  display: block;
1907
  }
1908
 
 
1913
  margin-bottom: 8px;
1914
  }
1915
 
 
 
 
 
1916
  .bte-ideal-stat--total span {
1917
  color: var(--bte-ink);
1918
  }
1919
 
1920
+ .bte-ideal-doc:has(#bte-filter-total:checked) .bte-ideal-stat--total,
1921
+ .bte-ideal-doc:has(#bte-filter-ideal:checked) .bte-ideal-stat--ideal,
1922
+ .bte-ideal-doc:has(#bte-filter-normal:checked) .bte-ideal-stat--normal,
1923
+ .bte-ideal-doc:has(#bte-filter-bad:checked) .bte-ideal-stat--bad {
1924
+ border-color: transparent;
1925
+ background:
1926
+ linear-gradient(var(--bte-stat-bg), var(--bte-stat-bg)) padding-box,
1927
+ var(--bte-stat-ring) border-box;
1928
+ box-shadow: var(--bte-shadow-strong);
1929
+ filter: saturate(1.08);
1930
+ }
1931
+
1932
  .bte-ideal-stat--ideal {
1933
+ --bte-stat-bg: var(--bte-green-soft);
1934
+ --bte-stat-ring: linear-gradient(120deg, var(--bte-green), #22c7a0, var(--bte-blue));
1935
  border-color: rgba(18, 128, 92, 0.22);
 
1936
  }
1937
 
1938
  .bte-ideal-stat--ideal span {
 
1940
  }
1941
 
1942
  .bte-ideal-stat--normal {
1943
+ --bte-stat-bg: var(--bte-blue-soft);
1944
+ --bte-stat-ring: linear-gradient(120deg, var(--bte-blue), #52a8ff, var(--bte-green));
1945
  border-color: rgba(37, 99, 235, 0.22);
 
1946
  }
1947
 
1948
  .bte-ideal-stat--normal span {
 
1950
  }
1951
 
1952
  .bte-ideal-stat--bad {
1953
+ --bte-stat-bg: var(--bte-red-soft);
1954
+ --bte-stat-ring: linear-gradient(120deg, var(--bte-red), #ff8f8f, var(--bte-blue));
1955
  border-color: rgba(191, 52, 52, 0.2);
 
1956
  }
1957
 
1958
  .bte-ideal-stat--bad span {
 
1962
  .bte-ideal-grid {
1963
  display: grid;
1964
  grid-template-columns: repeat(2, minmax(0, 1fr));
1965
+ align-items: start;
1966
+ gap: 16px;
1967
+ }
1968
+
1969
+ .bte-ideal-column {
1970
+ display: grid;
1971
+ align-content: start;
1972
+ gap: 16px;
1973
+ }
1974
+
1975
+ .bte-ideal-doc:has(#bte-filter-ideal:checked) .bte-ideal-marker:not(.bte-ideal-marker--ideal),
1976
+ .bte-ideal-doc:has(#bte-filter-normal:checked) .bte-ideal-marker:not(.bte-ideal-marker--normal),
1977
+ .bte-ideal-doc:has(#bte-filter-bad:checked) .bte-ideal-marker:not(.bte-ideal-marker--bad) {
1978
+ display: none;
1979
  }
1980
 
1981
  .bte-ideal-marker {
1982
+ --bte-marker-color: var(--bte-green);
1983
+ --bte-marker-soft: var(--bte-green-soft);
1984
  border: 1px solid var(--bte-line);
1985
+ border-radius: var(--bte-radius);
1986
+ padding: 18px;
1987
  background: var(--bte-surface);
1988
+ box-shadow: var(--bte-shadow);
1989
+ overflow: hidden;
1990
+ transition: transform 700ms ease, box-shadow 700ms ease, border-color 700ms ease;
1991
+ }
1992
+
1993
+ .bte-ideal-marker:hover,
1994
+ .bte-ideal-marker:focus-within {
1995
+ transform: translateY(-2px);
1996
+ box-shadow: var(--bte-shadow-strong);
1997
  }
1998
 
1999
  .bte-ideal-marker--ideal {
2000
+ --bte-marker-color: var(--bte-green);
2001
+ --bte-marker-soft: var(--bte-green-soft);
2002
  border-color: rgba(18, 128, 92, 0.22);
2003
  }
2004
 
2005
  .bte-ideal-marker--normal {
2006
+ --bte-marker-color: var(--bte-blue);
2007
+ --bte-marker-soft: var(--bte-blue-soft);
2008
  border-color: rgba(37, 99, 235, 0.22);
2009
  }
2010
 
2011
  .bte-ideal-marker--bad {
2012
+ --bte-marker-color: var(--bte-red);
2013
+ --bte-marker-soft: var(--bte-red-soft);
2014
  border-color: rgba(191, 52, 52, 0.2);
2015
  background: linear-gradient(180deg, #fff8f8, #ffffff);
2016
  }
2017
 
2018
  .bte-ideal-marker-head {
2019
+ display: grid;
2020
+ grid-template-columns: minmax(180px, 0.8fr) minmax(230px, 1fr);
2021
+ gap: 18px;
2022
+ align-items: center;
2023
+ padding-bottom: 0;
2024
+ border-bottom: 1px solid transparent;
2025
+ transition: padding-bottom 1300ms ease, border-color 1100ms ease;
2026
+ }
2027
+
2028
+ .bte-ideal-marker:hover .bte-ideal-marker-head,
2029
+ .bte-ideal-marker:focus-within .bte-ideal-marker-head {
2030
  padding-bottom: 12px;
2031
+ border-bottom-color: var(--bte-line);
2032
  }
2033
 
2034
  .bte-ideal-marker-head h3 {
2035
+ margin: 0 !important;
2036
  color: var(--bte-ink) !important;
2037
  font-size: 22px !important;
2038
  line-height: 1.1 !important;
2039
  }
2040
 
2041
+ .bte-ideal-title-line {
2042
+ display: flex;
2043
+ align-items: center;
2044
+ gap: 10px;
2045
+ min-width: 0;
2046
+ }
2047
+
2048
+ .bte-ideal-title-line h3 {
2049
+ min-width: 0;
2050
  }
2051
 
2052
  .bte-ideal-marker-head small {
 
2078
  background: var(--bte-red-soft);
2079
  }
2080
 
2081
+ .bte-range-scale {
2082
+ display: grid;
2083
+ gap: 8px;
2084
+ min-width: 0;
2085
+ }
2086
+
2087
+ .bte-range-value {
2088
+ position: relative;
2089
+ left: var(--value-position);
2090
+ display: inline-flex;
2091
+ align-items: baseline;
2092
+ justify-self: start;
2093
+ gap: 4px;
2094
+ color: var(--bte-marker-color);
2095
+ transform: translateX(-50%);
2096
+ white-space: nowrap;
2097
+ }
2098
+
2099
+ .bte-range-value strong {
2100
+ color: inherit;
2101
+ font-size: 19px;
2102
+ line-height: 1;
2103
+ }
2104
+
2105
+ .bte-range-value small {
2106
+ color: var(--bte-muted);
2107
+ font-size: 12px;
2108
+ font-weight: 760;
2109
+ }
2110
+
2111
+ .bte-range-track {
2112
+ position: relative;
2113
+ display: grid;
2114
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2115
+ min-height: 32px;
2116
+ border: 1px solid rgba(15, 23, 42, 0.08);
2117
+ border-radius: 999px;
2118
+ overflow: hidden;
2119
+ background: #ffffff;
2120
+ }
2121
+
2122
+ .bte-range-track::before {
2123
+ content: "";
2124
+ position: absolute;
2125
+ inset: 0 auto 0 0;
2126
+ width: var(--value-position);
2127
+ border-radius: inherit;
2128
+ background:
2129
+ linear-gradient(90deg, rgba(191, 52, 52, 0.86) 0%, rgba(37, 99, 235, 0.84) 52%, rgba(18, 128, 92, 0.86) 100%);
2130
+ background-size: calc(10000% / var(--value-position-number, 100)) 100%;
2131
+ box-shadow: inset 0 0 18px rgba(255, 255, 255, 0.2);
2132
+ }
2133
+
2134
+ .bte-range-track span {
2135
+ position: relative;
2136
+ z-index: 1;
2137
+ display: grid;
2138
+ place-items: center;
2139
+ color: var(--bte-ink);
2140
+ font-size: 10px;
2141
+ font-weight: 820;
2142
+ text-transform: uppercase;
2143
+ }
2144
+
2145
  .bte-ideal-marker-body {
2146
  display: grid;
2147
  gap: 12px;
2148
+ max-height: 0;
2149
+ overflow: hidden;
2150
+ padding-top: 0;
2151
+ opacity: 0;
2152
+ transform: translateY(-6px);
2153
+ transition: max-height 2200ms cubic-bezier(0.22, 1, 0.36, 1), padding-top 1700ms ease, opacity 1400ms ease, transform 1700ms ease;
2154
+ }
2155
+
2156
+ .bte-ideal-marker:hover .bte-ideal-marker-body,
2157
+ .bte-ideal-marker:focus-within .bte-ideal-marker-body {
2158
+ max-height: 520px;
2159
  padding-top: 14px;
2160
+ opacity: 1;
2161
+ transform: translateY(0);
2162
  }
2163
 
2164
  .bte-ideal-marker-body div {
 
2233
  .bte-title > *,
2234
  .bte-title *,
2235
  .bte-hero-grid > *,
2236
+ .bte-title-copy,
2237
+ .bte-title-copy * {
2238
  min-width: 0 !important;
2239
  max-width: 100% !important;
2240
  overflow-wrap: anywhere;
 
2290
  min-width: 0 !important;
2291
  }
2292
 
 
 
 
 
 
 
2293
  .bte-step-heading {
2294
  min-height: auto;
2295
  align-items: start;
 
2439
  "border:0 !important;box-shadow:none !important;padding:0 !important;}</style>"
2440
  )
2441
  with gr.Row(equal_height=True, elem_classes=["bte-title"]):
2442
+ with gr.Column(scale=1, min_width=420, elem_classes=["bte-title-copy"]):
2443
  gr.HTML(
2444
  """
2445
  <div>
 
2449
  </div>
2450
  """
2451
  )
 
 
 
 
 
 
 
 
2452
 
2453
  gr.HTML(
2454
  """
 
2517
  show_progress="hidden",
2518
  ).then(
2519
  extract_lab_values,
2520
+ inputs=[uploaded],
2521
  outputs=[status, report, report_panel],
2522
  scroll_to_output=True,
2523
  show_progress="hidden",
src/extraction/factory.py CHANGED
@@ -52,3 +52,7 @@ def _llamacpp_available() -> bool:
52
  except ImportError:
53
  return False
54
  return True
 
 
 
 
 
52
  except ImportError:
53
  return False
54
  return True
55
+
56
+
57
+ def _in_process_local_configured() -> bool:
58
+ return bool(os.getenv("LOCAL_MODEL_PATH") and os.getenv("LOCAL_MMPROJ_PATH"))