XQ commited on
Commit
64436c0
·
1 Parent(s): 2408f3f

Update UI

Browse files
Files changed (1) hide show
  1. src/ui/app.py +60 -394
src/ui/app.py CHANGED
@@ -1,7 +1,7 @@
1
  """Streamlit frontend for Dokumentassistent.
2
 
3
  Calls the FastAPI backend at http://localhost:8000.
4
- Single-page document search interface inspired by Danish university design.
5
  """
6
 
7
  import os
@@ -85,23 +85,6 @@ TEXTS: Dict[str, Dict[str, str]] = {
85
  "pipeline_rank": "#",
86
  "pipeline_no_results": "Ingen resultater",
87
  "pipeline_score_change": "Score-aendring",
88
- "nav_home": "Forside",
89
- "nav_docs": "Dokumenter",
90
- "nav_search": "Soegning",
91
- "nav_about": "Om systemet",
92
- "breadcrumb_home": "Forside",
93
- "breadcrumb_current": "Dokumentassistent",
94
- "footer_contact": "Kontakt",
95
- "footer_contact_text": "IT-support for administrativt personale",
96
- "footer_services": "Services",
97
- "footer_service_api": "API-dokumentation",
98
- "footer_service_status": "Systemstatus",
99
- "footer_service_guide": "Brugervejledning",
100
- "footer_about": "Om",
101
- "footer_about_privacy": "Privatlivspolitik",
102
- "footer_about_data": "Databehandling",
103
- "footer_about_access": "Tilgaengelighed",
104
- "footer_copyright": "Dokumentassistent — RAG-prototype",
105
  },
106
  "en": {
107
  "page_title": "Document Assistant",
@@ -172,23 +155,6 @@ TEXTS: Dict[str, Dict[str, str]] = {
172
  "pipeline_rank": "#",
173
  "pipeline_no_results": "No results",
174
  "pipeline_score_change": "Score change",
175
- "nav_home": "Home",
176
- "nav_docs": "Documents",
177
- "nav_search": "Search",
178
- "nav_about": "About",
179
- "breadcrumb_home": "Home",
180
- "breadcrumb_current": "Document Assistant",
181
- "footer_contact": "Contact",
182
- "footer_contact_text": "IT support for administrative staff",
183
- "footer_services": "Services",
184
- "footer_service_api": "API Documentation",
185
- "footer_service_status": "System Status",
186
- "footer_service_guide": "User Guide",
187
- "footer_about": "About",
188
- "footer_about_privacy": "Privacy Policy",
189
- "footer_about_data": "Data Processing",
190
- "footer_about_access": "Accessibility",
191
- "footer_copyright": "Document Assistant — RAG Prototype",
192
  },
193
  }
194
 
@@ -198,238 +164,120 @@ TEXTS: Dict[str, Dict[str, str]] = {
198
  st.set_page_config(
199
  page_title="Dokumentassistent",
200
  page_icon=None,
201
- layout="wide",
202
  )
203
 
204
  # ---------------------------------------------------------------------------
205
- # Custom CSS -- University-inspired visual identity
206
  # ---------------------------------------------------------------------------
207
  st.markdown(
208
  """
209
  <style>
210
  /* ---------- Global ---------- */
211
  html, body, [class*="css"] {
212
- font-family: Arial, Helvetica, sans-serif;
213
  color: #333333;
214
  background-color: #FFFFFF;
215
  }
216
 
217
- /* Hide default Streamlit branding */
218
  #MainMenu, footer {visibility: hidden;}
219
- header[data-testid="stHeader"] {background: transparent; height: 0;}
220
 
221
- /* Force hide Streamlit header space */
222
- .stApp > header {display: none;}
223
- .block-container {
224
- padding-top: 0 !important;
225
- max-width: 1100px;
226
- }
227
-
228
- /* ---------- Top utility bar ---------- */
229
- .top-utility-bar {
230
- background-color: #2B2B2B;
231
- color: #CCCCCC;
232
- font-size: 0.78rem;
233
- padding: 6px 0;
234
- margin: -1rem -4rem 0 -4rem;
235
- width: 100vw;
236
- position: relative;
237
- left: 50%;
238
- transform: translateX(-50%);
239
- }
240
- .top-utility-inner {
241
- max-width: 1100px;
242
- margin: 0 auto;
243
- padding: 0 2rem;
244
- display: flex;
245
- justify-content: flex-end;
246
- align-items: center;
247
- gap: 1.5rem;
248
- }
249
- .top-utility-bar a {
250
- color: #CCCCCC;
251
- text-decoration: none;
252
- transition: color 0.15s;
253
- }
254
- .top-utility-bar a:hover {
255
- color: #FFFFFF;
256
- }
257
- .utility-lang-switch {
258
- border-left: 1px solid #555;
259
- padding-left: 1.5rem;
260
  }
261
 
262
- /* ---------- Main navigation bar ---------- */
263
- .main-nav-bar {
264
- background-color: #FFFFFF;
265
- border-bottom: 3px solid #901A1E;
266
- margin: 0 -4rem;
267
- width: 100vw;
268
- position: relative;
269
- left: 50%;
270
- transform: translateX(-50%);
271
- }
272
- .main-nav-inner {
273
- max-width: 1100px;
274
- margin: 0 auto;
275
- padding: 0.9rem 2rem;
276
- display: flex;
277
- justify-content: space-between;
278
- align-items: center;
279
- }
280
- .nav-brand {
281
- font-family: Georgia, 'Times New Roman', serif;
282
- font-size: 1.5rem;
283
  font-weight: 700;
284
  color: #901A1E;
285
- text-decoration: none;
286
  letter-spacing: -0.02em;
287
  }
288
- .nav-links {
289
- display: flex;
290
- gap: 2rem;
291
- align-items: center;
292
- }
293
- .nav-links a {
294
- font-family: Arial, Helvetica, sans-serif;
295
- font-size: 0.92rem;
296
- color: #333333;
297
- text-decoration: none;
298
- font-weight: 500;
299
- padding: 0.3rem 0;
300
- border-bottom: 2px solid transparent;
301
- transition: border-color 0.15s, color 0.15s;
302
- }
303
- .nav-links a:hover, .nav-links a.active {
304
- color: #901A1E;
305
- border-bottom-color: #901A1E;
306
- }
307
-
308
- /* ---------- Breadcrumbs ---------- */
309
- .breadcrumbs {
310
- font-size: 0.82rem;
311
- color: #888888;
312
- padding: 0.7rem 0 0.4rem 0;
313
- margin-bottom: 0.2rem;
314
- }
315
- .breadcrumbs a {
316
- color: #901A1E;
317
- text-decoration: none;
318
- }
319
- .breadcrumbs a:hover {
320
- text-decoration: underline;
321
- }
322
- .breadcrumbs .separator {
323
- color: #AAAAAA;
324
- margin: 0 0.4rem;
325
- }
326
-
327
- /* ---------- Page title area ---------- */
328
- .page-title-area {
329
- border-bottom: 1px solid #E0E0E0;
330
- padding-bottom: 1.2rem;
331
- margin-bottom: 1.8rem;
332
- }
333
- .page-title {
334
- font-family: Georgia, 'Times New Roman', serif;
335
- font-size: 2rem;
336
- font-weight: 700;
337
- color: #2B2B2B;
338
- margin: 0.4rem 0 0.5rem 0;
339
- letter-spacing: -0.02em;
340
- line-height: 1.2;
341
- }
342
- .page-subtitle {
343
- font-family: Arial, Helvetica, sans-serif;
344
- font-size: 1rem;
345
  color: #666666;
346
- margin: 0;
347
  line-height: 1.6;
348
  }
349
 
350
  /* ---------- Sidebar ---------- */
351
  section[data-testid="stSidebar"] {
352
- background-color: #F7F7F7;
353
  border-right: 1px solid #E0E0E0;
354
  }
355
- section[data-testid="stSidebar"] .sidebar-section-heading {
356
- font-family: Georgia, 'Times New Roman', serif;
357
- font-size: 1.1rem;
358
  font-weight: 700;
359
- color: #2B2B2B;
360
  margin-bottom: 0.5rem;
361
- padding-bottom: 0.35rem;
362
- border-bottom: 2px solid #901A1E;
363
  }
364
  section[data-testid="stSidebar"] p,
365
  section[data-testid="stSidebar"] li {
366
- font-size: 0.88rem;
367
  color: #555555;
368
  line-height: 1.55;
369
  }
370
 
371
  /* ---------- Source card ---------- */
372
  .source-card {
373
- border-left: 3px solid #901A1E;
374
- border-top: 1px solid #E0E0E0;
375
- border-right: 1px solid #E0E0E0;
376
- border-bottom: 1px solid #E0E0E0;
377
  padding: 1rem 1.2rem;
378
  margin-bottom: 0.75rem;
379
  background-color: #FAFAFA;
380
- transition: box-shadow 0.15s;
381
- }
382
- .source-card:hover {
383
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
384
  }
385
  .source-card-title {
386
  font-weight: 600;
387
- color: #2B2B2B;
388
  font-size: 0.95rem;
389
  margin-bottom: 0.3rem;
390
  }
391
  .source-card-text {
392
- font-size: 0.85rem;
393
  color: #555555;
394
  line-height: 1.55;
395
  }
396
  .source-card-meta {
397
- font-size: 0.78rem;
398
  color: #888888;
399
  margin-top: 0.4rem;
400
  }
401
 
402
  /* ---------- Result metadata ---------- */
403
  .result-meta {
404
- font-size: 0.85rem;
405
  color: #666666;
406
  margin-bottom: 1.2rem;
407
- padding: 0.6rem 0.9rem;
408
- background-color: #F7F7F7;
409
- border-left: 3px solid #901A1E;
410
  }
411
 
412
  /* ---------- Answer area ---------- */
413
  .answer-block {
414
- font-size: 1.02rem;
415
  line-height: 1.7;
416
  color: #333333;
417
  margin-bottom: 1.5rem;
418
- padding: 1.2rem;
419
- background-color: #FFFFFF;
420
- border: 1px solid #E0E0E0;
421
  }
422
 
423
  /* ---------- Inputs ---------- */
424
  .stTextInput > div > div > input {
425
  border-radius: 0 !important;
426
- border: 1px solid #BBBBBB !important;
427
- font-family: Arial, Helvetica, sans-serif !important;
428
- padding: 0.6rem 0.8rem !important;
429
  }
430
  .stTextInput > div > div > input:focus {
431
  border-color: #901A1E !important;
432
- box-shadow: 0 0 0 1px #901A1E !important;
433
  }
434
 
435
  /* ---------- Button ---------- */
@@ -438,12 +286,10 @@ st.markdown(
438
  background-color: #901A1E !important;
439
  color: #FFFFFF !important;
440
  border: none !important;
441
- font-family: Arial, Helvetica, sans-serif !important;
442
- font-size: 0.92rem !important;
443
- padding: 0.55rem 2.2rem !important;
444
  letter-spacing: 0.02em;
445
- font-weight: 600 !important;
446
- text-transform: uppercase;
447
  }
448
  .stButton > button:hover {
449
  background-color: #7A1619 !important;
@@ -462,175 +308,38 @@ st.markdown(
462
  border-radius: 0 !important;
463
  }
464
 
465
- /* ---------- Pipeline tables ---------- */
466
- [data-testid="stExpander"] table {
467
- table-layout: fixed;
468
- width: 100%;
469
- word-break: break-all;
470
- overflow-wrap: break-word;
471
- }
472
- [data-testid="stExpander"] table td,
473
- [data-testid="stExpander"] table th {
474
- overflow: hidden;
475
- text-overflow: ellipsis;
476
- max-width: 0;
477
- font-size: 0.82rem;
478
- padding: 0.3rem 0.5rem;
479
- }
480
- [data-testid="stExpander"] table td:first-child,
481
- [data-testid="stExpander"] table th:first-child {
482
- width: 2.5rem;
483
- }
484
- [data-testid="stExpander"] table td:last-child,
485
- [data-testid="stExpander"] table th:last-child {
486
- width: 5rem;
487
- }
488
-
489
  /* ---------- Expander ---------- */
490
  .streamlit-expanderHeader {
491
- font-family: Arial, Helvetica, sans-serif !important;
492
- font-size: 0.95rem !important;
493
  color: #333333 !important;
494
  }
495
-
496
- /* ---------- Footer ---------- */
497
- .site-footer {
498
- background-color: #2B2B2B;
499
- color: #CCCCCC;
500
- margin: 3rem -4rem 0 -4rem;
501
- width: 100vw;
502
- position: relative;
503
- left: 50%;
504
- transform: translateX(-50%);
505
- padding: 2.5rem 0 1.5rem 0;
506
- }
507
- .footer-inner {
508
- max-width: 1100px;
509
- margin: 0 auto;
510
- padding: 0 2rem;
511
- display: grid;
512
- grid-template-columns: 2fr 1fr 1fr 1fr;
513
- gap: 2rem;
514
- }
515
- .footer-col h4 {
516
- font-family: Georgia, 'Times New Roman', serif;
517
- font-size: 0.95rem;
518
- font-weight: 700;
519
- color: #FFFFFF;
520
- margin: 0 0 0.8rem 0;
521
- padding-bottom: 0.4rem;
522
- border-bottom: 2px solid #901A1E;
523
- display: inline-block;
524
- }
525
- .footer-col p, .footer-col a {
526
- font-size: 0.82rem;
527
- color: #AAAAAA;
528
- line-height: 1.7;
529
- text-decoration: none;
530
- display: block;
531
- }
532
- .footer-col a:hover {
533
- color: #FFFFFF;
534
- }
535
- .footer-bottom {
536
- max-width: 1100px;
537
- margin: 1.5rem auto 0 auto;
538
- padding: 1rem 2rem 0 2rem;
539
- border-top: 1px solid #444444;
540
- font-size: 0.78rem;
541
- color: #888888;
542
- text-align: center;
543
- }
544
  </style>
545
  """,
546
  unsafe_allow_html=True,
547
  )
548
 
549
  # ---------------------------------------------------------------------------
550
- # Language state — use query param trick for the utility-bar lang switch
551
  # ---------------------------------------------------------------------------
552
- if "lang" not in st.session_state:
553
- st.session_state.lang = "da"
554
-
555
-
556
- def _set_lang(code: str) -> None:
557
- """Set the language in session state."""
558
- st.session_state.lang = code
559
-
560
-
561
- # A hidden selectbox drives the actual state; the visual switch is in the bar
562
- _col_hidden_lang = st.columns([1])[0]
563
- with _col_hidden_lang:
564
  lang = st.selectbox(
565
- "lang_sel",
566
  options=["da", "en"],
567
  format_func=lambda c: "Dansk" if c == "da" else "English",
568
- index=0 if st.session_state.lang == "da" else 1,
569
  label_visibility="collapsed",
570
- key="lang_select",
571
  )
572
- st.session_state.lang = lang
573
 
574
  t = TEXTS[lang]
575
 
576
- # ---------------------------------------------------------------------------
577
- # Top utility bar
578
- # ---------------------------------------------------------------------------
579
- st.markdown(
580
- f"""
581
- <div class="top-utility-bar">
582
- <div class="top-utility-inner">
583
- <span>{t["footer_service_status"]}</span>
584
- <span>{t["footer_service_guide"]}</span>
585
- <span class="utility-lang-switch">{"Dansk" if lang == "da" else "English"}</span>
586
- </div>
587
- </div>
588
- """,
589
- unsafe_allow_html=True,
590
- )
591
-
592
- # ---------------------------------------------------------------------------
593
- # Main navigation bar
594
- # ---------------------------------------------------------------------------
595
- st.markdown(
596
- f"""
597
- <div class="main-nav-bar">
598
- <div class="main-nav-inner">
599
- <span class="nav-brand">Dokumentassistent</span>
600
- <div class="nav-links">
601
- <a href="#" class="active">{t["nav_home"]}</a>
602
- <a href="#">{t["nav_docs"]}</a>
603
- <a href="#">{t["nav_search"]}</a>
604
- <a href="#">{t["nav_about"]}</a>
605
- </div>
606
- </div>
607
- </div>
608
- """,
609
- unsafe_allow_html=True,
610
- )
611
-
612
- # ---------------------------------------------------------------------------
613
- # Breadcrumbs
614
- # ---------------------------------------------------------------------------
615
- st.markdown(
616
- f"""
617
- <div class="breadcrumbs">
618
- <a href="#">{t["breadcrumb_home"]}</a>
619
- <span class="separator">&rsaquo;</span>
620
- <a href="#">{t["nav_search"]}</a>
621
- <span class="separator">&rsaquo;</span>
622
- <span>{t["breadcrumb_current"]}</span>
623
- </div>
624
- """,
625
- unsafe_allow_html=True,
626
- )
627
-
628
  # ---------------------------------------------------------------------------
629
  # Sidebar
630
  # ---------------------------------------------------------------------------
631
  with st.sidebar:
632
  st.markdown(
633
- f'<div class="sidebar-section-heading">{t["sidebar_heading"]}</div>',
634
  unsafe_allow_html=True,
635
  )
636
  st.markdown(t["sidebar_body"])
@@ -661,7 +370,7 @@ with st.sidebar:
661
  _emb = _health.get("embedding_model", "")
662
  _emb_prov = _health.get("embedding_provider", "")
663
  st.markdown(
664
- f'<div class="sidebar-section-heading">{t["model_heading"]}</div>',
665
  unsafe_allow_html=True,
666
  )
667
  st.markdown(
@@ -672,15 +381,16 @@ with st.sidebar:
672
  st.caption(t["model_unavailable"])
673
 
674
  # ---------------------------------------------------------------------------
675
- # Main content — page title area
676
  # ---------------------------------------------------------------------------
 
 
 
 
 
 
677
  st.markdown(
678
- f"""
679
- <div class="page-title-area">
680
- <div class="page-title">{t["title"]}</div>
681
- <p class="page-subtitle">{t["subtitle"]}</p>
682
- </div>
683
- """,
684
  unsafe_allow_html=True,
685
  )
686
 
@@ -785,12 +495,6 @@ if search_clicked and question.strip():
785
 
786
  st.markdown("---")
787
 
788
- def _truncate_doc_id(doc_id: str, max_len: int = 40) -> str:
789
- """Truncate long document IDs for table display."""
790
- if len(doc_id) <= max_len:
791
- return doc_id
792
- return doc_id[: max_len - 3] + "..."
793
-
794
  def _render_result_table(results: list[dict], label: str) -> None:
795
  """Render a ranked results table."""
796
  st.markdown(f"**{label}**")
@@ -799,7 +503,7 @@ if search_clicked and question.strip():
799
  return
800
  header = f'| {t["pipeline_rank"]} | {t["pipeline_doc"]} | {t["pipeline_score"]} |\n|---|---|---|'
801
  rows = "\n".join(
802
- f'| {i + 1} | {_truncate_doc_id(r.get("document_id", ""))} | {r.get("score", 0):.4f} |'
803
  for i, r in enumerate(results)
804
  )
805
  st.markdown(f"{header}\n{rows}")
@@ -843,7 +547,7 @@ if search_clicked and question.strip():
843
  else:
844
  change = "-"
845
  rows_list.append(
846
- f'| {i + 1} | {_truncate_doc_id(r.get("document_id", ""))} | {new_score:.4f} | {change} |'
847
  )
848
  st.markdown(f"{header}\n" + "\n".join(rows_list))
849
  else:
@@ -851,41 +555,3 @@ if search_clicked and question.strip():
851
 
852
  elif search_clicked:
853
  st.warning(t["empty_warning"])
854
-
855
- # ---------------------------------------------------------------------------
856
- # Footer
857
- # ---------------------------------------------------------------------------
858
- st.markdown(
859
- f"""
860
- <div class="site-footer">
861
- <div class="footer-inner">
862
- <div class="footer-col">
863
- <h4>Dokumentassistent</h4>
864
- <p>{t["footer_contact_text"]}</p>
865
- <p style="margin-top:0.5rem; color:#888;">RAG-pipeline &middot; Hybrid Search &middot; LLM</p>
866
- </div>
867
- <div class="footer-col">
868
- <h4>{t["footer_services"]}</h4>
869
- <a href="#">{t["footer_service_api"]}</a>
870
- <a href="#">{t["footer_service_status"]}</a>
871
- <a href="#">{t["footer_service_guide"]}</a>
872
- </div>
873
- <div class="footer-col">
874
- <h4>{t["footer_about"]}</h4>
875
- <a href="#">{t["footer_about_privacy"]}</a>
876
- <a href="#">{t["footer_about_data"]}</a>
877
- <a href="#">{t["footer_about_access"]}</a>
878
- </div>
879
- <div class="footer-col">
880
- <h4>{t["footer_contact"]}</h4>
881
- <p>support@example.dk</p>
882
- <p>+45 00 00 00 00</p>
883
- </div>
884
- </div>
885
- <div class="footer-bottom">
886
- {t["footer_copyright"]}
887
- </div>
888
- </div>
889
- """,
890
- unsafe_allow_html=True,
891
- )
 
1
  """Streamlit frontend for Dokumentassistent.
2
 
3
  Calls the FastAPI backend at http://localhost:8000.
4
+ Single-page document search interface with clean sans-serif design.
5
  """
6
 
7
  import os
 
85
  "pipeline_rank": "#",
86
  "pipeline_no_results": "Ingen resultater",
87
  "pipeline_score_change": "Score-aendring",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  },
89
  "en": {
90
  "page_title": "Document Assistant",
 
155
  "pipeline_rank": "#",
156
  "pipeline_no_results": "No results",
157
  "pipeline_score_change": "Score change",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  },
159
  }
