kappai commited on
Commit
1b68f7e
·
verified ·
1 Parent(s): f36f4af

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +166 -72
app.py CHANGED
@@ -25,8 +25,6 @@ MODEL_ID = os.environ.get("MODEL_ID", "google/gemma-2-2b-it")
25
  # Personal access token from your Hugging Face account (Space secret).
26
  HF_TOKEN = os.environ.get("HF_TOKEN")
27
 
28
-
29
-
30
  REPO_PATH = "/data/questions.json" # where we store generated questions
31
 
32
  CATEGORIES = [
@@ -54,6 +52,26 @@ GUIDES = {
54
  },
55
  }
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  # Few-shot + fallback pools
58
  FEWSHOTS = {
59
  "fr": {
@@ -258,7 +276,7 @@ def save_repo(data: Dict[str, List[str]]) -> None:
258
  # PROMPT + MODEL HELPERS
259
 
260
 
261
- def build_prompt(lang: str, category_key: str, variant: str) -> str:
262
  cat = next((c for c in CATEGORIES if c["key"] == category_key), None)
263
  if not cat:
264
  category_key = "alimentation"
@@ -266,13 +284,7 @@ def build_prompt(lang: str, category_key: str, variant: str) -> str:
266
 
267
  guide = GUIDES[lang][category_key]
268
  few = FEWSHOTS[lang][category_key]
269
-
270
- if variant == "best":
271
- tone_fr = "ludique, original, léger"
272
- tone_en = "playful, original, light"
273
- else:
274
- tone_fr = "sincère, introspectif, doux"
275
- tone_en = "sincere, introspective, gentle"
276
 
277
  schema = (
278
  "{\n"
@@ -280,7 +292,7 @@ def build_prompt(lang: str, category_key: str, variant: str) -> str:
280
  ' "language": "<fr|en>",\n'
281
  ' "questions": ["q1", "q2", "q3", "q4"],\n'
282
  ' "micro_actions": ["m1", "m2"],\n'
283
- ' "tone": "playful|sincere|ludique|sincère",\n'
284
  ' "safety_notes": ""\n'
285
  "}"
286
  )
@@ -295,7 +307,7 @@ Tu crées des cartes-question pour parler de routines du quotidien.
295
 
296
  - Catégorie: {cat['fr']} {cat['icon']}.
297
  - Focus: {guide}
298
- - Ton: {tone_fr}
299
  - Format: 4 questions + 2 micro-actions, 1 phrase courte chacune.
300
  - Style: concret, bienveillant, sans jugement.
301
  - Interdit: conseils médicaux, diagnostics, emojis.
@@ -316,7 +328,7 @@ You create question-cards about everyday routines.
316
 
317
  - Category: {cat['en']} {cat['icon']}.
318
  - Focus: {guide}
319
- - Tone: {tone_en}
320
  - Format: 4 questions + 2 micro-actions, one short sentence each.
321
  - Style: concrete, kind, non-judgmental.
322
  - Forbidden: medical advice, diagnoses, emojis.
@@ -367,7 +379,6 @@ def try_parse_json(text: str) -> Optional[Dict[str, Any]]:
367
  return None
368
 
369
 
370
- # 🔧 SIMPLIFIED, ROBUST MODEL CALL (no secrets required)
371
  def model_call(prompt: str) -> str:
372
  """
373
  Call Hugging Face Inference API using the conversational (chat) task.
@@ -437,7 +448,7 @@ def normalize_output(
437
  data: Dict[str, Any],
438
  lang: str,
439
  category_key: str,
440
- variant: str
441
  ) -> Dict[str, Any]:
442
  """
443
  Make model output always valid, even if the model returns emojis, wrong category labels,
@@ -480,14 +491,10 @@ def normalize_output(
480
  m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
481
  m = (m + [""] * 2)[:2]
482
 
483
- # --- FIX TONE ---
484
- if not data.get("tone"):
485
- if lang == "fr":
486
- tone = "ludique" if variant == "best" else "sincère"
487
- else:
488
- tone = "playful" if variant == "best" else "sincere"
489
- else:
490
- tone = str(data.get("tone")).strip().lower()
491
 
492
  # --- SAFETY NOTES ---
493
  safety_notes = str(data.get("safety_notes", ""))
@@ -497,24 +504,24 @@ def normalize_output(
497
  "language": lang,
498
  "questions": q,
499
  "micro_actions": m,
500
- "tone": tone,
501
  "safety_notes": safety_notes,
502
  }
503
 
504
 
505
- def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
506
  """
507
  Try to call the model. If anything fails or JSON is invalid,
508
  fall back to shuffling the few-shots and include a safety_notes message.
509
  """
510
- prompt = build_prompt(lang, category_key, variant)
511
 
512
  try:
513
  raw_text = model_call(prompt)
514
  parsed = try_parse_json(raw_text) if raw_text else None
515
 
516
  if parsed:
517
- return normalize_output(parsed, lang, category_key, variant)
518
 
519
  # Model replied but not valid JSON
520
  few = FEWSHOTS[lang][category_key]
@@ -527,7 +534,7 @@ def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
527
  "language": lang,
528
  "questions": (q_pool + [""] * 4)[:4],
529
  "micro_actions": (m_pool + [""] * 2)[:2],
530
- "tone": "fallback",
531
  "safety_notes": (
532
  "Model replied but JSON parsing failed. "
533
  f"raw_text starts with: {repr(raw_text[:160])}"
@@ -546,7 +553,7 @@ def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
546
  "language": lang,
547
  "questions": (q_pool + [""] * 4)[:4],
548
  "micro_actions": (m_pool + [""] * 2)[:2],
549
- "tone": "error",
550
  "safety_notes": f"Model call error: {type(e).__name__}: {e}",
551
  }
552
 
@@ -557,7 +564,7 @@ def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
557
  def get_questions_and_micro(
558
  lang: str,
559
  category_key: str,
560
- variant: str,
561
  seen: List[str],
562
  ) -> Dict[str, Any]:
563
  """
@@ -573,7 +580,7 @@ def get_questions_and_micro(
573
  unseen_repo = [q for q in repo_qs if q and q not in seen_set]
574
 
575
  used_ai = False
576
- tone = ""
577
  safety_notes = ""
578
 
579
  if len(unseen_repo) >= 4:
@@ -584,14 +591,13 @@ def get_questions_and_micro(
584
  m_pool = FEWSHOTS[lang][category_key]["micro_actions"][:]
585
  random.shuffle(m_pool)
586
  micro = (m_pool + ["", ""])[:2]
587
- tone = "repo"
588
  safety_notes = ""
589
  else:
590
  # need fresh AI content
591
- ai_out = ai_generate(lang, category_key, variant)
592
  questions = ai_out["questions"]
593
  micro = ai_out["micro_actions"]
594
- tone = ai_out.get("tone", "")
595
  safety_notes = ai_out.get("safety_notes", "")
596
  used_ai = True
597
 
@@ -613,12 +619,11 @@ def get_questions_and_micro(
613
  "questions": questions,
614
  "micro_actions": micro,
615
  "source": "ai" if used_ai else "repo",
616
- "tone": tone,
617
  "safety_notes": safety_notes,
618
  }
619
 
620
- # If you ever need the payload again, you can use it here;
621
- # for the UI we only return questions + micro + updated seen.
622
  return {
623
  "questions": questions,
624
  "micro_actions": micro,
@@ -633,10 +638,15 @@ def get_questions_and_micro(
633
  def _map_category(choice: str) -> str:
634
  mapping = {
635
  "alimentation 🍎": "alimentation",
 
636
  "mouvement 🦘": "mouvement",
 
637
  "cerveau 🧠": "cerveau",
 
638
  "liens 🤝": "liens",
 
639
  "bien-etre 💬": "bien-etre",
 
640
  }
641
  return mapping.get(choice, "alimentation")
642
 
@@ -652,9 +662,9 @@ def _card_html(category_key: str, kind: str, title: str, body: str, delay_s: flo
652
  )
653
 
654
 
655
- def update_cards(lang: str, category_choice: str, variant: str, seen: List[str]):
656
  category_key = _map_category(category_choice)
657
- result = get_questions_and_micro(lang, category_key, variant, seen or [])
658
  questions = result["questions"]
659
  micro = result["micro_actions"]
660
  new_seen = result["seen"]
@@ -663,6 +673,13 @@ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str])
663
  delays_q = [0.05, 0.10, 0.15, 0.20]
664
  delays_m = [0.25, 0.30]
665
 
 
 
 
 
 
 
 
666
  q_htmls = []
667
  for i in range(4):
668
  text = questions[i] if i < len(questions) else ""
@@ -670,7 +687,7 @@ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str])
670
  _card_html(
671
  category_key,
672
  "q",
673
- f"Question {i+1}",
674
  text,
675
  delays_q[i],
676
  )
@@ -683,7 +700,7 @@ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str])
683
  _card_html(
684
  category_key,
685
  "m",
686
- f"Micro-action {i+1}",
687
  text,
688
  delays_m[i],
689
  )
@@ -692,6 +709,83 @@ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str])
692
  return (*q_htmls, *m_htmls, new_seen)
693
 
694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  # ────────────────────────────────────────────────────────────────────────────────
696
  # GRADIO APP
697
 
@@ -703,23 +797,15 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
703
 
704
  seen_state = gr.State([]) # per-session list of seen questions
705
 
 
 
706
  with gr.Column(elem_classes="nv-shell"):
707
- gr.HTML(
708
- """
709
- <div>
710
- <div class="nv-badge">NEUROVIE · FINGER</div>
711
- <div class="nv-title">Question Studio</div>
712
- <div class="nv-subtitle">
713
- Minimal prompts for rich conversations — 4 questions and 2 micro-actions par tirage.
714
- </div>
715
- </div>
716
- """
717
- )
718
 
719
  # Settings
720
  with gr.Row(elem_classes="nv-section"):
721
  with gr.Column():
722
- gr.HTML("<div class='nv-label'>Language</div>")
723
  lang = gr.Radio(
724
  choices=["fr", "en"],
725
  value="fr",
@@ -727,54 +813,62 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
727
  elem_classes="nv-pills",
728
  )
729
  with gr.Column():
730
- gr.HTML("<div class='nv-label'>Tone</div>")
731
- variant = gr.Radio(
732
- choices=["best", "sincere"],
733
- value="best",
734
  show_label=False,
735
  elem_classes="nv-pills",
736
  )
737
 
738
  with gr.Column(elem_classes="nv-section"):
739
- gr.HTML("<div class='nv-label'>Category</div>")
740
  category = gr.Radio(
741
- choices=[
742
- "alimentation 🍎",
743
- "mouvement 🦘",
744
- "cerveau 🧠",
745
- "liens 🤝",
746
- "bien-etre 💬",
747
- ],
748
- value="alimentation 🍎",
749
  show_label=False,
750
  elem_classes="nv-pills",
751
  )
752
 
753
- btn = gr.Button("Generate card set ✨")
754
 
755
  # Question & micro-action cards
756
  with gr.Row(elem_classes="nv-section"):
757
  with gr.Column():
758
- gr.HTML("<div class='nv-label'>Questions</div>")
759
  with gr.Column(elem_classes="nv-card-grid"):
760
  q1 = gr.HTML()
761
  q2 = gr.HTML()
762
  q3 = gr.HTML()
763
  q4 = gr.HTML()
764
  with gr.Column():
765
- gr.HTML("<div class='nv-label'>Micro-actions</div>")
766
  with gr.Column(elem_classes="nv-card-grid"):
767
  m1 = gr.HTML()
768
  m2 = gr.HTML()
769
 
770
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
 
772
  btn.click(
773
  update_cards,
774
- [lang, category, variant, seen_state],
775
  [q1, q2, q3, q4, m1, m2, seen_state],
776
  show_progress=False, # hide Gradio built-in progress indicator
777
- )
778
 
779
 
780
  if __name__ == "__main__":
 
25
  # Personal access token from your Hugging Face account (Space secret).
26
  HF_TOKEN = os.environ.get("HF_TOKEN")
27
 
 
 
28
  REPO_PATH = "/data/questions.json" # where we store generated questions
29
 
30
  CATEGORIES = [
 
52
  },
53
  }
54
 
55
+ # Themes
56
+ THEME_KEYS = ["family", "friends", "romance", "silly", "education"]
57
+
58
+ THEME_DESCRIPTIONS = {
59
+ "fr": {
60
+ "family": "Thème famille : liens intergénérationnels, rituels familiaux, souvenirs partagés.",
61
+ "friends": "Thème amis : complicité, soutien, moments légers, retrouvailles.",
62
+ "romance": "Thème romance : connexion, douceur, attention à l’autre, moments à deux.",
63
+ "silly": "Thème décalé : questions ludiques, inattendues, créatives, pour faire rire.",
64
+ "education": "Thème éducation : curiosité, apprentissages, petites découvertes du quotidien.",
65
+ },
66
+ "en": {
67
+ "family": "Family theme: intergenerational bonds, family rituals, shared memories.",
68
+ "friends": "Friends theme: support, playfulness, shared moments, reconnection.",
69
+ "romance": "Romance theme: connection, tenderness, attention to each other, moments for two.",
70
+ "silly": "Silly theme: playful, unexpected, creative questions that invite laughter.",
71
+ "education": "Education theme: curiosity, learning, tiny everyday discoveries.",
72
+ },
73
+ }
74
+
75
  # Few-shot + fallback pools
76
  FEWSHOTS = {
77
  "fr": {
 
276
  # PROMPT + MODEL HELPERS
277
 
278
 
279
+ def build_prompt(lang: str, category_key: str, theme: str) -> str:
280
  cat = next((c for c in CATEGORIES if c["key"] == category_key), None)
281
  if not cat:
282
  category_key = "alimentation"
 
284
 
285
  guide = GUIDES[lang][category_key]
286
  few = FEWSHOTS[lang][category_key]
287
+ theme_desc = THEME_DESCRIPTIONS[lang].get(theme, "")
 
 
 
 
 
 
288
 
289
  schema = (
290
  "{\n"
 
292
  ' "language": "<fr|en>",\n'
293
  ' "questions": ["q1", "q2", "q3", "q4"],\n'
294
  ' "micro_actions": ["m1", "m2"],\n'
295
+ ' "theme": "<family|friends|romance|silly|education>",\n'
296
  ' "safety_notes": ""\n'
297
  "}"
298
  )
 
307
 
308
  - Catégorie: {cat['fr']} {cat['icon']}.
309
  - Focus: {guide}
310
+ - Thème actuel: {theme_desc}
311
  - Format: 4 questions + 2 micro-actions, 1 phrase courte chacune.
312
  - Style: concret, bienveillant, sans jugement.
313
  - Interdit: conseils médicaux, diagnostics, emojis.
 
328
 
329
  - Category: {cat['en']} {cat['icon']}.
330
  - Focus: {guide}
331
+ - Current theme: {theme_desc}
332
  - Format: 4 questions + 2 micro-actions, one short sentence each.
333
  - Style: concrete, kind, non-judgmental.
334
  - Forbidden: medical advice, diagnoses, emojis.
 
379
  return None
380
 
381
 
 
382
  def model_call(prompt: str) -> str:
383
  """
384
  Call Hugging Face Inference API using the conversational (chat) task.
 
448
  data: Dict[str, Any],
449
  lang: str,
450
  category_key: str,
451
+ theme: str,
452
  ) -> Dict[str, Any]:
453
  """
454
  Make model output always valid, even if the model returns emojis, wrong category labels,
 
491
  m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
492
  m = (m + [""] * 2)[:2]
493
 
494
+ # --- THEME ---
495
+ model_theme = str(data.get("theme", "")).strip().lower()
496
+ if model_theme not in THEME_KEYS:
497
+ model_theme = theme # fall back to selected theme key
 
 
 
 
498
 
499
  # --- SAFETY NOTES ---
500
  safety_notes = str(data.get("safety_notes", ""))
 
504
  "language": lang,
505
  "questions": q,
506
  "micro_actions": m,
507
+ "theme": model_theme,
508
  "safety_notes": safety_notes,
509
  }
510
 
511
 
512
+ def ai_generate(lang: str, category_key: str, theme: str) -> Dict[str, Any]:
513
  """
514
  Try to call the model. If anything fails or JSON is invalid,
515
  fall back to shuffling the few-shots and include a safety_notes message.
516
  """
517
+ prompt = build_prompt(lang, category_key, theme)
518
 
519
  try:
520
  raw_text = model_call(prompt)
521
  parsed = try_parse_json(raw_text) if raw_text else None
522
 
523
  if parsed:
524
+ return normalize_output(parsed, lang, category_key, theme)
525
 
526
  # Model replied but not valid JSON
527
  few = FEWSHOTS[lang][category_key]
 
534
  "language": lang,
535
  "questions": (q_pool + [""] * 4)[:4],
536
  "micro_actions": (m_pool + [""] * 2)[:2],
537
+ "theme": theme,
538
  "safety_notes": (
539
  "Model replied but JSON parsing failed. "
540
  f"raw_text starts with: {repr(raw_text[:160])}"
 
553
  "language": lang,
554
  "questions": (q_pool + [""] * 4)[:4],
555
  "micro_actions": (m_pool + [""] * 2)[:2],
556
+ "theme": theme,
557
  "safety_notes": f"Model call error: {type(e).__name__}: {e}",
558
  }
559
 
 
564
  def get_questions_and_micro(
565
  lang: str,
566
  category_key: str,
567
+ theme: str,
568
  seen: List[str],
569
  ) -> Dict[str, Any]:
570
  """
 
580
  unseen_repo = [q for q in repo_qs if q and q not in seen_set]
581
 
582
  used_ai = False
583
+ theme_used = theme
584
  safety_notes = ""
585
 
586
  if len(unseen_repo) >= 4:
 
591
  m_pool = FEWSHOTS[lang][category_key]["micro_actions"][:]
592
  random.shuffle(m_pool)
593
  micro = (m_pool + ["", ""])[:2]
 
594
  safety_notes = ""
595
  else:
596
  # need fresh AI content
597
+ ai_out = ai_generate(lang, category_key, theme)
598
  questions = ai_out["questions"]
599
  micro = ai_out["micro_actions"]
600
+ theme_used = ai_out.get("theme", theme)
601
  safety_notes = ai_out.get("safety_notes", "")
602
  used_ai = True
603
 
 
619
  "questions": questions,
620
  "micro_actions": micro,
621
  "source": "ai" if used_ai else "repo",
622
+ "theme": theme_used,
623
  "safety_notes": safety_notes,
624
  }
625
 
626
+ # For UI we only return questions + micro + updated seen.
 
627
  return {
628
  "questions": questions,
629
  "micro_actions": micro,
 
638
  def _map_category(choice: str) -> str:
639
  mapping = {
640
  "alimentation 🍎": "alimentation",
641
+ "nutrition 🍎": "alimentation",
642
  "mouvement 🦘": "mouvement",
643
+ "movement 🦘": "mouvement",
644
  "cerveau 🧠": "cerveau",
645
+ "brain 🧠": "cerveau",
646
  "liens 🤝": "liens",
647
+ "connections 🤝": "liens",
648
  "bien-etre 💬": "bien-etre",
649
+ "well-being 💬": "bien-etre",
650
  }
651
  return mapping.get(choice, "alimentation")
652
 
 
662
  )
663
 
664
 
665
+ def update_cards(lang: str, category_choice: str, theme: str, seen: List[str]):
666
  category_key = _map_category(category_choice)
667
+ result = get_questions_and_micro(lang, category_key, theme, seen or [])
668
  questions = result["questions"]
669
  micro = result["micro_actions"]
670
  new_seen = result["seen"]
 
673
  delays_q = [0.05, 0.10, 0.15, 0.20]
674
  delays_m = [0.25, 0.30]
675
 
676
+ if lang == "fr":
677
+ q_prefix = "Question"
678
+ m_prefix = "Micro-action"
679
+ else:
680
+ q_prefix = "Question"
681
+ m_prefix = "Micro-action"
682
+
683
  q_htmls = []
684
  for i in range(4):
685
  text = questions[i] if i < len(questions) else ""
 
687
  _card_html(
688
  category_key,
689
  "q",
690
+ f"{q_prefix} {i+1}",
691
  text,
692
  delays_q[i],
693
  )
 
700
  _card_html(
701
  category_key,
702
  "m",
703
+ f"{m_prefix} {i+1}",
704
  text,
705
  delays_m[i],
706
  )
 
709
  return (*q_htmls, *m_htmls, new_seen)
710
 
711
 
712
+ # ────────────────────────────────────────────────────────────────────────────────
713
+ # UI TEXT TRANSLATIONS
714
+
715
+
716
+ def get_ui_texts(lang: str) -> Dict[str, Any]:
717
+ if lang == "fr":
718
+ header = """
719
+ <div>
720
+ <div class="nv-badge">NEUROVIE · FINGER</div>
721
+ <div class="nv-title">Question Studio</div>
722
+ <div class="nv-subtitle">
723
+ Questions minimalistes pour conversations riches — 4 questions et 2 micro-actions par tirage.
724
+ </div>
725
+ </div>
726
+ """
727
+ category_choices = [
728
+ "alimentation 🍎",
729
+ "mouvement 🦘",
730
+ "cerveau 🧠",
731
+ "liens 🤝",
732
+ "bien-etre 💬",
733
+ ]
734
+ return {
735
+ "header_html": header,
736
+ "language_label": "Langue",
737
+ "theme_label": "Thème",
738
+ "category_label": "Catégorie",
739
+ "questions_label": "Questions",
740
+ "micro_label": "Micro-actions",
741
+ "button_text": "Générer un tirage ✨",
742
+ "category_choices": category_choices,
743
+ "category_default": "alimentation 🍎",
744
+ }
745
+ else:
746
+ header = """
747
+ <div>
748
+ <div class="nv-badge">NEUROVIE · FINGER</div>
749
+ <div class="nv-title">Question Studio</div>
750
+ <div class="nv-subtitle">
751
+ Minimal prompts for rich conversations — 4 questions and 2 micro-actions per draw.
752
+ </div>
753
+ </div>
754
+ """
755
+ category_choices = [
756
+ "Nutrition 🍎",
757
+ "Movement 🦘",
758
+ "Brain 🧠",
759
+ "Connections 🤝",
760
+ "Well-being 💬",
761
+ ]
762
+ return {
763
+ "header_html": header,
764
+ "language_label": "Language",
765
+ "theme_label": "Theme",
766
+ "category_label": "Category",
767
+ "questions_label": "Questions",
768
+ "micro_label": "Micro-actions",
769
+ "button_text": "Generate card set ✨",
770
+ "category_choices": category_choices,
771
+ "category_default": "Nutrition 🍎",
772
+ }
773
+
774
+
775
+ def update_ui_language(lang: str):
776
+ t = get_ui_texts(lang)
777
+ return (
778
+ t["header_html"],
779
+ f"<div class='nv-label'>{t['language_label']}</div>",
780
+ f"<div class='nv-label'>{t['theme_label']}</div>",
781
+ f"<div class='nv-label'>{t['category_label']}</div>",
782
+ f"<div class='nv-label'>{t['questions_label']}</div>",
783
+ f"<div class='nv-label'>{t['micro_label']}</div>",
784
+ gr.Radio.update(choices=t["category_choices"], value=t["category_default"]),
785
+ gr.Button.update(value=t["button_text"]),
786
+ )
787
+
788
+
789
  # ────────────────────────────────────────────────────────────────────────────────
790
  # GRADIO APP
791
 
 
797
 
798
  seen_state = gr.State([]) # per-session list of seen questions
799
 
800
+ ui_texts = get_ui_texts("fr")
801
+
802
  with gr.Column(elem_classes="nv-shell"):
803
+ header = gr.HTML(ui_texts["header_html"])
 
 
 
 
 
 
 
 
 
 
804
 
805
  # Settings
806
  with gr.Row(elem_classes="nv-section"):
807
  with gr.Column():
808
+ lang_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['language_label']}</div>")
809
  lang = gr.Radio(
810
  choices=["fr", "en"],
811
  value="fr",
 
813
  elem_classes="nv-pills",
814
  )
