Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files
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,
|
| 666 |
category_key = _map_category(category_choice)
|
| 667 |
-
|
|
|
|
| 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 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
</div>
|
| 725 |
-
|
| 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[
|
| 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=
|
| 831 |
-
value="
|
| 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;
|