160
 
 
164
  st.set_page_config(
165
  page_title="Dokumentassistent",
166
  page_icon=None,
167
+ layout="centered",
168
  )
169
 
170
  # ---------------------------------------------------------------------------
171
+ # Custom CSS -- Clean sans-serif design
172
  # ---------------------------------------------------------------------------
173
  st.markdown(
174
  """
175
  <style>
176
  /* ---------- Global ---------- */
177
  html, body, [class*="css"] {
178
+ font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
179
  color: #333333;
180
  background-color: #FFFFFF;
181
  }
182
 
183
+ /* Hide default Streamlit branding but keep the sidebar toggle */
184
  #MainMenu, footer {visibility: hidden;}
185
+ header[data-testid="stHeader"] {background: transparent;}
186
 
187
+ /* ---------- Accent line ---------- */
188
+ .accent-line {
189
+ width: 100%;
190
+ height: 4px;
191
+ background-color: #901A1E;
192
+ margin-bottom: 1.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  }
194
 
195
+ /* ---------- Title ---------- */
196
+ .app-title {
197
+ font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
198
+ font-size: 2.2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  font-weight: 700;
200
  color: #901A1E;
201
+ margin: 0 0 0.4rem 0;
202
  letter-spacing: -0.02em;
203
  }
204
+ .app-subtitle {
205
+ font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
206
+ font-size: 1.05rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  color: #666666;
208
+ margin: 0 0 2rem 0;
209
  line-height: 1.6;
210
  }
211
 
212
  /* ---------- Sidebar ---------- */
213
  section[data-testid="stSidebar"] {
214
+ background-color: #FAFAFA;
215
  border-right: 1px solid #E0E0E0;
216
  }
217
+ section[data-testid="stSidebar"] .sidebar-heading {
218
+ font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
219
+ font-size: 1.2rem;
220
  font-weight: 700;
221
+ color: #901A1E;
222
  margin-bottom: 0.5rem;
 
 
223
  }
224
  section[data-testid="stSidebar"] p,
225
  section[data-testid="stSidebar"] li {
226
+ font-size: 0.92rem;
227
  color: #555555;
228
  line-height: 1.55;
229
  }
230
 
231
  /* ---------- Source card ---------- */
232
  .source-card {
233
+ border: 1px solid #CCCCCC;
 
 
 
234
  padding: 1rem 1.2rem;
235
  margin-bottom: 0.75rem;
236
  background-color: #FAFAFA;
 
 
 
 
237
  }
238
  .source-card-title {
239
  font-weight: 600;
240
+ color: #333333;
241
  font-size: 0.95rem;
242
  margin-bottom: 0.3rem;
243
  }
244
  .source-card-text {
245
+ font-size: 0.88rem;
246
  color: #555555;
247
  line-height: 1.55;
248
  }
249
  .source-card-meta {
250
+ font-size: 0.8rem;
251
  color: #888888;
252
  margin-top: 0.4rem;
253
  }
254
 
255
  /* ---------- Result metadata ---------- */
256
  .result-meta {
257
+ font-size: 0.88rem;
258
  color: #666666;
259
  margin-bottom: 1.2rem;
260
+ padding-bottom: 0.8rem;
261
+ border-bottom: 1px solid #E0E0E0;
 
262
  }
263
 
264
  /* ---------- Answer area ---------- */
265
  .answer-block {
266
+ font-size: 1.05rem;
267
  line-height: 1.7;
268
  color: #333333;
269
  margin-bottom: 1.5rem;
 
 
 
270
  }
271
 
272
  /* ---------- Inputs ---------- */
273
  .stTextInput > div > div > input {
274
  border-radius: 0 !important;
275
+ border: 1px solid #999999 !important;
276
+ font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
 
277
  }
278
  .stTextInput > div > div > input:focus {
279
  border-color: #901A1E !important;
280
+ box-shadow: none !important;
281
  }
282
 
283
  /* ---------- Button ---------- */
 
286
  background-color: #901A1E !important;
287
  color: #FFFFFF !important;
288
  border: none !important;
289
+ font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
290
+ font-size: 0.95rem !important;
291
+ padding: 0.5rem 2rem !important;
292
  letter-spacing: 0.02em;
 
 
293
  }
294
  .stButton > button:hover {
295
  background-color: #7A1619 !important;
 
308
  border-radius: 0 !important;
309
  }
310
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  /* ---------- Expander ---------- */
312
  .streamlit-expanderHeader {
313
+ font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
314
+ font-size: 1rem !important;
315
  color: #333333 !important;
316
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  </style>
318
  """,
319
  unsafe_allow_html=True,
320
  )
