Spaces:
Sleeping
Sleeping
| 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 | |
| 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 | |
| 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.") |