kappai commited on
Commit
9dad147
Β·
verified Β·
1 Parent(s): 9b665fc

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +145 -12
  2. style.css +15 -0
app.py CHANGED
@@ -55,6 +55,23 @@ GUIDES = {
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.",
@@ -650,6 +667,25 @@ def _map_category(choice: str) -> str:
650
  }
651
  return mapping.get(choice, "alimentation")
652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
 
654
  def _card_html(category_key: str, kind: str, title: str, body: str, delay_s: float) -> str:
655
  kind_attr = "question" if kind == "q" else "micro"
@@ -662,9 +698,10 @@ def _card_html(category_key: str, kind: str, title: str, body: str, delay_s: flo
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"]
@@ -716,13 +753,13 @@ def update_cards(lang: str, category_choice: str, theme: str, seen: List[str]):
716
  def get_ui_texts(lang: str) -> Dict[str, Any]:
717
  if lang == "fr":
718
  header = """
719
- <div class="nv-fade">
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
 
728
  category_choices = [
@@ -732,6 +769,7 @@ def get_ui_texts(lang: str) -> Dict[str, Any]:
732
  "liens 🀝",
733
  "bien-etre πŸ’¬",
734
  ]
 
735
  return {
736
  "header_html": header,
737
  "language_label": "Langue",
@@ -742,6 +780,8 @@ def get_ui_texts(lang: str) -> Dict[str, Any]:
742
  "button_text": "GΓ©nΓ©rer un tirage ✨",
743
  "category_choices": category_choices,
744
  "category_default": "alimentation 🍎",
 
 
745
  }
746
  else:
747
  header = """
@@ -761,6 +801,7 @@ def get_ui_texts(lang: str) -> Dict[str, Any]:
761
  "Connections 🀝",
762
  "Well-being πŸ’¬",
763
  ]
 
764
  return {
765
  "header_html": header,
766
  "language_label": "Language",
@@ -771,6 +812,8 @@ def get_ui_texts(lang: str) -> Dict[str, Any]:
771
  "button_text": "Generate card set ✨",
772
  "category_choices": category_choices,
773
  "category_default": "Nutrition 🍎",
 
 
774
  }
775
 
776
 
@@ -783,13 +826,15 @@ def update_ui_language(lang: str):
783
  f"<div class='nv-label nv-fade'>{t['category_label']}</div>",
784
  f"<div class='nv-label nv-fade'>{t['questions_label']}</div>",
785
  f"<div class='nv-label nv-fade'>{t['micro_label']}</div>",
 
786
  gr.update(choices=t["category_choices"], value=t["category_default"]),
787
- gr.update(value=t['button_text']),
788
  )
789
 
790
 
791
 
792
 
 
793
  # ───────────────────────────────────────────────────────���────────────────────────
794
  # GRADIO APP
795
 
@@ -798,6 +843,92 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
798
  # Inline the CSS content from style.css
799
  if CUSTOM_CSS:
800
  gr.HTML(f"<style>{CUSTOM_CSS}</style>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
 
802
  seen_state = gr.State([]) # per-session list of seen questions
803
 
@@ -827,14 +958,15 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
827
  f"<div class='nv-label nv-fade'>{ui_texts['theme_label']}</div>"
828
  )
829
  theme = gr.Radio(
830
- choices=THEME_KEYS,
831
- value="family",
832
  show_label=False,
833
  elem_classes="nv-pills",
834
  )
835
 
836
 
837
 
 
838
  with gr.Column(elem_classes="nv-section"):
839
  category_label_html = gr.HTML(f"<div class='nv-label nv-fade'>{ui_texts['category_label']}</div>")
840
  category = gr.Radio(
@@ -872,6 +1004,7 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
872
  category_label_html,
873
  questions_label_html,
874
  micro_label_html,
 
875
  category,
876
  btn,
877
  ],
@@ -887,4 +1020,4 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
887
 
888
 
889
  if __name__ == "__main__":
890
- demo.launch()
 
55
  # Themes
56
  THEME_KEYS = ["family", "friends", "romance", "silly", "education"]
57
 
