clementBE commited on
Commit
3a8062b
·
verified ·
1 Parent(s): d8c35ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +740 -252
app.py CHANGED
@@ -1,269 +1,757 @@
1
  import gradio as gr
2
- import requests
3
- from bs4 import BeautifulSoup
4
- from urllib.parse import urljoin, urlparse
5
  import pandas as pd
6
- import tempfile
7
  import os
8
- import io
9
- import tldextract
10
- import tabulate # Required for df.to_markdown()
11
-
12
- # ----------------------------------------------------
13
- # ⚠️ SELENIUM IMPORTS AND CONFIGURATION ⚠️
14
- # ----------------------------------------------------
15
- # These imports are essential for simulating user interaction.
16
- from selenium import webdriver
17
- from selenium.webdriver.common.by import By
18
- from selenium.webdriver.chrome.service import Service
19
- from selenium.webdriver.chrome.options import Options
20
- from selenium.webdriver.support.ui import WebDriverWait
21
- from selenium.webdriver.support import expected_conditions as EC
22
- # If using webdriver-manager (recommended for local setup):
23
- # from webdriver_manager.chrome import ChromeDriverManager
24
  import time
 
 
25
 
26
- # -------------------------
27
- # Default excluded domains
28
- # -------------------------
29
- DEFAULT_EXCLUDED = [
30
- 'facebook.com', 'instagram.com', 'linkedin.com', 'youtube.com', 'google.com',
31
- 'apple.com', 'amazon.com', 'microsoft.com', 'meta.com', 't.co', 'x.com',
32
- 'twitter.com', 'whatsapp.com', 'github.com', 'tiktok.com',
33
- 'ulule.com', 'kisskissbankbank.com', 'leetchi.com', 'helloasso.com',
34
- 'gofundme.com', 'tipeee.com', 'patreon.com', 'paypal.com', 'stripe.com',
35
- 'buymeacoffee.com', 'wordpress.org', 'wordpress.com', 'wp.me', 'bsky.app', 'mamot.fr'
36
- ]
37
-
38
- # -------------------------
39
- # Load Tracker DB (placeholder/simulated for functionality)
40
- # -------------------------
41
- TRACKER_CSV_PATH = "tracker_db.csv"
42
- if os.path.exists(TRACKER_CSV_PATH):
43
- TRACKER_DB = pd.read_csv(TRACKER_CSV_PATH)
44
- print(f"Tracker DB loaded ({len(TRACKER_DB)} entries).")
45
- else:
46
- print("Warning: tracker_db.csv not found. Partner names will be 'Unknown'. Using simulated data for testing.")
47
- TRACKER_DB = pd.DataFrame({
48
- "domain": ["doubleclick.net", "trksite.biz", "adservice.com", "analytics.com", "mediarithmics.com"],
49
- "name": ["Google Marketing", "Tracker Site", "Ad Service Network", "Web Analytics", "Mediarithmics"],
50
- "type": ["Advertising", "Tracking", "Advertising", "Analytics", "Tracking"],
51
- "commercial": ["Yes", "Yes", "Yes", "No", "Yes"]
52
- })
53
-
54
- # -------------------------
55
- # Helper functions
56
- # -------------------------
57
- def get_registered_domain(domain):
58
- ext = tldextract.extract(domain)
59
- return ext.registered_domain.lower() if ext.registered_domain else domain.lower()
60
-
61
- def lookup_partner(domain):
62
- rd = get_registered_domain(domain)
63
- row = TRACKER_DB[TRACKER_DB['domain'] == rd]
64
- if not row.empty:
65
- return row.iloc[0]['name'], row.iloc[0]['type'], row.iloc[0]['commercial']
66
- else:
67
- return rd, "Unknown", "Unknown"
68
-
69
- def detect_cookie_purpose(cookie_name):
70
- n = cookie_name.lower()
71
- if any(x in n for x in ("_ga","_gid","analytics","_gat","matomo")):
72
- return "Analytics"
73
- if any(x in n for x in ("ads","ad","banners","pixel","tracking","trk","fbp","tld")):
74
- return "Advertising/Tracking"
75
- if any(x in n for x in ("session","auth","login","user","uid","csrftoken","lang")):
76
- return "Functional"
77
- return "Unknown"
78
-
79
- def build_partner_summary(cookie_records):
80
- df = pd.DataFrame(cookie_records)
81
- if df.empty:
82
- return pd.DataFrame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- grp = df.groupby("partner_domain").agg(
85
- partner_name=pd.NamedAgg(column="partner_name", aggfunc=lambda x: x.iloc[0]),
86
- partner_type=pd.NamedAgg(column="partner_type", aggfunc=lambda x: x.iloc[0]),
87
- partner_commercial=pd.NamedAgg(column="partner_commercial", aggfunc=lambda x: x.iloc[0]),
88
- num_cookies=pd.NamedAgg(column="cookie_name", aggfunc="count"),
89
- num_pages=pd.NamedAgg(column="page_domain", aggfunc=lambda x: x.nunique()),
90
- ).reset_index()
91
-
92
- commercial_partners = grp[
93
- (grp['partner_commercial'] == 'Yes') |
94
- (grp['partner_type'] == 'Advertising') |
95
- (grp['partner_type'] == 'Tracking')
96
- ]
97
- return commercial_partners.sort_values(by='num_cookies', ascending=False)
98
-
99
- def classify_cookies(cookie_list, page_urls):
100
- records = []
101
- third_party = []
102
- base_domains = [urlparse(u).netloc.lower() for u in page_urls]
103
-
104
- for c in cookie_list:
105
- cookie_name = c.get("name")
106
- cookie_value = c.get("value", "")
107
- cookie_domain = c.get("domain", "").lstrip(".").lower()
108
- page_url = c.get("page_url", "N/A")
109
 
110
- # Crucial fix for KeyError: Extract page_domain
111
- page_domain = urlparse(page_url).netloc.lower()
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- partner_name, partner_type, partner_commercial = lookup_partner(cookie_domain)
114
- purpose = detect_cookie_purpose(cookie_name)
 
 
 
 
 
 
115
 
116
- is_first = any(urlparse(base).netloc.lower() in cookie_domain for base in base_domains)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- rec = {
119
- "page_url": page_url,
120
- "page_domain": page_domain, # Now correctly included
121
- "cookie_name": cookie_name,
122
- "cookie_value": cookie_value,
123
- "cookie_domain": cookie_domain,
124
- "partner_domain": get_registered_domain(cookie_domain),
125
- "partner_name": partner_name,
126
- "partner_type": partner_type,
127
- "partner_commercial": partner_commercial,
128
- "type": "first-party" if is_first else "third-party",
129
- "purpose": purpose
130
- }
131
- records.append(rec)
132
- if rec["type"] == "third-party":
133
- third_party.append(rec)
134
-
135
- return records, third_party
136
-
137
- # ----------------------------------------------------
138
- # 🟢 SELENIUM IMPLEMENTATION TEMPLATE 🟢
139
- # ----------------------------------------------------
140
-
141
- # --- Fallback/Simulation Function (Used if Selenium setup fails) ---
142
- def fetch_page_simulated(url):
143
- """Returns simulated cookie data for testing when Selenium is unavailable."""
144
- simulated_cookies = [
145
- {'name': 'session_id', 'value': 'abc', 'domain': urlparse(url).netloc},
146
- {'name': 'consent', 'value': 'yes', 'domain': urlparse(url).netloc},
147
- {'name': 'test_cookie_1', 'value': 'cf_ch', 'domain': '.doubleclick.net', 'expiry': time.time() + 3600},
148
- {'name': 'test_cookie_2', 'value': 'cf_ch', 'domain': '.doubleclick.net', 'expiry': time.time() + 3600},
149
- {'name': 'm_id', 'value': 'xyz', 'domain': '.mediarithmics.com', 'expiry': time.time() + 3600},
150
- {'name': 'Ad_ID', 'value': 'trt', 'domain': '.trksite.biz', 'expiry': time.time() + 3600},
151
- {'name': '_ga', 'value': '123', 'domain': '.analytics.com', 'expiry': time.time() + 3600},
152
- ]
153
- for c in simulated_cookies:
154
- c['page_url'] = url
155
- return simulated_cookies
156
- # --- End of Fallback ---
157
-
158
- def fetch_page_with_cookie_acceptance(url):
159
- """
160
- Attempts to use Selenium to navigate, click accept, and retrieve cookies.
161
- If Selenium fails (e.g., driver missing), it falls back to simulated data.
162
- """
163
- print(f"Attempting to fetch {url} and simulate cookie acceptance...")
164
-
165
- # ------------------------------------------------------
166
- # ⚠️ SELENIUM SETUP BLOCK - CONFIGURE THIS LOCALLY ⚠️
167
- # ------------------------------------------------------
168
- try:
169
- # Use ChromeDriverManager for automatic driver management (recommended)
170
- # service = Service(ChromeDriverManager().install())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- # --- OR ---
173
- # If running in a restricted environment, you must manually specify the service path:
174
- # service = Service('/usr/local/bin/chromedriver')
175
-
176
- options = Options()
177
- options.add_argument('--headless')
178
- options.add_argument('--no-sandbox')
179
- options.add_argument('--disable-dev-shm-usage')
 
 
 
 
 
 
180
 
181
- # driver = webdriver.Chrome(service=service, options=options)
 
 
 
 
182
 
183
- # ------------------------------------------------------
184
- # ⚠️ Replace the 'return fetch_page_simulated(url)' below
185
- # with the actual driver logic (like fetch_page_selenium(driver, url))
186
- # once your environment is configured.
187
- # ------------------------------------------------------
188
 
189
- return fetch_page_simulated(url)
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- except Exception as e:
192
- print(f"Selenium setup failed ({e}). Falling back to simulated data.")
193
- return fetch_page_simulated(url)
194
-
195
- # -------------------------
196
- # Main crawler logic
197
- # -------------------------
198
- def run_crawler(url_input):
199
- url_list = [u.strip() for u in url_input.strip().splitlines() if u.strip()]
200
- if not url_list:
201
- return "Please provide at least one URL.", *(None,)*4
202
-
203
- all_cookies = []
204
-
205
- for u in url_list:
206
- cookies = fetch_page_with_cookie_acceptance(u)
207
- all_cookies.extend(cookies)
208
-
209
- # 1. Classify and summarize
210
- cookie_records, third_party_records = classify_cookies(all_cookies, url_list)
211
- df_cookies = pd.DataFrame(cookie_records)
212
- df_partner = build_partner_summary(cookie_records)
213
-
214
- # 2. Prepare outputs
215
- if df_partner.empty:
216
- head_table = "No commercial partners detected via cookies after simulated acceptance."
217
- message = "No commercial partners detected via cookies after simulated acceptance."
218
- else:
219
- df_head = df_partner.head(10).rename(columns={
220
- 'partner_domain': 'Domain',
221
- 'partner_name': 'Name',
222
- 'partner_type': 'Type',
223
- 'partner_commercial': 'Commercial?',
224
- 'num_cookies': 'Cookie Count',
225
- 'num_pages': 'Pages Found'
226
- })
227
- # This line requires the 'tabulate' package
228
- head_table = df_head.to_markdown(index=False, floatfmt=".0f")
229
- message = f"Successfully detected {len(df_partner)} potential commercial partners. Details below."
230
-
231
- # 3. Save results to temporary files
232
- cookies_path = os.path.join(tempfile.gettempdir(), "cookies_detailed_post_acceptance.xlsx")
233
- df_cookies.to_excel(cookies_path, index=False)
234
-
235
- df_tp = pd.DataFrame(third_party_records)
236
- third_party_path = os.path.join(tempfile.gettempdir(), "third_party_cookies_post_acceptance.xlsx")
237
- df_tp.to_excel(third_party_path, index=False)
238
-
239
- partner_summary_path = os.path.join(tempfile.gettempdir(), "partner_summary_commercial_only.xlsx")
240
- df_partner.to_excel(partner_summary_path, index=False)
241
-
242
- return (
243
- message,
244
- head_table,
245
- cookies_path,
246
- third_party_path,
247
- partner_summary_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  )
249
 
250
- # -------------------------
251
- # Gradio Interface
252
- # -------------------------
253
- iface = gr.Interface(
254
- fn=run_crawler,
255
- inputs=[
256
- gr.Textbox(label="🌐 Paste URL(s) to crawl (one per line)", lines=6, placeholder="https://example.com/"),
257
- ],
258
- outputs=[
259
- gr.Textbox(label="Status"),
260
- gr.Markdown(label="📈 Top Commercial Partner Summary (Head of Data)"),
261
- gr.File(label="🍪 XLSX of All Cookies (detailed)"),
262
- gr.File(label="📊 XLSX of Third-Party Cookies Only"),
263
- gr.File(label="📈 XLSX Commercial Partner Summary (Full)"),
264
- ],
265
- title="🍪 Commercial Partner Cookie Detector (Post-Acceptance)",
266
- description="Loads pages using a simulated browser, clicks 'Accept' on a cookie banner (requires Selenium setup), and lists commercial partners detected via third-party cookies."
267
- )
268
-
269
- iface.launch()
 
1
  import gradio as gr
 
 
 
2
  import pandas as pd
 
3
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import time
5
+ import json
6
+ from typing import List, Union, Dict, Any
7
 
