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()