Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import os | |
| import time | |
| from typing import List, Union, Dict, Any, Tuple | |
| from datetime import datetime | |
| # ================================================================= | |
| # CONFIGURATION ET DÉFINITIONS LINGUISTIQUES | |
| # ================================================================= | |
| # CRITICAL FIX: Base count was 91. Adding SPACE9_CHOICE and SPACE9_SCALE brings it to 93. | |
| EXPECTED_DATA_COUNT = 93 | |
| EXPECTED_COUNT = 93 | |
| # --- Shared Choices (French Only) --- | |
| FREQ_FR = ["Jamais", "Rarement", "Parfois", "Souvent"] | |
| THEMES_ACTU_FR = ["politique", "environnement", "économie/finance", "santé", "social/société", "situation internationale/géopolitique", "science", "modes de vie (lifestyle)", "vie culturelle", "autre/précisez"] | |
| ACTIVITY_CHOICES = ["Lecture (de livres, articles)", "Cinéma/Séries", "Musique/Concerts", "Théâtre/Spectacles", "Expositions/Musées", "Sport", "Voyages", "Jeux vidéo", "Bricolage/Jardinage", "Réseaux sociaux"] | |
| PLATFORM_CHOICES = ["Instagram", "TikTok", "YouTube", "Twitter/X", "Facebook", "Snapchat", "Twitch", "Reddit", "LinkedIn", "Autre"] | |
| PURPOSE_CHOICES = ["Actualité/Info", "Loisirs/Divertissement", "Éducation/Apprentissage", "Engagement/Politique", "Social/Communication", "Professionnel", "Autre (précisez)"] | |
| INSEE_CHOICES = ["Agriculteur", "Ouvrier", "Employé", "Profession intermédiaire", "Artisan, commerçant, chef d'entreprise", "Cadre, profession intellectuelle supérieure", "Retraité", "Demandeur d'emploi", "Étudiant"] | |
| INCOME_CHOICES = ["Tu vis confortablement", "Tu t’en sors", "Tu trouves la vie difficile", "Tu ne t’en s’en sors vraiment pas"] | |
| DOMAIN_CHOICES_FR = [ | |
| "écologie / action environnementale", | |
| "inclusion et lutte contre les discriminations / Accueil de populations vulnérables", | |
| "démocratie radicale", | |
| "Création et experimentation artistiques", | |
| "Éducation et transmission", | |
| "Économie alternative", | |
| "Ancrage local (bar associatif, aides aux personnes agées, bibliothèque associative…)", | |
| "autre - préciser" | |
| ] | |
| ENGAGE3_CHOICES_FR = [ | |
| "écologie", | |
| "lutte contre la pauvreté", | |
| "action contre les discriminations", | |
| "vie politique locale ou nationale", | |
| "action syndicale", | |
| "causes internationales", | |
| "autre / précisez" | |
| ] | |
| SOCIAL_MEDIA_CHOICES = [ | |
| "Instagram", "YouTube", "TikTok", "Facebook", "WhatsApp", "BlueSky", "Signal", | |
| "Telegram", "Mastodon", "Discord", "Twitter", "Reddit", "messagerie de jeu en ligne", | |
| "autre : préciser", "je ne sais pas" | |
| ] | |
| ANXIETY_MANIFESTATION_CHOICES = [ | |
| "J’ai tendance à l’ignorer", "Je cherche à agir par moi-même", "Je lis des livres et je me documente sur le sujet", | |
| "J’ai un journal intime", "Je discute avec mes amis / parents", "Je pose des questions à mes enseignants", | |
| "Je rejoins une association ou je cherche une activité collective", "Je consulte un spécialiste de santé", | |
| "Je fais des recherches sur internet", "J’interroge / je confie à un chatbot", | |
| "Je rejoins une communauté spécialisée sur les réseaux sociaux" | |
| ] | |
| INFO4_CHOICES = ["Curieux", "Indifférent·e", "Légèrement agacé·e", "Inquiet·ète", "En colère", "Déstabilisé·e", "J'ignore son opinion"] | |
| INFO5_CHOICES = [ | |
| "Je le lis/regarde avec attention.", | |
| "Je le ferme ou l’ignore.", | |
| "Je cherche des arguments pour le réfuter.", | |
| "Je le partage pour en discuter avec d’autres.", | |
| "autre réaction" | |
| ] | |
| ENGAGE1_CHOICES = [ | |
| "non", | |
| "en tant que sympathisant", | |
| "comme donateur-rice", | |
| "en tant que bénévole", | |
| "comme participant·e/spectateur-rice", | |
| "comme organisateur-rice/responsable" | |
| ] | |
| ENGAGE2_CHOICES = [ | |
| "une association", | |
| "un collectif", | |
| "une ONG", | |
| "un parti politique", | |
| "un syndicat étudiant", | |
| "une assemblée ou une instance représentative (collectivité, communes…)", | |
| "autre (préciser)", | |
| "aucune de ces formes" | |
| ] | |
| DEMO_INSCRIPTION_CHOICES = ["L1", "L2", "L3", "M1", "M2", "D", "DU", "autre niveau d’étude/diplôme"] | |
| DEMO_DISCIPLINE_CHOICES = [ | |
| "Cinéma", "Communication", "Langues", "LEA", "Lettres", "Médiation", "Musique", | |
| "Sciences du langage", "Théâtre", "Traduction", "Autres" | |
| ] | |
| # --- Language Definitions (French Only) --- | |
| LANG_FR = { | |
| "TITLE": "Questionnaire : Pratiques culturelles et Engagement Citoyen", | |
| "INTRO_TEXT": "Bonjour, je suis étudiant.e en **master de communication** et j’effectue une enquête sur les **pratiques culturelles** et les **formes d’engagements citoyen** dans le cadre de mon stage.", | |
| "ENQUETEUR_LABEL": "Identifiant de l'enquêteur", | |
| "TAB_1_TITLE": "1. Contact & Lieux Fréquentés", "TAB_2_TITLE": "2. Engagement Citoyen", | |
| "TAB_3_TITLE": "3. Consommation d’actualités", "TAB_4_TITLE": "4. Pratiques culturelles et usages numériques", | |
| "TAB_5_TITLE": "5. Profil Démographique", "TAB_6_TITLE": "6. Questions ouvertes", | |
| "TAB_7_TITLE": "7. Soumission et Téléchargement", | |
| "APPROACH_LABEL": "APPROACH - Accepterais-tu de répondre à un questionnaire ? (10 min)", | |
| "REFUSAL_REASON_LABEL": "ANSWER_NO1 (si APPROACH=non) - Pourquoi ?", | |
| "CONTACT_LATER_LABEL": "ANSWER_NO2 (si APPROACH=non) - On peut prendre rendez-vous plus tard ?", | |
| "SPACE1_LABEL": "SPACE1 - Fréquentes-tu des lieux culturels (au sens large, y compris alternatifs, non institutionnels) ?", | |
| "SPACE2A_TITLE": "Derniers lieux culturels fréquentés (3 max. - si Rarement/Parfois/Souvent)", | |
| "SPACE2B_LABEL": "SPACE2b (si “jamais”) - Pourquoi ne fréquentes-tu jamais de lieux culturels ? (plusieurs réponses possibles)", | |
| "SPACE3_TITLE": "SPACE4 - Pourrais-tu qualifier ces lieux ?", | |
| "SPACE3_QUAL_1": "Lieu 1 : Alternatif/Underground (1) à Institutionnel/Mainstream (10)", | |
| "SPACE3_QUAL_2": "Lieu 2 : Alternatif (1) à Institutionnel (10)", | |
| "SPACE3_QUAL_3": "Lieu 3 : Alternatif (1) à Institutionnel (10)", | |
| "SPACE4_LABEL": "SPACE4_bis - Est-ce que tu suis régulièrement une communauté alternative en ligne (médias sociaux, newsletter, groupe dédié...)", | |
| "SPACE5_LABEL": "SPACE5 - Peux-tu indiquer son nom ou son url ? (si SPACE4_bis=oui)", | |
| "SPACE6_LABEL": "SPACE6 - As-tu connaissance de pratiques d’engagement citoyen dans ces espaces physiques et numériques ?", | |
| "SPACE7_TITLE": "SPACE7 (si SPACE6=oui) - Cite ces pratiques (3 maximum)", | |
| "SPACE_DOMAIN_LABEL": "Peux-tu caractériser le domaine dans lequel s’inscrit principalement cette pratique ?", | |
| # NOUVELLES ÉTIQUETTES DE QUESTION SPACE9 | |
| "SPACE9_CHOICE_LABEL": "SPACE9 - Laquelle de ces 3 pratiques te semble la plus stimulante ? (1, 2 ou 3)", | |
| "SPACE9_SCALE_LABEL": "SPACE9_BIS - Intensité de la stimulation (1 'pas du tout' à 10 'extrêmement')", | |
| "SPACE11_LABEL": "SPACE11 - Sais-tu si elle s’appuie sur une communauté en ligne et l’usage d’un média social en ligne ?", | |
| "SPACE2B_OPTIONS": ["Pas le temps", "Trop cher", "Je ne sais pas où aller", "Pas intéressé", "Autre"], | |
| "ENGAGE1_LABEL": "ENGAGE1 - Toi-même, participes-tu à des pratiques d’engagement citoyen ?", | |
| "ENGAGEMENT_ORGANISATION_LABEL": "ENGAGE2 - Dans quelle(s) organisation(s) es-tu engagé·e ?", | |
| "ENGAGE2_AUTRE_DETAIL_LABEL": "ENGAGE2 - Précisez 'autre'", | |
| "ENGAGEMENT_DOMAINE_LABEL": "ENGAGE3 - Dans quel(s) domaine(s) es-tu engagé·e ?", | |
| "ENGAGE3_AUTRE_DETAIL_LABEL": "ENGAGE3 - Précisez 'autre'", # NOUVELLE ÉTIQUETTE | |
| "INFO_ACTIVITES_LABEL": "ENGAGE4 - Participes-tu ou as-tu participé à une ou plusieurs des activités suivantes ?", | |
| "INFO_RESEAUX_SOCIAUX_LABEL": "ENGAGE5 - Utilises-tu les réseaux sociaux pour t’engager dans des causes ?", | |
| "INFO_FREQUENCE_ACTU_LABEL": "INFO1 - À quelle fréquence te tiens-tu informé·e de l’actualité ?", | |
| "INFO2_LABEL": "INFO2 - Quels sont le ou les thèmes d’actualité que tu as suivis avec le plus d’intérêt ?", | |
| "INFO3_LABEL": "INFO3 - Coche **jusqu’à 3 thèmes** qui te semblent poser des enjeux publics majeurs :", | |
| "OPPOSITE_FEELING_BASE_LABEL": "INFO4 - Quand un·e ami·e exprime une opinion opposée à la tienne sur l’un des thèmes qui te semblent représenter des enjeux majeurs, comment te sens-tu ?", | |
| "INFO_CONTRADICT_OPINION_LABEL": "INFO5 - Quand tu tombes sur une vidéo ou un article de presse qui contredit tes croyances, quelle est ta première réaction ?", | |
| "INFO4_LABEL": "INFO6 - Parmi ces thèmes, lequel ou lesquels génèrent chez toi un sentiment d’anxiété ?", | |
| "INFO5_LABEL": "INFO7 - Comment se manifeste ce sentiment d’anxiété ?", | |
| "INFO6_TITLE": "INFO8 - Éléments les plus problématiques pour toi (Échelle 1 'sans importance' à 10 'essentiel')", | |
| "INFO6_ITEMS": ["Ton logement", "Politique/Gouvernement", "Études/Avenir professionnel", "Crise climatique/Environnement", "Inégalités sociales", "Ta vie sentimentale/familiale", "Ta sécurité personnelle", "Estime de tes proches", "Ta liberté individuelle"], | |
| "PRAT_CULT1_LABEL": "PRAT_CULT1 - Combien de sorties culturelles environ par mois :", | |
| "PRAT_CULT_PRACTICES_TITLE": "PRAT_CULT2 - Parmi les pratiques suivantes, lesquelles effectues-tu régulièrement ?", | |
| "PRAT_NATURE_LABEL": "PRAT_CULT3 - Pratiques-tu une activité liée à la nature (randonnée, observation, bénévolat environnemental...) ?", | |
| "PLATFORM_TITLE": "DIGITAL1 - Quelles plateformes utilises-tu le plus ? (Heures/jour approx.)", | |
| "PURPOSE_TITLE": "DIGITAL2 - Pour quel(s) usage(s) emploies-tu ces plateformes ?", | |
| "DEMO_GENDER_LABEL": "DEMO_GENDER - Ton genre :", | |
| "DEMO_AGE_LABEL": "DEMO_AGE - Ton âge (années) :", | |
| "DEMO_LOCATION_COMMUNE_LABEL": "DEMO_LOCATION - Commune de résidence habituelle :", | |
| "DEMO_LOCATION_ARROND_LABEL": "Arrondissement (si Paris) :", | |
| "DEMO_PARENTS_LOCATION_LABEL": "DEMO_PARENTS - Est-ce le lieu d’habitation de tes parents ?", | |
| "DEMO_INSCRIPTION_LABEL": "DEMO_INSCRIPTION : Tu es inscrit.e. à titre principal en quelle année d’études ?", | |
| "DEMO_DISCIPLINE_LABEL": "DEMO_DISCIPLINE : Dans quelle discipline/filière s’inscrivent principalement tes études ?", | |
| "DEMO_JOB_LABEL": "DEMO_JOB - Exerces-tu une activité professionnelle en parallèle ?", | |
| "DEMO_INCOME_LABEL": "DEMO_INCOME - Opinion sur votre revenu actuel :", | |
| "DEMO_SOCIALCAPITAL1_PARENT1_LABEL": "DEMO_SOCIALCAPITAL1 - Activité principale Parent 1 (ou responsable légal) :", | |
| "DEMO_SOCIALCAPITAL1_PARENT2_LABEL": "Activite principale Parent 2 :", | |
| "DEMO_SOCIALCAPITAL2_LABEL": "DEMO_SOCIALCAPITAL2 : Sur combien de personnes (hors famille) peux-tu compter en cas de coup dur ?", | |
| "OPEN_NON_INSTITUTIONNEL_LABEL": "OEQ1 - Qu’est-ce qu’un lieu culturel non institutionnel selon toi ?", | |
| "OPEN_ALTERNATIVES_LABEL": "OEQ2 - Comment définirais-tu le terme “alternatif” dans ce cas ?", | |
| "OPEN_MOTIVATIONS_LABEL": "OEQ3 - Si tu es engagé.e, quelles en sont les motivations principales ?", | |
| "SUBMIT_BUTTON": "Soumettre le Questionnaire", | |
| "RESET_BUTTON": "Recommencer", | |
| } | |
| LANG = LANG_FR | |
| # ================================================================= | |
| # COMPONENT DEFINITIONS | |
| # ================================================================= | |
| # === Global === | |
| enqueteur_id = gr.Textbox(label=LANG["ENQUETEUR_LABEL"], placeholder="Entrez votre identifiant") | |
| # === Submission/Output Components === | |
| submit_btn = gr.Button(LANG["SUBMIT_BUTTON"], variant="primary") | |
| reset_btn = gr.Button(LANG["RESET_BUTTON"]) | |
| output_status = gr.Markdown("---") | |
| output_message = gr.File(label="Télécharger les Données", file_types=['.csv'], visible=False, elem_id="output_message") | |
| # === TAB 1 Components (23 + 2 = 25 total) === | |
| approach_answer = gr.Radio(label=LANG["APPROACH_LABEL"], choices=["Oui", "Non"]) | |
| refusal_reason = gr.CheckboxGroup(label=LANG["REFUSAL_REASON_LABEL"], choices=["Pas le temps", "Cela ne m’intéresse pas", "Autre"], visible=False) | |
| refusal_reason_other = gr.Textbox(label="Précisez 'Autre'", visible=False) | |
| contact_later = gr.Radio(label=LANG["CONTACT_LATER_LABEL"], choices=["Oui", "Non"], visible=False) | |
| space1 = gr.Radio(label=LANG["SPACE1_LABEL"], choices=FREQ_FR) | |
| space2a_1 = gr.Textbox(label="Lieu 1", placeholder="Nom du dernier lieu fréquenté...", visible=False) | |
| space2a_2 = gr.Textbox(label="Lieu 2", visible=False) | |
| space2a_3 = gr.Textbox(label="Lieu 3", visible=False) | |
| space2b = gr.CheckboxGroup(label=LANG["SPACE2B_LABEL"], choices=LANG["SPACE2B_OPTIONS"], visible=False) | |
| space3_1 = gr.Slider(label=LANG["SPACE3_QUAL_1"], minimum=1, maximum=10, step=1, visible=False) | |
| space3_2 = gr.Slider(label=LANG["SPACE3_QUAL_2"], minimum=1, maximum=10, step=1, visible=False) | |
| space3_3 = gr.Slider(label=LANG["SPACE3_QUAL_3"], minimum=1, maximum=10, step=1, visible=False) | |
| space4 = gr.Radio(label=LANG["SPACE4_LABEL"], choices=["Oui", "Non"]) | |
| space5 = gr.Textbox(label=LANG["SPACE5_LABEL"], visible=False) | |
| space6 = gr.Radio(label=LANG["SPACE6_LABEL"], choices=["Oui", "Non", "Ne sait pas"]) | |
| space7_1 = gr.Textbox(label="Pratique 1", placeholder="Décrivez la pratique d'engagement 1", visible=False) | |
| space7_2 = gr.Textbox(label="Pratique 2", placeholder="Décrivez la pratique d'engagement 2", visible=False) | |
| space7_3 = gr.Textbox(label="Pratique 3", placeholder="Décrivez la pratique d'engagement 3", visible=False) | |
| # Domaine components (CheckboxGroup) | |
| space_domain_1 = gr.CheckboxGroup(label=f"Domaines pour Pratique 1", choices=DOMAIN_CHOICES_FR, visible=False) | |
| space_domain_2 = gr.CheckboxGroup(label=f"Domaines pour Pratique 2", choices=DOMAIN_CHOICES_FR, visible=False) | |
| space_domain_3 = gr.CheckboxGroup(label=f"Domaines pour Pratique 3", choices=DOMAIN_CHOICES_FR, visible=False) | |
| # NOUVEAUX COMPOSANTS: SPACE9_CHOICE et SPACE9_SCALE | |
| space9_choice = gr.Radio(label=LANG["SPACE9_CHOICE_LABEL"], choices=["1", "2", "3"], visible=False) | |
| space9_scale = gr.Slider(label=LANG["SPACE9_SCALE_LABEL"], minimum=1, maximum=10, step=1, visible=False) | |
| space11 = gr.CheckboxGroup(label=LANG["SPACE11_LABEL"], choices=SOCIAL_MEDIA_CHOICES) | |
| # === TAB 2 Components === | |
| engage1_role = gr.CheckboxGroup(label=LANG["ENGAGE1_LABEL"], choices=ENGAGE1_CHOICES) | |
| engage2_type = gr.CheckboxGroup(label=LANG["ENGAGEMENT_ORGANISATION_LABEL"], choices=ENGAGE2_CHOICES) | |
| engage2_autre_detail = gr.Textbox(label=LANG["ENGAGE2_AUTRE_DETAIL_LABEL"], visible=False) | |
| # ENGAGE3 : Changement de Textbox à CheckboxGroup + Textbox pour 'autre' | |
| engagement_domaine = gr.CheckboxGroup(label=LANG["ENGAGEMENT_DOMAINE_LABEL"], choices=ENGAGE3_CHOICES_FR) | |
| engage3_autre_detail = gr.Textbox(label=LANG["ENGAGE3_AUTRE_DETAIL_LABEL"], visible=False) # Champ de précision pour 'autre' | |
| info_activites = gr.CheckboxGroup(label=LANG["INFO_ACTIVITES_LABEL"], choices=["Participer à une manifestation/grève", "Contacter un élu", "Signer une pétition en ligne", "Boycotter un produit/marque", "Faire un don à une association", "Autre"], interactive=True) | |
| info_reseaux_sociaux = gr.Radio(label=LANG["INFO_RESEAUX_SOCIAUX_LABEL"], choices=["Oui", "Non", "Parfois"]) | |
| # === TAB 3 Components === | |
| info_frequence_actu = gr.Radio(label=LANG["INFO_FREQUENCE_ACTU_LABEL"], choices=["Plusieurs fois par jour", "Une fois par jour", "Quelques fois par semaine", "Rarement", "Jamais"]) | |
| info2 = gr.CheckboxGroup(label=LANG["INFO2_LABEL"], choices=THEMES_ACTU_FR) | |
| info3 = gr.CheckboxGroup(label=LANG["INFO3_LABEL"], choices=THEMES_ACTU_FR) | |
| info_opposite_feeling = gr.Radio(label=LANG["OPPOSITE_FEELING_BASE_LABEL"], choices=INFO4_CHOICES) | |
| info_contradict_opinion = gr.Radio(label=LANG["INFO_CONTRADICT_OPINION_LABEL"], choices=INFO5_CHOICES) | |
| info4 = gr.CheckboxGroup(label=LANG["INFO4_LABEL"], choices=THEMES_ACTU_FR) | |
| info5 = gr.CheckboxGroup(label=LANG["INFO5_LABEL"], choices=ANXIETY_MANIFESTATION_CHOICES) | |
| # Info 8 Sliders (9 components) | |
| info6_sliders = {} | |
| for item in LANG["INFO6_ITEMS"]: | |
| clean_id = item.split('/')[0].lower().replace(' ', '_').replace('é', 'e').replace('è', 'e') | |
| info6_sliders[clean_id] = gr.Slider(label=item, minimum=1, maximum=10, step=1) | |
| info6_slider_components = list(info6_sliders.values()) | |
| info6_logement, info6_politique, info6_etudes, info6_climat, info6_sociales, info6_sentimentale, info6_securite, info6_estime, info6_liberte = info6_slider_components | |
| # === TAB 4 Components === | |
| prat_cult1 = gr.Number(label=LANG["PRAT_CULT1_LABEL"], minimum=0, maximum=100, step=1) | |
| # Cultural Frequency Radios (10 components) | |
| prat_cult_freq_components = [ | |
| gr.Radio(label=f"{activity}", choices=FREQ_FR) for activity in ACTIVITY_CHOICES | |
| ] | |
| (prat_cult_lecture, prat_cult_cinema, prat_cult_musique, prat_cult_theatre, prat_cult_expositions, | |
| prat_cult_sport, prat_cult_voyages, prat_cult_jeux_video, prat_cult_bricolage, prat_cult_reseaux) = prat_cult_freq_components | |
| prat_nature = gr.Radio(label=LANG["PRAT_NATURE_LABEL"], choices=["Oui", "Non", "Parfois"]) | |
| # Platform Usage Numbers (10 components) | |
| platform_components = [ | |
| gr.Number(label=f"Heures/jour pour {platform}", placeholder="0.5, 1, 2...", minimum=0, step=0.5) for platform in PLATFORM_CHOICES | |
| ] | |
| (plat_instagram, plat_tiktok, plat_youtube, plat_twitter, plat_facebook, | |
| plat_snapchat, plat_twitch, plat_reddit, plat_linkedin, plat_autre) = platform_components | |
| # Purpose Frequency Radios (7 components) | |
| purpose_components = [ | |
| gr.Radio(label=f"Fréquence pour: {purpose}", choices=FREQ_FR) for purpose in PURPOSE_CHOICES[:-1] | |
| ] | |
| purpose_autre_detail = gr.Textbox(label="Précisez l'usage 'Autre'", visible=True) | |
| purpose_components.append(gr.Radio(label=f"Fréquence pour: {PURPOSE_CHOICES[-1]}", choices=FREQ_FR)) | |
| (purpose_actu, purpose_loisirs, purpose_education, purpose_engagement, | |
| purpose_social, purpose_professionnel, purpose_autre_freq) = purpose_components | |
| # === TAB 5 Components === | |
| demo_gender = gr.Radio(label=LANG["DEMO_GENDER_LABEL"], choices=["Homme", "Femme", "Non-binaire", "Préfère ne pas dire"]) | |
| demo_age = gr.Number(label=LANG["DEMO_AGE_LABEL"], minimum=18, maximum=100, step=1, placeholder="ex: 25") | |
| demo_location_commune = gr.Textbox(label=LANG["DEMO_LOCATION_COMMUNE_LABEL"], placeholder="Nom de la commune") | |
| demo_location_arrond = gr.Textbox(label=LANG["DEMO_LOCATION_ARROND_LABEL"], placeholder="ex: 75005") | |
| demo_parents_location = gr.Radio(label=LANG["DEMO_PARENTS_LOCATION_LABEL"], choices=["Oui", "Non"]) | |
| demo_inscription = gr.CheckboxGroup(label=LANG["DEMO_INSCRIPTION_LABEL"], choices=DEMO_INSCRIPTION_CHOICES) | |
| demo_discipline = gr.CheckboxGroup(label=LANG["DEMO_DISCIPLINE_LABEL"], choices=DEMO_DISCIPLINE_CHOICES) | |
| demo_job = gr.Radio(label=LANG["DEMO_JOB_LABEL"], choices=["Oui", "Non"]) | |
| demo_income = gr.Radio(label=LANG["DEMO_INCOME_LABEL"], choices=INCOME_CHOICES) | |
| demo_socialcapital1_parent1 = gr.Radio(label=LANG["DEMO_SOCIALCAPITAL1_PARENT1_LABEL"], choices=INSEE_CHOICES) | |
| demo_socialcapital1_parent2 = gr.Radio(label=LANG["DEMO_SOCIALCAPITAL1_PARENT2_LABEL"], choices=INSEE_CHOICES) | |
| demo_socialcapital2 = gr.Number(label=LANG["DEMO_SOCIALCAPITAL2_LABEL"], minimum=0, step=1) | |
| # === TAB 6 Components === | |
| open_non_institutionnel = gr.Textbox(label=LANG["OPEN_NON_INSTITUTIONNEL_LABEL"], lines=3) | |
| open_alternatives = gr.Textbox(label=LANG["OPEN_ALTERNATIVES_LABEL"], placeholder="5 mots maximum") | |
| open_motivations = gr.Textbox(label=LANG["OPEN_MOTIVATIONS_LABEL"], lines=3) | |
| # Helper function to generate robust column names | |
| def get_column_names(data_components: list) -> list: | |
| names = [] | |
| for i, component in enumerate(data_components): | |
| label_attr = getattr(component, 'label', None) | |
| if label_attr: | |
| name = label_attr | |
| # Nettoyage et simplification du nom de colonne | |
| clean_name = name.split('-')[0].strip().replace(' ', '_').replace('.', '').replace(':', '') | |
| # Gestion des noms spécifiques | |
| if "Domaines pour Pratique 1" in name: | |
| clean_name = "P1_DOMAINES" | |
| elif "Domaines pour Pratique 2" in name: | |
| clean_name = "P2_DOMAINES" | |
| elif "Domaines pour Pratique 3" in name: | |
| clean_name = "P3_DOMAINES" | |
| elif "Alternatif/Underground" in name: | |
| clean_name = "SPACE4_QUAL_1" | |
| elif "Lieu 2 : Alternatif" in name: | |
| clean_name = "SPACE4_QUAL_2" | |
| elif "Lieu 3 : Alternatif" in name: | |
| clean_name = "SPACE4_QUAL_3" | |
| elif clean_name == "ENGAGE2_Précisez_'autre'": | |
| clean_name = "ENGAGE2_Autre_Precision" | |
| # NOUVEAU NOM DE COLONNE POUR ENGAGE3_AUTRE | |
| elif clean_name == "ENGAGE3_Précisez_'autre'": | |
| clean_name = "ENGAGE3_Autre_Precision" | |
| # NOUVEAU NOM DE COLONNE POUR SPACE9_CHOICE | |
| elif clean_name == "SPACE9": | |
| clean_name = "SPACE9_CHOICE" | |
| # NOUVEAU NOM DE COLONNE POUR SPACE9_SCALE | |
| elif clean_name == "SPACE9_BIS": | |
| clean_name = "SPACE9_SCALE" | |
| names.append(clean_name) | |
| else: | |
| names.append(f"Component_{i+1}") | |
| return names | |
| # ================================================================= | |
| # GRADIO UI & LOGIC - UTILITY FUNCTIONS | |
| # ================================================================= | |
| def update_visibility_approach(approach): | |
| updates = {} | |
| is_non = approach == "Non" | |
| updates[refusal_reason] = gr.update(visible=is_non) | |
| updates[contact_later] = gr.update(visible=is_non) | |
| if not is_non: | |
| updates[refusal_reason_other] = gr.update(visible=False) | |
| return updates[refusal_reason], updates[contact_later] | |
| def update_visibility_refusal(reasons): | |
| if reasons and "Autre" in reasons: | |
| return gr.update(visible=True) | |
| return gr.update(visible=False) | |
| def update_visibility_space1(frequency): | |
| is_frequent = frequency in ["Rarement", "Parfois", "Souvent"] | |
| is_never = frequency == "Jamais" | |
| return { | |
| space2a_1: gr.update(visible=is_frequent), | |
| space2a_2: gr.update(visible=is_frequent), | |
| space2a_3: gr.update(visible=is_frequent), | |
| space3_1: gr.update(visible=is_frequent), | |
| space3_2: gr.update(visible=is_frequent), | |
| space3_3: gr.update(visible=is_frequent), | |
| space2b: gr.update(visible=is_never), | |
| } | |
| def update_visibility_space4(follow_community): | |
| return gr.update(visible=follow_community == "Oui") | |
| def update_visibility_space6(knows_practices): | |
| is_yes = knows_practices == "Oui" | |
| outputs = { | |
| space7_1: gr.update(visible=is_yes), | |
| space7_2: gr.update(visible=is_yes), | |
| space7_3: gr.update(visible=is_yes), | |
| space9_choice: gr.update(visible=is_yes), # NOUVEAU | |
| space9_scale: gr.update(visible=is_yes) # NOUVEAU | |
| } | |
| # If not "Oui", hide both the practice description and its domains | |
| if not is_yes: | |
| outputs[space_domain_1] = gr.update(visible=False) | |
| outputs[space_domain_2] = gr.update(visible=False) | |
| outputs[space_domain_3] = gr.update(visible=False) | |
| # If "Oui", the practice description (space7_X) is visible, but the domains (space_domain_X) | |
| # must be hidden by default until a practice is actually typed in (handled by space7_X.change) | |
| else: | |
| outputs[space_domain_1] = gr.update(visible=False) | |
| outputs[space_domain_2] = gr.update(visible=False) | |
| outputs[space_domain_3] = gr.update(visible=False) | |
| return outputs | |
| def update_domain_visibility(practice_text): | |
| is_cited = bool(practice_text.strip()) | |
| # Note: CheckboxGroup (or Radio) is now visible if the corresponding practice text is present | |
| return gr.update(visible=is_cited) | |
| def update_visibility_engage2_autre(engage2_choices: List[str]): | |
| return gr.update(visible="autre (préciser)" in engage2_choices) | |
| # NOUVELLE FONCTION de visibilité pour ENGAGE3 | |
| def update_visibility_engage3_autre(engage3_choices: List[str]): | |
| return gr.update(visible="autre / précisez" in engage3_choices) | |
| def process_survey(*data_values: Any) -> Tuple[str, gr.File]: | |
| """ | |
| Processes survey data, creates a temporary CSV file, and returns the path | |
| and a success message for download. | |
| """ | |
| all_inputs_list = list(data_values) | |
| if len(all_inputs_list) != EXPECTED_COUNT: | |
| error_msg = f"Erreur critique: Le nombre d'entrées reçues est incorrect ({len(all_inputs_list)} au lieu de {EXPECTED_COUNT}). Veuillez contacter le développeur." | |
| return error_msg, gr.update(visible=False, value=None) | |
| language = "FR" | |
| try: | |
| column_names = get_column_names(DATA_INPUT_COMPONENTS) | |
| column_names.insert(0, "DEMO_LANGUAGE") | |
| data_row = [language] + all_inputs_list | |
| df = pd.DataFrame([data_row], columns=column_names) | |
| # Nommage du fichier: Enquêteur ID + Date | |
| enqueteur_id_val = all_inputs_list[0] | |
| current_date_str = datetime.now().strftime("%Y-%m-%d") | |
| safe_enqueteur_id = str(enqueteur_id_val).strip() | |
| safe_enqueteur_id = "".join(c if c.isalnum() else '_' for c in safe_enqueteur_id) | |
| if not safe_enqueteur_id: | |
| safe_enqueteur_id = "NO_ID" | |
| temp_filename = f"/tmp/{safe_enqueteur_id}_{current_date_str}.csv" | |
| os.makedirs(os.path.dirname(temp_filename), exist_ok=True) | |
| df.to_csv(temp_filename, index=False, encoding='utf-8') | |
| success_msg = f"✨ Succès ! Le questionnaire a été soumis. Cliquez sur le lien ci-dessous pour **Télécharger le Fichier CSV** nommé `{safe_enqueteur_id}_{current_date_str}.csv`." | |
| return success_msg, gr.update(value=temp_filename, visible=True) | |
| except Exception as e: | |
| error_msg = f"Erreur lors de la génération du CSV: {str(e)}" | |
| return error_msg, gr.update(visible=False, value=None) | |
| # ================================================================= | |
| # DATA COMPONENT FINAL ASSEMBLY (93 components) | |
| # ================================================================= | |
| DATA_INPUT_COMPONENTS = [ | |
| # Global (1) | |
| enqueteur_id, | |
| # TAB 1 - Contact & Lieux (25) | |
| approach_answer, refusal_reason, refusal_reason_other, contact_later, | |
| space1, space2a_1, space2a_2, space2a_3, space2b, | |
| space3_1, space3_2, space3_3, | |
| space4, space5, | |
| space6, | |
| space7_1, space_domain_1, | |
| space7_2, space_domain_2, | |
| space7_3, space_domain_3, | |
| space9_choice, # NOUVEAU COMPOSANT | |
| space9_scale, # NOUVEAU COMPOSANT | |
| space11, | |
| # TAB 2 - Engagement (7 composants) | |
| engage1_role, engage2_type, engage2_autre_detail, | |
| engagement_domaine, engage3_autre_detail, | |
| info_activites, info_reseaux_sociaux, | |
| # TAB 3 - Actualité (16) | |
| info_frequence_actu, info2, info3, info_opposite_feeling, info_contradict_opinion, | |
| info4, info5, | |
| info6_logement, info6_politique, info6_etudes, info6_climat, info6_sociales, | |
| info6_sentimentale, info6_securite, info6_estime, info6_liberte, | |
| # TAB 4 - Cultural & Digital (30) | |
| prat_cult1, | |
| prat_cult_lecture, prat_cult_cinema, prat_cult_musique, prat_cult_theatre, prat_cult_expositions, | |
| prat_cult_sport, prat_cult_voyages, prat_cult_jeux_video, prat_cult_bricolage, prat_cult_reseaux, | |
| prat_nature, | |
| plat_instagram, plat_tiktok, plat_youtube, plat_twitter, plat_facebook, | |
| plat_snapchat, plat_twitch, plat_reddit, plat_linkedin, plat_autre, | |
| purpose_autre_detail, | |
| purpose_actu, purpose_loisirs, purpose_education, purpose_engagement, | |
| purpose_social, purpose_professionnel, purpose_autre_freq, | |
| # TAB 5 - Demographics (12) | |
| demo_gender, demo_age, demo_location_commune, demo_location_arrond, demo_parents_location, | |
| demo_inscription, demo_discipline, demo_job, demo_income, | |
| demo_socialcapital1_parent1, demo_socialcapital1_parent2, demo_socialcapital2, | |
| # TAB 6 - Open Questions (3) | |
| open_non_institutionnel, open_alternatives, open_motivations, | |
| ] | |
| if len(DATA_INPUT_COMPONENTS) != EXPECTED_DATA_COUNT: | |
| raise RuntimeError(f"Internal component count mismatch. Expected {EXPECTED_DATA_COUNT}, got {len(DATA_INPUT_COMPONENTS)}. Please verify the DATA_INPUT_COMPONENTS list definition.") | |
| SUBMIT_INPUT_COMPONENTS = DATA_INPUT_COMPONENTS | |
| # ================================================================= | |
| # GRADIO UI SETUP (Utilisation des Accordions pour le F2F) | |
| # ================================================================= | |
| with gr.Blocks(title=LANG["TITLE"], css=".gradio-container { max-width: 1200px; }") as demo: | |
| gr.Markdown(f"## {LANG['TITLE']}") | |
| gr.Markdown(LANG["INTRO_TEXT"]) | |
| with gr.Row(): | |
| enqueteur_id.render() | |
| # --- Bloc Accordions --- | |
| # ================================================================= | |
| # ACCORDION 1: Contact & Lieux Fréquentés (Ouvert par défaut) | |
| # ================================================================= | |
| with gr.Accordion(LANG["TAB_1_TITLE"], open=True): | |
| gr.Markdown("### Phase d'approche") | |
| with gr.Row(): | |
| approach_answer.render() | |
| with gr.Column() as refusal_block: | |
| refusal_reason.render() | |
| refusal_reason_other.render() | |
| contact_later.render() | |
| approach_answer.change( | |
| update_visibility_approach, | |
| inputs=[approach_answer], | |
| outputs=[refusal_reason, contact_later] | |
| ) | |
| refusal_reason.change(update_visibility_refusal, inputs=[refusal_reason], outputs=[refusal_reason_other]) | |
| gr.Markdown("### Lieux et Communautés") | |
| space1.render() | |
| gr.Markdown(f"#### {LANG['SPACE2A_TITLE']}") | |
| with gr.Row() as space2_row: | |
| space2a_1.render() | |
| space2a_2.render() | |
| space2a_3.render() | |
| gr.Markdown(f"#### {LANG['SPACE3_TITLE']}") | |
| with gr.Row() as space3_row: | |
| space3_1.render() | |
| space3_2.render() | |
| space3_3.render() | |
| space2b.render() | |
| space1.change(update_visibility_space1, inputs=[space1], outputs=[space2a_1, space2a_2, space2a_3, space3_1, space3_2, space3_3, space2b]) | |
| gr.Markdown("### Usages alternatifs et Engagement") | |
| space4.render() | |
| space5.render() | |
| space4.change(update_visibility_space4, inputs=[space4], outputs=[space5]) | |
| space6.render() | |
| gr.Markdown(f"#### {LANG['SPACE7_TITLE']} (3 max.) et leurs Domaines (Choix multiples)") | |
| # Pratique 1 et son Domaine (Mis côte-à-côte) | |
| with gr.Row(): | |
| with gr.Column(scale=1): # Moins de place pour le champ de texte | |
| space7_1.render() | |
| with gr.Column(scale=2): # Plus de place pour les cases à cocher | |
| space_domain_1.render() | |
| # Pratique 2 et son Domaine (Mis côte-à-côte) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| space7_2.render() | |
| with gr.Column(scale=2): | |
| space_domain_2.render() | |
| # Pratique 3 et son Domaine (Mis côte-à-côte) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| space7_3.render() | |
| with gr.Column(scale=2): | |
| space_domain_3.render() | |
| # NOUVELLE SECTION SPACE9 | |
| gr.Markdown("#### Stimulation de la pratique choisie") | |
| space9_choice.render() | |
| space9_scale.render() | |
| gr.Markdown(f"#### {LANG['SPACE11_LABEL']}") | |
| space11.render() | |
| # Logique de visibilité pour SPACE7, les domaines et SPACE9 | |
| space6.change( | |
| fn=update_visibility_space6, | |
| inputs=[space6], | |
| outputs=[space7_1, space7_2, space7_3, space_domain_1, space_domain_2, space_domain_3, space9_choice, space9_scale] | |
| ) | |
| # Les domaines ne sont visibles que si une pratique est citée | |
| space7_1.change(fn=update_domain_visibility, inputs=[space7_1], outputs=[space_domain_1]) | |
| space7_2.change(fn=update_domain_visibility, inputs=[space7_2], outputs=[space_domain_2]) | |
| space7_3.change(fn=update_domain_visibility, inputs=[space7_3], outputs=[space_domain_3]) | |
| # ================================================================= | |
| # ACCORDION 2: Engagement Citoyen | |
| # ================================================================= | |
| with gr.Accordion(LANG["TAB_2_TITLE"], open=False): | |
| engage1_role.render() | |
| engage2_type.render() | |
| engage2_autre_detail.render() | |
| # ENGAGE3 - Domaine de l'engagement (CheckboxGroup) | |
| engagement_domaine.render() | |
| engage3_autre_detail.render() # Champ de précision pour 'autre' | |
| info_activites.render() | |
| info_reseaux_sociaux.render() | |
| engage2_type.change( | |
| fn=update_visibility_engage2_autre, | |
| inputs=[engage2_type], | |
| outputs=[engage2_autre_detail] | |
| ) | |
| # NOUVELLE LOGIQUE pour ENGAGE3 'autre' | |
| engagement_domaine.change( | |
| fn=update_visibility_engage3_autre, | |
| inputs=[engagement_domaine], | |
| outputs=[engage3_autre_detail] | |
| ) | |
| # ================================================================= | |
| # ACCORDION 3: Consommation d’actualités | |
| # ================================================================= | |
| with gr.Accordion(LANG["TAB_3_TITLE"], open=False): | |
| info_frequence_actu.render() | |
| info2.render() | |
| info3.render() | |
| info_opposite_feeling.render() | |
| info_contradict_opinion.render() | |
| gr.Markdown("---") | |
| info4.render() | |
| info5.render() | |
| gr.Markdown(f"### {LANG['INFO6_TITLE']}") | |
| with gr.Column(): | |
| info6_logement.render() | |
| info6_politique.render() | |
| info6_etudes.render() | |
| info6_climat.render() | |
| info6_sociales.render() | |
| info6_sentimentale.render() | |
| info6_securite.render() | |
| info6_estime.render() | |
| info6_liberte.render() | |
| # ================================================================= | |
| # ACCORDION 4: Pratiques culturelles et usages numériques | |
| # ================================================================= | |
| with gr.Accordion(LANG["TAB_4_TITLE"], open=False): | |
| prat_cult1.render() | |
| gr.Markdown(f"### {LANG['PRAT_CULT_PRACTICES_TITLE']}") | |
| with gr.Row(): | |
| with gr.Column(): | |
| prat_cult_lecture.render() | |
| prat_cult_cinema.render() | |
| prat_cult_musique.render() | |
| prat_cult_theatre.render() | |
| prat_cult_expositions.render() | |
| with gr.Column(): | |
| prat_cult_sport.render() | |
| prat_cult_voyages.render() | |
| prat_cult_jeux_video.render() | |
| prat_cult_bricolage.render() | |
| prat_cult_reseaux.render() | |
| prat_nature.render() | |
| gr.Markdown(f"### {LANG['PLATFORM_TITLE']}") | |
| with gr.Row(): | |
| with gr.Column(): | |
| plat_instagram.render() | |
| plat_tiktok.render() | |
| plat_youtube.render() | |
| plat_twitter.render() | |
| plat_facebook.render() | |
| with gr.Column(): | |
| plat_snapchat.render() | |
| plat_twitch.render() | |
| plat_reddit.render() | |
| plat_linkedin.render() | |
| plat_autre.render() | |
| gr.Markdown(f"### {LANG['PURPOSE_TITLE']}") | |
| with gr.Row(): | |
| with gr.Column(): | |
| purpose_actu.render() | |
| purpose_loisirs.render() | |
| purpose_education.render() | |
| purpose_engagement.render() | |
| with gr.Column(): | |
| purpose_social.render() | |
| purpose_professionnel.render() | |
| purpose_autre_freq.render() | |
| purpose_autre_detail.render() | |
| # ================================================================= | |
| # ACCORDION 5: Profil Démographique | |
| # ================================================================= | |
| with gr.Accordion(LANG["TAB_5_TITLE"], open=False): | |
| demo_gender.render() | |
| demo_age.render() | |
| with gr.Row(): | |
| demo_location_commune.render() | |
| demo_location_arrond.render() | |
| demo_parents_location.render() | |
| with gr.Row(): | |
| demo_inscription.render() | |
| demo_discipline.render() | |
| with gr.Row(): | |
| demo_job.render() | |
| demo_income.render() | |
| gr.Markdown("### Capital Social") | |
| with gr.Row(): | |
| demo_socialcapital1_parent1.render() | |
| demo_socialcapital1_parent2.render() | |
| demo_socialcapital2.render() | |
| # ================================================================= | |
| # ACCORDION 6: Questions ouvertes | |
| # ================================================================= | |
| with gr.Accordion(LANG["TAB_6_TITLE"], open=False): | |
| open_non_institutionnel.render() | |
| open_alternatives.render() | |
| open_motivations.render() | |
| # ================================================================= | |
| # Soumission et Téléchargement | |
| # ================================================================= | |
| with gr.Accordion(LANG["TAB_7_TITLE"], open=False): | |
| gr.Markdown("### Finalisation du Questionnaire") | |
| gr.Markdown("Vérifiez que toutes vos réponses sont complètes avant de soumettre. Une fois soumis, le fichier CSV (nommé avec l'identifiant de l'enquêteur et la date) apparaîtra ci-dessous pour téléchargement.") | |
| with gr.Row(): | |
| submit_btn.render() | |
| reset_btn.render() | |
| output_status.render() | |
| output_message.render() | |
| # Submit action | |
| submit_btn.click( | |
| fn=process_survey, | |
| inputs=SUBMIT_INPUT_COMPONENTS, | |
| outputs=[output_status, output_message] | |
| ) | |
| # Reset action | |
| reset_btn.click( | |
| fn=lambda: ([None] * EXPECTED_DATA_COUNT) + ["---", gr.update(value=None, visible=False)], | |
| inputs=[], | |
| outputs=DATA_INPUT_COMPONENTS + [output_status, output_message], | |
| js="() => { document.getElementById('output_message').innerHTML = '---'; }" | |
| ) | |
| if __name__ == "__main__": | |
| import sys | |
| # Assurez-vous que le compte final est correct avant le lancement | |
| sys.modules[__name__].EXPECTED_DATA_COUNT = len(DATA_INPUT_COMPONENTS) | |
| sys.modules[__name__].EXPECTED_COUNT = len(DATA_INPUT_COMPONENTS) | |
| demo.launch() |