Spaces:
Sleeping
Sleeping
| import os | |
| import re | |
| import json | |
| import random | |
| from typing import Dict, Any, List, Optional | |
| import gradio as gr | |
| from huggingface_hub import InferenceClient | |
| # Load external CSS file into a string so Gradio can inject it | |
| CSS_PATH = os.path.join(os.path.dirname(__file__), "style.css") | |
| try: | |
| with open(CSS_PATH, encoding="utf-8") as f: | |
| CUSTOM_CSS = f.read() | |
| except FileNotFoundError: | |
| CUSTOM_CSS = "" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # CONFIG | |
| # Default to a serverless text-generation model. You can override this in Space | |
| # settings by defining a MODEL_ID variable if you want to experiment. | |
| MODEL_ID = os.environ.get("MODEL_ID", "google/gemma-2-2b-it") | |
| # Personal access token from your Hugging Face account (Space secret). | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| REPO_PATH = "/data/questions.json" # where we store generated questions | |
| CATEGORIES = [ | |
| {"key": "alimentation", "icon": "๐", "fr": "Alimentation", "en": "Nutrition"}, | |
| {"key": "mouvement", "icon": "๐ฆ", "fr": "Mouvement", "en": "Movement"}, | |
| {"key": "cerveau", "icon": "๐ง ", "fr": "Cerveau", "en": "Brain"}, | |
| {"key": "liens", "icon": "๐ค", "fr": "Liens", "en": "Connections"}, | |
| {"key": "bien-etre", "icon": "๐ฌ", "fr": "Bien-รชtre", "en": "Well-being"}, | |
| ] | |
| GUIDES = { | |
| "fr": { | |
| "alimentation": "Habitudes simples: hydratation, fruits/lรฉgumes, collations, rythme des repas. Pas de rรฉgime strict, pas de moralisation.", | |
| "mouvement": "Mouvement du quotidien: marche, escaliers, รฉtirements courts, pauses actives. Pas de performance sportive.", | |
| "cerveau": "Stimulation douce: curiositรฉ, respiration, mini-jeux, petit apprentissage. Zรฉro jargon mรฉdical.", | |
| "liens": "Interactions simples: gratitude, messages courts, appels brefs, moments partagรฉs. Ton chaleureux, inclusif.", | |
| "bien-etre": "Micro bien-รชtre: pauses, sommeil rรฉgulier, respirations, petits rituels qui apaisent. Jamais culpabilisant.", | |
| }, | |
| "en": { | |
| "alimentation": "Simple habits: hydration, fruit/veg, snacks, meal rhythm. No strict diets, no moralizing.", | |
| "mouvement": "Daily movement: walking, stairs, light stretches, active breaks. No performance pressure.", | |
| "cerveau": "Gentle stimulation: curiosity, breathing, tiny games, small learning moments. No medical jargon.", | |
| "liens": "Simple connections: gratitude, short texts, quick calls, shared moments. Warm and inclusive tone.", | |
| "bien-etre": "Micro well-being: breaks, sleep rhythm, breathing, tiny soothing rituals. Never guilt-based.", | |
| }, | |
| } | |
| # Few-shot + fallback pools | |
| FEWSHOTS = { | |
| "fr": { | |
| "alimentation": { | |
| "questions": [ | |
| "Quelle boisson te donne envie de boire plus dโeau dans la journรฉe ?", | |
| "Quel ajout simple rend ton petit-dรฉj plus rassasiant ?", | |
| "Quand as-tu naturellement faim dโun fruit ou dโun yaourt ?", | |
| "Quelle petite habitude tโaide ร ne pas sauter de repas ?", | |
| "Quel plat simple te fait du bien aprรจs une journรฉe chargรฉe ?", | |
| "Quelle collation tโaide ร tenir jusquโau dรฎner sans avoir trop faim ?", | |
| ], | |
| "micro_actions": [ | |
| "Remplir une gourde ce matin.", | |
| "Ajouter un fruit ร la collation de lโaprรจs-midi.", | |
| "Remplacer une boisson sucrรฉe par un verre dโeau aujourdโhui.", | |
| "Prรฉparer un snack simple pour demain.", | |
| ], | |
| }, | |
| "mouvement": { | |
| "questions": [ | |
| "Quel trajet pourrais-tu faire ร pied au moins une fois cette semaine ?", | |
| "Quelle pause-active de 2 minutes peux-tu glisser entre deux tรขches ?", | |
| "Quโest-ce qui te fait bouger sans y penser (ex: marcher au tรฉlรฉphone) ?", | |
| "Quel moment conviendrait pour quelques รฉtirements doux chaque jour ?", | |
| "Avec qui aimerais-tu partager une courte marche ?", | |
| "Quel geste te rรฉveille le matin (รฉtirement, marche, danse exprรจsโฆ) ?", | |
| ], | |
| "micro_actions": [ | |
| "Monter un รฉtage par les escaliers aujourdโhui.", | |
| "Faire 5 รฉtirements doux aprรจs le cafรฉ.", | |
| "Se lever pendant un appel et marcher quelques pas.", | |
| "Programmer une mini-alarme ยซ bouger ยป dans lโaprรจs-midi.", | |
| ], | |
| }, | |
| "cerveau": { | |
| "questions": [ | |
| "Quโest-ce qui a suscitรฉ ta curiositรฉ aujourdโhui ?", | |
| "Quel moment tโirait pour 3 minutes de respiration ?", | |
| "Quel mini-jeu aimes-tu pour rรฉveiller lโesprit (ex: 3 mots flรฉchรฉs) ?", | |
| "Quel petit sujet aimerais-tu explorer cette semaine ?", | |
| "Quelle activitรฉ calme tโaide ร passer du travail au repos ?", | |
| "Quel souvenir rรฉcent tโa fait sourire en y repensant ?", | |
| ], | |
| "micro_actions": [ | |
| "Programmer un minuteur de 3 minutes pour respirer.", | |
| "Lire un paragraphe dโun sujet nouveau ce soir.", | |
| "Faire un mini-jeu de cerveau (3 mots flรฉchรฉs, Sudoku, etc.).", | |
| "Noter une idรฉe ou question qui tโintrigue.", | |
| ], | |
| }, | |
| "liens": { | |
| "questions": [ | |
| "Qui pourrais-tu remercier aujourdโhui et comment ?", | |
| "ร qui enverrais-tu un message court pour reprendre contact ?", | |
| "Avec qui partagerais-tu une courte marche cette semaine ?", | |
| "Avec qui aimerais-tu avoir une vraie conversation bientรดt ?", | |
| "Quand tโes-tu sentiยทe soutenuยทe pour la derniรจre fois, et par qui ?", | |
| "Qui aimerais-tu encourager cette semaine ?", | |
| ], | |
| "micro_actions": [ | |
| "Envoyer un message de gratitude ร une personne.", | |
| "Proposer une pause-cafรฉ de 10 minutes.", | |
| "Envoyer une photo ou un souvenir ร quelquโun avec un petit mot.", | |
| "Poser une vraie question ร quelquโun sur sa journรฉe.", | |
| ], | |
| }, | |
| "bien-etre": { | |
| "questions": [ | |
| "Quel signal tโindique quโil est temps de faire une pause ?", | |
| "Quelle routine de 2 minutes tโaide ร te recentrer ?", | |
| "Quel moment favorise un coucher plus rรฉgulier ?", | |
| "Quโest-ce qui tโaide ร te sentir plus lรฉgerยทe en fin de journรฉe ?", | |
| "Quel endroit de ton quotidien te donne une sensation de calme ?", | |
| "Quand as-tu lโimpression de vraiment respirer ?", | |
| ], | |
| "micro_actions": [ | |
| "รteindre les รฉcrans 10 minutes plus tรดt ce soir.", | |
| "รcrire 3 lignes sur ton humeur du jour.", | |
| "Prendre 5 respirations lentes avant de changer dโactivitรฉ.", | |
| "Planifier une mini-pause de 5 minutes pour toi demain.", | |
| ], | |
| }, | |
| }, | |
| "en": { | |
| "alimentation": { | |
| "questions": [ | |
| "What drink makes you want to sip more water through the day?", | |
| "What small add-on makes your breakfast more filling?", | |
| "When do you naturally crave a fruit or yogurt?", | |
| "What tiny habit helps you not skip meals?", | |
| "What simple dinner feels gentle after a long day?", | |
| "Which snack helps you stay focused without a big energy crash?", | |
| ], | |
| "micro_actions": [ | |
| "Fill a water bottle this morning.", | |
| "Add one fruit to your afternoon snack.", | |
| "Swap one sugary drink for water today.", | |
| "Plan a simple snack for tomorrow.", | |
| ], | |
| }, | |
| "mouvement": { | |
| "questions": [ | |
| "Which short trip could you walk at least once this week?", | |
| "Which 2-minute active break fits between two tasks?", | |
| "What makes you move without noticing (e.g., walking on calls)?", | |
| "When would a short stretch break feel good each day?", | |
| "Where do you naturally end up walking more?", | |
| "What small move helps you wake up in the morning?", | |
| ], | |
| "micro_actions": [ | |
| "Take one flight of stairs today.", | |
| "Do 5 light stretches after coffee.", | |
| "Stand up and walk during one call.", | |
| "Set a tiny โmoveโ reminder for this afternoon.", | |
| ], | |
| }, | |
| "cerveau": { | |
| "questions": [ | |
| "What sparked your curiosity today?", | |
| "When could you do 3 minutes of breathing?", | |
| "Which mini-game wakes you up (e.g., 3 crossword clues)?", | |
| "What small topic would you like to learn about this week?", | |
| "What gentle activity helps you shift from work to rest?", | |
| "What recent memory made you smile when you thought of it again?", | |
| ], | |
| "micro_actions": [ | |
| "Set a 3-minute timer to breathe.", | |
| "Read one paragraph on a new topic tonight.", | |
| "Play a tiny brain game.", | |
| "Write down one idea or question that interests you.", | |
| ], | |
| }, | |
| "liens": { | |
| "questions": [ | |
| "Who could you thank todayโand how?", | |
| "Who might you text briefly to reconnect?", | |
| "Who could you invite for a short walk this week?", | |
| "Who would you like to have a real conversation with soon?", | |
| "When did you last feel supported, and by whom?", | |
| "Who would you like to encourage this week?", | |
| ], | |
| "micro_actions": [ | |
| "Send a gratitude message to one person.", | |
| "Offer a 10-minute coffee break.", | |
| "Send a photo or memory to someone with a short note.", | |
| "Ask someone one genuine question about their day.", | |
| ], | |
| }, | |
| "bien-etre": { | |
| "questions": [ | |
| "What cue tells you itโs time for a pause?", | |
| "What 2-minute routine helps you reset?", | |
| "What time supports a steadier bedtime?", | |
| "What helps you feel lighter at the end of the day?", | |
| "Which place in your daily life feels calming?", | |
| "When do you feel like you can really breathe?", | |
| ], | |
| "micro_actions": [ | |
| "Turn screens off 10 minutes earlier tonight.", | |
| "Write three lines about your mood today.", | |
| "Take 5 slow breaths before changing tasks.", | |
| "Schedule a 5-minute mini-break for yourself tomorrow.", | |
| ], | |
| }, | |
| }, | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # REPOSITORY HELPERS (questions only, per category) | |
| def _default_repo() -> Dict[str, List[str]]: | |
| return {c["key"]: [] for c in CATEGORIES} | |
| def load_repo() -> Dict[str, List[str]]: | |
| os.makedirs(os.path.dirname(REPO_PATH), exist_ok=True) | |
| if not os.path.exists(REPO_PATH): | |
| data = _default_repo() | |
| with open(REPO_PATH, "w", encoding="utf-8") as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| return data | |
| try: | |
| with open(REPO_PATH, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| except Exception: | |
| data = _default_repo() | |
| # Ensure all categories exist | |
| base = _default_repo() | |
| base.update({k: v for k, v in data.items() if isinstance(v, list)}) | |
| return base | |
| def save_repo(data: Dict[str, List[str]]) -> None: | |
| os.makedirs(os.path.dirname(REPO_PATH), exist_ok=True) | |
| with open(REPO_PATH, "w", encoding="utf-8") as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # PROMPT + MODEL HELPERS | |
| def build_prompt(lang: str, category_key: str, variant: str) -> str: | |
| cat = next((c for c in CATEGORIES if c["key"] == category_key), None) | |
| if not cat: | |
| category_key = "alimentation" | |
| cat = next((c for c in CATEGORIES if c["key"] == category_key), None) | |
| guide = GUIDES[lang][category_key] | |
| few = FEWSHOTS[lang][category_key] | |
| if variant == "best": | |
| tone_fr = "ludique, original, lรฉger" | |
| tone_en = "playful, original, light" | |
| else: | |
| tone_fr = "sincรจre, introspectif, doux" | |
| tone_en = "sincere, introspective, gentle" | |
| schema = ( | |
| "{\n" | |
| ' "category": "<category_key>",\n' | |
| ' "language": "<fr|en>",\n' | |
| ' "questions": ["q1", "q2", "q3", "q4"],\n' | |
| ' "micro_actions": ["m1", "m2"],\n' | |
| ' "tone": "playful|sincere|ludique|sincรจre",\n' | |
| ' "safety_notes": ""\n' | |
| "}" | |
| ) | |
| example_questions = few["questions"][:2] | |
| example_micro = few["micro_actions"][:2] | |
| if lang == "fr": | |
| return f""" | |
| Tu es lโIA du jeu de cartes Neurovie (modรจle FINGER). | |
| Tu crรฉes des cartes-question pour parler de routines du quotidien. | |
| - Catรฉgorie: {cat['fr']} {cat['icon']}. | |
| - Focus: {guide} | |
| - Ton: {tone_fr} | |
| - Format: 4 questions + 2 micro-actions, 1 phrase courte chacune. | |
| - Style: concret, bienveillant, sans jugement. | |
| - Interdit: conseils mรฉdicaux, diagnostics, emojis. | |
| Rรฉponds UNIQUEMENT en JSON, sans texte autour, selon ce schรฉma: | |
| {schema} | |
| Exemple de style (ร VARIER, ne pas copier): | |
| Questions: {example_questions} | |
| Micro-actions: {example_micro} | |
| Maintenant, renvoie un NOUVEAU JSON diffรฉrent de l'exemple. | |
| """.strip() | |
| else: | |
| return f""" | |
| You are the AI for the Neurovie card game (FINGER model). | |
| You create question-cards about everyday routines. | |
| - Category: {cat['en']} {cat['icon']}. | |
| - Focus: {guide} | |
| - Tone: {tone_en} | |
| - Format: 4 questions + 2 micro-actions, one short sentence each. | |
| - Style: concrete, kind, non-judgmental. | |
| - Forbidden: medical advice, diagnoses, emojis. | |
| Reply ONLY with JSON, no extra text, in this shape: | |
| {schema} | |
| Style example (to vary, do NOT copy): | |
| Questions: {example_questions} | |
| Micro-actions: {example_micro} | |
| Now return a NEW JSON different from the example. | |
| """.strip() | |
| def try_parse_json(text: str) -> Optional[Dict[str, Any]]: | |
| """ | |
| Try to extract a JSON object from the model output. | |
| Handles cases where the model wraps JSON in ``` or ```json fences. | |
| """ | |
| if not text: | |
| return None | |
| stripped = text.strip() | |
| # If the model wrapped the JSON in ``` or ```json fences, strip them. | |
| if stripped.startswith("```"): | |
| lines = stripped.splitlines() | |
| # Drop the first line (``` or ```json) | |
| lines = lines[1:] | |
| # Drop final line if it's a closing fence | |
| if lines and lines[-1].strip().startswith("```"): | |
| lines = lines[:-1] | |
| stripped = "\n".join(lines).strip() | |
| # Now look for the first {...} block | |
| match = re.search(r"\{[\s\S]*\}", stripped) | |
| if not match: | |
| return None | |
| candidate = match.group(0) | |
| try: | |
| return json.loads(candidate) | |
| except Exception: | |
| return None | |
| # ๐ง SIMPLIFIED, ROBUST MODEL CALL (no secrets required) | |
| def model_call(prompt: str) -> str: | |
| """ | |
| Call Hugging Face Inference API using the conversational (chat) task. | |
| This matches models like google/gemma-2-2b-it which only support 'conversational'. | |
| """ | |
| if not MODEL_ID: | |
| raise RuntimeError("MODEL_ID env var is empty. Set it or use the default.") | |
| if not HF_TOKEN: | |
| raise RuntimeError( | |
| "HF_TOKEN is not set. Add a Hugging Face token as a Space secret named HF_TOKEN." | |
| ) | |
| client = InferenceClient(model=MODEL_ID, token=HF_TOKEN) | |
| try: | |
| resp = client.chat.completions.create( | |
| model=MODEL_ID, | |
| messages=[ | |
| { | |
| "role": "system", | |
| "content": ( | |
| "You generate JSON only. " | |
| "Do not add any explanation outside of the JSON object." | |
| ), | |
| }, | |
| { | |
| "role": "user", | |
| "content": prompt, | |
| }, | |
| ], | |
| max_tokens=260, | |
| temperature=0.9, | |
| top_p=0.92, | |
| ) | |
| except Exception as e: | |
| raise RuntimeError(f"Inference API error: {e}") from e | |
| # Extract text from the first choice | |
| try: | |
| message = resp.choices[0].message | |
| content = message.content | |
| except Exception as e: | |
| raise RuntimeError(f"Unexpected chat response format: {e}") from e | |
| # content can be a string or a list of parts | |
| if isinstance(content, list): | |
| # Newer HF SDK sometimes uses list-of-parts format | |
| parts = [] | |
| for part in content: | |
| # part may be a dict like {"type": "text", "text": "..."} | |
| if isinstance(part, dict) and "text" in part: | |
| parts.append(part["text"]) | |
| else: | |
| parts.append(str(part)) | |
| text = "".join(parts) | |
| else: | |
| text = str(content) | |
| text = text.strip() | |
| if not text: | |
| raise RuntimeError("Empty response from model.") | |
| return text | |
| def normalize_output( | |
| data: Dict[str, Any], | |
| lang: str, | |
| category_key: str, | |
| variant: str | |
| ) -> Dict[str, Any]: | |
| """ | |
| Make model output always valid, even if the model returns emojis, wrong category labels, | |
| capitalized names, or unexpected keys. | |
| """ | |
| # --- FIX CATEGORY --- | |
| raw_cat = str(data.get("category", "")).strip() | |
| # Strip emojis | |
| raw_cat = re.sub(r"[^\w\- ]+", "", raw_cat) | |
| # Lowercase | |
| raw_cat = raw_cat.lower() | |
| # Map likely variants to internal keys | |
| mapping = { | |
| "alimentation": "alimentation", | |
| "nutrition": "alimentation", | |
| "mouvement": "mouvement", | |
| "movement": "mouvement", | |
| "cerveau": "cerveau", | |
| "brain": "cerveau", | |
| "liens": "liens", | |
| "links": "liens", | |
| "bienetre": "bien-etre", | |
| "bien-etre": "bien-etre", | |
| "wellbeing": "bien-etre", | |
| "well being": "bien-etre", | |
| } | |
| # Choose corrected category | |
| clean_cat = mapping.get(raw_cat, category_key) | |
| # --- FIX QUESTIONS --- | |
| q = [str(x).strip() for x in data.get("questions", []) if str(x).strip()] | |
| q = (q + [""] * 4)[:4] | |
| # --- FIX MICRO-ACTIONS --- | |
| m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()] | |
| m = (m + [""] * 2)[:2] | |
| # --- FIX TONE --- | |
| if not data.get("tone"): | |
| if lang == "fr": | |
| tone = "ludique" if variant == "best" else "sincรจre" | |
| else: | |
| tone = "playful" if variant == "best" else "sincere" | |
| else: | |
| tone = str(data.get("tone")).strip().lower() | |
| # --- SAFETY NOTES --- | |
| safety_notes = str(data.get("safety_notes", "")) | |
| return { | |
| "category": clean_cat, | |
| "language": lang, | |
| "questions": q, | |
| "micro_actions": m, | |
| "tone": tone, | |
| "safety_notes": safety_notes, | |
| } | |
| def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]: | |
| """ | |
| Try to call the model. If anything fails or JSON is invalid, | |
| fall back to shuffling the few-shots and include a safety_notes message. | |
| """ | |
| prompt = build_prompt(lang, category_key, variant) | |
| try: | |
| raw_text = model_call(prompt) | |
| parsed = try_parse_json(raw_text) if raw_text else None | |
| if parsed: | |
| return normalize_output(parsed, lang, category_key, variant) | |
| # Model replied but not valid JSON | |
| few = FEWSHOTS[lang][category_key] | |
| q_pool = few["questions"][:] | |
| m_pool = few["micro_actions"][:] | |
| random.shuffle(q_pool) | |
| random.shuffle(m_pool) | |
| return { | |
| "category": category_key, | |
| "language": lang, | |
| "questions": (q_pool + [""] * 4)[:4], | |
| "micro_actions": (m_pool + [""] * 2)[:2], | |
| "tone": "fallback", | |
| "safety_notes": ( | |
| "Model replied but JSON parsing failed. " | |
| f"raw_text starts with: {repr(raw_text[:160])}" | |
| ), | |
| } | |
| except Exception as e: | |
| # Any HF / network / auth error ends up here | |
| few = FEWSHOTS[lang][category_key] | |
| q_pool = few["questions"][:] | |
| m_pool = few["micro_actions"][:] | |
| random.shuffle(q_pool) | |
| random.shuffle(m_pool) | |
| return { | |
| "category": category_key, | |
| "language": lang, | |
| "questions": (q_pool + [""] * 4)[:4], | |
| "micro_actions": (m_pool + [""] * 2)[:2], | |
| "tone": "error", | |
| "safety_notes": f"Model call error: {type(e).__name__}: {e}", | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # MAIN LOGIC: REPO + SESSION "SEEN" QUESTIONS | |
| def get_questions_and_micro( | |
| lang: str, | |
| category_key: str, | |
| variant: str, | |
| seen: List[str], | |
| ) -> Dict[str, Any]: | |
| """ | |
| 1. Load /data/questions.json | |
| 2. If repo has >=4 unseen questions -> sample 4 from repo, micro from local pool. | |
| 3. Else -> call AI, store any new questions into repo, use AI's questions + micro. | |
| 4. Update seen list so this session won't see the same question twice. | |
| """ | |
| seen_set = set(seen or []) | |
| repo = load_repo() | |
| repo_qs = repo.get(category_key, []) | |
| unseen_repo = [q for q in repo_qs if q and q not in seen_set] | |
| used_ai = False | |
| tone = "" | |
| safety_notes = "" | |
| if len(unseen_repo) >= 4: | |
| # entirely from repo, no AI call | |
| random.shuffle(unseen_repo) | |
| questions = unseen_repo[:4] | |
| # micro-actions from local fewshot pool (cheap) | |
| m_pool = FEWSHOTS[lang][category_key]["micro_actions"][:] | |
| random.shuffle(m_pool) | |
| micro = (m_pool + ["", ""])[:2] | |
| tone = "repo" | |
| safety_notes = "" | |
| else: | |
| # need fresh AI content | |
| ai_out = ai_generate(lang, category_key, variant) | |
| questions = ai_out["questions"] | |
| micro = ai_out["micro_actions"] | |
| tone = ai_out.get("tone", "") | |
| safety_notes = ai_out.get("safety_notes", "") | |
| used_ai = True | |
| # store new questions in repo | |
| new_qs = [q for q in questions if q and q not in repo_qs] | |
| if new_qs: | |
| repo_qs_extended = repo_qs + new_qs | |
| repo[category_key] = repo_qs_extended | |
| save_repo(repo) | |
| # update seen for this session | |
| for q in questions: | |
| if q: | |
| seen_set.add(q) | |
| payload = { | |
| "category": category_key, | |
| "language": lang, | |
| "questions": questions, | |
| "micro_actions": micro, | |
| "source": "ai" if used_ai else "repo", | |
| "tone": tone, | |
| "safety_notes": safety_notes, | |
| } | |
| # If you ever need the payload again, you can use it here; | |
| # for the UI we only return questions + micro + updated seen. | |
| return { | |
| "questions": questions, | |
| "micro_actions": micro, | |
| "seen": list(seen_set), | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # UI โ pastel, animated, color-coded cards | |
| def _map_category(choice: str) -> str: | |
| mapping = { | |
| "alimentation ๐": "alimentation", | |
| "mouvement ๐ฆ": "mouvement", | |
| "cerveau ๐ง ": "cerveau", | |
| "liens ๐ค": "liens", | |
| "bien-etre ๐ฌ": "bien-etre", | |
| } | |
| return mapping.get(choice, "alimentation") | |
| def _card_html(category_key: str, kind: str, title: str, body: str, delay_s: float) -> str: | |
| kind_attr = "question" if kind == "q" else "micro" | |
| cat_class = f"nv-card--cat-{category_key}" | |
| # each card has its own animation delay => "dealing" feel | |
| return ( | |
| f"<div class='nv-card {cat_class}' data-kind='{kind_attr}' " | |
| f"style='animation-delay:{delay_s:.2f}s'><div class='nv-card-title'>{title}</div>" | |
| f"<div>{body}</div></div>" | |
| ) | |
| def update_cards(lang: str, category_choice: str, variant: str, seen: List[str]): | |
| category_key = _map_category(category_choice) | |
| result = get_questions_and_micro(lang, category_key, variant, seen or []) | |
| questions = result["questions"] | |
| micro = result["micro_actions"] | |
| new_seen = result["seen"] | |
| # Stagger cards a bit | |
| delays_q = [0.05, 0.10, 0.15, 0.20] | |
| delays_m = [0.25, 0.30] | |
| q_htmls = [] | |
| for i in range(4): | |
| text = questions[i] if i < len(questions) else "" | |
| q_htmls.append( | |
| _card_html( | |
| category_key, | |
| "q", | |
| f"Question {i+1}", | |
| text, | |
| delays_q[i], | |
| ) | |
| ) | |
| m_htmls = [] | |
| for i in range(2): | |
| text = micro[i] if i < len(micro) else "" | |
| m_htmls.append( | |
| _card_html( | |
| category_key, | |
| "m", | |
| f"Micro-action {i+1}", | |
| text, | |
| delays_m[i], | |
| ) | |
| ) | |
| return (*q_htmls, *m_htmls, new_seen) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # GRADIO APP | |
| with gr.Blocks( | |
| title="Neurovie โ Question Studio", | |
| css=CUSTOM_CSS, # <- inject CSS content here | |
| ) as demo: | |
| # no need for gr.HTML("<link ...>") | |
| seen_state = gr.State([]) # per-session list of seen questions | |
| with gr.Column(elem_classes="nv-shell"): | |
| gr.HTML( | |
| """ | |
| <div> | |
| <div class="nv-badge">NEUROVIE ยท FINGER</div> | |
| <div class="nv-title">Question Studio</div> | |
| <div class="nv-subtitle"> | |
| Minimal prompts for rich conversations โ 4 questions and 2 micro-actions par tirage. | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| # Settings | |
| with gr.Row(elem_classes="nv-section"): | |
| with gr.Column(): | |
| gr.HTML("<div class='nv-label'>Language</div>") | |
| lang = gr.Radio( | |
| choices=["fr", "en"], | |
| value="fr", | |
| show_label=False, | |
| elem_classes="nv-pills", | |
| ) | |
| with gr.Column(): | |
| gr.HTML("<div class='nv-label'>Tone</div>") | |
| variant = gr.Radio( | |
| choices=["best", "sincere"], | |
| value="best", | |
| show_label=False, | |
| elem_classes="nv-pills", | |
| ) | |
| with gr.Column(elem_classes="nv-section"): | |
| gr.HTML("<div class='nv-label'>Category</div>") | |
| category = gr.Radio( | |
| choices=[ | |
| "alimentation ๐", | |
| "mouvement ๐ฆ", | |
| "cerveau ๐ง ", | |
| "liens ๐ค", | |
| "bien-etre ๐ฌ", | |
| ], | |
| value="alimentation ๐", | |
| show_label=False, | |
| elem_classes="nv-pills", | |
| ) | |
| btn = gr.Button("Generate card set โจ") | |
| # Question & micro-action cards | |
| with gr.Row(elem_classes="nv-section"): | |
| with gr.Column(): | |
| gr.HTML("<div class='nv-label'>Questions</div>") | |
| with gr.Column(elem_classes="nv-card-grid"): | |
| q1 = gr.HTML() | |
| q2 = gr.HTML() | |
| q3 = gr.HTML() | |
| q4 = gr.HTML() | |
| with gr.Column(): | |
| gr.HTML("<div class='nv-label'>Micro-actions</div>") | |
| with gr.Column(elem_classes="nv-card-grid"): | |
| m1 = gr.HTML() | |
| m2 = gr.HTML() | |
| btn.click( | |
| update_cards, | |
| [lang, category, variant, seen_state], | |
| [q1, q2, q3, q4, m1, m2, seen_state], | |
| show_progress=False, # hide Gradio built-in progress indicator | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |