FSCSurveyFR / app.py
MMOON's picture
Update app.py
035eed7 verified
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import streamlit as st
from random import shuffle
import logging
import plotly.graph_objects as go
from PIL import Image
from io import BytesIO
import requests
# Configuration du logging
logging.basicConfig(level=logging.ERROR)
# Fonction pour détecter le type d'appareil (mobile ou desktop)
def is_mobile():
# Cette fonction utilise la largeur de la fenêtre comme heuristique
# pour déterminer si l'utilisateur est sur mobile
return st.session_state.get('device_type', 'mobile') == 'mobile'
# Initialisation de l'état de la session pour le type d'appareil
if 'device_type' not in st.session_state:
st.session_state.device_type = 'mobile' # Par défaut sur mobile
# Fonction pour charger l'image du logo
@st.cache_data(ttl=3600)
def load_logo(url):
try:
response = requests.get(url)
return Image.open(BytesIO(response.content))
except Exception as e:
st.error(f"Erreur lors du chargement du logo: {e}")
logging.error(e)
return None
# Configuration de la page
st.set_page_config(
page_title="FSC: Culture de Sécurité Alimentaire",
page_icon="🍽️",
layout="wide" if not is_mobile() else "centered",
initial_sidebar_state="expanded" if not is_mobile() else "collapsed"
)
# CSS Personnalisé en fonction du type d'appareil
def get_custom_css():
mobile_css = """
.main > div {
padding-left: 5px;
padding-right: 5px;
}
.stButton > button {
width: 100%;
border-radius: 20px;
height: 3em;
background-color: #2398B2;
color: white;
font-weight: bold;
}
.stSelectbox > div > div {
background-color: #f0f8ff;
border-radius: 10px;
}
h1, h2, h3 {
text-align: center;
}
.question-card {
background-color: white;
border-radius: 15px;
padding: 15px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-container {
text-align: center;
margin-bottom: 20px;
}
.logo-image {
max-width: 150px;
margin: 0 auto;
display: block;
}
"""
desktop_css = """
.stButton > button {
border-radius: 20px;
height: 2.5em;
min-width: 200px;
background-color: #2398B2;
color: white;
font-weight: bold;
}
.stSelectbox > div > div {
background-color: #f0f8ff;
border-radius: 10px;
}
.question-card {
background-color: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
[data-testid="stSidebar"] {
background-color: #2398B2;
padding-top: 20px;
}
.sidebar-title {
font-size: 1.5em;
font-weight: bold;
text-align: center;
margin-top: 20px;
color: white;
}
.sidebar-logo-container {
text-align: center;
margin-top: 20px;
}
.sidebar-logo {
max-width: 80%;
height: auto;
}
"""
return mobile_css if is_mobile() else desktop_css
st.markdown(f"<style>{get_custom_css()}</style>", unsafe_allow_html=True)
# Bascule entre mobile et desktop
col1, col2 = st.columns([1, 1])
with col1:
if st.button("📱 Version Mobile", key="mobile_button", type="primary" if is_mobile() else "secondary"):
st.session_state.device_type = 'mobile'
st.rerun()
with col2:
if st.button("💻 Version Desktop", key="desktop_button", type="primary" if not is_mobile() else "secondary"):
st.session_state.device_type = 'desktop'
st.rerun()
# Afficher le logo et le titre
logo_url = "https://raw.githubusercontent.com/M00N69/RAPPELCONSO/main/logo%2004%20copie.jpg"
logo = load_logo(logo_url)
if is_mobile():
# Interface mobile
st.markdown('<div class="header-container">', unsafe_allow_html=True)
if logo:
st.image(logo, width=120, use_container_width=False)
st.title("FSC: Culture de Sécurité Alimentaire")
st.markdown('</div>', unsafe_allow_html=True)
else:
# Interface desktop - Utiliser la sidebar
with st.sidebar:
st.image(logo, width=200)
st.markdown('<div class="sidebar-title">FSC: projet en construction....</div>', unsafe_allow_html=True)
st.markdown("<hr>", unsafe_allow_html=True)
# Dictionnaire des options de listes de questions
question_lists = {
"Questions de Base Food": "https://raw.githubusercontent.com/M00N69/CSVINSIGHT/refs/heads/main/listequestionFOOD.csv",
"Questions type C355 UE": "https://raw.githubusercontent.com/M00N69/CSVINSIGHT/refs/heads/main/listeC355like.csv",
"Questions broker": "https://raw.githubusercontent.com/M00N69/CSVINSIGHT/refs/heads/main/listequestionbroker.csv"
}
# Initialiser l'état de la session pour le choix de la liste de questions
if 'selected_list' not in st.session_state:
st.session_state.selected_list = None
# Sélecteur pour choisir la liste de questions
if st.session_state.selected_list is None:
st.markdown('<div class="question-card">', unsafe_allow_html=True)
st.subheader("Sélection de questionnaire")
selected_list = st.selectbox(
"Choisissez la liste de questions :",
options=list(question_lists.keys()),
format_func=lambda x: f"📋 {x}"
)
if st.button("✅ Confirmer le choix", key="confirm_choice_button"):
if selected_list:
st.session_state.selected_list = selected_list
else:
st.warning("Veuillez sélectionner une liste de questions pour continuer.")
st.markdown('</div>', unsafe_allow_html=True)
else:
selected_list = st.session_state.selected_list
# Vérifier si une liste est sélectionnée
if st.session_state.selected_list is not None:
# URL du fichier CSV sélectionné
url = question_lists[selected_list]
# Chargement des données avec gestion du cache
@st.cache_data(ttl=3600)
def load_data(url):
try:
return pd.read_csv(url, encoding='utf-8')
except Exception as e:
st.error(f"Erreur lors du chargement des questions depuis GitHub : {e}")
logging.error(e)
return pd.DataFrame()
# Charger le CSV depuis GitHub
questions_df = load_data(url)
if not questions_df.empty:
# Initialiser l'état de la session
if "questions_selectionnees" not in st.session_state:
st.session_state.questions_selectionnees = questions_df.sample(20).reset_index(drop=True)
st.session_state.current_question = 0
st.session_state.scores = []
st.session_state.responses = []
st.session_state.categories_scores = {}
st.success("Questions chargées avec succès !")
# Afficher la progression
progress = st.session_state.current_question / len(st.session_state.questions_selectionnees)
progress_text = f"Question {st.session_state.current_question + 1}/{len(st.session_state.questions_selectionnees)}"
st.progress(progress, text=progress_text)
# Vérifier si toutes les questions ont été répondues
if st.session_state.current_question < len(st.session_state.questions_selectionnees):
# Afficher la question actuelle
question = st.session_state.questions_selectionnees.iloc[st.session_state.current_question]
question_num = st.session_state.current_question + 1
st.markdown('<div class="question-card">', unsafe_allow_html=True)
st.subheader(f"Question {question_num}")
st.markdown(f"""
<div style='
background-color: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
border-left: 5px solid #2398B2;
font-size: {'18px' if is_mobile() else '20px'};
font-weight: 500;
'>
{question['Question']}
</div>
""", unsafe_allow_html=True)
# Mélanger les réponses aléatoirement
reponses = [
(question['Réponse 1'], question['Score 1']),
(question['Réponse 2'], question['Score 2']),
(question['Réponse 3'], question['Score 3'])
]
shuffle(reponses)
# Utiliser des boutons radio pour les réponses avec style amélioré
st.markdown("<label style='font-weight: 500; margin-bottom: 10px;'>Sélectionnez votre réponse :</label>", unsafe_allow_html=True)
# Ajout de style CSS pour les boutons radio
st.markdown("""
<style>
div.row-widget.stRadio > div {
flex-direction: column;
gap: 10px;
}
div.row-widget.stRadio > div[role="radiogroup"] > label {
background-color: #f0f8ff;
padding: 10px 15px;
border-radius: 10px;
border: 1px solid #e0e0e0;
transition: all 0.3s;
}
div.row-widget.stRadio > div[role="radiogroup"] > label:hover {
background-color: #e0f0ff;
border-color: #2398B2;
}
</style>
""", unsafe_allow_html=True)
choix = st.radio(
"", # Label vide car déjà ajouté au-dessus
[rep[0] for rep in reponses],
key=f"radio_{st.session_state.current_question}",
label_visibility="collapsed" # Masquer le label par défaut
)
# Bouton pour confirmer la réponse
if st.button("✅ Valider et continuer", key=f"confirm_button_{st.session_state.current_question}"):
# Enregistrer le score correspondant à la réponse choisie
score_choisi = next(score for rep, score in reponses if rep == choix)
# Ajouter le score dans la session
st.session_state.scores.append(int(score_choisi))
# Enregistrer la réponse choisie
st.session_state.responses.append((question['Question'], choix))
# Enregistrer le score par catégorie
categorie = question['Catégorie']
st.session_state.categories_scores[categorie] = st.session_state.categories_scores.get(categorie, 0) + int(score_choisi)
# Passer à la question suivante
st.session_state.current_question += 1
st.rerun()
st.markdown('</div>', unsafe_allow_html=True)
else:
# Résultats finaux
st.markdown('<div class="question-card">', unsafe_allow_html=True)
st.header("📊 Résultats de l'évaluation")
# Calculer le score total
score_total = sum(st.session_state.scores)
max_score_possible = len(st.session_state.questions_selectionnees) * 5
pourcentage_total = (score_total / max_score_possible) * 100
# Affichage du score global
st.metric(
label="Score Global",
value=f"{pourcentage_total:.1f}%",
delta=f"{score_total}/{max_score_possible} points"
)
# Calculer les scores par catégorie
categories = st.session_state.questions_selectionnees['Catégorie'].unique()
scores_par_categorie = {}
max_scores_par_categorie = {}
for categorie in categories:
questions_categorie = st.session_state.questions_selectionnees[st.session_state.questions_selectionnees['Catégorie'] == categorie]
scores_obtenus = [
score for i, score in enumerate(st.session_state.scores)
if st.session_state.questions_selectionnees.iloc[i]['Catégorie'] == categorie
]
scores_par_categorie[categorie] = sum(scores_obtenus)
max_scores_par_categorie[categorie] = len(scores_obtenus) * 5
# Normalisation des scores par catégorie
scores_normalises = {
categorie: (scores_par_categorie.get(categorie, 0) / max_scores_par_categorie.get(categorie, 1)) * 100
for categorie in categories
}
st.markdown('</div>', unsafe_allow_html=True)
# Graphique en radar avec Plotly (plus interactif et beau)
st.markdown('<div class="question-card">', unsafe_allow_html=True)
st.subheader("Analyse par catégorie")
categories_list = list(scores_normalises.keys())
scores_list = list(scores_normalises.values())
# Créer un graphique radar interactif avec Plotly
fig = go.Figure()
fig.add_trace(go.Scatterpolar(
r=scores_list,
theta=categories_list,
fill='toself',
name='Score',
line_color='#2398B2',
fillcolor='rgba(35, 152, 178, 0.3)'
))
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, 100]
)
),
showlegend=False,
margin=dict(l=10, r=10, t=30, b=10),
height=500 if not is_mobile() else 400
)
st.plotly_chart(fig, use_container_width=True)
# Barres de progression pour chaque catégorie
for categorie, score in scores_normalises.items():
col1, col2 = st.columns([3, 1])
with col1:
st.progress(score/100, text=categorie)
with col2:
st.write(f"{score:.1f}%")
st.markdown('</div>', unsafe_allow_html=True)
# Résumé des réponses
with st.expander("📝 Voir le détail de vos réponses"):
for i, (question, response) in enumerate(st.session_state.responses):
st.markdown(f"**Q{i+1}:** {question}")
st.markdown(f"**R:** {response}")
st.markdown("---")
# Conclusion
st.markdown('<div class="question-card">', unsafe_allow_html=True)
if pourcentage_total >= 80:
conclusion = "Félicitations ! Votre culture de sécurité alimentaire est excellente. Continuez ainsi !"
emoji = "🏆"
color = "green"
elif pourcentage_total >= 50:
conclusion = "Votre culture de sécurité alimentaire est satisfaisante, mais il y a des améliorations à apporter."
emoji = "✅"
color = "orange"
else:
conclusion = "Votre culture de sécurité alimentaire nécessite des améliorations significatives."
emoji = "⚠️"
color = "red"
st.markdown(f"<h2 style='color: {color}; text-align: center;'>{emoji} {conclusion}</h2>", unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)
# Bouton pour recommencer l'évaluation
if st.button("🔄 Recommencer l'évaluation", key="restart_button"):
# Conserver seulement le type d'appareil, réinitialiser le reste
device_type = st.session_state.device_type
for key in list(st.session_state.keys()):
if key != 'device_type':
del st.session_state[key]
st.session_state.device_type = device_type
st.rerun()
# Bouton pour changer de questionnaire
if st.button("📋 Choisir un autre questionnaire", key="change_list_button"):
device_type = st.session_state.device_type
for key in list(st.session_state.keys()):
if key != 'device_type':
del st.session_state[key]
st.session_state.device_type = device_type
st.rerun()
else:
st.write("Veuillez choisir une liste de questions pour commencer.")