8
+ # =================================================================
9
+ # CONFIGURATION AND LANGUAGE DEFINITIONS
10
+ # =================================================================
11
+
12
+ CSV_FILENAME = f"survey_data_{int(time.time())}.csv"
13
+ # Corrected count: 1 (lang_code) + 119 (data inputs) = 120
14
+ EXPECTED_COUNT = 120
15
+
16
+ # --- Shared Choices (Keys are in French for data consistency) ---
17
+ FREQ_FR = ["Jamais", "Rarement", "Parfois", "Souvent"]
18
+ FREQ_PT = ["Nunca", "Raramente", "Às vezes", "Frequentemente"]
19
+
20
+ THEMES_ACTU_FR = ["politique", "environnement", "économie", "santé", "social/société", "situation internationale/géopolitique", "science", "modes de vie (lifestyle)", "vie culturelle", "autre/précisez"]
21
+ THEMES_ACTU_PT = ["política", "ambiente", "economia", "saúde", "social/sociiedade", "situação internacional/geopolítica", "ciência", "estilos de vida (lifestyle)", "vida cultural", "outro/especifique"]
22
+
23
+
24
+ # --- Language Definitions ---
25
+ LANGUAGES = {}
26
+ try:
27
+ with open("fr.json", "r", encoding="utf-8") as f:
28
+ LANGUAGES["FR"] = json.load(f)
29
+ with open("pt.json", "r", encoding="utf-8") as f:
30
+ LANGUAGES["PT"] = json.load(f)
31
+ except Exception as e:
32
+ print(f"Error loading JSON file: {e}")
33
+ # Fallback to a minimal structure if loading fails to prevent immediate crash
34
+ LANGUAGES["FR"] = LANGUAGES.get("FR", {"TITLE": "Erreur de chargement", "LANG_LABEL": "Sélectionner la Langue", "SUBMIT_BUTTON_LABEL": "Submit", "SUBMISSION_MESSAGE_INIT": "Error loading language files. Check console.", "DOWNLOAD_LABEL": "Download"})
35
+ LANGUAGES["PT"] = LANGUAGES.get("PT", {"TITLE": "Erro de carregamento", "LANG_LABEL": "Selecionar Idioma", "SUBMIT_BUTTON_LABEL": "Submit", "SUBMISSION_MESSAGE_INIT": "Erro ao carregar arquivos de idioma. Verifique o console.", "DOWNLOAD_LABEL": "Download"})
36
+
37
+
38
+ # =================================================================
39
+ # GRADING APP DEFINITIONS
40
+ # =================================================================
41
+
42
+ def load_data(lang: str) -> Dict[str, Any]:
43
+ """Retrieves data for the selected language, defaulting to French keys if missing."""
44
+ data = LANGUAGES.get(lang, LANGUAGES["FR"])
45
+
46
+ # Check for critical nested structure completeness
47
+ # If the file was truncated, these keys might be missing. Fall back to French content.
48
+ if 'PRAT_CULT_PRACTICES' not in data:
49
+ data['PRAT_CULT_PRACTICES'] = LANGUAGES["FR"]['PRAT_CULT_PRACTICES']
50
+ if 'PLATFORM_ITEMS' not in data:
51
+ data['PLATFORM_ITEMS'] = LANGUAGES["FR"]['PLATFORM_ITEMS']
52
+ if 'PURPOSE_ITEMS' not in data:
53
+ data['PURPOSE_ITEMS'] = LANGUAGES["FR"]['PURPOSE_ITEMS']
54
+ if 'PCS_CHOICES' not in data:
55
+ data['PCS_CHOICES'] = LANGUAGES["FR"]['PCS_CHOICES']
56
+
57
+ return data
58
+
59
+ # --- Data Collection and Submission Function ---
60
+
61
+ def submit_survey(
62
+ lang_code: str,
63
+ # TAB 1 - Contact (6)
64
+ enqueteur: str, approach: str, refusal_reason: str, refusal_other: str, contact_later: str, firstname: str,
65
+ # TAB 1 - Lieux (17)
66
+ space1: str, lieu_1: str, lieu_2: str, lieu_3: str, space2b: List[str], space3_1: int, space3_2: int, space3_3: int, space4: str, space5: str, space6: str, space7_1: str, space7_2: str, space7_3: str, space8: str, space9: str, space10: List[str],
67
+ # TAB 2 - Engagement (5)
68
+ info1: List[str], engagement_organisation: List[str], engagement_domaine: List[str], info_activites: List[str], info_reseaux_sociaux: str,
69
+ # TAB 3 - News (12)
70
+ info_frequence_actu: str, info2: List[str], info3: List[str], info_opposite_feeling: str, info_contradict_opinion: str, info4: List[str], info5: List[str],
71
+ info6_logement: int, info6_politique: int, info6_etudes: int, info6_climat: int, info6_sociales: int, info6_sentimentale: int, info6_securite: int, info6_estime: int, info6_liberte: int,
72
+ # TAB 4 - Culturel (58)
73
+ prat_cult1: str,
74
+ # 14 checkboxes + 14 sliders for cultural practices
75
+ *cultural_practices_values, # 28 arguments <--- COMMA REMOVED
76
+ prat_nature: str,
77
+ # 10 checkboxes + 10 number fields for platforms
78
+ *platform_values, # 20 arguments <--- COMMA REMOVED
79
+ purpose_autre_detail: str,
80
+ # 8 checkboxes + 5 sliders for purpose
81
+ *purpose_values, # 13 arguments <--- COMMA REMOVED if it was there
82
+
83
+ # TAB 5 - Démographie (12)
84
+ demo_gender: str, demo_age: int,
85
+ demo_location_commune: str, demo_location_arrond: str,
86
+ demo_parents_location: str, demo_inscription: str,
87
+ demo_discipline: str, demo_job: str, demo_income: str,
88
+ demo_socialcapital1_parent1: str, demo_socialcapital1_parent2: str,
89
+ demo_socialcapital2: int,
90
+
91
+ # TAB 7 - Open Questions (3)
92
+ open_non_institutionnel: str, open_alternatives: str, open_motivations: str
93
+
94
+ ) -> List[Union[str, gr.File]]:
95
+ """Collects survey data, saves it to a CSV, and returns a success message and the file path."""
96
+
97
+ # --------------------------------------------------------------------------------
98
+ # 1. Sanity Check on Arguments
99
+ # --------------------------------------------------------------------------------
100
+
101
+ total_expected_params = EXPECTED_COUNT
102
+
103
+ # Get all locals (arguments)
104
+ all_args = locals()
105
+
106
+ # Arguments that are *args tuples:
107
+ received_cultural = len(all_args['cultural_practices_values'])
108
+ received_platform = len(all_args['platform_values'])
109
+ received_purpose = len(all_args['purpose_values'])
110
+
111
+ # Count of named arguments: 120 - 28 - 20 - 13 = 59 (including lang_code)
112
+ named_args_count = 59
113
+
114
+ total_received_params = named_args_count + received_cultural + received_platform + received_purpose
115
+
116
+ if total_received_params != total_expected_params:
117
+ print(f"ERROR: Expected {total_expected_params} inputs, but received {total_received_params}. Check component count.")
118
+ # Load language data for error message
119
+ current_lang_data = load_data(lang_code)
120
+ error_msg = current_lang_data.get("SUBMISSION_MESSAGE_ERROR", "❌ Submission Error. Check component count.")
121
+ error_msg = error_msg.format(received=total_received_params, expected=total_expected_params)
122
+ return [error_msg, None]
123
+
124
+
125
+ # --------------------------------------------------------------------------------
126
+ # 2. Structure Data
127
+ # --------------------------------------------------------------------------------
128
+
129
+ # Load language data to get correct keys for cultural, platform, and purpose components
130
+ lang_data = load_data(lang_code)
131
+
132
+ data = {
133
+ # Metadata
134
+ "TIMESTAMP": time.strftime("%Y-%m-%d %H:%M:%S"),
135
+ "ENQUETEUR": enqueteur,
136
+ "LANG_CODE": lang_code,
137
 
138
+ # TAB 1 - Contact
139
+ "APPROACH": approach,
140
+ "REFUSAL_REASON": refusal_reason,
141
+ "REFUSAL_OTHER": refusal_other,
142
+ "CONTACT_LATER": contact_later,
143
+ "FIRSTNAME": firstname,
144
+
145
+ # TAB 1 - Lieux
146
+ "SPACE1": space1, "LIEU_1": lieu_1, "LIEU_2": lieu_2, "LIEU_3": lieu_3,
147
+ "SPACE2B": "; ".join(space2b),
148
+ "SPACE3_1": space3_1, "SPACE3_2": space3_2, "SPACE3_3": space3_3,
149
+ "SPACE4": space4, "SPACE5": space5, "SPACE6": space6,
150
+ "SPACE7_1": space7_1, "SPACE7_2": space7_2, "SPACE7_3": space7_3,
151
+ "SPACE8": space8, "SPACE9": space9,
152
+ "SPACE10": "; ".join(space10),
153
+
154
+ # TAB 2 - Engagement
155
+ "INFO1": "; ".join(info1),
156
+ "ENGAGEMENT_ORGANISATION": "; ".join(engagement_organisation),
157
+ "ENGAGEMENT_DOMAINE": "; ".join(engagement_domaine),
158
+ "INFO_ACTIVITES": "; ".join(info_activites),
159
+ "INFO_RESEAUX_SOCIAUX": info_reseaux_sociaux,
 
 
 
160
 
161
+ # TAB 3 - News
162
+ "INFO_FREQUENCE_ACTU": info_frequence_actu,
163
+ "INFO2": "; ".join(info2), "INFO3": "; ".join(info3),
164
+ "INFO_OPPOSITE_FEELING": info_opposite_feeling,
165
+ "INFO_CONTRADICT_OPINION": info_contradict_opinion,
166
+ "INFO4": "; ".join(info4), "INFO5": "; ".join(info5),
167
+ "INFO6_LOGEMENT": info6_logement, "INFO6_POLITIQUE": info6_politique, "INFO6_ETUDES": info6_etudes,
168
+ "INFO6_CLIMAT": info6_climat, "INFO6_SOCIALES": info6_sociales, "INFO6_SENTIMENTALE": info6_sentimentale,
169
+ "INFO6_SECURITE": info6_securite, "INFO6_ESTIME": info6_estime, "INFO6_LIBERTE": info6_liberte,
170
+
171
+ # TAB 4 - Culturel
172
+ "PRAT_CULT1": prat_cult1,
173
+ "PRAT_NATURE": prat_nature,
174
+ "PURPOSE_AUTRE_DETAIL": purpose_autre_detail,
175
 
176
+ # TAB 5 - Démographie
177
+ "DEMO_GENDER": demo_gender, "DEMO_AGE": demo_age,
178
+ "DEMO_LOCATION_COMMUNE": demo_location_commune, "DEMO_LOCATION_ARROND": demo_location_arrond,
179
+ "DEMO_PARENTS_LOCATION": demo_parents_location, "DEMO_INSCRIPTION": demo_inscription,
180
+ "DEMO_DISCIPLINE": demo_discipline, "DEMO_JOB": demo_job, "DEMO_INCOME": demo_income,
181
+ "DEMO_SOCIALCAPITAL1_PARENT1": demo_socialcapital1_parent1,
182
+ "DEMO_SOCIALCAPITAL1_PARENT2": demo_socialcapital1_parent2,
183
+ "DEMO_SOCIALCAPITAL2": demo_socialcapital2,
184
 
185
+ # TAB 7 - Open Questions
186
+ "OPEN_NON_INSTITUTIONNEL": open_non_institutionnel,
187
+ "OPEN_ALTERNATIVES": open_alternatives,
188
+ "OPEN_MOTIVATIONS": open_motivations,
189
+ }
190
+
191
+ # --- Process Cultural Practices (28 values) ---
192
+ cultural_map = {item[1]: item[0] for item in lang_data['PRAT_CULT_PRACTICES']}
193
+
194
+ # cultural_practices_values is a tuple of 28 elements: 14 checkboxes, then 14 sliders
195
+ for i, (label_fr, key) in enumerate(LANGUAGES["FR"]['PRAT_CULT_PRACTICES']):
196
+ # Checkbox key: "CB_<KEY>"
197
+ data[f"CB_{key}"] = "Oui" if cultural_practices_values[i] else "Non"
198
+ # Slider key: "SLIDER_<KEY>"
199
+ data[f"SLIDER_{key}"] = cultural_practices_values[i + 14]
200
 