58
+ THEME_LABELS = {
59
+ "fr": {
60
+ "family": "famille",
61
+ "friends": "amis",
62
+ "romance": "romance",
63
+ "silly": "dΓ©calΓ©",
64
+ "education": "Γ©ducation",
65
+ },
66
+ "en": {
67
+ "family": "family",
68
+ "friends": "friends",
69
+ "romance": "romance",
70
+ "silly": "silly",
71
+ "education": "education",
72
+ },
73
+ }
74
+
75
  THEME_DESCRIPTIONS = {
76
  "fr": {
77
  "family": "ThΓ¨me famille : liens intergΓ©nΓ©rationnels, rituels familiaux, souvenirs partagΓ©s.",
 
667
  }
668
  return mapping.get(choice, "alimentation")
669
 
670
+ def _map_theme(label: str, lang: str) -> str:
671
+ """
672
+ Convert the user-visible theme label back to its internal key.
673
+ Works for both languages and also accepts the key itself as fallback.
674
+ """
675
+ label = (label or "").strip().lower()
676
+
677
+ # First check if it's already a key
678
+ if label in THEME_KEYS:
679
+ return label
680
+
681
+ # Otherwise map via THEME_LABELS
682
+ for key in THEME_KEYS:
683
+ if label == THEME_LABELS["fr"][key].lower() or label == THEME_LABELS["en"][key].lower():
684
+ return key
685
+
686
+ # Fallback
687
+ return "family"
688
+
689
 
690
  def _card_html(category_key: str, kind: str, title: str, body: str, delay_s: float) -> str:
691
  kind_attr = "question" if kind == "q" else "micro"
 
698
  )
699
 
700
 
701
+ def update_cards(lang: str, category_choice: str, theme_choice: str, seen: List[str]):
702
  category_key = _map_category(category_choice)
703
+ theme_key = _map_theme(theme_choice, lang)
704
+ result = get_questions_and_micro(lang, category_key, theme_key, seen or [])
705
  questions = result["questions"]
706
  micro = result["micro_actions"]
707
  new_seen = result["seen"]
 
753
  def get_ui_texts(lang: str) -> Dict[str, Any]:
754
  if lang == "fr":
755
  header = """
756
+ <div class="nv-fade">
757
+ <div class="nv-badge">NEUROVIE Β· FINGER</div>
758
+ <div class="nv-title">Question Studio</div>
759
+ <div class="nv-subtitle">
760
+ Questions minimalistes pour conversations riches β€” 4 questions et 2 micro-actions par tirage.
761
  </div>
762
+ </div>
763
  """
764
 
765
  category_choices = [
 
769
  "liens 🀝",
770
  "bien-etre πŸ’¬",
771
  ]
772
+ theme_choices = [THEME_LABELS["fr"][k] for k in THEME_KEYS]
773
  return {
774
  "header_html": header,
775
  "language_label": "Langue",
 
780
  "button_text": "GΓ©nΓ©rer un tirage ✨",
781
  "category_choices": category_choices,
782
  "category_default": "alimentation 🍎",
783
+ "theme_choices": theme_choices,
784
+ "theme_default": THEME_LABELS["fr"]["family"],
785
  }
786
  else:
