jackmedda commited on
Commit
3c184b3
Β·
1 Parent(s): bc88605

Demo first draft

Browse files
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__
2
+ database/
__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import os
2
+
3
+ RESOURCE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources")
app.py ADDED
@@ -0,0 +1,822 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import os
4
+ import random
5
+ from functools import partial
6
+
7
+ import gradio as gr
8
+
9
+ from app import RESOURCE_DIR
10
+ from app.utils.poi_search_utils import (
11
+ clear_all_selections,
12
+ get_selection_summary,
13
+ pois_list,
14
+ search_pois,
15
+ toggle_poi_selection,
16
+ )
17
+ from app.utils.recommender_poi_utils import (
18
+ add_poi_to_completed,
19
+ back_to_recommendations,
20
+ get_recommendations_ui,
21
+ show_recommendation_list,
22
+ view_poi_details,
23
+ )
24
+ from app.utils.survey_utils import (
25
+ feedback_message,
26
+ handle_all_answered_responses,
27
+ handle_next_navigation,
28
+ handle_prev_navigation,
29
+ handle_submit_survey,
30
+ submit_exp_feedback,
31
+ submit_rec_feedback,
32
+ update_advanced_responses,
33
+ )
34
+ from app.utils.user_profile_utils import (
35
+ login_user,
36
+ mark_profile_complete,
37
+ update_user_selected_pois,
38
+ update_user_sensory_aversions
39
+ )
40
+
41
+ with open(os.path.join(RESOURCE_DIR, "aversion_config_questions.json"), "r") as f:
42
+ AVERSIONS_QUESTIONS = json.load(f)
43
+ with open(os.path.join(RESOURCE_DIR, "aversions.json"), "r") as f:
44
+ AVERSIONS = json.load(f)
45
+ AVERSIONS = {v["Name"]: v for v in AVERSIONS}
46
+
47
+
48
+ feedback_message_delay = 1500
49
+ js_feedback_message_delay = f"""() => {{
50
+ return new Promise(resolve => {{
51
+ setTimeout(() => {{
52
+ resolve();
53
+ }}, {feedback_message_delay});
54
+ }});
55
+ }}"""
56
+
57
+
58
+ def create_main_app(self):
59
+ with gr.Blocks(
60
+ title="Sistema di Raccomandazione di Punti di Interesse con Personalizzazione Sensoriale",
61
+ theme=gr.themes.Soft(),
62
+ css="""
63
+ footer{display:none !important}
64
+ .navigation-container {
65
+ background: #f8f9fa;
66
+ padding: 15px;
67
+ border-radius: 10px;
68
+ margin-bottom: 20px;
69
+ border-left: 4px solid #007bff;
70
+ }
71
+ .page-container {
72
+ min-height: 500px;
73
+ padding: 20px;
74
+ }
75
+ .poi-card {
76
+ transition: all 0.3s ease;
77
+ }
78
+ .poi-card:hover {
79
+ transform: scale(1.02);
80
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
81
+ }
82
+ .search-container {
83
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
84
+ padding: 30px;
85
+ border-radius: 15px;
86
+ color: white;
87
+ margin-bottom: 30px;
88
+ }
89
+ .continue-button {
90
+ background-color: #28a745 !important;
91
+ border-color: #28a745 !important;
92
+ font-size: 16px !important;
93
+ padding: 12px 24px !important;
94
+ }
95
+ .continue-button:hover {
96
+ background-color: #218838 !important;
97
+ border-color: #1e7e34 !important;
98
+ }
99
+ .clear-button {
100
+ background-color: #dc3545 !important;
101
+ border-color: #dc3545 !important;
102
+ }
103
+ .clear-button:hover {
104
+ background-color: #c82333 !important;
105
+ border-color: #bd2130 !important;
106
+ }
107
+ .sensory-config-main-container {
108
+ max-width: 900px;
109
+ margin: 0 auto;
110
+ padding: 20px;
111
+ }
112
+ .header-section {
113
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
114
+ color: white;
115
+ padding: 20px;
116
+ border-radius: 12px;
117
+ margin-bottom: 25px;
118
+ text-align: center;
119
+ }
120
+ .radio-aversion-grid {
121
+ display: grid;
122
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
123
+ gap: 20px;
124
+ margin-bottom: 25px;
125
+ }
126
+ .radio-aversion-card {
127
+ background: #f8f9fa;
128
+ border: 1px solid #e9ecef;
129
+ border-radius: 12px;
130
+ padding: 15px;
131
+ }
132
+ .continue-to-recommendations-button {
133
+ background-color: #28a745 !important;
134
+ border-color: #28a745 !important;
135
+ font-size: 16px !important;
136
+ padding: 12px 30px !important;
137
+ width: 100% !important;
138
+ }
139
+ .continue-to-recommendations-button:hover {
140
+ background-color: #218838 !important;
141
+ border-color: #1e7e34 !important;
142
+ }
143
+ .feedback-indicator {
144
+ margin-top: 5px;
145
+ }
146
+ .back-button-custom {
147
+ background-color: #32568f !important;
148
+ border-color: #32568f !important;
149
+ color: white !important;
150
+ }
151
+ .back-button-custom:hover {
152
+ background-color: #132137 !important;
153
+ border-color: #132137 !important;
154
+ }
155
+ .login-button {
156
+ background-color: #007bff !important;
157
+ border-color: #007bff !important;
158
+ }
159
+ .login-button:hover {
160
+ background-color: #0056b3 !important;
161
+ border-color: #004085 !important;
162
+ }
163
+ #poi-search-input {
164
+ color: black;
165
+ }
166
+ """,
167
+ ) as app:
168
+ # Stati globali dell'applicazione
169
+ app_user_id = gr.State("")
170
+ current_page = gr.State("login") # "login", "poi_selection", "sensory_config", "recommendations"
171
+ selected_poi_names = gr.State(set())
172
+ recommendations_state = gr.State([])
173
+ selected_poi_index = gr.State(-1)
174
+ completed_survey_pois = gr.State(set())
175
+ current_poi_info = gr.State({}) # Store current POI info for survey
176
+ user_status = gr.State("") # "new_user", "existing_incomplete", "existing_complete"
177
+
178
+ # Container per le diverse pagine
179
+ with gr.Column(elem_classes="page-container"):
180
+ # Pagina 1: Login/Profile
181
+ with gr.Column(visible=True) as login_page:
182
+ gr.Markdown("# πŸ‘€ Login / Registrazione")
183
+
184
+ # Interfaccia di login semplificata
185
+ with gr.Row():
186
+ user_id_input = gr.Textbox(
187
+ placeholder="es: mario_rossi, alice_123, user001...",
188
+ label="πŸ†” Inserisci il tuo ID Utente",
189
+ info="Scegli un ID univoco che userai per accedere al sistema",
190
+ elem_id="user-id-input",
191
+ scale=2
192
+ )
193
+ login_button = gr.Button(
194
+ "πŸš€ Accedi / Registrati",
195
+ variant="primary",
196
+ elem_classes="action-button login-button",
197
+ )
198
+
199
+ login_status_message = gr.Markdown("", visible=False)
200
+
201
+ with gr.Row(visible=False) as login_actions:
202
+ start_setup_btn = gr.Button(
203
+ "πŸ“ Configura Profilo", variant="secondary"
204
+ )
205
+ go_to_recommendations_btn = gr.Button(
206
+ "🎯 Vai alle Raccomandazioni", variant="primary"
207
+ )
208
+
209
+ with gr.Column(visible=False) as poi_selection_page:
210
+ with gr.Column(elem_classes="search-container"):
211
+ gr.Markdown("# πŸ” Cerca e seleziona i **Luoghi di Interesse** che preferisci cliccando sulla card corrispondente")
212
+ gr.Markdown(
213
+ "Esplora i luoghi disponibili e seleziona quelli che preferisci maggiormente. La ricerca Γ¨ dinamica: inizia a digitare per filtrare i risultati."
214
+ )
215
+
216
+ # Search input (dynamic, no button needed)
217
+ search_input = gr.Textbox(
218
+ placeholder="Cerca per nome, descrizione o localitΓ ...",
219
+ label="",
220
+ show_label=False,
221
+ container=False,
222
+ elem_id="poi-search-input",
223
+ )
224
+
225
+ with gr.Column():
226
+ # Selection summary
227
+ selection_summary = gr.Markdown(get_selection_summary(set()))
228
+
229
+ # Navigation buttons
230
+ with gr.Row():
231
+ clear_button = gr.Button(
232
+ "πŸ—‘οΈ Cancella Selezioni",
233
+ variant="secondary",
234
+ elem_classes="clear-button",
235
+ scale=1,
236
+ )
237
+ continue_to_sensory_btn = gr.Button(
238
+ "Continua alla Configurazione Sensoriale ➑️",
239
+ variant="primary",
240
+ elem_classes="continue_to_sensory_btn",
241
+ scale=3,
242
+ )
243
+
244
+ # POI display area
245
+ poi_display = gr.HTML(
246
+ value=search_pois("", set()), elem_id="poi-display-area"
247
+ )
248
+
249
+ # Create hidden buttons for each POI
250
+ poi_buttons = {}
251
+ for poi in pois_list:
252
+ poi_name = poi["name"]
253
+ safe_name = (
254
+ poi_name.replace(" ", "-").replace("'", "").replace('"', "")
255
+ )
256
+ button = gr.Button(
257
+ poi_name, visible=False, elem_id=f"poi-btn-{safe_name}"
258
+ )
259
+ poi_buttons[poi_name] = button
260
+
261
+ with gr.Column(
262
+ visible=False, elem_classes="sensory-config-main-container"
263
+ ) as sensory_config_page:
264
+ # Header compatto
265
+ gr.HTML("""
266
+ <div class="header-section">
267
+ <h2 style="margin: 0 0 10px 0;">βš™οΈ Configura le tue Preferenze Sensoriali</h2>
268
+ <p style="margin: 0; opacity: 0.9;">Indica il tuo livello di sensibilitΓ  per ogni caratteristica</p>
269
+ </div>
270
+ """)
271
+
272
+ aversion_radios = []
273
+ with gr.Column():
274
+ for i, aversion_question in enumerate(AVERSIONS_QUESTIONS):
275
+ question = aversion_question.get("question", "")
276
+ aversion_name = aversion_question.get("aversion_name", "")
277
+ aversion = AVERSIONS[aversion_name]
278
+
279
+ with gr.Group(elem_classes="radio-aversion-card"):
280
+ with gr.Column():
281
+ # Titolo compatto con icona
282
+ gr.HTML(f"""
283
+ <div style="display: flex; align-items: center; margin-bottom: 8px;">
284
+ <span style="font-size: 20px; margin-right: 10px;">{aversion['Icon']} {aversion_name}</span>
285
+ <div>
286
+ <div style="font-size: 14px; color: #D7D7D7; margin-top: 2px;">{question}</div>
287
+ </div>
288
+ </div>
289
+ """)
290
+
291
+ # Quanto ti dΓ  fastidio questo fenomeno sensoriale?
292
+ av_radio = gr.Radio(
293
+ choices=[("Nessuno", 1), ("Poco", 2), ("Moderato", 3), ("Molto", 4), ("Estremo", 5)],
294
+ value=3,
295
+ show_label=False,
296
+ interactive=True,
297
+ elem_id=f"radio-{aversion_name.lower().replace(' ', '-')}",
298
+ )
299
+
300
+ aversion_radios.append(av_radio)
301
+
302
+ # Pulsante continua
303
+ with gr.Row():
304
+ continue_to_recommendations_button = gr.Button(
305
+ "Continua alle Raccomandazioni β†’",
306
+ variant="primary",
307
+ elem_classes="continue-to-recommendations-button",
308
+ size="lg",
309
+ )
310
+
311
+ with gr.Column(visible=False) as recommendations_page:
312
+ with gr.Column(visible=True) as recommendations_list_page:
313
+ gr.Markdown(
314
+ "# Sistema di Raccomandazione di Punti di Interesse"
315
+ )
316
+ gr.Markdown("""
317
+ Questo sistema raccomanda punti di interesse personalizzati per le preferenze sensoriali degli utenti.
318
+ Ogni raccomandazione include informazioni sensoriali dettagliate e spiegazioni personalizzate.
319
+ """)
320
+
321
+ recommendation_list = gr.HTML(label="Raccomandazioni")
322
+
323
+ with gr.Row(visible=False):
324
+ select_buttons = [
325
+ gr.Button(f"Select_{i}", elem_id=f"select-button-{i}")
326
+ for i in range(args.top_k)
327
+ ]
328
+
329
+ with gr.Column(visible=False) as details_page_block:
330
+ gr.Markdown("# Dettagli POI")
331
+ poi_details = gr.HTML(label="Dettagli Punto di Interesse")
332
+
333
+ with gr.Column(visible=False) as recommendation_feedback_block:
334
+ gr.Markdown("## Ti Γ¨ piaciuto questo luogo che ti abbiamo suggerito?")
335
+
336
+ with gr.Row(equal_height=True):
337
+ with gr.Column(scale=1):
338
+ rec_like_btn = gr.Button("πŸ‘ SΓ¬", size="lg")
339
+ with gr.Column(scale=1):
340
+ rec_dislike_btn = gr.Button("πŸ‘Ž No", size="lg")
341
+
342
+ recommendation_feedback_result = gr.State(False)
343
+ recommendation_feedback_submitted = gr.State(False)
344
+ recommendation_feedback_message = gr.HTML(visible=False)
345
+
346
+ # SIMPLE SURVEY
347
+ with gr.Column(visible=False) as simple_survey_block:
348
+ gr.Markdown("## Ti Γ¨ stata utile questa spiegazione?")
349
+
350
+ with gr.Row(equal_height=True):
351
+ with gr.Column(scale=1):
352
+ exp_like_btn = gr.Button("πŸ‘ SΓ¬", size="lg")
353
+ with gr.Column(scale=1):
354
+ exp_dislike_btn = gr.Button("πŸ‘Ž No", size="lg")
355
+
356
+ simple_survey_result = gr.State(False)
357
+ simple_feedback_submitted = gr.State(False)
358
+ simple_feedback_message = gr.HTML(visible=False)
359
+
360
+ # ADVANCED SURVEY
361
+ with gr.Column(visible=False) as advanced_survey_block:
362
+ gr.Markdown("## Valuta questa spiegazione della raccomandazione")
363
+
364
+ # Stato per tenere traccia del gruppo corrente
365
+ advanced_current_group_idx = gr.State(0)
366
+
367
+ # Carica i dati del questionario
368
+ from app.utils.survey_utils import LIKERT_OPTIONS, SURVEY_STATEMENTS
369
+
370
+ # Contenitori per i gruppi di affermazioni
371
+ advanced_containers = []
372
+ advanced_responses = [[], [], []]
373
+ statements = SURVEY_STATEMENTS.copy()
374
+ random.shuffle(statements)
375
+
376
+ # Prendiamo il massimo di affermazioni per gruppo
377
+ max_statements_per_group = 2 # Per i primi due gruppi
378
+ for i in range(3):
379
+ with gr.Column(visible=(i == 0)) as container:
380
+ gr.Markdown(f"### Parte {i + 1} di 3")
381
+
382
+ group_responses = []
383
+ # Creiamo spazio per il massimo numero di affermazioni possibili
384
+ for j in range(3 if i == 2 else max_statements_per_group):
385
+ response = gr.Radio(
386
+ choices=LIKERT_OPTIONS,
387
+ label=statements[i * max_statements_per_group + j],
388
+ type="value",
389
+ value=None,
390
+ )
391
+ group_responses.append(response)
392
+
393
+ advanced_responses[i] = group_responses
394
+ advanced_containers.append(container)
395
+
396
+ # Navigazione tra i gruppi
397
+ with gr.Row():
398
+ advanced_prev_btn = gr.Button("← Indietro", interactive=False)
399
+ advanced_next_btn = gr.Button("Avanti β†’")
400
+ advanced_submit_btn = gr.Button(
401
+ "Conferma Risposte", visible=False
402
+ )
403
+
404
+ advanced_feedback_submitted = gr.State(False)
405
+ advanced_feedback_message = gr.HTML(visible=False)
406
+
407
+ with gr.Column(visible=False) as back_to_recommendations_block:
408
+ gr.HTML(
409
+ '<hr style="margin: 80px 0 2px 0; border-top: 1px solid #ddd;">'
410
+ )
411
+ back_to_recommendations_button = gr.Button(
412
+ "← Torna alla lista di raccomandazioni",
413
+ variant="secondary",
414
+ elem_classes="back-button-custom",
415
+ )
416
+
417
+ def handle_login(user_id_value):
418
+ """Gestisce il processo di login"""
419
+ success, message, login_status = login_user(user_id_value)
420
+
421
+ if not success:
422
+ return (
423
+ gr.update(value=message, visible=True), # status_message
424
+ gr.update(visible=False), # login_actions
425
+ gr.update(visible=False), # setup_profile_button
426
+ gr.update(visible=False), # proceed_to_recommendations_button
427
+ user_id_value, # current_user_id
428
+ login_status, # login_status
429
+ )
430
+
431
+ # Determina quale pulsante mostrare
432
+ show_setup = login_status in ["new_user", "existing_incomplete"]
433
+ show_proceed = login_status == "existing_complete"
434
+
435
+ return (
436
+ gr.update(value=message, visible=True), # status_message
437
+ gr.update(visible=True), # login_actions
438
+ gr.update(visible=show_setup), # setup_profile_button
439
+ gr.update(visible=show_proceed), # proceed_to_recommendations_button
440
+ user_id_value, # current_user_id
441
+ login_status, # login_status
442
+ )
443
+
444
+ def handle_poi_click(poi_name, selected_names, query):
445
+ """Handle POI click - toggle selection"""
446
+ print(f"πŸ”₯ POI Button clicked: {poi_name}")
447
+ return toggle_poi_selection(poi_name, selected_names, query)
448
+
449
+ def handle_proceed_to_sensory_config(user_id, selected_names):
450
+ """Handle proceeding to next step"""
451
+ if not selected_names:
452
+ gr.Warning("⚠️ Seleziona la card di almeno un Punto di Interesse per continuare!")
453
+ return
454
+
455
+ update_user_selected_pois(user_id, selected_names)
456
+ gr.Info(f"βœ… Procedendo con {len(selected_names)} Punti di Interesse selezionati")
457
+ return navigate_to_sensory_config()
458
+
459
+ def navigate_to_poi_selection():
460
+ """Naviga alla selezione POI"""
461
+ return (
462
+ gr.update(visible=False), # login_page
463
+ gr.update(visible=True), # poi_selection_page
464
+ gr.update(visible=False), # sensory_config_page
465
+ gr.update(visible=False), # recommendations_page
466
+ "poi_selection", # current_page
467
+ )
468
+
469
+ def navigate_to_sensory_config():
470
+ """Naviga alla configurazione sensoriale"""
471
+ return (
472
+ gr.update(visible=False), # login_page
473
+ gr.update(visible=False), # poi_selection_page
474
+ gr.update(visible=True), # sensory_config_page
475
+ gr.update(visible=False), # recommendations_page
476
+ "sensory_config", # current_page
477
+ )
478
+
479
+ def navigate_to_recommendations():
480
+ """Naviga alle raccomandazioni"""
481
+ return (
482
+ gr.update(visible=False), # login_page
483
+ gr.update(visible=False), # poi_selection_page
484
+ gr.update(visible=False), # sensory_config_page
485
+ gr.update(visible=True), # recommendations_page
486
+ "recommendations", # current_page
487
+ )
488
+
489
+ def complete_profile_setup(user_id):
490
+ """Completa la configurazione del profilo"""
491
+ if user_id:
492
+ mark_profile_complete(user_id)
493
+ gr.Info(f"βœ… Profilo {user_id} completato con successo!")
494
+ return navigate_to_recommendations()
495
+
496
+ login_button.click(
497
+ fn=handle_login,
498
+ inputs=[user_id_input],
499
+ outputs=[
500
+ login_status_message,
501
+ login_actions,
502
+ start_setup_btn,
503
+ go_to_recommendations_btn,
504
+ app_user_id,
505
+ user_status,
506
+ ],
507
+ )
508
+
509
+ start_setup_btn.click(
510
+ fn=navigate_to_poi_selection,
511
+ outputs=[
512
+ login_page,
513
+ poi_selection_page,
514
+ sensory_config_page,
515
+ recommendations_page,
516
+ current_page,
517
+ ],
518
+ )
519
+
520
+ go_to_recommendations_btn.click(
521
+ fn=complete_profile_setup,
522
+ inputs=[app_user_id],
523
+ outputs=[
524
+ login_page,
525
+ poi_selection_page,
526
+ sensory_config_page,
527
+ recommendations_page,
528
+ current_page,
529
+ ],
530
+ ).then(
531
+ fn=partial(get_recommendations_ui, k=args.top_k),
532
+ inputs=[app_user_id],
533
+ outputs=[recommendations_state, app_user_id, recommendation_list],
534
+ )
535
+
536
+ # Dynamic search - triggers on every keystroke
537
+ search_input.change(
538
+ fn=search_pois,
539
+ inputs=[search_input, selected_poi_names],
540
+ outputs=[poi_display],
541
+ )
542
+
543
+ # Set up click handlers for each POI button
544
+ for poi_name, button in poi_buttons.items():
545
+ button.click(
546
+ fn=partial(handle_poi_click, poi_name),
547
+ inputs=[selected_poi_names, search_input],
548
+ outputs=[selected_poi_names, poi_display, selection_summary],
549
+ )
550
+
551
+ # Clear all selections
552
+ clear_button.click(
553
+ fn=clear_all_selections,
554
+ inputs=[search_input],
555
+ outputs=[selected_poi_names, poi_display, selection_summary],
556
+ )
557
+
558
+ continue_to_sensory_btn.click(
559
+ fn=handle_proceed_to_sensory_config,
560
+ inputs=[app_user_id, selected_poi_names],
561
+ outputs=[
562
+ login_page,
563
+ poi_selection_page,
564
+ sensory_config_page,
565
+ recommendations_page,
566
+ current_page,
567
+ ],
568
+ )
569
+
570
+ # Salva preferenze
571
+ continue_to_recommendations_button.click(
572
+ fn=update_user_sensory_aversions,
573
+ inputs=[app_user_id, *aversion_radios],
574
+ ).then(
575
+ fn=complete_profile_setup,
576
+ inputs=[app_user_id],
577
+ outputs=[
578
+ login_page,
579
+ poi_selection_page,
580
+ sensory_config_page,
581
+ recommendations_page,
582
+ current_page,
583
+ ],
584
+ ).then(
585
+ fn=partial(get_recommendations_ui, k=args.top_k),
586
+ inputs=[app_user_id],
587
+ outputs=[recommendations_state, app_user_id, recommendation_list],
588
+ )
589
+
590
+ for i, btn in enumerate(select_buttons):
591
+ btn.click(
592
+ fn=lambda current_idx=i: current_idx,
593
+ inputs=[],
594
+ outputs=[selected_poi_index],
595
+ ).then(
596
+ fn=view_poi_details,
597
+ inputs=[recommendations_state, app_user_id, selected_poi_index, completed_survey_pois],
598
+ outputs=[
599
+ recommendations_list_page,
600
+ details_page_block,
601
+ poi_details,
602
+ recommendation_feedback_block,
603
+ back_to_recommendations_block,
604
+ current_poi_info,
605
+ ],
606
+ )
607
+
608
+ surveys_objects = [
609
+ recommendation_feedback_result,
610
+ recommendation_feedback_submitted,
611
+ recommendation_feedback_message,
612
+ simple_survey_result,
613
+ simple_feedback_submitted,
614
+ simple_feedback_message,
615
+ advanced_current_group_idx,
616
+ advanced_feedback_submitted,
617
+ advanced_feedback_message,
618
+ ]
619
+
620
+ back_to_recommendations_button.click(
621
+ fn=lambda: back_to_recommendations(advanced_responses, advanced_containers),
622
+ inputs=[],
623
+ outputs=[
624
+ recommendations_list_page,
625
+ details_page_block,
626
+ poi_details,
627
+ recommendation_feedback_block,
628
+ simple_survey_block,
629
+ advanced_survey_block,
630
+ back_to_recommendations_block,
631
+ *surveys_objects,
632
+ *[radio for group in advanced_responses for radio in group],
633
+ *advanced_containers,
634
+ ],
635
+ ).then(
636
+ fn=show_recommendation_list,
637
+ inputs=[recommendations_state, completed_survey_pois],
638
+ outputs=[recommendation_list]
639
+ )
640
+
641
+ rec_like_btn.click(
642
+ fn=feedback_message,
643
+ inputs=[],
644
+ outputs=[
645
+ recommendation_feedback_message,
646
+ ],
647
+ show_progress="hidden",
648
+ ).then(
649
+ fn=lambda: submit_rec_feedback(True),
650
+ inputs=[],
651
+ outputs=[
652
+ recommendation_feedback_result,
653
+ recommendation_feedback_submitted,
654
+ recommendation_feedback_message,
655
+ simple_survey_block,
656
+ recommendation_feedback_block,
657
+ ],
658
+ js=js_feedback_message_delay,
659
+ show_progress="hidden",
660
+ )
661
+
662
+ rec_dislike_btn.click(
663
+ fn=feedback_message,
664
+ inputs=[],
665
+ outputs=[
666
+ recommendation_feedback_message,
667
+ ],
668
+ show_progress="hidden",
669
+ ).then(
670
+ fn=lambda: submit_rec_feedback(False),
671
+ inputs=[],
672
+ outputs=[
673
+ recommendation_feedback_result,
674
+ recommendation_feedback_submitted,
675
+ recommendation_feedback_message,
676
+ simple_survey_block,
677
+ recommendation_feedback_block,
678
+ ],
679
+ js=js_feedback_message_delay,
680
+ show_progress="hidden",
681
+ )
682
+
683
+ # Configurazione eventi per il questionario semplice
684
+ exp_like_btn.click(
685
+ fn=feedback_message,
686
+ inputs=[],
687
+ outputs=[
688
+ simple_feedback_message,
689
+ ],
690
+ show_progress="hidden",
691
+ ).then(
692
+ fn=lambda: submit_exp_feedback(True),
693
+ inputs=[],
694
+ outputs=[
695
+ simple_survey_result,
696
+ simple_feedback_submitted,
697
+ simple_feedback_message,
698
+ advanced_survey_block,
699
+ simple_survey_block,
700
+ advanced_prev_btn,
701
+ advanced_next_btn,
702
+ advanced_submit_btn,
703
+ ],
704
+ js=js_feedback_message_delay,
705
+ show_progress="hidden",
706
+ )
707
+
708
+ exp_dislike_btn.click(
709
+ fn=feedback_message,
710
+ inputs=[],
711
+ outputs=[
712
+ simple_feedback_message,
713
+ ],
714
+ show_progress="hidden",
715
+ ).then(
716
+ fn=lambda: submit_exp_feedback(False),
717
+ inputs=[],
718
+ outputs=[
719
+ simple_survey_result,
720
+ simple_feedback_submitted,
721
+ simple_feedback_message,
722
+ advanced_survey_block,
723
+ simple_survey_block,
724
+ advanced_prev_btn,
725
+ advanced_next_btn,
726
+ advanced_submit_btn,
727
+ ],
728
+ js=js_feedback_message_delay,
729
+ show_progress="hidden",
730
+ )
731
+
732
+ # Create a list of outputs for navigation functions
733
+ navigation_outputs = [
734
+ advanced_current_group_idx, # next_group_idx
735
+ advanced_prev_btn, # prev_btn interactivity update
736
+ advanced_next_btn, # next_btn visibility update
737
+ advanced_submit_btn, # submit_btn visibility update
738
+ ] + advanced_containers # container updates
739
+
740
+ advanced_next_btn.click(
741
+ fn=partial(handle_next_navigation, advanced_responses, advanced_containers),
742
+ inputs=[advanced_current_group_idx],
743
+ outputs=navigation_outputs,
744
+ ).then(
745
+ fn=partial(handle_all_answered_responses, advanced_responses),
746
+ inputs=[advanced_current_group_idx],
747
+ outputs=[advanced_submit_btn, advanced_next_btn],
748
+ )
749
+
750
+ advanced_prev_btn.click(
751
+ fn=partial(handle_prev_navigation, advanced_containers),
752
+ inputs=[
753
+ advanced_current_group_idx,
754
+ ],
755
+ outputs=navigation_outputs,
756
+ )
757
+
758
+ for group_idx, radio_group in enumerate(advanced_responses):
759
+ for radio_idx, radio_button in enumerate(radio_group):
760
+ radio_button.change(
761
+ fn=partial(
762
+ update_advanced_responses,
763
+ group_idx,
764
+ radio_idx,
765
+ advanced_responses,
766
+ ),
767
+ inputs=[radio_button],
768
+ outputs=[],
769
+ ).then(
770
+ fn=partial(handle_all_answered_responses, advanced_responses),
771
+ inputs=[advanced_current_group_idx],
772
+ outputs=[advanced_submit_btn, advanced_next_btn],
773
+ )
774
+
775
+ advanced_submit_btn.click(
776
+ fn=partial(
777
+ handle_submit_survey,
778
+ advanced_responses,
779
+ ),
780
+ inputs=[
781
+ app_user_id,
782
+ current_poi_info,
783
+ recommendation_feedback_result,
784
+ simple_survey_result,
785
+ ],
786
+ outputs=[
787
+ advanced_feedback_submitted,
788
+ advanced_feedback_message,
789
+ advanced_prev_btn,
790
+ advanced_next_btn,
791
+ advanced_submit_btn,
792
+ *advanced_containers,
793
+ ],
794
+ ).then(
795
+ fn=add_poi_to_completed,
796
+ inputs=[selected_poi_index, completed_survey_pois],
797
+ outputs=[completed_survey_pois],
798
+ )
799
+
800
+ return app
801
+
802
+
803
+ if __name__ == "__main__":
804
+ parser = argparse.ArgumentParser(description="Run the POI Search UI")
805
+ parser.add_argument(
806
+ "user_type",
807
+ choices=["autistic", "neurotypical"],
808
+ help="Type of user for the interface",
809
+ )
810
+ parser.add_argument(
811
+ "--top-k",
812
+ type=int,
813
+ default=5,
814
+ help="Number of top recommendations to show",
815
+ )
816
+ parser.add_argument(
817
+ "--port", type=int, default=7860, help="Port to run the Gradio app on"
818
+ )
819
+ parser.add_argument("--share", action="store_true", help="Share the app publicly")
820
+ args = parser.parse_args()
821
+ app = create_main_app(args.user_type)
822
+ app.launch()
html_items.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ from app import RESOURCE_DIR
5
+ from app.utils.utils import get_aversion_color, get_qualitative_aversion
6
+
7
+ with open(os.path.join(RESOURCE_DIR, "aversions.json"), "r") as f:
8
+ aversions_data = json.load(f)
9
+ AVERSIONS = {item["Name"]: item for item in aversions_data}
10
+
11
+
12
+ def create_poi_html(poi: dict, user_id: str) -> str:
13
+ """Create HTML representation of a POI with sensory features"""
14
+ html = f"""
15
+ <div style="border: 1px solid #ddd; padding: 15px; border-radius: 10px; margin-bottom: 20px;">
16
+ <div style="display: flex; flex-direction: row; gap: 20px;">
17
+ <div style="flex: 0 0 300px;">
18
+ <img src="{poi["image_url"]}" style="width: 100%; height: auto; border-radius: 8px; object-fit: cover;" alt="{poi["name"]}">
19
+ </div>
20
+ <div style="flex: 1;">
21
+ <h3 style="margin-top: 0;">{poi["name"]}</h3>
22
+ <p><strong>Descrizione:</strong> {poi["description"]}</p>
23
+ <p><strong>Luogo:</strong> {poi["location"]}</p>
24
+
25
+ <div style="margin: 15px 0;">
26
+ <h4>Caratteristiche Sensoriali:</h4>
27
+ <table style="width: 100%; border-collapse: collapse;">
28
+ <thead>
29
+ <tr style="background-color: #4a4a4a; color: white;">
30
+ <th style="text-align: left; padding: 8px; border: 1px solid #ddd; width: 35%;">Caratteristica</th>
31
+ <th style="text-align: center; padding: 8px; border: 1px solid #ddd; width: 65%;">Livello (1-5)</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ """
36
+
37
+ for feature, level in poi["sensory_features"].items():
38
+ icon = AVERSIONS[feature]["Icon"]
39
+ bar_color = get_aversion_color(level, AVERSIONS[feature]["Type"])
40
+ html += f"""
41
+ <tr>
42
+ <td style="text-align: left; padding: 8px; border: 1px solid #ddd;">
43
+ <div style="display: flex; align-items: center;">
44
+ <span style="font-size: 18px; margin-right: 8px;">{icon}</span>
45
+ {feature}
46
+ </div>
47
+ </td>
48
+ <td style="text-align: left; padding: 8px; border: 1px solid #ddd;">
49
+ <div style="position: relative; background-color: #f0f0f0; width: 100%; height: 24px; border-radius: 12px;">
50
+ <div style="position: absolute; background-color: {bar_color}; width: {level * 20}%; height: 100%; border-radius: 12px; display: flex; align-items: center;">
51
+ <span style="position: absolute; left: 50%; color: 'black'; font-weight: bold; text-shadow: 0px 0px 2px black;">{level}</span>
52
+ </div>
53
+ <div style="position: absolute; width: 100%; height: 100%; display: flex; justify-content: space-between; align-items: center; padding: 0 10px; box-sizing: border-box; pointer-events: none;">
54
+ <span style="font-size: 10px; opacity: 0.9; color: black">Basso</span>
55
+ <span style="font-size: 10px; opacity: 0.9; color: black">Alto</span>
56
+ </div>
57
+ </div>
58
+ </td>
59
+ </tr>
60
+ """
61
+ html += f""" </tbody>
62
+ </table>
63
+ <div style="margin: 15px 0; padding: 10px; background-color: #3a3a3a; color: white; border-left: 5px solid #7FB77E; border-radius: 5px;" --explanation_type="{poi["explanation_type"]}">
64
+ <h3>Spiegazione della raccomandazione: ti suggeriamo questo luogo perchΓ© ...</h3>
65
+ <p>{poi["explanation"]}</p>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ """
72
+
73
+ return html
74
+
75
+
76
+ def create_recommendation_item(rec, index, selected=False) -> str:
77
+ """Create HTML for a single recommendation item"""
78
+ return f"""
79
+ <div onclick="document.getElementById('select-button-{index}').click();"
80
+ style="display: flex; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid #ddd;
81
+ border-radius: 10px; cursor: pointer; transition: background-color 0.3s; background-color: {'#888888' if selected else 'transparent'};"
82
+ onmouseover="this.style.backgroundColor='{'#888888' if selected else '#393939'}';"
83
+ onmouseout="this.style.backgroundColor='{'#888888' if selected else 'transparent'}';">
84
+ <div style="width: 80px; height: 80px; overflow: hidden; border-radius: 8px; margin-right: 15px;">
85
+ <img src="{rec["image_url"]}" style="width: 100%; height: 100%; object-fit: cover;" alt="{rec["name"]}">
86
+ </div>
87
+ <div>
88
+ <h3 style="margin: 0 0 5px 0;">{rec["name"]}</h3>
89
+ <p style="margin: 0 0 5px 0; font-size: 0.9em;">{rec["description"]}</p>
90
+ </div>
91
+ <div style="margin-left: 50px; font-size: 40px; color: white; font-weight: bold; z-index: 10; text-shadow: 0 0 4px white;">
92
+ {"βœ“" if selected else ""}
93
+ </div>
94
+ </div>
95
+ """
96
+
97
+
98
+ def create_poi_card_selectable(poi: dict, is_selected: bool = False) -> str:
99
+ """Create HTML representation of a selectable POI card for search interface"""
100
+
101
+ # Enhanced visual feedback based on selection state
102
+ if is_selected:
103
+ border_color = "#30892F"
104
+ background_color = "#f0f8f0" # Slightly darker green tint
105
+ tick_icon = "βœ“"
106
+ # Dark overlay for selected state
107
+ overlay_style = "background-color: rgba(0, 0, 0, 0.15);"
108
+ # Darker content background
109
+ content_bg = "#e8f5e8"
110
+ # Darker text colors for selected state
111
+ title_color = "#2c5530"
112
+ location_color = "#4a5c4d"
113
+ description_color = "#5a6b5d"
114
+ else:
115
+ border_color = "#ddd"
116
+ background_color = "white"
117
+ tick_icon = ""
118
+ overlay_style = ""
119
+ content_bg = "white"
120
+ title_color = "#333"
121
+ location_color = "#666"
122
+ description_color = "#777"
123
+
124
+ # Create compact sensory features display
125
+ sensory_html = ""
126
+ if poi.get("sensory_features"):
127
+ sensory_items = []
128
+ for feature, level in poi["sensory_features"].items():
129
+ icon = AVERSIONS[feature]["Icon"]
130
+ color = get_aversion_color(level, AVERSIONS[feature]["Type"])
131
+ qualitative_level = get_qualitative_aversion(
132
+ level, AVERSIONS[feature]["Type"]
133
+ )
134
+ qualitative_level = {"GOOD": "Basso", "ACCEPTABLE": "Medio", "BAD": "Alto"}[
135
+ qualitative_level
136
+ ]
137
+
138
+ sensory_items.append(
139
+ f'<span style="color: {color}; margin-right: 8px;" title="{feature}: {level}/5">{icon} {feature}: {qualitative_level}</span>'
140
+ )
141
+
142
+ sensory_html = f"""
143
+ <div style="margin: 8px 0; padding: 8px; background: white; border-radius: 6px; border-left: 3px solid #7FB77E;">
144
+ <div style="font-size: 12px; color: #666; margin-bottom: 4px;">Caratteristiche Sensoriali:</div>
145
+ <div style="font-size: 14px;"><ul><li>{"</li><li>".join(sensory_items)}</li></ul></div>
146
+ </div>
147
+ """
148
+
149
+ # Escape single quotes in POI name for JavaScript
150
+ poi_name_escaped = poi["name"].replace("'", "\\'")
151
+
152
+ html = f"""
153
+ <div class="poi-card" data-poi-name="{poi["name"]}"
154
+ style="border: 2px solid {border_color};
155
+ background-color: {background_color};
156
+ padding: 0;
157
+ border-radius: 12px;
158
+ margin-bottom: 20px;
159
+ cursor: pointer;
160
+ transition: all 0.3s ease;
161
+ position: relative;
162
+ overflow: hidden;
163
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);"
164
+ onclick="document.getElementById('poi-btn-{poi_name_escaped.replace(" ", "-")}').click();"
165
+ onmouseover="this.style.transform='scale(1.02)'; this.style.boxShadow='0 4px 12px rgba(0,0,0,0.15)';"
166
+ onmouseout="this.style.transform='scale(1)'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)';">
167
+
168
+ <!-- Dark overlay for selected state -->
169
+ <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 1; {overlay_style} pointer-events: none;"></div>
170
+
171
+ <div style="position: absolute; top: 15px; right: 15px; font-size: 28px; color: #7FB77E; font-weight: bold; z-index: 10; text-shadow: 0 0 4px white;">
172
+ {tick_icon}
173
+ </div>
174
+
175
+ <!-- Image filling horizontally -->
176
+ <div style="width: 100%; height: 200px; overflow: hidden; position: relative;">
177
+ <img src="{poi["image_url"]}"
178
+ style="width: 100%; height: 100%; object-fit: cover;"
179
+ alt="{poi["name"]}">
180
+ </div>
181
+
182
+ <!-- Content below image -->
183
+ <div style="padding: 15px; background-color: {content_bg}; position: relative; z-index: 2;">
184
+ <h3 style="margin: 0 0 8px 0; color: {title_color}; font-size: 18px;">{poi["name"]}</h3>
185
+ <p style="margin: 0 0 8px 0; color: {location_color}; font-size: 14px; font-weight: 500;">
186
+ πŸ“ {poi["location"]}
187
+ </p>
188
+ <p style="margin: 0 0 12px 0; color: {description_color}; font-size: 14px; line-height: 1.4;">
189
+ {poi["description"][:120]}{"..." if len(poi["description"]) > 120 else ""}
190
+ </p>
191
+
192
+ {sensory_html}
193
+
194
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
195
+ <span style="display: inline-block; padding: 6px 12px; background-color: {"#d4edda" if is_selected else "#e8f4e8"}; color: {title_color}; border-radius: 20px; font-size: 12px; font-weight: bold;">
196
+ {("βœ“ Selezionato" if is_selected else "Clicca per selezionare")}
197
+ </span>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ """
202
+
203
+ return html
204
+
205
+
206
+ def create_poi_cards_list(pois: list, selected_names: set) -> str:
207
+ if not pois:
208
+ return "<div style='text-align:center; color:#666; margin: 40px; padding: 40px; background: #f8f9fa; border-radius: 12px;'><h3>πŸ” Nessun punto di interesse trovato</h3><p>Prova con termini di ricerca diversi</p></div>"
209
+
210
+ # Genera HTML per i POI filtrati
211
+ poi_cards_html = []
212
+ for poi in pois:
213
+ is_selected = poi.get("name") in selected_names
214
+ card_html = create_poi_card_selectable(poi, is_selected)
215
+ poi_cards_html.append(card_html)
216
+
217
+ html = f"""
218
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; margin: 20px 0;">
219
+ {"".join(poi_cards_html)}
220
+ </div>
221
+ <div style='text-align: center; color: black; margin-top: 20px; padding: 15px; background: #f0f0f0; border-radius: 8px;'>
222
+ <strong style='color: black;'>Trovati {len(pois)} punti di interesse</strong> β€’ <span style='color: #7FB77E;'>{len(selected_names)} selezionati</span>
223
+ </div>
224
+ """
225
+
226
+ return html
resources/_defaults.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "aversion_levels_colors": {
3
+ "BAD": "#f9cecc",
4
+ "ACCEPTABLE": "#ffe6cd",
5
+ "GOOD": "#d5e9d1"
6
+ }
7
+ }
resources/aversion_config_questions.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "question": "Da 1 a 5, quanto ti dΓ  fastidio in un luogo che ci sia tanto rumore?",
4
+ "aversion_name": "Livello di Rumore"
5
+ },
6
+ {
7
+ "question": "Da 1 a 5, quanto ti dΓ  fastidio in un luogo che ci siano odori forti?",
8
+ "aversion_name": "Stimolazione Olfattiva"
9
+ },
10
+ {
11
+ "question": "Da 1 a 5, quanto ti dΓ  fastidio in un luogo che ci sia tanta gente?",
12
+ "aversion_name": "DensitΓ  di Persone"
13
+ },
14
+ {
15
+ "question": "Da 1 a 5, quanto ti dΓ  fastidio in un luogo avere poca luce?",
16
+ "aversion_name": "IntensitΓ  Luminosa"
17
+ },
18
+ {
19
+ "question": "Da 1 a 5, quanto ti dΓ  fastidio in un luogo avere molta luce?",
20
+ "aversion_name": "IntensitΓ  Luminosa"
21
+ },
22
+ {
23
+ "question": "Da 1 a 5, quanto ti dΓ  fastidio che in un luogo ci siano spazi stretti?",
24
+ "aversion_name": "Dimensione degli Spazi"
25
+ },
26
+ {
27
+ "question": "Da 1 a 5, quanto ti dΓ  fastidio che in un luogo ci siano spazi ampi?",
28
+ "aversion_name": "Dimensione degli Spazi"
29
+ }
30
+ ]
resources/aversions.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "Name": "Livello di Rumore",
4
+ "Type": "HIGH",
5
+ "Description": "Luoghi con rumore elevato possono essere fastidiosi per chi ha sensibilitΓ  uditive.",
6
+ "Icon": "πŸ”Š"
7
+ },
8
+ {
9
+ "Name": "Stimolazione Olfattiva",
10
+ "Type": "HIGH",
11
+ "Description": "Luoghi con forti odori possono risultare sgradevoli per chi ha sensibilitΓ  olfattive.",
12
+ "Icon": "πŸ‘ƒ"
13
+ },
14
+ {
15
+ "Name": "DensitΓ  di Persone",
16
+ "Type": "HIGH",
17
+ "Description": "Luoghi affollati possono causare disagio a chi Γ¨ sensibile alla sovrapposizione di stimoli.",
18
+ "Icon": "πŸ‘₯"
19
+ },
20
+ {
21
+ "Name": "IntensitΓ  Luminosa",
22
+ "Type": "LOW-HIGH",
23
+ "Description": "L'illuminazione intensa o limitata puΓ² essere fastidiosa e causare disagio.",
24
+ "Icon": "πŸ’‘"
25
+ },
26
+ {
27
+ "Name": "Dimensione degli Spazi",
28
+ "Type": "LOW-HIGH",
29
+ "Description": "Spazi ristretti o spazi ampi possono far sentire a disagio.",
30
+ "Icon": "πŸ›οΈ"
31
+ }
32
+ ]
resources/explanation_types.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "type": 2,
4
+ "template": "Ti suggeriamo questo luogo perchè ti è piaciuto un altro luogo con caratteristiche sensoriali simili."
5
+ },
6
+ {
7
+ "type": 4,
8
+ "template": "Ti suggeriamo questo luogo perchΓ© Γ¨ piaciuto ad una persona che ha i tuoi stessi gusti."
9
+ },
10
+ {
11
+ "type": 6,
12
+ "template": "Ti suggeriamo questo luogo perchΓ© ha caratteristiche sensoriali che non ti danno fastidio."
13
+ },
14
+ {
15
+ "type": 7,
16
+ "template": "Ti suggeriamo questo luogo perchΓ© Γ¨ piaciuto ad una persona a cui, come te, danno fastidio le stesse caratteristiche sensoriali."
17
+ }
18
+ ]
resources/mock_data.json ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "Mole Antonelliana",
4
+ "image_url": "https://images.unsplash.com/photo-1578072445064-bd0b3f83b847?q=80&w=1000",
5
+ "description": "Un simbolo iconico di Torino, con un museo del cinema e una vista panoramica sulla cittΓ .",
6
+ "location": "Via Montebello, 20, Torino",
7
+ "coordinates": "45.070, 7.686",
8
+ "sensory_features": {
9
+ "Livello di Rumore": 3,
10
+ "Stimolazione Olfattiva": 5,
11
+ "DensitΓ  di Persone": 4,
12
+ "IntensitΓ  Luminosa": 4,
13
+ "Dimensione degli Spazi": 5
14
+ },
15
+ "explanation": "Ti suggeriamo questo luogo perchΓ© Γ¨ piaciuto ad una persona che ha i tuoi stessi gusti. Questo luogo ha un'alta stimolazione olfattiva e luminosa, con una densitΓ  di persone moderata. La struttura Γ¨ ben definita, ma potrebbe risultare un po' caotica per chi cerca tranquillitΓ .",
16
+ "explanation_type": 4
17
+ },
18
+ {
19
+ "name": "Parco del Valentino",
20
+ "image_url": "https://plus.unsplash.com/premium_photo-1733436277181-a19049e64753?q=80&w=1000",
21
+ "description": "Un grande parco urbano con sentieri lungo il fiume Po e un castello medievale.",
22
+ "location": "Corso Massimo d'Azeglio, Torino",
23
+ "coordinates": "45.054, 7.694",
24
+ "sensory_features": {
25
+ "Livello di Rumore": 3,
26
+ "Stimolazione Olfattiva": 3,
27
+ "DensitΓ  di Persone": 2,
28
+ "IntensitΓ  Luminosa": 4,
29
+ "Dimensione degli Spazi": 3
30
+ },
31
+ "explanation": "Ti suggeriamo questo luogo perchè ti è piaciuto un altro luogo con caratteristiche sensoriali simili. Questo parco offre un ambiente con un livello moderato di stimolazione sensoriale e sentieri ben definiti che permettono di esplorare liberamente.",
32
+ "explanation_type": 2
33
+ },
34
+ {
35
+ "name": "La Teiera Caffè Eccentrico",
36
+ "image_url": "https://images.unsplash.com/photo-1550305080-4e029753abcf?q=80&w=1000",
37
+ "description": "Un accogliente caffè con sale tranquille separate e spazi adatti a persone con sensibilità sensoriali.",
38
+ "location": "Via Bertola, 29, Torino",
39
+ "coordinates": "45.071, 7.675",
40
+ "sensory_features": {
41
+ "Livello di Rumore": 2,
42
+ "Stimolazione Olfattiva": 2,
43
+ "DensitΓ  di Persone": 2,
44
+ "IntensitΓ  Luminosa": 2,
45
+ "Dimensione degli Spazi": 4
46
+ },
47
+ "explanation": "Ti suggeriamo questo luogo perchΓ© ha caratteristiche sensoriali che non ti danno fastidio. Il layout prevedibile e la bassa stimolazione sensoriale (tutte le caratteristiche sono valutate 2/5 tranne la struttura) creano un ambiente confortevole per te.",
48
+ "explanation_type": 6
49
+ },
50
+ {
51
+ "name": "Libreria di Lucia",
52
+ "image_url": "https://images.unsplash.com/photo-1552566626-52f8b828add9?q=80&w=1000",
53
+ "description": "Una libreria indipendente con angoli lettura e orari dedicati alla tranquillitΓ .",
54
+ "location": "Via Po, 18, Torino",
55
+ "coordinates": "45.067, 7.693",
56
+ "sensory_features": {
57
+ "Livello di Rumore": 1,
58
+ "Stimolazione Olfattiva": 3,
59
+ "DensitΓ  di Persone": 2,
60
+ "IntensitΓ  Luminosa": 3,
61
+ "Dimensione degli Spazi": 4
62
+ },
63
+ "explanation": "Ti suggeriamo questo luogo perchΓ© Γ¨ piaciuto ad una persona a cui, come te, danno fastidio le stesse caratteristiche sensoriali. Questa libreria offre un ambiente prevedibile e silenzioso (1/5) con una stimolazione olfattiva moderata, ideale per chi cerca spazi tranquilli.",
64
+ "explanation_type": 7
65
+ },
66
+ {
67
+ "name": "Orto Botanico",
68
+ "image_url": "https://www.guidatorino.com/wp-content/uploads/2020/05/orto-botanico-torino-3.jpg",
69
+ "description": "Un giardino tranquillo con percorsi ben segnalati e una sezione di piante sensoriali.",
70
+ "location": "Viale Mattioli, 25, Torino",
71
+ "coordinates": "45.063, 7.688",
72
+ "sensory_features": {
73
+ "Livello di Rumore": 1,
74
+ "Stimolazione Olfattiva": 4,
75
+ "DensitΓ  di Persone": 1,
76
+ "IntensitΓ  Luminosa": 4,
77
+ "Dimensione degli Spazi": 5
78
+ },
79
+ "explanation": "Ti suggeriamo questo luogo perchΓ© ha caratteristiche sensoriali che non ti danno fastidio. Questo giardino ha una densitΓ  di persone molto bassa (1/5) e un livello di rumore minimo (1/5). L'alta strutturazione (5/5) offre una navigazione chiara, anche se presenta una ricca stimolazione olfattiva e luminosa.",
80
+ "explanation_type": 6
81
+ },
82
+ {
83
+ "name": "Orto Botanico 2",
84
+ "image_url": "https://www.guidatorino.com/wp-content/uploads/2020/05/orto-botanico-torino-3.jpg",
85
+ "description": "Un giardino tranquillo con percorsi ben segnalati e una sezione di piante sensoriali.",
86
+ "location": "Viale Mattioli, 25, Torino",
87
+ "coordinates": "45.063, 7.688",
88
+ "sensory_features": {
89
+ "Livello di Rumore": 1,
90
+ "Stimolazione Olfattiva": 4,
91
+ "DensitΓ  di Persone": 1,
92
+ "IntensitΓ  Luminosa": 4,
93
+ "Dimensione degli Spazi": 5
94
+ },
95
+ "explanation": "Ti suggeriamo questo luogo perchΓ© ha caratteristiche sensoriali che non ti danno fastidio. Questo giardino ha una densitΓ  di persone molto bassa (1/5) e un livello di rumore minimo (1/5). L'alta strutturazione (5/5) offre una navigazione chiara, anche se presenta una ricca stimolazione olfattiva e luminosa.",
96
+ "explanation_type": 6
97
+ },
98
+ {
99
+ "name": "Orto Botanico 3",
100
+ "image_url": "https://www.guidatorino.com/wp-content/uploads/2020/05/orto-botanico-torino-3.jpg",
101
+ "description": "Un giardino tranquillo con percorsi ben segnalati e una sezione di piante sensoriali.",
102
+ "location": "Viale Mattioli, 25, Torino",
103
+ "coordinates": "45.063, 7.688",
104
+ "sensory_features": {
105
+ "Livello di Rumore": 1,
106
+ "Stimolazione Olfattiva": 4,
107
+ "DensitΓ  di Persone": 1,
108
+ "IntensitΓ  Luminosa": 4,
109
+ "Dimensione degli Spazi": 5
110
+ },
111
+ "explanation": "Ti suggeriamo questo luogo perchΓ© ha caratteristiche sensoriali che non ti danno fastidio. Questo giardino ha una densitΓ  di persone molto bassa (1/5) e un livello di rumore minimo (1/5). L'alta strutturazione (5/5) offre una navigazione chiara, anche se presenta una ricca stimolazione olfattiva e luminosa.",
112
+ "explanation_type": 6
113
+ },
114
+ {
115
+ "name": "Orto Botanico 4",
116
+ "image_url": "https://www.guidatorino.com/wp-content/uploads/2020/05/orto-botanico-torino-3.jpg",
117
+ "description": "Un giardino tranquillo con percorsi ben segnalati e una sezione di piante sensoriali.",
118
+ "location": "Viale Mattioli, 25, Torino",
119
+ "coordinates": "45.063, 7.688",
120
+ "sensory_features": {
121
+ "Livello di Rumore": 1,
122
+ "Stimolazione Olfattiva": 4,
123
+ "DensitΓ  di Persone": 1,
124
+ "IntensitΓ  Luminosa": 4,
125
+ "Dimensione degli Spazi": 5
126
+ },
127
+ "explanation": "Ti suggeriamo questo luogo perchΓ© ha caratteristiche sensoriali che non ti danno fastidio. Questo giardino ha una densitΓ  di persone molto bassa (1/5) e un livello di rumore minimo (1/5). L'alta strutturazione (5/5) offre una navigazione chiara, anche se presenta una ricca stimolazione olfattiva e luminosa.",
128
+ "explanation_type": 6
129
+ }
130
+ ]
resources/survey_statements.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "statements": [
3
+ "Mi ha aiutato a capire se mi piacerΓ  quel posto.",
4
+ "Mi ha fatto decidere piΓΉ velocemente se mi piace quel posto.",
5
+ "Mi ha fatto venire voglia di andare lì.",
6
+ "Mi Γ¨ piaciuto come scelgono il posto da consigliare.",
7
+ "Mi ha fatto capire se i suggerimenti rispecchiano i miei gusti.",
8
+ "Mi ha fatto capire perchΓ© mi consigliano quel posto.",
9
+ "Mi ha fatto fidare del suggerimento."
10
+ ],
11
+ "likert_options": [
12
+ "In disaccordo",
13
+ "Parzialmente in disaccordo",
14
+ "Neutrale",
15
+ "Parzialmente d'accordo",
16
+ "D'accordo"
17
+ ]
18
+ }
utils/__init__.py ADDED
File without changes
utils/poi_search_utils.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import json
3
+ import os
4
+
5
+ from app import RESOURCE_DIR
6
+ from app.html_items import create_poi_cards_list
7
+
8
+ # Carica i dati dei POI
9
+ with open(os.path.join(RESOURCE_DIR, "mock_data.json"), "r") as f:
10
+ LOADED_MOCK_POIS = json.load(f)
11
+ pois_list = LOADED_MOCK_POIS
12
+
13
+
14
+ def search_pois(search_query: str, selected_names: set) -> str:
15
+ """Cerca POI basandosi sulla query di ricerca e restituisce HTML"""
16
+
17
+ # Filtra i POI basandosi sulla query
18
+ filtered_pois = []
19
+ if search_query is None or not search_query.strip():
20
+ # Se non c'Γ¨ query, mostra tutti i POI
21
+ filtered_pois = pois_list
22
+ else:
23
+ query_lower = search_query.lower()
24
+ for poi in pois_list:
25
+ # Cerca nel nome e indirizzo
26
+ if (
27
+ query_lower in poi.get("name", "").lower()
28
+ or
29
+ # query_lower in poi.get("description", "").lower() or
30
+ query_lower in poi.get("location", "").lower()
31
+ ):
32
+ filtered_pois.append(poi)
33
+
34
+ return create_poi_cards_list(filtered_pois, selected_names)
35
+
36
+
37
+ def toggle_poi_selection(
38
+ poi_name: str, selected_names: set, current_query: str
39
+ ) -> tuple[set, str, str]:
40
+ """Alterna la selezione di un POI e aggiorna il display"""
41
+
42
+ # Copia il set per evitare modifiche dirette
43
+ new_selected = selected_names.copy()
44
+
45
+ print(f"DEBUG: Toggling POI '{poi_name}'")
46
+ print(f"DEBUG: Current selected: {selected_names}")
47
+
48
+ if poi_name in new_selected:
49
+ new_selected.remove(poi_name)
50
+ print(f"DEBUG: Removed '{poi_name}' from selection")
51
+ else:
52
+ new_selected.add(poi_name)
53
+ print(f"DEBUG: Added '{poi_name}' to selection")
54
+
55
+ print(f"DEBUG: New selected: {new_selected}")
56
+
57
+ # Rigenera l'HTML con lo stato aggiornato
58
+ updated_html = search_pois(current_query, new_selected)
59
+ summary = get_selection_summary(new_selected)
60
+
61
+ print(f"DEBUG: Generated HTML length: {len(updated_html)}")
62
+ print(f"DEBUG: Summary: {summary}")
63
+
64
+ return new_selected, updated_html, summary
65
+
66
+
67
+ def get_selection_summary(selected_names: set) -> str:
68
+ """Restituisce un riassunto dei POI selezionati"""
69
+ if not selected_names:
70
+ return "**Punti di Interesse selezionati: 0** - Seleziona i luoghi di tuo interesse cliccando sulle card"
71
+
72
+ names_list = list(selected_names)
73
+ return f"**Punti di Interesse selezionati: {len(selected_names)}** => {' - '.join(names_list[:3])}{' e altri...' if len(selected_names) > 3 else ''}"
74
+
75
+
76
+ def clear_all_selections(current_query: str) -> tuple[set, str, str]:
77
+ """Cancella tutte le selezioni"""
78
+ empty_set = set()
79
+ updated_html = search_pois(current_query, empty_set)
80
+ summary = get_selection_summary(empty_set)
81
+ return empty_set, updated_html, summary
utils/recommender_poi_utils.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+ import json
3
+ import os
4
+ import random
5
+
6
+ import gradio as gr
7
+ from app.html_items import create_poi_html, create_recommendation_item
8
+
9
+ from app import RESOURCE_DIR
10
+ from app.utils.survey_utils import reset_surveys
11
+
12
+ # Carica i dati mock dei POI
13
+ with open(os.path.join(RESOURCE_DIR, "mock_data.json"), "r") as f:
14
+ LOADED_MOCK_POIS = json.load(f)
15
+
16
+
17
+ @lru_cache(maxsize=512)
18
+ def get_recommendations(user_id: str, k: int = 10) -> list[dict]:
19
+ """Function to get recommendations from external system"""
20
+ raise NotImplementedError("This function should be implemented to fetch recommendations from an external system.")
21
+ recommendations = bla_bla
22
+ random.shuffle(recommendations)
23
+
24
+ return recommendations[:k]
25
+
26
+
27
+ def display_recommendations(user_id: str, k: int = 10) -> list[dict]:
28
+ """Get recommendations for a user"""
29
+ try:
30
+ recommendations = get_recommendations(user_id, k=k)
31
+ except Exception as e:
32
+ print(f"Errore durante il recupero delle raccomandazioni: {str(e)}")
33
+ if not LOADED_MOCK_POIS:
34
+ recommendations = []
35
+ else:
36
+ available_pois = [poi.copy() for poi in LOADED_MOCK_POIS]
37
+ random.shuffle(available_pois)
38
+ recommendations = available_pois[:k]
39
+
40
+ for rec in recommendations:
41
+ if rec.get("image_url") is None:
42
+ rec["image_url"] = (
43
+ "https://developers.elementor.com/docs/assets/img/elementor-placeholder-image.png" # Default placeholder
44
+ )
45
+
46
+ return recommendations
47
+
48
+
49
+ def get_recommendations_ui(user_id: str, k: int = 10, completed_pois: set = None) -> tuple[list[dict], str, str]:
50
+ recs = display_recommendations(user_id, k=k)
51
+ if not recs:
52
+ return (
53
+ [],
54
+ user_id,
55
+ "<p style='text-align:center; color:grey;'>Impossibile ottenere raccomandazioni per questo utente.</p>",
56
+ )
57
+
58
+ if completed_pois is None:
59
+ completed_pois = set()
60
+
61
+ html_content = show_recommendation_list(recs, completed_pois=completed_pois)
62
+ return recs, user_id, html_content
63
+
64
+
65
+ def show_recommendation_list(recommendations, completed_pois: set = None) -> str:
66
+ """Create a simple list of recommendations that users can click on"""
67
+ html = "<div>"
68
+
69
+ for i, rec in enumerate(recommendations):
70
+ print(f"DEBUG: Creating recommendation item {i} with POI name '{rec.get('name')}' and it {'is' if i in completed_pois else 'is not'} completed.")
71
+ html += create_recommendation_item(rec, i, selected=i in completed_pois)
72
+
73
+ html += "</div>"
74
+ return html
75
+
76
+
77
+ def add_poi_to_completed(selected_poi_index, completed_set):
78
+ """Add current POI to the set of completed survey POIs"""
79
+ if selected_poi_index not in completed_set:
80
+ new_completed = completed_set.copy()
81
+ new_completed.add(selected_poi_index)
82
+ print(f"DEBUG: Added POI index {selected_poi_index} to completed set.")
83
+ return new_completed
84
+ return completed_set
85
+
86
+
87
+ def view_poi_details(
88
+ recs: list[dict], user_id: str, poi_idx: int, completed_pois: set = None
89
+ ) -> tuple[gr.update, gr.update, gr.update, gr.update, gr.update, dict]:
90
+ if not recs or poi_idx < 0 or poi_idx >= len(recs):
91
+ return (
92
+ gr.update(visible=False), # home_page_block
93
+ gr.update(visible=True), # details_page_block
94
+ gr.update(
95
+ value="<p style='color:red;'>Errore: Punto di Interesse non trovato o raccomandazioni non caricate.</p>"
96
+ ), # poi_details
97
+ gr.update(visible=False), # simple_survey_block
98
+ gr.update(visible=False), # advanced_survey_block
99
+ {}, # current_poi_info
100
+ )
101
+
102
+ poi = recs[poi_idx]
103
+ if poi.get("name") in completed_pois:
104
+ return
105
+
106
+ html = create_poi_html(poi, user_id)
107
+
108
+ # Ottieni il tipo di spiegazione dal POI
109
+ explanation_type = poi.get("explanation_type", 0)
110
+
111
+ # Salva le informazioni del POI corrente per il questionario
112
+ poi_info = {
113
+ "id": poi.get("id"),
114
+ "name": poi.get("name"),
115
+ "category": poi.get("category"),
116
+ "explanation_type": explanation_type,
117
+ "explanation": poi.get("explanation"),
118
+ }
119
+
120
+ return (
121
+ gr.update(visible=False), # home_page_block
122
+ gr.update(visible=True), # details_page_block
123
+ gr.update(value=html), # poi_details
124
+ gr.update(visible=True), # recommendation_feedback_block
125
+ gr.update(visible=True), # back_to_recommendations_block
126
+ poi_info, # current_poi_info
127
+ )
128
+
129
+
130
+ def back_to_recommendations(
131
+ advanced_responses, advanced_containers
132
+ ) -> tuple[gr.update, gr.update, gr.update, gr.update, gr.update]:
133
+ survey_updates = reset_surveys(advanced_responses, advanced_containers)
134
+
135
+ return (
136
+ gr.update(visible=True), # home_page_block
137
+ gr.update(visible=False), # details_page_block
138
+ gr.update(
139
+ value="<p style='text-align:center; color:grey; margin-top: 20px;'>Seleziona un Punto di Interesse per vedere i dettagli.</p>"
140
+ ), # poi_details
141
+ gr.update(visible=False), # recommmendation_feedback_block
142
+ gr.update(visible=False), # simple_survey_block
143
+ gr.update(visible=False), # advanced_survey_block
144
+ gr.update(visible=False), # back_to_recommendations_block
145
+ *survey_updates,
146
+ )
utils/sensory_config_utils.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ from app import RESOURCE_DIR
5
+ from app.utils import get_aversion_color, get_qualitative_aversion
6
+
7
+
8
+ with open(os.path.join(RESOURCE_DIR, "aversion_config_questions.json"), "r") as f:
9
+ AVERSIONS_QUESTIONS = json.load(f)
10
+ with open(os.path.join(RESOURCE_DIR, "aversions.json"), "r") as f:
11
+ AVERSIONS = json.load(f)
12
+ AVERSIONS = {v["Name"]: v for v in AVERSIONS}
13
+
14
+
15
+ def get_feedback_html(value: int, aversion_type: str) -> str:
16
+ """Genera HTML per il feedback colorato"""
17
+ color = get_aversion_color(value, aversion_type)
18
+ feedback = get_qualitative_aversion(value, aversion_type)
19
+
20
+ # Semplifica il feedback text
21
+ if feedback == "GOOD":
22
+ text = "βœ“ Ottimo"
23
+ bg_color = "#d4edda"
24
+ elif feedback == "ACCEPTABLE":
25
+ text = "⚠ Moderato"
26
+ bg_color = "#fff3cd"
27
+ else:
28
+ text = "⚑ Elevato"
29
+ bg_color = "#f8d7da"
30
+
31
+ return f"""
32
+ <div style="
33
+ background-color: {bg_color};
34
+ color: {color};
35
+ padding: 6px 12px;
36
+ border-radius: 15px;
37
+ text-align: center;
38
+ font-weight: bold;
39
+ font-size: 13px;
40
+ border: 1px solid {color}20;
41
+ margin-top: 5px;
42
+ ">
43
+ {text}
44
+ </div>
45
+ """
46
+
47
+
48
+ # Funzioni per gestire gli eventi
49
+ def extract_sensory_preferences(radio_values: list) -> dict:
50
+ """Estrae le preferenze sensoriali"""
51
+ preferences = {}
52
+ preferences["question_answers"] = []
53
+ print("TODO: Bisogna trasformare la risposta in un valore compatibile per le raccomandazioni come nei dati")
54
+ for question, value in zip(AVERSIONS_QUESTIONS, radio_values):
55
+ question_text = question["question"]
56
+ aversion_name = question["aversion_name"]
57
+ aversion = AVERSIONS.get(aversion_name, {})
58
+ preferences["question_answers"].append({
59
+ "question": question_text,
60
+ "answer": value,
61
+ "aversion_name": aversion_name,
62
+ "type": aversion.get("Type", ""),
63
+ })
64
+
65
+ # TODO: Bisogna trasformare la risposta in un valore compatibile per le raccomandazioni come nei dati
66
+ preferences[aversion_name] = {
67
+ "value": value,
68
+ "type": aversion.get("Type", ""),
69
+ "description": aversion.get("Description", ""),
70
+ }
71
+
72
+ return preferences
73
+
74
+
75
+ def get_slider_values(aversion_sliders: dict) -> dict:
76
+ """Ottiene i valori correnti degli slider"""
77
+ values = {}
78
+ for name, slider in aversion_sliders.items():
79
+ values[name] = slider.value if hasattr(slider, "value") else 3
80
+ return values
utils/survey_utils.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import json
3
+ import os
4
+
5
+ import gradio as gr
6
+
7
+ from app import RESOURCE_DIR
8
+
9
+ # Carica le affermazioni per il questionario avanzato
10
+ with open(os.path.join(RESOURCE_DIR, "survey_statements.json"), "r") as f:
11
+ survey_data = json.load(f)
12
+ SURVEY_STATEMENTS = survey_data.get("statements", [])
13
+ LIKERT_OPTIONS = survey_data.get("likert_options", [])
14
+
15
+
16
+ def feedback_message():
17
+ return gr.update(
18
+ visible=True,
19
+ value="""
20
+ <div style="
21
+ background-color: #30892F;
22
+ color: white;
23
+ padding: 15px 20px;
24
+ border-radius: 10px;
25
+ text-align: center;
26
+ width: 100%;
27
+ box-sizing: border-box;
28
+ font-weight: 500;
29
+ margin: 10px 0;
30
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
31
+ border: 1px solid #c3e6ba;
32
+ ">
33
+ βœ… Grazie per il tuo feedback!
34
+ </div>
35
+ """,
36
+ )
37
+
38
+
39
+ def submit_simple_feedback(choice: bool) -> tuple:
40
+ """Registra un feedback semplice"""
41
+ return (
42
+ choice,
43
+ True,
44
+ gr.update(visible=False),
45
+ gr.update(visible=True),
46
+ gr.update(visible=False),
47
+ )
48
+
49
+
50
+ def submit_rec_feedback(choice: bool) -> tuple:
51
+ """Registra il feedback semplice sulla raccomandazione"""
52
+ return submit_simple_feedback(choice)
53
+
54
+ def submit_exp_feedback(choice: bool) -> tuple:
55
+ """Registra il feedback semplice sull'utilitΓ  della spiegazione"""
56
+ return (
57
+ *submit_simple_feedback(choice),
58
+ gr.update(visible=True, interactive=False),
59
+ gr.update(visible=True, interactive=True),
60
+ gr.update(visible=False),
61
+ )
62
+
63
+
64
+ # Configurazione eventi per il questionario avanzato
65
+ def handle_next_navigation(advanced_responses, advanced_containers, current_group_idx):
66
+ # Controlla che l'indice sia valido
67
+ if current_group_idx < 0 or current_group_idx >= len(advanced_responses):
68
+ print(
69
+ f"Errore: current_group_idx={current_group_idx} fuori range, max={len(advanced_responses) - 1}"
70
+ )
71
+ # Ritorna valori di default per evitare errori
72
+ empty_updates = [gr.update() for _ in range(len(advanced_containers))]
73
+ return (
74
+ 0,
75
+ gr.update(interactive=False),
76
+ gr.update(visible=True),
77
+ gr.update(visible=False),
78
+ *empty_updates,
79
+ )
80
+
81
+ next_group_idx = current_group_idx + 1
82
+
83
+ # Naviga al prossimo gruppo se possibile
84
+ container_updates = []
85
+ if current_group_idx < len(advanced_containers) - 1:
86
+ # Prepara gli aggiornamenti per i container
87
+ for i in range(len(advanced_containers)):
88
+ container_updates.append(gr.update(visible=(i == next_group_idx)))
89
+
90
+ ready_to_submit = next_group_idx == len(advanced_containers) - 1
91
+
92
+ return (
93
+ next_group_idx,
94
+ gr.update(interactive=True),
95
+ gr.update(visible=not ready_to_submit),
96
+ gr.update(visible=ready_to_submit, interactive=False),
97
+ *container_updates,
98
+ )
99
+
100
+
101
+ def handle_prev_navigation(advanced_containers, current_group_idx):
102
+ # Controlla che l'indice sia valido
103
+ if current_group_idx <= 0:
104
+ return (
105
+ 0,
106
+ gr.update(interactive=False),
107
+ gr.update(visible=True),
108
+ gr.update(visible=False),
109
+ *[gr.update() for _ in range(len(advanced_containers))],
110
+ )
111
+
112
+ # Torna al gruppo precedente
113
+ prev_group_idx = current_group_idx - 1
114
+
115
+ # Prepara gli aggiornamenti per i container
116
+ container_updates = []
117
+ for i in range(len(advanced_containers)):
118
+ container_updates.append(gr.update(visible=(i == prev_group_idx)))
119
+
120
+ # Ritorna il nuovo indice, le risposte aggiornate e gli aggiornamenti dei pulsanti
121
+ return (
122
+ prev_group_idx,
123
+ gr.update(interactive=(prev_group_idx > 0)),
124
+ gr.update(interactive=True, visible=True),
125
+ gr.update(visible=False),
126
+ *container_updates,
127
+ )
128
+
129
+
130
+ def update_advanced_responses(
131
+ current_group_idx, current_radio_idx, advanced_responses, radio_value
132
+ ):
133
+ advanced_responses[current_group_idx][current_radio_idx].value = radio_value
134
+
135
+
136
+ def handle_all_answered_responses(advanced_responses, current_group_idx):
137
+ all_response_values = [resp.value for group in advanced_responses for resp in group]
138
+
139
+ all_answered = all(resp is not None for resp in all_response_values)
140
+ submit_visible = current_group_idx == len(advanced_responses) - 1
141
+
142
+ return (
143
+ gr.update(visible=submit_visible, interactive=all_answered), # submit button
144
+ gr.update(visible=not submit_visible), # next button
145
+ )
146
+
147
+
148
+ def handle_submit_survey(
149
+ advanced_responses,
150
+ current_user_id,
151
+ current_poi_info,
152
+ recommendation_feedback,
153
+ simple_survey_feedback,
154
+ ):
155
+ survey_data = {
156
+ "user_id": current_user_id,
157
+ "poi_info": current_poi_info,
158
+ "recommendation_like": recommendation_feedback,
159
+ "explanation_useful": simple_survey_feedback,
160
+ "advanced_survey_feedback": {},
161
+ }
162
+ for resp in [resp for group in advanced_responses for resp in group]:
163
+ survey_data["advanced_survey_feedback"][resp.label] = resp.value
164
+
165
+ save_survey_response(survey_data)
166
+
167
+ return (
168
+ True,
169
+ gr.update(
170
+ visible=True,
171
+ value="""
172
+ <div style="
173
+ background-color: #30892F;
174
+ color: white;
175
+ padding: 15px 20px;
176
+ border-radius: 10px;
177
+ text-align: center;
178
+ width: 100%;
179
+ box-sizing: border-box;
180
+ font-weight: 500;
181
+ margin: 10px 0;
182
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
183
+ border: 1px solid #c3e6ba;
184
+ ">
185
+ βœ… Grazie per aver completato il questionario!
186
+ </div>
187
+ """,
188
+ ),
189
+ gr.update(visible=False), # prev button
190
+ gr.update(visible=False), # next button
191
+ gr.update(visible=False), # submit button
192
+ *[
193
+ gr.update(visible=False) for _ in range(len(advanced_responses))
194
+ ], # hide all containers
195
+ )
196
+
197
+
198
+ def save_survey_response(survey_data):
199
+ """Salva le risposte del questionario in un file JSON singolo per utente."""
200
+ # Crea directory se non esiste
201
+ survey_dir = os.path.join(RESOURCE_DIR, os.pardir, "database", "survey_responses")
202
+ os.makedirs(survey_dir, exist_ok=True)
203
+
204
+ # Nome file basato su user_id
205
+ user_id = survey_data.get("user_id", "anonymous")
206
+ filename = os.path.join(survey_dir, f"user_{user_id}_surveys.json")
207
+
208
+ # Prepara i dati della nuova risposta
209
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
210
+ survey_data["timestamp"] = timestamp
211
+
212
+ # Carica risposte esistenti o crea lista vuota
213
+ surveys = []
214
+ if os.path.exists(filename):
215
+ try:
216
+ with open(filename, "r", encoding="utf-8") as f:
217
+ surveys = json.load(f)
218
+ except (json.JSONDecodeError, FileNotFoundError):
219
+ surveys = []
220
+
221
+ # Aggiungi la nuova risposta
222
+ surveys.append(survey_data)
223
+
224
+ # Ordina per timestamp (piΓΉ recente per primo)
225
+ surveys.sort(key=lambda x: x["timestamp"], reverse=True)
226
+
227
+ # Salva il file aggiornato
228
+ with open(filename, "w", encoding="utf-8") as f:
229
+ json.dump(surveys, f, indent=2, ensure_ascii=False)
230
+
231
+ return filename
232
+
233
+
234
+ def reset_surveys(advanced_responses, advanced_containers):
235
+ """Resetta lo stato dei questionari"""
236
+ reset_radio_buttons = []
237
+ for group_radios in advanced_responses:
238
+ for radio_button in group_radios:
239
+ reset_radio_buttons.append(
240
+ gr.Radio(
241
+ label=radio_button.label,
242
+ choices=radio_button.choices,
243
+ value=None, # Reset the value
244
+ visible=True, # Ensure visibility is reset
245
+ )
246
+ )
247
+
248
+ return (
249
+ False,
250
+ False,
251
+ "",
252
+ False,
253
+ False,
254
+ "",
255
+ 0,
256
+ False,
257
+ "",
258
+ *reset_radio_buttons,
259
+ *[gr.update(visible=i == 0) for i in range(len(advanced_containers))],
260
+ )
utils/user_profile_utils.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import json
3
+ import os
4
+ from typing import Optional
5
+
6
+ from app import RESOURCE_DIR
7
+ from app.utils.sensory_config_utils import extract_sensory_preferences
8
+
9
+ USER_PROFILES_FILE = os.path.join(RESOURCE_DIR, os.pardir, "database", "user_profiles.json")
10
+ os.makedirs(os.path.dirname(USER_PROFILES_FILE), exist_ok=True)
11
+
12
+
13
+ def load_user_profiles():
14
+ """Carica i profili utente dal file JSON"""
15
+ if os.path.exists(USER_PROFILES_FILE):
16
+ with open(USER_PROFILES_FILE, "r", encoding="utf-8") as f:
17
+ profiles = json.load(f)
18
+ return {profile["user_id"]: profile for profile in profiles}
19
+ return {}
20
+
21
+
22
+ def save_user_profiles(profiles):
23
+ """Salva i profili utente nel file JSON"""
24
+ with open(USER_PROFILES_FILE, "w", encoding="utf-8") as f:
25
+ json.dump(list(profiles.values()), f, indent=2, ensure_ascii=False)
26
+
27
+
28
+ def get_user_profile(user_id):
29
+ """Ottiene il profilo di un utente specifico"""
30
+ profiles = load_user_profiles()
31
+ return profiles.get(
32
+ user_id,
33
+ {
34
+ "user_metadata": {"created_at": None, "last_updated": None},
35
+ "selected_pois": [],
36
+ "sensory_aversions": {},
37
+ },
38
+ )
39
+
40
+
41
+ def update_user_selected_pois(user_id, selected_pois):
42
+ """Aggiorna i POI selezionati per un utente"""
43
+ profiles = load_user_profiles()
44
+ current_time = datetime.datetime.now().isoformat()
45
+
46
+ if user_id not in profiles:
47
+ profiles[user_id] = {
48
+ "user_metadata": {
49
+ "created_at": current_time,
50
+ "last_updated": current_time,
51
+ "profile_complete": False,
52
+ },
53
+ "selected_pois": [],
54
+ "sensory_aversions": {},
55
+ }
56
+
57
+ profiles[user_id]["selected_pois"] = list(selected_pois)
58
+ profiles[user_id]["user_metadata"]["last_updated"] = current_time
59
+
60
+ save_user_profiles(profiles)
61
+
62
+
63
+ def update_user_sensory_aversions(user_id, *radio_values):
64
+ """Aggiorna le aversioni sensoriali per un utente"""
65
+ profiles = load_user_profiles()
66
+ current_time = datetime.datetime.now().isoformat()
67
+ sensory_aversions = extract_sensory_preferences(radio_values)
68
+
69
+ if user_id not in profiles:
70
+ profiles[user_id] = {
71
+ "user_metadata": {
72
+ "created_at": current_time,
73
+ "last_updated": current_time,
74
+ "profile_complete": False,
75
+ },
76
+ "selected_pois": [],
77
+ "sensory_aversions": {},
78
+ }
79
+
80
+ profiles[user_id]["sensory_aversions"] = sensory_aversions
81
+ profiles[user_id]["user_metadata"]["last_updated"] = current_time
82
+
83
+ save_user_profiles(profiles)
84
+
85
+
86
+ def mark_profile_complete(user_id):
87
+ """Marca il profilo di un utente come completo"""
88
+ profiles = load_user_profiles()
89
+ current_time = datetime.datetime.now().isoformat()
90
+
91
+ if user_id in profiles:
92
+ profiles[user_id]["user_metadata"]["profile_complete"] = True
93
+ profiles[user_id]["user_metadata"]["last_updated"] = current_time
94
+ save_user_profiles(profiles)
95
+ return profiles[user_id]
96
+
97
+ return None
98
+
99
+
100
+ def create_new_user_profile(user_id: str) -> dict:
101
+ """Crea un nuovo profilo utente"""
102
+ current_time = datetime.datetime.now().isoformat()
103
+ return {
104
+ "user_id": user_id.strip(),
105
+ "user_metadata": {
106
+ "created_at": current_time,
107
+ "last_updated": current_time,
108
+ "profile_complete": False,
109
+ },
110
+ "selected_pois": [],
111
+ "sensory_aversions": {},
112
+ }
113
+
114
+
115
+ def check_user_exists(user_id: str) -> tuple[bool, Optional[dict]]:
116
+ """Controlla se un utente esiste e restituisce il suo profilo"""
117
+ if not user_id or not user_id.strip():
118
+ return False, None
119
+
120
+ profiles = load_user_profiles()
121
+ user_profile = profiles.get(user_id.strip())
122
+
123
+ if user_profile:
124
+ return True, user_profile
125
+ else:
126
+ return False, None
127
+
128
+
129
+ def login_user(user_id: str) -> tuple[bool, str, str, dict]:
130
+ """Gestisce il login dell'utente"""
131
+ if not user_id or not user_id.strip():
132
+ return False, "⚠️ Inserisci un ID utente valido", "", {}
133
+
134
+ user_id = user_id.strip()
135
+ user_exists, user_profile = check_user_exists(user_id)
136
+
137
+ if user_exists:
138
+ profile_complete = user_profile.get("user_metadata", {}).get(
139
+ "profile_complete", False
140
+ )
141
+ last_updated = user_profile.get("user_metadata", {}).get("last_updated", "N/A")
142
+
143
+ if profile_complete:
144
+ message = f"βœ… Benvenuto/a {user_id}! Profilo completo trovato. Reindirizzamento al sistema di raccomandazione..."
145
+ return True, message, "existing_complete"
146
+ else:
147
+ message = f"πŸ“ Utente {user_id} trovato ma profilo incompleto. Completare preferenze e aversioni sensoriali..."
148
+ return True, message, "existing_incomplete"
149
+ else:
150
+ message = f"πŸ†• Nuovo utente {user_id}. Creazione profilo in corso..."
151
+ new_profile = create_new_user_profile(user_id)
152
+
153
+ # Salva il nuovo profilo
154
+ profiles = load_user_profiles()
155
+ profiles[user_id] = new_profile
156
+ save_user_profiles(profiles)
157
+
158
+ return True, message, "new_user"
utils/utils.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ from app import RESOURCE_DIR
5
+
6
+ with open(os.path.join(RESOURCE_DIR, "_defaults.json"), "r") as f:
7
+ _defaults = json.load(f)
8
+
9
+
10
+ def get_qualitative_aversion(level: float, aversion_type: str) -> str:
11
+ if aversion_type == "HIGH":
12
+ if level <= 2:
13
+ return "GOOD"
14
+ elif level <= 3:
15
+ return "ACCEPTABLE"
16
+ else:
17
+ return "BAD"
18
+ elif aversion_type == "LOW-HIGH":
19
+ if level <= 1 or level >= 5:
20
+ return "BAD"
21
+ elif level <= 2 or level >= 4:
22
+ return "ACCEPTABLE"
23
+ else:
24
+ return "GOOD"
25
+ else:
26
+ raise ValueError(f"Unknown aversion type: {aversion_type}")
27
+
28
+
29
+ def get_aversion_color(value: float, aversion_type: str) -> str:
30
+ return _defaults["aversion_levels_colors"][
31
+ get_qualitative_aversion(value, aversion_type)
32
+ ]
33
+
34
+
35
+ def get_aversion_feedback(value: float, aversion_type: str) -> str:
36
+ aversion_quality = _defaults["aversion_levels_colors"][
37
+ get_qualitative_aversion(value, aversion_type)
38
+ ]
39
+ if aversion_quality == "GOOD":
40
+ return "βœ“ Livello di avversione basso - Buono"
41
+ elif aversion_quality == "ACCEPTABLE":
42
+ return "⚠ Livello di avversione moderato - Attenzione"
43
+ elif aversion_quality == "BAD":
44
+ return "βœ— Livello di avversione alto - Evitare"
45
+ else:
46
+ return "Livello di avversione registrato"
47
+
48
+
49
+ def get_qualitative_compatibility(level: float) -> str:
50
+ if level >= 85:
51
+ return "GOOD"
52
+ elif level >= 70:
53
+ return "ACCEPTABLE"
54
+ else:
55
+ return "BAD"
56
+
57
+
58
+ def get_compatibility_color(level: float) -> str:
59
+ return _defaults["aversion_levels_colors"][get_qualitative_compatibility(level)]