201
+ # --- Process Platforms (20 values) ---
202
+ platform_map = {item[1]: item[0] for item in lang_data['PLATFORM_ITEMS']}
203
+
204
+ # platform_values is a tuple of 20 elements: 10 checkboxes, then 10 number fields
205
+ for i, (label_fr, key) in enumerate(LANGUAGES["FR"]['PLATFORM_ITEMS']):
206
+ # Checkbox key: "PLATFORM_CB_<KEY>"
207
+ data[f"PLATFORM_CB_{key}"] = "Oui" if platform_values[i] else "Non"
208
+ # Number field key: "PLATFORM_HRS_<KEY>"
209
+ data[f"PLATFORM_HRS_{key}"] = platform_values[i + 10]
210
+
211
+ # --- Process Purpose (13 values) ---
212
+ purpose_map = {item[1]: item[0] for item in lang_data['PURPOSE_ITEMS']}
213
+
214
+ # purpose_values is a tuple of 13 elements: 8 checkboxes, then 5 sliders
215
+ slider_indices_fr = [i for i, item in enumerate(LANGUAGES["FR"]['PURPOSE_ITEMS']) if item[2] == True] # Indices in FR PURPOSE_ITEMS that have a slider
216
+
217
+ # Checkboxes (all 8 items)
218
+ for i, (label_fr, key, _) in enumerate(LANGUAGES["FR"]['PURPOSE_ITEMS']):
219
+ # Checkbox key: "PURPOSE_CB_<KEY>"
220
+ data[f"PURPOSE_CB_{key}"] = "Oui" if purpose_values[i] else "Non"
221
+
222
+ # Sliders (only 5 items where the third element is True)
223
+ for i, purpose_index in enumerate(slider_indices_fr):
224
+ key = LANGUAGES["FR"]['PURPOSE_ITEMS'][purpose_index][1]
225
+ # Slider value comes after all 8 checkboxes, so index is 8 + i
226
+ data[f"PURPOSE_SLIDER_{key}"] = purpose_values[8 + i]
227
+
228
+
229
+ # --------------------------------------------------------------------------------
230
+ # 3. Save Data to CSV
231
+ # --------------------------------------------------------------------------------
232
+
233
+ # Ensure CSV file exists with header
234
+ if not os.path.exists(CSV_FILENAME):
235
+ df = pd.DataFrame([data])
236
+ df.to_csv(CSV_FILENAME, index=False, sep=";")
237
+ else:
238
+ # Append data to existing CSV
239
+ df = pd.DataFrame([data])
240
+ df.to_csv(CSV_FILENAME, mode='a', header=False, index=False, sep=";")
241
+
242
+ # --------------------------------------------------------------------------------
243
+ # 4. Return Output
244
+ # --------------------------------------------------------------------------------
245
+
246
+ current_lang_data = load_data(lang_code)
247
+ success_msg = current_lang_data.get("SUBMISSION_MESSAGE_SUCCESS", "✅ Success! File submitted.")
248
+ success_msg = success_msg.format(firstname=firstname)
249
+
250
+ return [success_msg, gr.File(value=CSV_FILENAME, label=current_lang_data.get("DOWNLOAD_LABEL", "Download Data"))]
251
+
252
+
253
+ # --- Language Update Function ---
254
+
255
+ def update_language(lang_code: str) -> List[gr.update]:
256
+ """Updates all components with labels and choices for the selected language."""
257
+
258
+ lang_data = load_data(lang_code)
259
+
260
+ # Determine which themes and frequencies to use based on language
261
+ themes_actu = THEMES_ACTU_PT if lang_code == "PT" else THEMES_ACTU_FR
262
+ freq_choices = FREQ_PT if lang_code == "PT" else FREQ_FR
263
+
264
+ # Create lists of updates for dynamic components
265
+ updates = []
266
+
267
+ # TAB 4 Components that are generated dynamically
268
+
269
+ # 28 Cultural Practice updates (14 Checkboxes + 14 Sliders)
270
+ for label_fr, key in LANGUAGES["FR"]['PRAT_CULT_PRACTICES']:
271
+ # Find the correct label in the target language's data, matching by key
272
+ label_in_target_lang = next((item[0] for item in lang_data['PRAT_CULT_PRACTICES'] if item[1] == key), label_fr)
273
 
274
+ # Checkbox update: label is the cultural practice name
275
+ updates.append(gr.Checkbox.update(label=label_in_target_lang))
276
+ # Slider update: label is cultural practice name
277
+ updates.append(gr.Slider.update(label=label_in_target_lang))
278
+
279
+ # 20 Platform Usage updates (10 Checkboxes + 10 Number fields)
280
+ for label_fr, key in LANGUAGES["FR"]['PLATFORM_ITEMS']:
281
+ # Find the correct label in the target language's data, matching by key
282
+ label_in_target_lang = next((item[0] for item in lang_data['PLATFORM_ITEMS'] if item[1] == key), label_fr)
283
+
284
+ # Checkbox update: label is the platform name
285
+ updates.append(gr.Checkbox.update(label=label_in_target_lang))
286
+ # Number field update: label is platform name
287
+ updates.append(gr.Number.update(label=label_in_target_lang))
288
 
289
+ # 13 Purpose Usage updates (8 Checkboxes + 5 Sliders)
290
+ # Checkboxes (all 8)
291
+ for label_fr, key, has_slider in LANGUAGES["FR"]['PURPOSE_ITEMS']:
292
+ # Find the correct label in the target language's data, matching by key
293
+ label_in_target_lang = next((item[0] for item in lang_data['PURPOSE_ITEMS'] if item[1] == key), label_fr)
294
 
295
+ updates.append(gr.Checkbox.update(label=label_in_target_lang))
 
 
 
 
296
 