815
  with gr.Column():
816
+ theme_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['theme_label']}</div>")
817
+ theme = gr.Radio(
818
+ choices=THEME_KEYS,
819
+ value="family",
820
  show_label=False,
821
  elem_classes="nv-pills",
822
  )
823
 
824
  with gr.Column(elem_classes="nv-section"):
825
+ category_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['category_label']}</div>")
826
  category = gr.Radio(
827
+ choices=ui_texts["category_choices"],
828
+ value=ui_texts["category_default"],
 
 
 
 
 
 
829
  show_label=False,
830
  elem_classes="nv-pills",
831
  )
832
 
833
+ btn = gr.Button(ui_texts["button_text"])
834
 
835
  # Question & micro-action cards
836
  with gr.Row(elem_classes="nv-section"):
837
  with gr.Column():
838
+ questions_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['questions_label']}</div>")
839
  with gr.Column(elem_classes="nv-card-grid"):
840
  q1 = gr.HTML()
841
  q2 = gr.HTML()
842
  q3 = gr.HTML()
843
  q4 = gr.HTML()
844
  with gr.Column():
845
+ micro_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['micro_label']}</div>")
846
  with gr.Column(elem_classes="nv-card-grid"):
847
  m1 = gr.HTML()
848
  m2 = gr.HTML()
849
 
850
+ # Update labels & category choices when language changes
851
+ lang.change(
852
+ fn=update_ui_language,
853
+ inputs=[lang],
854
+ outputs=[
855
+ header,
856
+ lang_label_html,
857
+ theme_label_html,
858
+ category_label_html,
859
+ questions_label_html,
860
+ micro_label_html,
861
+ category,
862
+ btn,
863
+ ],
864
+ )
865
 
866
  btn.click(
867
  update_cards,
868
+ [lang, category, theme, seen_state],
869
  [q1, q2, q3, q4, m1, m2, seen_state],
870
  show_progress=False, # hide Gradio built-in progress indicator
871
+ )
872
 
873
 
874
  if __name__ == "__main__":