787
  header = """
 
801
  "Connections 🀝",
802
  "Well-being πŸ’¬",
803
  ]
804
+ theme_choices = [THEME_LABELS["en"][k] for k in THEME_KEYS]
805
  return {
806
  "header_html": header,
807
  "language_label": "Language",
 
812
  "button_text": "Generate card set ✨",
813
  "category_choices": category_choices,
814
  "category_default": "Nutrition 🍎",
815
+ "theme_choices": theme_choices,
816
+ "theme_default": THEME_LABELS["en"]["family"],
817
  }
818
 
819
 
 
826
  f"<div class='nv-label nv-fade'>{t['category_label']}</div>",
827
  f"<div class='nv-label nv-fade'>{t['questions_label']}</div>",
828
  f"<div class='nv-label nv-fade'>{t['micro_label']}</div>",
829
+ gr.update(choices=t["theme_choices"], value=t["theme_default"]),
830
  gr.update(choices=t["category_choices"], value=t["category_default"]),
831
+ gr.update(value=t["button_text"]),
832
  )
833
 
834
 
835
 
836
 
837
+
838
  # ───────────────────────────────────────────────────────���────────────────────────
839
  # GRADIO APP
840
 
 
843
  # Inline the CSS content from style.css
844
  if CUSTOM_CSS:
845
  gr.HTML(f"<style>{CUSTOM_CSS}</style>")
846
+ gr.HTML(
847
+ """
848
+ <script>
849
+ (function () {
850
+ // Initialize fading behaviour on any .nv-card found under `root`
851
+ function initCardFading(root) {
852
+ const cards = (root || document).querySelectorAll('.nv-card');
853
+ cards.forEach((card) => {
854
+ if (card.dataset.fadeInit === '1') return;
855
+ card.dataset.fadeInit = '1';
856
+
857
+ // initial opacity
858
+ if (!card.dataset.opacity) {
859
+ card.dataset.opacity = '1';
860
+ card.style.opacity = 1;
861
+ }
862
+
863
+ // track hover state
864
+ card.addEventListener('mouseenter', () => {
865
+ card.dataset.hover = '1';
866
+ });
867
+
868
+ card.addEventListener('mouseleave', () => {
869
+ card.dataset.hover = '0';
870
+ // fast fade step when the user leaves the card
871
+ let current = parseFloat(card.dataset.opacity || '1');
872
+ // ~7% per hover, never below 0.25
873
+ let next = Math.max(0.25, current - 0.07);
874
+ card.dataset.opacity = String(next);
875
+ card.style.opacity = next;
876
+ });
877
+ });
878
+ }
879
+
880
+ // run once on page load
881
+ window.addEventListener('load', () => {
882
+ initCardFading(document);
883
+ });
884
+
885
+ // Watch for new cards when a new set is generated
886
+ const observer = new MutationObserver((mutations) => {
887
+ for (const m of mutations) {
888
+ if (!m.addedNodes) continue;
889
+ m.addedNodes.forEach((node) => {
890
+ if (!(node instanceof HTMLElement)) return;
891
+ if (node.classList.contains('nv-card') || node.querySelector('.nv-card')) {
892
+ initCardFading(node);
893
+ }
894
+ });
895
+ }
896
+ });
897
+
898
+ observer.observe(document.documentElement || document.body, {
899
+ childList: true,
900
+ subtree: true
901
+ });
902
+
903
+ // ─────────────────────────────────────────────
904
+ // Slow background fading over time
905
+ // ─────────────────────────────────────────────
906
+ // Every 20 seconds, gently fade all cards that are not currently hovered.
907
+ // Much slower than the hover fade step above.
908
+ const TIME_STEP_MS = 20000; // 20s
909
+ const TIME_DECAY = 0.02; // ~2% per tick
910
+
911
+ setInterval(() => {
912
+ const cards = document.querySelectorAll('.nv-card');
913
+ cards.forEach((card) => {
914
+ let current = parseFloat(card.dataset.opacity || '1');
915
+ if (isNaN(current)) current = 1;
916
+
917
+ // don't fade if already very faint
918
+ if (current <= 0.25) return;
919
+
920
+ // don't fade while actively hovered
921
+ if (card.dataset.hover === '1') return;
922
+
923
+ let next = Math.max(0.25, current - TIME_DECAY);
924
+ card.dataset.opacity = String(next);
925
+ card.style.opacity = next;
926
+ });
927
+ }, TIME_STEP_MS);
928
+ })();
929
+ </script>
930
+ """
931
+ )
932
 
933
  seen_state = gr.State([]) # per-session list of seen questions
934
 
 
958
  f"<div class='nv-label nv-fade'>{ui_texts['theme_label']}</div>"
959
  )
960
  theme = gr.Radio(
961
+ choices=ui_texts["theme_choices"],
962
+ value=ui_texts["theme_default"],
963
  show_label=False,
964
  elem_classes="nv-pills",
965
  )
966
 
967
 
968
 
969
+
970
  with gr.Column(elem_classes="nv-section"):
971
  category_label_html = gr.HTML(f"<div class='nv-label nv-fade'>{ui_texts['category_label']}</div>")
972
  category = gr.Radio(
 
1004
  category_label_html,
1005
  questions_label_html,
1006
  micro_label_html,
1007
+ theme,
1008
  category,
1009
  btn,
1010
  ],
 
1020
 
1021
 
1022
  if __name__ == "__main__":
1023
+ demo.launch(ssr_mode=False)
style.css CHANGED
@@ -151,6 +151,21 @@
151
  box-shadow 120ms ease;
152
  }
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  .nv-pills input:checked + label {
155
  background: linear-gradient(135deg, #ffe4d4, #ffd6c6) !important;
156
  border-color: var(--nv-accent) !important;
 
151
  box-shadow 120ms ease;
152
  }
153
 
154
+ /* Center all radio pill groups */
155
+ .nv-pills {
156
+ display: flex !important;
157
+ justify-content: center !important;
158
+ gap: 10px;
159
+ }
160
+
161
+ /* Center the inner column for language, theme, category sections */
162
+ .nv-section > .gr-column,
163
+ .nv-section > .gr-row {
164
+ justify-content: center !important;
165
+ text-align: center;
166
+ }
167
+
168
+
169
  .nv-pills input:checked + label {
170
  background: linear-gradient(135deg, #ffe4d4, #ffd6c6) !important;
171
  border-color: var(--nv-accent) !important;