297
+ # Sliders (only 5 where has_slider is True)
298
+ for label_fr, key, has_slider in LANGUAGES["FR"]['PURPOSE_ITEMS']:
299
+ if has_slider:
300
+ # Find the correct label in the target language's data, matching by key
301
+ label_in_target_lang = next((item[0] for item in lang_data['PURPOSE_ITEMS'] if item[1] == key), label_fr)
302
+ updates.append(gr.Slider.update(label=label_in_target_lang))
303
+
304
+ # All other static components (order is crucial, must match the interface definition)
305
+ static_updates = [
306
+ # GLOBAL (1)
307
+ gr.Markdown.update(value=f"# {lang_data['TITLE']}"),
308
+ # TABS (6)
309
+ gr.Tab.update(label=lang_data['TAB_1_TITLE']), gr.Tab.update(label=lang_data['TAB_2_TITLE']), gr.Tab.update(label=lang_data['TAB_3_TITLE']),
310
+ gr.Tab.update(label=lang_data['TAB_4_TITLE']), gr.Tab.update(label=lang_data['TAB_5_TITLE']), gr.Tab.update(label=lang_data['TAB_6_TITLE']),
311
 
312
+ # TAB 1 - Contact (7)
313
+ gr.Markdown.update(value=f"## {lang_data['SECTION_CONTACT']}"),
314
+ gr.Radio.update(label=lang_data['APPROACH_LABEL'], choices=[lang_data['CHOICE_OUI'], lang_data['CHOICE_NON']]),
315
+ gr.Dropdown.update(label=lang_data['REFUSAL_REASON_LABEL'], choices=lang_data['CHOICES_REFUSAL']),
316
+ gr.Textbox.update(label=lang_data['REFUSAL_OTHER_LABEL']),
317
+ gr.Radio.update(label=lang_data['CONTACT_LATER_LABEL'], choices=[lang_data['CHOICE_OUI'], lang_data['CHOICE_NON']]),
318
+ gr.Textbox.update(label=lang_data['FIRSTNAME_LABEL'], placeholder=lang_data['FIRSTNAME_PLACEHOLDER']),
319
+ gr.Markdown.update(value=f"### 1. {lang_data['SECTION_1_TITLE']}"),
320
+
321
+ # TAB 1 - Lieux (18)
322
+ gr.Radio.update(label=lang_data['SPACE1_LABEL'], choices=freq_choices),
323
+ gr.Markdown.update(value=lang_data['SPACE2A_TITLE']),
324
+ gr.Textbox.update(label=lang_data['LIEU_1_LABEL'], placeholder=lang_data['LIEU_PLACEHOLDER']),
325
+ gr.Textbox.update(label=lang_data['LIEU_2_LABEL'], placeholder=lang_data['LIEU_PLACEHOLDER']),
326
+ gr.Textbox.update(label=lang_data['LIEU_3_LABEL'], placeholder=lang_data['LIEU_PLACEHOLDER']),
327
+ gr.CheckboxGroup.update(label=lang_data['SPACE2B_LABEL'], choices=lang_data['SPACE2B_CHOICES']),
328
+ gr.Markdown.update(value=lang_data['SPACE3_TITLE']),
329
+ gr.Slider.update(label=lang_data['SPACE3_1_LABEL']),
330
+ gr.Slider.update(label=lang_data['SPACE3_2_LABEL']),
331
+ gr.Slider.update(label=lang_data['SPACE3_3_LABEL']),
332
+ gr.Radio.update(label=lang_data['SPACE4_LABEL'], choices=lang_data['SPACE4_CHOICES']),
333
+ gr.Textbox.update(label=lang_data['SPACE5_LABEL'], placeholder=lang_data['SPACE5_PLACEHOLDER']),
334
+ gr.Radio.update(label=lang_data['SPACE6_LABEL'], choices=[lang_data['CHOICE_OUI'], lang_data['CHOICE_NON']]),
335
+ gr.Textbox.update(label=lang_data['SPACE7_1_LABEL']),
336
+ gr.Textbox.update(label=lang_data['SPACE7_2_LABEL']),
337
+ gr.Textbox.update(label=lang_data['SPACE7_3_LABEL']),
338
+ gr.Textbox.update(label=lang_data['SPACE8_LABEL']),
339
+ gr.Dropdown.update(label=lang_data['SPACE9_LABEL'], choices=lang_data['SPACE9_CHOICES']),
340
+ gr.CheckboxGroup.update(label=lang_data['SPACE10_LABEL'], choices=lang_data['SPACE10_CHOICES']),
341
+
342
+ # TAB 2 - Engagement (5)
343
+ gr.Markdown.update(value=lang_data['SECTION_2_TITLE']),
344
+ gr.CheckboxGroup.update(label=lang_data['INFO1_LABEL'], choices=lang_data['INFO1_CHOICES']),
345
+ gr.CheckboxGroup.update(label=lang_data['ENGAGEMENT_ORGANISATION_LABEL'], choices=lang_data['ENGAGEMENT_ORGANISATION_CHOICES']),
346
+ gr.CheckboxGroup.update(label=lang_data['ENGAGEMENT_DOMAINE_LABEL'], choices=lang_data['ENGAGEMENT_DOMAINE_CHOICES']),
347
+ gr.CheckboxGroup.update(label=lang_data['INFO_ACTIVITES_LABEL'], choices=lang_data['INFO_ACTIVITES_CHOICES']),
348
+ gr.Radio.update(label=lang_data['INFO_RESEAUX_SOCIAUX_LABEL'], choices=lang_data['INFO_RESEAUX_SOCIAUX_CHOICES']),
349
+
350
+ # TAB 3 - News (17)
351
+ gr.Markdown.update(value=lang_data['SECTION_3_TITLE']),
352
+ gr.Radio.update(label=lang_data['INFO_FREQUENCE_ACTU_LABEL'], choices=freq_choices),
353
+ gr.CheckboxGroup.update(label=lang_data['INFO2_LABEL'], choices=themes_actu),
354
+ gr.CheckboxGroup.update(label=lang_data['INFO3_LABEL'], choices=themes_actu),
355
+ gr.Radio.update(label=lang_data['OPPOSITE_FEELING_BASE_LABEL'], choices=lang_data['OPPOSITE_FEELING_CHOICES']),
356
+ gr.Radio.update(label=lang_data['INFO_CONTRADICT_OPINION_LABEL'], choices=lang_data['INFO_CONTRADICT_OPINION_CHOICES']),
357
+ gr.CheckboxGroup.update(label=lang_data['INFO4_LABEL'], choices=themes_actu),
358
+ gr.CheckboxGroup.update(label=lang_data['INFO5_LABEL'], choices=lang_data['INFO5_CHOICES']),
359
+ gr.Markdown.update(value=lang_data['INFO6_TITLE']),
360
+ gr.Slider.update(label=lang_data['INFO6_LOGEMENT_LABEL']),
361
+ gr.Slider.update(label=lang_data['INFO6_POLITIQUE_LABEL']),
362
+ gr.Slider.update(label=lang_data['INFO6_ETUDES_LABEL']),
363
+ gr.Slider.update(label=lang_data['INFO6_CLIMAT_LABEL']),
364
+ gr.Slider.update(label=lang_data['INFO6_SOCIALES_LABEL']),
365
+ gr.Slider.update(label=lang_data['INFO6_SENTIMENTALE_LABEL']),
366
+ gr.Slider.update(label=lang_data['INFO6_SECURITE_LABEL']),
367
+ gr.Slider.update(label=lang_data['INFO6_ESTIME_LABEL']),
368
+ gr.Slider.update(label=lang_data['INFO6_LIBERTE_LABEL']),
369
+
370
+ # TAB 4 - Culturel (5)
371
+ gr.Markdown.update(value=lang_data['SECTION_4_TITLE']),
372
+ gr.Radio.update(label=lang_data['PRAT_CULT1_LABEL'], choices=lang_data['PRAT_CULT1_CHOICES']),
373
+ gr.Markdown.update(value=lang_data['PRAT_PREF_TITLE']),
374
+ gr.Radio.update(label=lang_data['PRAT_NATURE_LABEL'], choices=lang_data['PRAT_NATURE_CHOICES']),
375
+ gr.Markdown.update(value=lang_data['PLATFORM_TITLE']),
376
+ gr.Markdown.update(value=lang_data['PURPOSE_TITLE']),
377
+ gr.Textbox.update(label=lang_data['PURPOSE_AUTRE_DETAIL_LABEL'], placeholder=lang_data['PURPOSE_AUTRE_DETAIL_PLACEHOLDER']),
378
+
379
+ # TAB 5 - Démographie (14)
380
+ gr.Markdown.update(value=lang_data['SECTION_5_TITLE']),
381
+ gr.Radio.update(label=lang_data['DEMO_GENDER_LABEL'], choices=lang_data['DEMO_GENDER_CHOICES']),
382
+ gr.Number.update(label=lang_data['DEMO_AGE_LABEL'], placeholder=lang_data['DEMO_AGE_PLACEHOLDER']),
383
+ gr.Textbox.update(label=lang_data['DEMO_LOCATION_COMMUNE_LABEL'], placeholder=lang_data['DEMO_LOCATION_COMMUNE_PLACEHOLDER']),
384
+ gr.Textbox.update(label=lang_data['DEMO_LOCATION_ARROND_LABEL'], placeholder=lang_data['DEMO_LOCATION_ARROND_PLACEHOLDER']),
385
+ gr.Radio.update(label=lang_data['DEMO_PARENTS_LOCATION_LABEL'], choices=lang_data['DEMO_PARENTS_LOCATION_CHOICES']),
386
+ gr.Dropdown.update(label=lang_data['DEMO_INSCRIPTION_LABEL'], choices=lang_data['DEMO_INSCRIPTION_CHOICES']),
387
+ gr.Dropdown.update(label=lang_data['DEMO_DISCIPLINE_LABEL'], choices=lang_data['DEMO_DISCIPLINE_CHOICES']),
388
+ gr.Radio.update(label=lang_data['DEMO_JOB_LABEL'], choices=lang_data['DEMO_JOB_CHOICES']),
389
+ gr.Dropdown.update(label=lang_data['DEMO_INCOME_LABEL'], choices=lang_data['DEMO_INCOME_CHOICES']),
390
+ gr.Dropdown.update(label=lang_data['DEMO_SOCIALCAPITAL1_PARENT1_LABEL'], choices=lang_data['PCS_CHOICES']),
391
+ gr.Dropdown.update(label=lang_data['DEMO_SOCIALCAPITAL1_PARENT2_LABEL'], choices=lang_data['PCS_CHOICES']),
392
+ gr.Number.update(label=lang_data['DEMO_SOCIALCAPITAL2_LABEL']),
393
+
394
+ # TAB 7 - Open Questions (4)
395
+ gr.Markdown.update(value=lang_data['SECTION_7_TITLE']),
396
+ gr.Textbox.update(label=lang_data['OPEN_NON_INSTITUTIONNEL_LABEL']),
397
+ gr.Textbox.update(label=lang_data['OPEN_ALTERNATIVES_LABEL']),
398
+ gr.Textbox.update(label=lang_data['OPEN_MOTIVATIONS_LABEL']),
399
+
400
+ # SUBMISSION (3)
401
+ gr.Markdown.update(value=lang_data['SUBMISSION_TITLE']),
402
+ gr.Button.update(value=lang_data['SUBMIT_BUTTON_LABEL']),
403
+ gr.Markdown.update(value=lang_data['SUBMISSION_MESSAGE_INIT']),
404
+ # Note: gr.File does not support update() for label/value in this context, but its label is handled by submit_survey's gr.File update.
405
+ ]
406
+
407
+ return static_updates + updates
408
+
409
+
410
+ # =================================================================
411
+ # GRADIO INTERFACE CONSTRUCTION
412
+ # =================================================================
413
+
414
+ # Load initial French data for component creation
415
+ default_lang_data = load_data("FR")
416
+
417
+ # Determine which themes and frequencies to use (initial load)
418
+ themes_actu_fr = THEMES_ACTU_FR
419
+ freq_choices_fr = FREQ_FR
420
+
421
+ # --- Component Lists for Dynamic Sections ---
422
+
423
+ # TAB 4 - Cultural Practices (28 components: 14 Checkboxes + 14 Sliders)
424
+ prat_cult_freq_components = []
425
+ for label, key in default_lang_data['PRAT_CULT_PRACTICES']:
426
+ # Checkbox
427
+ prat_cult_freq_components.append(gr.Checkbox(label=label, value=False, interactive=True, elem_id=f"CB_{key}"))
428
+ # Slider
429
+ prat_cult_freq_components.append(gr.Slider(
430
+ label=label,
431
+ minimum=1, maximum=10, step=1, value=5, interactive=True, elem_id=f"SLIDER_{key}"
432
+ ))
433
+
434
+ # TAB 4 - Platform Usage (20 components: 10 Checkboxes + 10 Number fields)
435
+ platform_components = []
436
+ for label, key in default_lang_data['PLATFORM_ITEMS']:
437
+ # Checkbox
438
+ platform_components.append(gr.Checkbox(label=label, value=False, interactive=True, elem_id=f"PLATFORM_CB_{key}"))
439
+ # Number field
440
+ platform_components.append(gr.Number(
441
+ label=label,
442
+ minimum=0, maximum=24, step=0.5, value=0, interactive=True, elem_id=f"PLATFORM_HRS_{key}"
443
+ ))
444
+
445
+ # TAB 4 - Purpose Usage (13 components: 8 Checkboxes + 5 Sliders)
446
+ purpose_components = []
447
+ slider_indices = [i for i, item in enumerate(default_lang_data['PURPOSE_ITEMS']) if item[2] == True] # Indices in PURPOSE_ITEMS that have a slider
448
+
449
+ # 8 Checkboxes
450
+ for label, key, _ in default_lang_data['PURPOSE_ITEMS']:
451
+ purpose_components.append(gr.Checkbox(label=label, value=False, interactive=True, elem_id=f"PURPOSE_CB_{key}"))
452
+
453
+ # 5 Sliders (only for items marked with True)
454
+ for purpose_index in slider_indices:
455
+ label, key, _ = default_lang_data['PURPOSE_ITEMS'][purpose_index]
456
+ purpose_components.append(gr.Slider(
457
+ label=label,
458
+ minimum=1, maximum=10, step=1, value=5, interactive=True, elem_id=f"PURPOSE_SLIDER_{key}"
459
+ ))
460
+
461
+ # --- The Gradio Interface ---
462
+
463
+ with gr.Blocks(title=default_lang_data['TITLE']) as demo:
464
+
465
+ # --- Language Selector (always visible) ---
466
+ language_selector = gr.Dropdown(
467
+ label=default_lang_data['LANG_LABEL'],
468
+ choices=["FR", "PT"],
469
+ value="FR",
470
+ interactive=True,
471
+ scale=1
472
+ )
473
+
474
+ gr.Markdown(f"# {default_lang_data['TITLE']}", elem_id="main_title")
475
+ gr.Markdown(default_lang_data['INTRO_TEXT'])
476
+
477
+ with gr.Tabs():
478
+
479
+ # =================================================================
480
+ # TAB 1: Contact & Lieux Fréquentés
481
+ # =================================================================
482
+ with gr.Tab(label=default_lang_data['TAB_1_TITLE']):
483
+
484
+ # --- Contact / Refus ---
485
+ markdown_contact = gr.Markdown(f"## {default_lang_data['SECTION_CONTACT']}")
486
+
487
+ with gr.Row():
488
+ enqueteur = gr.Textbox(label=default_lang_data['ENQUETEUR_LABEL'], value="ENQ1", interactive=True)
489
+ approach = gr.Radio(label=default_lang_data['APPROACH_LABEL'], choices=[default_lang_data['CHOICE_OUI'], default_lang_data['CHOICE_NON']], value=default_lang_data['CHOICE_NON'])
490
+
491
+ with gr.Column(visible=True) as refusal_details:
492
+ refusal_reason = gr.Dropdown(label=default_lang_data['REFUSAL_REASON_LABEL'], choices=default_lang_data['CHOICES_REFUSAL'])
493
+ refusal_other = gr.Textbox(label=default_lang_data['REFUSAL_OTHER_LABEL'], placeholder="Autre raison", interactive=True)
494
+ contact_later = gr.Radio(label=default_lang_data['CONTACT_LATER_LABEL'], choices=[default_lang_data['CHOICE_OUI'], default_lang_data['CHOICE_NON']], value=default_lang_data['CHOICE_NON'])
495
+ gr.Markdown(default_lang_data['REFUSAL_INFO'])
496
+
497
+ with gr.Column(visible=False) as contact_details:
498
+ firstname = gr.Textbox(label=default_lang_data['FIRSTNAME_LABEL'], placeholder=default_lang_data['FIRSTNAME_PLACEHOLDER'], interactive=True)
499
+ gr.Markdown(default_lang_data['FIRSTNAME_INFO'])
500
+
501
+ # Logic to show/hide details based on APPROACH
502
+ approach.change(
503
+ fn=lambda x: [gr.Column.update(visible=x == default_lang_data['CHOICE_NON']), gr.Column.update(visible=x == default_lang_data['CHOICE_OUI'])],
504
+ inputs=[approach],
505
+ outputs=[refusal_details, contact_details]
506
+ )
507
+
508
+ # --- Lieux Fréquentés ---
509
+ markdown_section_1 = gr.Markdown(f"### 1. {default_lang_data['SECTION_1_TITLE']}")
510
+ space1 = gr.Radio(label=default_lang_data['SPACE1_LABEL'], choices=freq_choices_fr, value="Jamais")
511
+
512
+ markdown_space2a = gr.Markdown(default_lang_data['SPACE2A_TITLE'])
513
+ with gr.Row():
514
+ lieu_1 = gr.Textbox(label=default_lang_data['LIEU_1_LABEL'], placeholder=default_lang_data['LIEU_PLACEHOLDER'])
515
+ lieu_2 = gr.Textbox(label=default_lang_data['LIEU_2_LABEL'], placeholder=default_lang_data['LIEU_PLACEHOLDER'])
516
+ lieu_3 = gr.Textbox(label=default_lang_data['LIEU_3_LABEL'], placeholder=default_lang_data['LIEU_PLACEHOLDER'])
517
+
518
+ space2b = gr.CheckboxGroup(label=default_lang_data['SPACE2B_LABEL'], choices=default_lang_data['SPACE2B_CHOICES'], visible=True)
519
+
520
+ markdown_space3 = gr.Markdown(default_lang_data['SPACE3_TITLE'])
521
+ space3_1 = gr.Slider(label=default_lang_data['SPACE3_1_LABEL'], minimum=1, maximum=10, step=1, value=5)
522
+ space3_2 = gr.Slider(label=default_lang_data['SPACE3_2_LABEL'], minimum=1, maximum=10, step=1, value=5)
523
+ space3_3 = gr.Slider(label=default_lang_data['SPACE3_3_LABEL'], minimum=1, maximum=10, step=1, value=5)
524
+
525
+ space4 = gr.Radio(label=default_lang_data['SPACE4_LABEL'], choices=default_lang_data['SPACE4_CHOICES'], value="non")
526
+ space5 = gr.Textbox(label=default_lang_data['SPACE5_LABEL'], placeholder=default_lang_data['SPACE5_PLACEHOLDER'], visible=False)
527
+
528
+ space4.change(
529
+ fn=lambda x: gr.Textbox.update(visible=x == "oui"),
530
+ inputs=[space4],
531
+ outputs=[space5]
532
+ )
533
+
534
+ space6 = gr.Radio(label=default_lang_data['SPACE6_LABEL'], choices=[default_lang_data['CHOICE_OUI'], default_lang_data['CHOICE_NON']], value=default_lang_data['CHOICE_NON'])
535
+ gr.Markdown(default_lang_data['SPACE6_INFO'])
536
+
537
+ # Engagement Practices in these spaces
538
+ with gr.Column(visible=False) as engagement_practices:
539
+ with gr.Row():
540
+ space7_1 = gr.Textbox(label=default_lang_data['SPACE7_1_LABEL'])
541
+ space7_2 = gr.Textbox(label=default_lang_data['SPACE7_2_LABEL'])
542
+ space7_3 = gr.Textbox(label=default_lang_data['SPACE7_3_LABEL'])
543
+
544
+ space8 = gr.Textbox(label=default_lang_data['SPACE8_LABEL'])
545
+ space9 = gr.Dropdown(label=default_lang_data['SPACE9_LABEL'], choices=default_lang_data['SPACE9_CHOICES'])
546
+ space10 = gr.CheckboxGroup(label=default_lang_data['SPACE10_LABEL'], choices=default_lang_data['SPACE10_CHOICES'])
547
+
548
+ space6.change(
549
+ fn=lambda x: gr.Column.update(visible=x == default_lang_data['CHOICE_OUI']),
550
+ inputs=[space6],
551
+ outputs=[engagement_practices]
552
+ )
553
+
554
+ # =================================================================
555
+ # TAB 2: Engagement Citoyen
556
+ # =================================================================
557
+ with gr.Tab(label=default_lang_data['TAB_2_TITLE']):
558
+ markdown_section_2 = gr.Markdown(default_lang_data['SECTION_2_TITLE'])
559
+ info1 = gr.CheckboxGroup(label=default_lang_data['INFO1_LABEL'], choices=default_lang_data['INFO1_CHOICES'])
560
+
561
+ # Additional details only visible if engaged
562
+ with gr.Column() as engagement_details:
563
+ engagement_organisation = gr.CheckboxGroup(label=default_lang_data['ENGAGEMENT_ORGANISATION_LABEL'], choices=default_lang_data['ENGAGEMENT_ORGANISATION_CHOICES'])
564
+ engagement_domaine = gr.CheckboxGroup(label=default_lang_data['ENGAGEMENT_DOMAINE_LABEL'], choices=default_lang_data['ENGAGEMENT_DOMAINE_CHOICES'])
565
+
566
+ info_activites = gr.CheckboxGroup(label=default_lang_data['INFO_ACTIVITES_LABEL'], choices=default_lang_data['INFO_ACTIVITES_CHOICES'])
567
+ info_reseaux_sociaux = gr.Radio(label=default_lang_data['INFO_RESEAUX_SOCIAUX_LABEL'], choices=default_lang_data['INFO_RESEAUX_SOCIAUX_CHOICES'])
568
+
569
+ # =================================================================
570
+ # TAB 3: Consommation d'actualités
571
+ # =================================================================
572
+ with gr.Tab(label=default_lang_data['TAB_3_TITLE']):
573
+ markdown_section_3 = gr.Markdown(default_lang_data['SECTION_3_TITLE'])
574
+ info_frequence_actu = gr.Radio(label=default_lang_data['INFO_FREQUENCE_ACTU_LABEL'], choices=freq_choices_fr, value="Parfois")
575
+
576
+ with gr.Row():
577
+ info2 = gr.CheckboxGroup(label=default_lang_data['INFO2_LABEL'], choices=themes_actu_fr, max_selectable=3)
578
+ info3 = gr.CheckboxGroup(label=default_lang_data['INFO3_LABEL'], choices=themes_actu_fr, max_selectable=3)
579
+
580
+ with gr.Row():
581
+ info_opposite_feeling = gr.Radio(label=default_lang_data['OPPOSITE_FEELING_BASE_LABEL'], choices=default_lang_data['OPPOSITE_FEELING_CHOICES'], value="Oui, parfois")
582
+ info_contradict_opinion = gr.Radio(label=default_lang_data['INFO_CONTRADICT_OPINION_LABEL'], choices=default_lang_data['INFO_CONTRADICT_OPINION_CHOICES'], value="Non, jamais")
583
+
584
+ info4 = gr.CheckboxGroup(label=default_lang_data['INFO4_LABEL'], choices=themes_actu_fr)
585
+ info5 = gr.CheckboxGroup(label=default_lang_data['INFO5_LABEL'], choices=default_lang_data['INFO5_CHOICES'])
586
+
587
+ # --- Sentiment de contrôle ---
588
+ markdown_info6 = gr.Markdown(default_lang_data['INFO6_TITLE'])
589
+ with gr.Row():
590
+ info6_logement = gr.Slider(label=default_lang_data['INFO6_LOGEMENT_LABEL'], minimum=1, maximum=10, step=1, value=5)
591
+ info6_politique = gr.Slider(label=default_lang_data['INFO6_POLITIQUE_LABEL'], minimum=1, maximum=10, step=1, value=5)
592
+ with gr.Row():
593
+ info6_etudes = gr.Slider(label=default_lang_data['INFO6_ETUDES_LABEL'], minimum=1, maximum=10, step=1, value=5)
594
+ info6_climat = gr.Slider(label=default_lang_data['INFO6_CLIMAT_LABEL'], minimum=1, maximum=10, step=1, value=5)
595
+ with gr.Row():
596
+ info6_sociales = gr.Slider(label=default_lang_data['INFO6_SOCIALES_LABEL'], minimum=1, maximum=10, step=1, value=5)
597
+ info6_sentimentale = gr.Slider(label=default_lang_data['INFO6_SENTIMENTALE_LABEL'], minimum=1, maximum=10, step=1, value=5)
598
+ with gr.Row():
599
+ info6_securite = gr.Slider(label=default_lang_data['INFO6_SECURITE_LABEL'], minimum=1, maximum=10, step=1, value=5)
600
+ info6_estime = gr.Slider(label=default_lang_data['INFO6_ESTIME_LABEL'], minimum=1, maximum=10, step=1, value=5)
601
+ info6_liberte = gr.Slider(label=default_lang_data['INFO6_LIBERTE_LABEL'], minimum=1, maximum=10, step=1, value=5)
602
+
603
+
604
+ # =================================================================
605
+ # TAB 4: Pratiques culturelles et usages numériques
606
+ # =================================================================
607
+ with gr.Tab(label=default_lang_data['TAB_4_TITLE']):
608
+ markdown_section_4 = gr.Markdown(default_lang_data['SECTION_4_TITLE'])
609
+
610
+ # Importance of Culture
611
+ prat_cult1 = gr.Radio(label=default_lang_data['PRAT_CULT1_LABEL'], choices=default_lang_data['PRAT_CULT1_CHOICES'], value="important")
612
+
613
+ # Cultural Practices Preferences
614
+ markdown_prat_pref = gr.Markdown(default_lang_data['PRAT_PREF_TITLE'])
615
+ gr.Markdown(default_lang_data['PRAT_CULT_FREQ_LABEL'])
616
+ with gr.Column():
617
+ for i in range(0, len(prat_cult_freq_components), 2):
618
+ with gr.Row():
619
+ prat_cult_freq_components[i] # Checkbox
620
+ prat_cult_freq_components[i+1] # Slider
621
+
622
+ # Nature Frequency
623
+ prat_nature = gr.Radio(label=default_lang_data['PRAT_NATURE_LABEL'], choices=default_lang_data['PRAT_NATURE_CHOICES'], value="Parfois")
624
+
625
+ # Platform Usage
626
+ markdown_platform_title = gr.Markdown(default_lang_data['PLATFORM_TITLE'])
627
+ gr.Markdown(default_lang_data['PLATFORM_HOURS_LABEL'])
628
+ with gr.Column():
629
+ for i in range(0, len(platform_components), 2):
630
+ with gr.Row():
631
+ platform_components[i] # Checkbox
632
+ platform_components[i+1] # Number field
633
+
634
+ # Purpose Usage
635
+ markdown_purpose_title = gr.Markdown(default_lang_data['PURPOSE_TITLE'])
636
+ gr.Markdown(default_lang_data['PURPOSE_FREQ_LABEL'])
637
+
638
+ # Checkboxes (8) and Sliders (5) for Purpose
639
+ with gr.Column():
640
+ slider_counter = 0
641
+ for i in range(len(default_lang_data['PURPOSE_ITEMS'])):
642
+ label, key, has_slider = default_lang_data['PURPOSE_ITEMS'][i]
643
+
644
+ if has_slider:
645
+ with gr.Row():
646
+ purpose_components[i] # Checkbox
647
+ purpose_components[8 + slider_counter] # Slider
648
+ slider_counter += 1
649
+ else:
650
+ purpose_components[i] # Checkbox only
651
+
652
+ purpose_autre_detail = gr.Textbox(label=default_lang_data['PURPOSE_AUTRE_DETAIL_LABEL'], placeholder=default_lang_data['PURPOSE_AUTRE_DETAIL_PLACEHOLDER'], interactive=True)
653
+
654
+ # =================================================================
655
+ # TAB 5: Profil Démographique et Capital Social
656
+ # =================================================================
657
+ with gr.Tab(label=default_lang_data['TAB_5_TITLE']):
658
+ markdown_section_5 = gr.Markdown(default_lang_data['SECTION_5_TITLE'])
659
+
660
+ # Demographics
661
+ demo_gender = gr.Radio(label=default_lang_data['DEMO_GENDER_LABEL'], choices=default_lang_data['DEMO_GENDER_CHOICES'], value=default_lang_data['DEMO_GENDER_CHOICES'][0])
662
+ demo_age = gr.Number(label=default_lang_data['DEMO_AGE_LABEL'], placeholder=default_lang_data['DEMO_AGE_PLACEHOLDER'], minimum=15, maximum=99, step=1, value=20)
663
+
664
+ with gr.Row():
665
+ demo_location_commune = gr.Textbox(label=default_lang_data['DEMO_LOCATION_COMMUNE_LABEL'], placeholder=default_lang_data['DEMO_LOCATION_COMMUNE_PLACEHOLDER'])
666
+ demo_location_arrond = gr.Textbox(label=default_lang_data['DEMO_LOCATION_ARROND_LABEL'], placeholder=default_lang_data['DEMO_LOCATION_ARROND_PLACEHOLDER'])
667
+
668
+ demo_parents_location = gr.Radio(label=default_lang_data['DEMO_PARENTS_LOCATION_LABEL'], choices=default_lang_data['DEMO_PARENTS_LOCATION_CHOICES'], value=default_lang_data['DEMO_PARENTS_LOCATION_CHOICES'][0])
669
+
670
+ # Education/Job
671
+ demo_inscription = gr.Dropdown(label=default_lang_data['DEMO_INSCRIPTION_LABEL'], choices=default_lang_data['DEMO_INSCRIPTION_CHOICES'], value=default_lang_data['DEMO_INSCRIPTION_CHOICES'][0])
672
+ demo_discipline = gr.Dropdown(label=default_lang_data['DEMO_DISCIPLINE_LABEL'], choices=default_lang_data['DEMO_DISCIPLINE_CHOICES'], value=default_lang_data['DEMO_DISCIPLINE_CHOICES'][0])
673
+ demo_job = gr.Radio(label=default_lang_data['DEMO_JOB_LABEL'], choices=default_lang_data['DEMO_JOB_CHOICES'], value=default_lang_data['DEMO_JOB_CHOICES'][2])
674
+ demo_income = gr.Dropdown(label=default_lang_data['DEMO_INCOME_LABEL'], choices=default_lang_data['DEMO_INCOME_CHOICES'], value=default_lang_data['DEMO_INCOME_CHOICES'][0])
675
+
676
+ # Social Capital
677
+ demo_socialcapital1_parent1 = gr.Dropdown(label=default_lang_data['DEMO_SOCIALCAPITAL1_PARENT1_LABEL'], choices=default_lang_data['PCS_CHOICES'])
678
+ demo_socialcapital1_parent2 = gr.Dropdown(label=default_lang_data['DEMO_SOCIALCAPITAL1_PARENT2_LABEL'], choices=default_lang_data['PCS_CHOICES'])
679
+ demo_socialcapital2 = gr.Number(label=default_lang_data['DEMO_SOCIALCAPITAL2_LABEL'], minimum=0, maximum=100, step=1, value=5)
680
+
681
+ # --- Open Questions (Moved to Tab 5 for simplicity and correct component count) ---
682
+ markdown_section_7 = gr.Markdown(default_lang_data['SECTION_7_TITLE'])
683
+ open_non_institutionnel = gr.Textbox(label=default_lang_data['OPEN_NON_INSTITUTIONNEL_LABEL'], lines=3)
684
+ open_alternatives = gr.Textbox(label=default_lang_data['OPEN_ALTERNATIVES_LABEL'], lines=3)
685
+ open_motivations = gr.Textbox(label=default_lang_data['OPEN_MOTIVATIONS_LABEL'], lines=3)
686
+
687
+ # =================================================================
688
+ # TAB 6: Soumission et Téléchargement
689
+ # =================================================================
690
+ with gr.Tab(label=default_lang_data['TAB_6_TITLE']):
691
+ markdown_submission_title = gr.Markdown(default_lang_data['SUBMISSION_TITLE'])
692
+ submit_button = gr.Button(default_lang_data['SUBMIT_BUTTON_LABEL'])
693
+ submission_message = gr.Markdown(default_lang_data['SUBMISSION_MESSAGE_INIT'])
694
+ data_download = gr.File(label=default_lang_data['DOWNLOAD_LABEL'], value=CSV_FILENAME, interactive=False)
695
+
696
+ # --- Collect all input components in the correct order for submission ---
697
+ input_components = [
698
+ language_selector,
699
+ # TAB 1 - Contact (6)
700
+ enqueteur, approach, refusal_reason, refusal_other, contact_later, firstname,
701
+ # TAB 1 - Lieux (17)
702
+ space1, lieu_1, lieu_2, lieu_3, space2b, space3_1, space3_2, space3_3, space4, space5, space6, space7_1, space7_2, space7_3, space8, space9, space10,
703
+ # TAB 2 - Engagement (5)
704
+ info1, engagement_organisation, engagement_domaine, info_activites, info_reseaux_sociaux,
705
+ # TAB 3 - News (12)
706
+ info_frequence_actu, info2, info3, info_opposite_feeling, info_contradict_opinion, info4, info5,
707
+ info6_logement, info6_politique, info6_etudes, info6_climat, info6_sociales, info6_sentimentale, info6_securite, info6_estime, info6_liberte,
708
+ # TAB 4 - Culturel (58)
709
+ prat_cult1,
710
+ *prat_cult_freq_components, # 28 items
711
+ prat_nature,
712
+ *platform_components, # 20 items
713
+ purpose_autre_detail,
714
+ *purpose_components, # 13 items
715
+ # TAB 5 - Démographie (15)
716
+ demo_gender, demo_age, demo_location_commune, demo_location_arrond, demo_parents_location, demo_inscription, demo_discipline, demo_job, demo_income,
717
+ demo_socialcapital1_parent1, demo_socialcapital1_parent2, demo_socialcapital2,
718
+ open_non_institutionnel, open_alternatives, open_motivations,
719
+ # Total: 1 (lang_code) + 119 (data inputs) = 120
720
+ ]
721
+
722
+ # Connect the language selector to the update function
723
+ # NOTE: The order of outputs here must exactly match the order of updates in update_language
724
+ language_selector.change(
725
+ fn=update_language,
726
+ inputs=[language_selector],
727
+ outputs=[
728
+ # Static components (82)
729
+ gr.Markdown.update(elem_id="main_title"), # 1
730
+ gr.Tab.update(label=default_lang_data['TAB_1_TITLE']), gr.Tab.update(label=default_lang_data['TAB_2_TITLE']), gr.Tab.update(label=default_lang_data['TAB_3_TITLE']),
731
+ gr.Tab.update(label=default_lang_data['TAB_4_TITLE']), gr.Tab.update(label=default_lang_data['TAB_5_TITLE']), gr.Tab.update(label=default_lang_data['TAB_6_TITLE']), # 6
732
+ markdown_contact, # 7
733
+ approach, refusal_reason, refusal_other, contact_later, firstname, markdown_section_1, # 13
734
+ space1, markdown_space2a, lieu_1, lieu_2, lieu_3, space2b, markdown_space3, space3_1, space3_2, space3_3, space4, space5, space6, space7_1, space7_2, space7_3, space8, space9, space10, # 31
735
+ markdown_section_2, info1, engagement_organisation, engagement_domaine, info_activites, info_reseaux_sociaux, # 37
736
+ markdown_section_3, info_frequence_actu, info2, info3, info_opposite_feeling, info_contradict_opinion, info4, info5, markdown_info6, info6_logement, info6_politique, info6_etudes, info6_climat, info6_sociales, info6_sentimentale, info6_securite, info6_estime, info6_liberte, # 55
737
+ markdown_section_4, prat_cult1, markdown_prat_pref, prat_nature, markdown_platform_title, markdown_purpose_title, purpose_autre_detail, # 62
738
+ markdown_section_5, 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, # 75
739
+ markdown_section_7, open_non_institutionnel, open_alternatives, open_motivations, # 79
740
+ markdown_submission_title, submit_button, submission_message, # 82
741
+
742
+ # Dynamic components (28 + 20 + 13 = 61 components)
743
+ *prat_cult_freq_components, # 28
744
+ *platform_components, # 20
745
+ *purpose_components # 13
746
+ ]
747
+ )
748
+
749
+ # Connect the submit button to the submission function
750
+ submit_button.click(
751
+ fn=submit_survey,
752
+ inputs=input_components,
753
+ outputs=[submission_message, data_download],
754
  )
755
 
756
+ if __name__ == "__main__":
757
+ demo.launch()