jackmedda
Add default lang
a15d197
import argparse
import json
import os
import random
from functools import partial
import gradio as gr
from utils.poi_search_utils import (
clear_all_selections,
get_selection_summary,
pois_list,
search_pois,
toggle_poi_selection,
)
from utils.recommender_poi_utils import (
add_poi_to_completed,
back_to_recommendations,
get_recommendations_ui,
show_recommendation_list,
view_poi_details,
)
from utils.survey_utils import (
feedback_message,
handle_all_answered_responses,
handle_next_navigation,
handle_prev_navigation,
handle_submit_survey,
submit_exp_feedback,
submit_rec_feedback,
update_advanced_responses,
)
from utils.user_profile_utils import (
login_user,
mark_profile_complete,
update_user_selected_pois,
update_user_sensory_aversions
)
RESOURCE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources")
with open(os.path.join(RESOURCE_DIR, "aversion_config_questions.json"), "r") as f:
AVERSIONS_QUESTIONS = json.load(f)
with open(os.path.join(RESOURCE_DIR, "aversions.json"), "r") as f:
AVERSIONS = json.load(f)
AVERSIONS = {v["Name"]: v for v in AVERSIONS}
feedback_message_delay = 1500
js_feedback_message_delay = f"""() => {{
return new Promise(resolve => {{
setTimeout(() => {{
resolve();
}}, {feedback_message_delay});
}});
}}"""
def create_main_app(self):
with gr.Blocks(
title="Sistema di Raccomandazione di Punti di Interesse con Personalizzazione Sensoriale",
theme=gr.themes.Soft(),
css="""
footer{display:none !important}
.navigation-container {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
border-left: 4px solid #007bff;
}
.page-container {
min-height: 500px;
padding: 20px;
}
.poi-card {
transition: all 0.3s ease;
}
.poi-card:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.search-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px;
border-radius: 15px;
color: white;
margin-bottom: 30px;
}
.continue-button {
background-color: #28a745 !important;
border-color: #28a745 !important;
font-size: 16px !important;
padding: 12px 24px !important;
}
.continue-button:hover {
background-color: #218838 !important;
border-color: #1e7e34 !important;
}
.clear-button {
background-color: #dc3545 !important;
border-color: #dc3545 !important;
}
.clear-button:hover {
background-color: #c82333 !important;
border-color: #bd2130 !important;
}
.sensory-config-main-container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 25px;
text-align: center;
}
.radio-aversion-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 25px;
}
.radio-aversion-card {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 15px;
}
.continue-to-recommendations-button {
background-color: #28a745 !important;
border-color: #28a745 !important;
font-size: 16px !important;
padding: 12px 30px !important;
width: 100% !important;
}
.continue-to-recommendations-button:hover {
background-color: #218838 !important;
border-color: #1e7e34 !important;
}
.feedback-indicator {
margin-top: 5px;
}
.back-button-custom {
background-color: #32568f !important;
border-color: #32568f !important;
color: white !important;
}
.back-button-custom:hover {
background-color: #132137 !important;
border-color: #132137 !important;
}
.login-button {
background-color: #007bff !important;
border-color: #007bff !important;
}
.login-button:hover {
background-color: #0056b3 !important;
border-color: #004085 !important;
}
#poi-search-input {
color: black;
}
""",
) as app:
# Stati globali dell'applicazione
app_user_id = gr.State("")
current_page = gr.State("login") # "login", "poi_selection", "sensory_config", "recommendations"
current_selected_pois = gr.State(set())
recommendations_state = gr.State([])
selected_poi_index = gr.State(-1)
completed_survey_pois = gr.State(set())
current_poi_info = gr.State({}) # Store current POI info for survey
user_status = gr.State("") # "new_user", "existing_incomplete", "existing_complete"
# Container per le diverse pagine
with gr.Column(elem_classes="page-container"):
# Pagina 1: Login/Profile
with gr.Column(visible=True) as login_page:
gr.Markdown("# 👤 Login / Registrazione")
# Interfaccia di login semplificata
with gr.Row():
user_id_input = gr.Textbox(
placeholder="es: mario_rossi, alice_123, user001...",
label="🆔 Inserisci il tuo ID Utente",
info="Scegli un ID univoco che userai per accedere al sistema",
elem_id="user-id-input",
scale=2
)
login_button = gr.Button(
"🚀 Accedi / Registrati",
variant="primary",
elem_classes="action-button login-button",
)
login_status_message = gr.Markdown("", visible=False)
with gr.Row(visible=False) as login_actions:
start_setup_btn = gr.Button(
"📝 Configura Profilo", variant="secondary"
)
go_to_recommendations_btn = gr.Button(
"🎯 Vai alle Raccomandazioni", variant="primary"
)
with gr.Column(visible=False) as poi_selection_page:
with gr.Column(elem_classes="search-container"):
gr.Markdown("# 🔍 Cerca e seleziona i **Luoghi di Interesse** che preferisci cliccando sulla card corrispondente")
gr.Markdown(
"Esplora i luoghi disponibili e seleziona quelli che preferisci maggiormente. La ricerca è dinamica: inizia a digitare per filtrare i risultati."
)
# Search input (dynamic, no button needed)
search_input = gr.Textbox(
placeholder="Cerca per nome, descrizione o località...",
label="",
show_label=False,
container=False,
elem_id="poi-search-input",
)
with gr.Column():
# Selection summary
selection_summary = gr.Markdown(get_selection_summary(set()))
# Navigation buttons
with gr.Row():
clear_button = gr.Button(
"🗑️ Cancella Selezioni",
variant="secondary",
elem_classes="clear-button",
scale=1,
)
continue_to_sensory_btn = gr.Button(
"Continua alla Configurazione Sensoriale ➡️",
variant="primary",
elem_classes="continue_to_sensory_btn",
scale=3,
)
# POI display area
poi_display = gr.HTML(
value=search_pois("", set()), elem_id="poi-display-area"
)
# Create hidden buttons for each POI
poi_buttons = {}
for poi in pois_list:
poi_id = poi["poi_id:token"]
poi_name = poi["name:token_seq"]
safe_id = (
poi_id.replace(" ", "-").replace("'", "").replace('"', "")
)
button = gr.Button(
poi_name, visible=False, elem_id=f"poi-btn-{safe_id}"
)
poi_buttons[(poi_id, poi_name)] = button
with gr.Column(
visible=False, elem_classes="sensory-config-main-container"
) as sensory_config_page:
# Header compatto
gr.HTML("""
<div class="header-section">
<h2 style="margin: 0 0 10px 0;">⚙️ Configura le tue Preferenze Sensoriali</h2>
<p style="margin: 0; opacity: 0.9;">Indica il tuo livello di sensibilità per ogni caratteristica</p>
</div>
""")
aversion_radios = []
with gr.Column():
for i, aversion_question in enumerate(AVERSIONS_QUESTIONS):
question = aversion_question.get("question", "")
aversion_name = aversion_question.get("aversion_name", "")
aversion = AVERSIONS[aversion_name]
with gr.Group(elem_classes="radio-aversion-card"):
with gr.Column():
# Titolo compatto con icona
gr.HTML(f"""
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">{aversion['Icon']} {aversion['DisplayName']}</span>
<div>
<div style="font-size: 14px; color: #D7D7D7; margin-top: 2px;">{question}</div>
</div>
</div>
""")
# Quanto ti dà fastidio questo fenomeno sensoriale?
av_radio = gr.Radio(
choices=[("Nessuno", 1), ("Poco", 2), ("Moderato", 3), ("Molto", 4), ("Estremo", 5)],
value=3,
show_label=False,
interactive=True,
elem_id=f"radio-{aversion_name.lower().replace(' ', '-')}",
)
aversion_radios.append(av_radio)
# Pulsante continua
with gr.Row():
continue_to_recommendations_button = gr.Button(
"Continua alle Raccomandazioni →",
variant="primary",
elem_classes="continue-to-recommendations-button",
size="lg",
)
with gr.Column(visible=False) as recommendations_page:
with gr.Column(visible=True) as recommendations_list_page:
gr.Markdown(
"# Sistema di Raccomandazione di Punti di Interesse"
)
gr.Markdown("""
Questo sistema raccomanda punti di interesse personalizzati per le preferenze sensoriali degli utenti.
Ogni raccomandazione include informazioni sensoriali dettagliate e spiegazioni personalizzate.
""")
recommendation_list = gr.HTML(label="Raccomandazioni")
with gr.Row(visible=False):
select_buttons = [
gr.Button(f"Select_{i}", elem_id=f"select-button-{i}")
for i in range(args.top_k)
]
with gr.Column(visible=False) as details_page_block:
gr.Markdown("# Dettagli POI")
poi_details = gr.HTML(label="Dettagli Punto di Interesse")
with gr.Column(visible=False) as recommendation_feedback_block:
gr.Markdown("## Ti è piaciuto questo luogo che ti abbiamo suggerito?")
with gr.Row(equal_height=True):
with gr.Column(scale=1):
rec_like_btn = gr.Button("👍 Sì", size="lg")
with gr.Column(scale=1):
rec_dislike_btn = gr.Button("👎 No", size="lg")
recommendation_feedback_result = gr.State(False)
recommendation_feedback_submitted = gr.State(False)
recommendation_feedback_message = gr.HTML(visible=False)
# SIMPLE SURVEY
with gr.Column(visible=False) as simple_survey_block:
gr.Markdown("## Ti è stata utile questa spiegazione?")
with gr.Row(equal_height=True):
with gr.Column(scale=1):
exp_like_btn = gr.Button("👍 Sì", size="lg")
with gr.Column(scale=1):
exp_dislike_btn = gr.Button("👎 No", size="lg")
simple_survey_result = gr.State(False)
simple_feedback_submitted = gr.State(False)
simple_feedback_message = gr.HTML(visible=False)
# ADVANCED SURVEY
with gr.Column(visible=False) as advanced_survey_block:
if args.survey_language == "it":
gr.Markdown("## Valuta questa spiegazione della raccomandazione")
else:
gr.Markdown("## Rate this explanation of the recommendation")
# Stato per tenere traccia del gruppo corrente
advanced_current_group_idx = gr.State(0)
# Carica i dati del questionario
from utils.survey_utils import SURVEY_OPTIONS
SURVEY_STATEMENTS = SURVEY_OPTIONS[args.survey_language].get("statements", {})
LIKERT_OPTIONS = SURVEY_OPTIONS[args.survey_language].get("likert_options", [])
# Contenitori per i gruppi di affermazioni
advanced_containers = []
advanced_responses = [[], [], []]
statements = SURVEY_STATEMENTS.copy()
random.shuffle(statements)
# Prendiamo il massimo di affermazioni per gruppo
max_statements_per_group = 2 # Per i primi due gruppi
for i in range(3):
with gr.Column(visible=(i == 0)) as container:
if args.survey_language == "it":
gr.Markdown(f"### Parte {i + 1} di 3")
else:
gr.Markdown(f"### Part {i + 1} of 3")
group_responses = []
# Creiamo spazio per il massimo numero di affermazioni possibili
for j in range(3 if i == 2 else max_statements_per_group):
response = gr.Radio(
choices=LIKERT_OPTIONS,
label=statements[i * max_statements_per_group + j],
type="value",
value=None,
)
group_responses.append(response)
advanced_responses[i] = group_responses
advanced_containers.append(container)
# Navigazione tra i gruppi
with gr.Row():
advanced_prev_btn = gr.Button("← Indietro", interactive=False)
advanced_next_btn = gr.Button("Avanti →")
advanced_submit_btn = gr.Button(
"Conferma Risposte", visible=False
)
advanced_feedback_submitted = gr.State(False)
advanced_feedback_message = gr.HTML(visible=False)
with gr.Column(visible=False) as back_to_recommendations_block:
gr.HTML(
'<hr style="margin: 80px 0 2px 0; border-top: 1px solid #ddd;">'
)
back_to_recommendations_button = gr.Button(
"← Torna alla lista di raccomandazioni",
variant="secondary",
elem_classes="back-button-custom",
)
def handle_login(user_id_value):
"""Gestisce il processo di login"""
success, message, login_status = login_user(user_id_value)
if not success:
return (
gr.update(value=message, visible=True), # status_message
gr.update(visible=False), # login_actions
gr.update(visible=False), # setup_profile_button
gr.update(visible=False), # proceed_to_recommendations_button
user_id_value, # current_user_id
login_status, # login_status
)
# Determina quale pulsante mostrare
show_setup = login_status in ["new_user", "existing_incomplete"]
show_proceed = login_status == "existing_complete"
return (
gr.update(value=message, visible=True), # status_message
gr.update(visible=True), # login_actions
gr.update(visible=show_setup), # setup_profile_button
gr.update(visible=show_proceed), # proceed_to_recommendations_button
user_id_value, # current_user_id
login_status, # login_status
)
def handle_poi_click(poi_id, poi_name, selected_pois, query):
"""Handle POI click - toggle selection"""
print(f"🔥 POI Button clicked: {poi_name}")
return toggle_poi_selection(poi_id, poi_name, selected_pois, query)
def handle_proceed_to_sensory_config(user_id, selected_pois):
"""Handle proceeding to next step"""
if not selected_pois:
gr.Warning("⚠️ Seleziona la card di almeno un Punto di Interesse per continuare!")
return
update_user_selected_pois(user_id, selected_pois)
gr.Info(f"✅ Procedendo con {len(selected_pois)} Punti di Interesse selezionati")
return navigate_to_sensory_config()
def navigate_to_poi_selection():
"""Naviga alla selezione POI"""
return (
gr.update(visible=False), # login_page
gr.update(visible=True), # poi_selection_page
gr.update(visible=False), # sensory_config_page
gr.update(visible=False), # recommendations_page
"poi_selection", # current_page
)
def navigate_to_sensory_config():
"""Naviga alla configurazione sensoriale"""
return (
gr.update(visible=False), # login_page
gr.update(visible=False), # poi_selection_page
gr.update(visible=True), # sensory_config_page
gr.update(visible=False), # recommendations_page
"sensory_config", # current_page
)
def navigate_to_recommendations():
"""Naviga alle raccomandazioni"""
return (
gr.update(visible=False), # login_page
gr.update(visible=False), # poi_selection_page
gr.update(visible=False), # sensory_config_page
gr.update(visible=True), # recommendations_page
"recommendations", # current_page
)
def complete_profile_setup(user_id):
"""Completa la configurazione del profilo"""
if user_id:
mark_profile_complete(user_id)
gr.Info(f"✅ Profilo {user_id} completato con successo!")
return navigate_to_recommendations()
login_button.click(
fn=handle_login,
inputs=[user_id_input],
outputs=[
login_status_message,
login_actions,
start_setup_btn,
go_to_recommendations_btn,
app_user_id,
user_status,
],
)
start_setup_btn.click(
fn=navigate_to_poi_selection,
outputs=[
login_page,
poi_selection_page,
sensory_config_page,
recommendations_page,
current_page,
],
)
go_to_recommendations_btn.click(
fn=complete_profile_setup,
inputs=[app_user_id],
outputs=[
login_page,
poi_selection_page,
sensory_config_page,
recommendations_page,
current_page,
],
).then(
fn=partial(get_recommendations_ui, k=args.top_k),
inputs=[app_user_id],
outputs=[recommendations_state, app_user_id, recommendation_list],
)
# Dynamic search - triggers on every keystroke
search_input.change(
fn=search_pois,
inputs=[search_input, current_selected_pois],
outputs=[poi_display],
)
# Set up click handlers for each POI button
for (poi_id, poi_name), button in poi_buttons.items():
button.click(
fn=partial(handle_poi_click, poi_id, poi_name),
inputs=[current_selected_pois, search_input],
outputs=[current_selected_pois, poi_display, selection_summary],
)
# Clear all selections
clear_button.click(
fn=clear_all_selections,
inputs=[search_input],
outputs=[current_selected_pois, poi_display, selection_summary],
)
continue_to_sensory_btn.click(
fn=handle_proceed_to_sensory_config,
inputs=[app_user_id, current_selected_pois],
outputs=[
login_page,
poi_selection_page,
sensory_config_page,
recommendations_page,
current_page,
],
)
# Salva preferenze
continue_to_recommendations_button.click(
fn=update_user_sensory_aversions,
inputs=[app_user_id, *aversion_radios],
).then(
fn=complete_profile_setup,
inputs=[app_user_id],
outputs=[
login_page,
poi_selection_page,
sensory_config_page,
recommendations_page,
current_page,
],
).then(
fn=partial(get_recommendations_ui, k=args.top_k),
inputs=[app_user_id],
outputs=[recommendations_state, app_user_id, recommendation_list],
)
for i, btn in enumerate(select_buttons):
btn.click(
fn=lambda current_idx=i: current_idx,
inputs=[],
outputs=[selected_poi_index],
).then(
fn=view_poi_details,
inputs=[recommendations_state, app_user_id, selected_poi_index, completed_survey_pois],
outputs=[
recommendations_list_page,
details_page_block,
poi_details,
recommendation_feedback_block,
back_to_recommendations_block,
current_poi_info,
],
)
surveys_objects = [
recommendation_feedback_result,
recommendation_feedback_submitted,
recommendation_feedback_message,
simple_survey_result,
simple_feedback_submitted,
simple_feedback_message,
advanced_current_group_idx,
advanced_feedback_submitted,
advanced_feedback_message,
]
back_to_recommendations_button.click(
fn=lambda: back_to_recommendations(advanced_responses, advanced_containers),
inputs=[],
outputs=[
recommendations_list_page,
details_page_block,
poi_details,
recommendation_feedback_block,
simple_survey_block,
advanced_survey_block,
back_to_recommendations_block,
*surveys_objects,
*[radio for group in advanced_responses for radio in group],
*advanced_containers,
],
).then(
fn=show_recommendation_list,
inputs=[recommendations_state, completed_survey_pois],
outputs=[recommendation_list]
)
rec_like_btn.click(
fn=feedback_message,
inputs=[],
outputs=[
recommendation_feedback_message,
],
show_progress="hidden",
).then(
fn=lambda: submit_rec_feedback(True),
inputs=[],
outputs=[
recommendation_feedback_result,
recommendation_feedback_submitted,
recommendation_feedback_message,
simple_survey_block,
recommendation_feedback_block,
],
js=js_feedback_message_delay,
show_progress="hidden",
)
rec_dislike_btn.click(
fn=feedback_message,
inputs=[],
outputs=[
recommendation_feedback_message,
],
show_progress="hidden",
).then(
fn=lambda: submit_rec_feedback(False),
inputs=[],
outputs=[
recommendation_feedback_result,
recommendation_feedback_submitted,
recommendation_feedback_message,
simple_survey_block,
recommendation_feedback_block,
],
js=js_feedback_message_delay,
show_progress="hidden",
)
# Configurazione eventi per il questionario semplice
exp_like_btn.click(
fn=feedback_message,
inputs=[],
outputs=[
simple_feedback_message,
],
show_progress="hidden",
).then(
fn=lambda: submit_exp_feedback(True),
inputs=[],
outputs=[
simple_survey_result,
simple_feedback_submitted,
simple_feedback_message,
advanced_survey_block,
simple_survey_block,
advanced_prev_btn,
advanced_next_btn,
advanced_submit_btn,
],
js=js_feedback_message_delay,
show_progress="hidden",
)
exp_dislike_btn.click(
fn=feedback_message,
inputs=[],
outputs=[
simple_feedback_message,
],
show_progress="hidden",
).then(
fn=lambda: submit_exp_feedback(False),
inputs=[],
outputs=[
simple_survey_result,
simple_feedback_submitted,
simple_feedback_message,
advanced_survey_block,
simple_survey_block,
advanced_prev_btn,
advanced_next_btn,
advanced_submit_btn,
],
js=js_feedback_message_delay,
show_progress="hidden",
)
# Create a list of outputs for navigation functions
navigation_outputs = [
advanced_current_group_idx, # next_group_idx
advanced_prev_btn, # prev_btn interactivity update
advanced_next_btn, # next_btn visibility update
advanced_submit_btn, # submit_btn visibility update
] + advanced_containers # container updates
advanced_next_btn.click(
fn=partial(handle_next_navigation, advanced_responses, advanced_containers),
inputs=[advanced_current_group_idx],
outputs=navigation_outputs,
).then(
fn=partial(handle_all_answered_responses, advanced_responses),
inputs=[advanced_current_group_idx],
outputs=[advanced_submit_btn, advanced_next_btn],
)
advanced_prev_btn.click(
fn=partial(handle_prev_navigation, advanced_containers),
inputs=[
advanced_current_group_idx,
],
outputs=navigation_outputs,
)
for group_idx, radio_group in enumerate(advanced_responses):
for radio_idx, radio_button in enumerate(radio_group):
radio_button.change(
fn=partial(
update_advanced_responses,
group_idx,
radio_idx,
advanced_responses,
),
inputs=[radio_button],
outputs=[],
).then(
fn=partial(handle_all_answered_responses, advanced_responses),
inputs=[advanced_current_group_idx],
outputs=[advanced_submit_btn, advanced_next_btn],
)
advanced_submit_btn.click(
fn=partial(
handle_submit_survey,
advanced_responses,
),
inputs=[
app_user_id,
current_poi_info,
recommendation_feedback_result,
simple_survey_result,
],
outputs=[
advanced_feedback_submitted,
advanced_feedback_message,
advanced_prev_btn,
advanced_next_btn,
advanced_submit_btn,
*advanced_containers,
],
).then(
fn=add_poi_to_completed,
inputs=[selected_poi_index, completed_survey_pois],
outputs=[completed_survey_pois],
)
return app
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the POI Search UI")
parser.add_argument(
"user_type",
choices=["autistic", "neurotypical"],
help="Type of user for the interface",
)
parser.add_argument(
"--top-k",
type=int,
default=5,
help="Number of top recommendations to show",
)
parser.add_argument(
"--survey_language",
choices=["it", "en"],
default="it",
help="Language for the survey questions",
)
parser.add_argument(
"--port", type=int, default=7860, help="Port to run the Gradio app on"
)
parser.add_argument("--share", action="store_true", help="Share the app publicly")
args = parser.parse_args()
app = create_main_app(args.user_type)
app.launch()