321
 
322
  # ---------------------------------------------------------------------------
323
+ # Language selector -- top-right corner via columns
324
  # ---------------------------------------------------------------------------
325
+ _col_spacer, _col_lang = st.columns([5, 1])
326
+ with _col_lang:
 
 
 
 
 
 
 
 
 
 
327
  lang = st.selectbox(
328
+ "🌐",
329
  options=["da", "en"],
330
  format_func=lambda c: "Dansk" if c == "da" else "English",
331
+ index=0,
332
  label_visibility="collapsed",
 
333
  )
 
334
 
335
  t = TEXTS[lang]
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  # ---------------------------------------------------------------------------
338
  # Sidebar
339
  # ---------------------------------------------------------------------------
340
  with st.sidebar:
341
  st.markdown(
342
+ f'<div class="sidebar-heading">{t["sidebar_heading"]}</div>',
343
  unsafe_allow_html=True,
344
  )
345
  st.markdown(t["sidebar_body"])
 
370
  _emb = _health.get("embedding_model", "")
371
  _emb_prov = _health.get("embedding_provider", "")
372
  st.markdown(
373
+ f'<div class="sidebar-heading">{t["model_heading"]}</div>',
374
  unsafe_allow_html=True,
375
  )
376
  st.markdown(
 
381
  st.caption(t["model_unavailable"])
382
 
383
  # ---------------------------------------------------------------------------
384
+ # Main content
385
  # ---------------------------------------------------------------------------
386
+
387
+ # Accent line
388
+ st.markdown('<div class="accent-line"></div>', unsafe_allow_html=True)
389
+
390
+ # Title block
391
+ st.markdown(f'<div class="app-title">{t["title"]}</div>', unsafe_allow_html=True)
392
  st.markdown(
393
+ f'<div class="app-subtitle">{t["subtitle"]}</div>',
 
 
 
 
 
394
  unsafe_allow_html=True,
395
  )
396
 
 
495
 
496
  st.markdown("---")
497
 
 
 
 
 
 
 
498
  def _render_result_table(results: list[dict], label: str) -> None:
499
  """Render a ranked results table."""
500
  st.markdown(f"**{label}**")
 
503
  return
504
  header = f'| {t["pipeline_rank"]} | {t["pipeline_doc"]} | {t["pipeline_score"]} |\n|---|---|---|'
505
  rows = "\n".join(
506
+ f'| {i + 1} | {r.get("document_id", "")} | {r.get("score", 0):.4f} |'
507
  for i, r in enumerate(results)
508
  )
509
  st.markdown(f"{header}\n{rows}")
 
547
  else:
548
  change = "-"
549
  rows_list.append(
550
+ f'| {i + 1} | {r.get("document_id", "")} | {new_score:.4f} | {change} |'
551
  )
552
  st.markdown(f"{header}\n" + "\n".join(rows_list))
553
  else:
 
555
 
556
  elif search_clicked:
557
  st.warning(t["empty_warning"])