diff --git a/.env.template b/.env.template index e4306af..4511e20 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,5 @@ -TYPESENSE_API_KEY= -TYPESENSE_DATA_DIR= \ No newline at end of file +TYPESENSE_API_KEY=RANDOM_STRING_HERE +TYPESENSE_DATA_DIR=/data +TYPESENSE_HOST=localhost +TYPESENSE_PORT=8108 +TYPESENSE_PROTOCOL=http diff --git a/README.md b/README.md index 230e12b..709dc0d 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,9 @@ Tu peux obtenir une sauvegarde des données de tes réseaux sociaux. Je t'ai mis - Clone le projet avec Git - Configure ta clé API en copiant `.env.template` dans `.env` et en y mettant une clé API de ton choix +- Configure les autres variables d'environnement dans `.env`. - Exécute le fichier `docker-compose.yml` avec Docker Compose pour installer le moteur de recherche TypeSense -- Connecte-toi à l'application en lançant run_streamlit_app.py et en allant au http://localhost:8501 +- Connecte-toi à l'application en lançant run_streamlit_app.py et en allant au http://localhost:8501. - Si tout fonctionne, tu vas accéder à l'interface de recherche ## Mettre les fichiers au bon endroit diff --git a/import_data/10_install_nlp_models.py b/import_data/10_install_nlp_models.py new file mode 100644 index 0000000..61e4892 --- /dev/null +++ b/import_data/10_install_nlp_models.py @@ -0,0 +1,30 @@ +import spacy +import subprocess +import sys + +def download_spacy_model(model_name): + print(f"Downloading and installing spaCy model: {model_name}") + try: + subprocess.check_call([sys.executable, "-m", "spacy", "download", model_name]) + print(f"Successfully installed {model_name}") + except subprocess.CalledProcessError: + print(f"Error installing {model_name}. Please make sure you have the necessary permissions.") + +# Download and install English model +download_spacy_model("en_core_web_sm") + +# Download and install French model +download_spacy_model("fr_core_news_sm") + +# Load the models to verify installation +try: + nlp_en = spacy.load("en_core_web_sm") + print("English model loaded successfully") +except: + print("Error loading English model") + +try: + nlp_fr = spacy.load("fr_core_news_sm") + print("French model loaded successfully") +except: + print("Error loading French model") \ No newline at end of file diff --git a/import_data/requirements.txt b/import_data/requirements.txt deleted file mode 100644 index 6446f73..0000000 --- a/import_data/requirements.txt +++ /dev/null @@ -1,97 +0,0 @@ -altair==5.5.0 -annotated-types==0.7.0 -attrs==24.2.0 -av==13.1.0 -beautifulsoup4==4.12.3 -blinker==1.9.0 -blis==1.0.1 -cachetools==5.5.0 -catalogue==2.0.10 -certifi==2024.8.30 -charset-normalizer==3.4.0 -click==8.1.7 -cloudpathlib==0.20.0 -coloredlogs==15.0.1 -confection==0.1.5 -contourpy==1.3.1 -ctranslate2==4.5.0 -cycler==0.12.1 -cymem==2.0.10 -en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85 -faster-whisper==1.1.0 -filelock==3.16.1 -flatbuffers==24.3.25 -fonttools==4.55.0 -fr_core_news_sm @ https://github.com/explosion/spacy-models/releases/download/fr_core_news_sm-3.8.0/fr_core_news_sm-3.8.0-py3-none-any.whl#sha256=7d6ad14cd5078e53147bfbf70fb9d433c6a3865b695fda2657140bbc59a27e29 -fsspec==2024.10.0 -gitdb==4.0.11 -GitPython==3.1.43 -huggingface-hub==0.26.3 -humanfriendly==10.0 -idna==3.10 -Jinja2==3.1.4 -jsonschema==4.23.0 -jsonschema-specifications==2024.10.1 -kiwisolver==1.4.7 -langcodes==3.5.0 -langdetect==1.0.9 -language_data==1.3.0 -marisa-trie==1.2.1 -markdown-it-py==3.0.0 -markdownify==0.11.6 -MarkupSafe==3.0.2 -matplotlib==3.9.3 -mdurl==0.1.2 -mpmath==1.3.0 -murmurhash==1.0.11 -narwhals==1.15.0 -numpy==2.0.2 -onnxruntime==1.20.1 -packaging==24.2 -pandas==2.2.3 -pillow==11.0.0 -plotly==5.24.1 -preshed==3.0.9 -protobuf==5.29.0 -pyarrow==17.0.0 -pydantic==2.10.2 -pydantic_core==2.27.1 -pydeck==0.9.1 -Pygments==2.18.0 -pyparsing==3.2.0 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -pytz==2024.2 -PyYAML==6.0.2 -referencing==0.35.1 -requests==2.31.0 -rich==13.9.4 -rpds-py==0.21.0 -setuptools==75.6.0 -shellingham==1.5.4 -six==1.16.0 -smart-open==7.0.5 -smmap==5.0.1 -soupsieve==2.6 -spacy==3.8.2 -spacy-language-detection==0.2.1 -spacy-legacy==3.0.12 -spacy-loggers==1.0.5 -srsly==2.4.8 -streamlit==1.40.2 -sympy==1.13.3 -tenacity==9.0.0 -thinc==8.3.2 -tokenizers==0.21.0 -toml==0.10.2 -tornado==6.4.2 -tqdm==4.67.1 -typer==0.14.0 -typesense==0.21.0 -typing_extensions==4.12.2 -tzdata==2024.2 -urllib3==2.2.3 -wasabi==1.1.3 -weasel==0.4.1 -wrapt==1.17.0 -xmltodict==0.13.0 diff --git a/import_data/utils/reseau_social_data.py b/import_data/utils/reseau_social_data.py index e87bfec..13d8f0f 100644 --- a/import_data/utils/reseau_social_data.py +++ b/import_data/utils/reseau_social_data.py @@ -1,4 +1,4 @@ -import utils.config as config +from import_data.utils import config wordpress_names = config.WORDPRESS_NAMES.split(",") @@ -13,5 +13,5 @@ reseau_social_data = [{"nom": "LinkedIn", {"nom": "FacebookBusiness", "repertoires": ["posts"]}, {"nom": "Podcast", - "repertoires": ["shownotes", "audio"]} + "repertoires": ["shownotes", "audio", "feeds"]} ] diff --git a/import_data/utils/typesense_client.py b/import_data/utils/typesense_client.py index dc2f8c4..ff79cd8 100644 --- a/import_data/utils/typesense_client.py +++ b/import_data/utils/typesense_client.py @@ -6,10 +6,10 @@ load_dotenv() client = typesense.Client({ 'nodes': [{ - 'host': 'localhost', - 'port': '8108', - 'protocol': 'http' + 'host': os.getenv('TYPESENSE_HOST','localhost'), + 'port': os.getenv('TYPESENSE_PORT','8108'), + 'protocol': os.getenv('TYPESENSE_PROTOCOL','http'), }], 'api_key': os.getenv('TYPESENSE_API_KEY'), - 'connection_timeout_seconds': 2 + 'connection_timeout_seconds': 10 }) diff --git a/search_app_ui/rechercher_documents.py b/search_app_ui/rechercher_documents.py new file mode 100644 index 0000000..d48cafd --- /dev/null +++ b/search_app_ui/rechercher_documents.py @@ -0,0 +1,40 @@ +import streamlit as st +from import_data.utils.typesense_client import client + +def rechercher_documents(cette_requete, + ces_filtres=None, + facette_par=None, + query_by="texte,embedding", + sort_by="_text_match:desc,creation_timestamp:desc", + nb_buckets=10, + prefix=False, + per_page=10, + page=1): + parametres_recherche = { + 'q': cette_requete, + 'query_by': query_by, + 'sort_by': sort_by.replace( + "_text_match", + f"_text_match(buckets: {nb_buckets})"), + "exclude_fields": "embedding", + "prefix": str(prefix).lower(), + 'per_page': per_page, + 'page': page + } + + if ces_filtres: + parametres_recherche['filter_by'] = ces_filtres + + if facette_par: + parametres_recherche['facet_by'] = facette_par + + st.write("Search parameters:", parametres_recherche) + + all_results = [] + try: + results = client.collections['social_media_posts'].documents.search(parametres_recherche) + all_results.extend(results['hits']) + return results + + except Exception as e: + st.error(f"Error during search: {str(e)}") # Error handling \ No newline at end of file diff --git a/search_app_ui/recuperer_reseaux.py b/search_app_ui/recuperer_reseaux.py new file mode 100644 index 0000000..4d18660 --- /dev/null +++ b/search_app_ui/recuperer_reseaux.py @@ -0,0 +1,19 @@ +import streamlit as st + +from import_data.utils.typesense_client import client + + +def recuperer_reseaux(): + search_parameters = { + 'q': '*', + 'query_by': 'network', + 'facet_by': 'network', + 'per_page': 0 + } + try: + results = client.collections['social_media_posts'].documents.search(search_parameters) + networks = [facet['value'] for facet in results['facet_counts'][0]['counts']] + return networks + except Exception as e: + st.error(f"Erreur lors de la récupération des réseaux : {str(e)}") + return ['Facebook', 'Instagram', 'Threads', 'LinkedIn', 'WordPress'] # Valeurs par défaut en cas d'erreur diff --git a/search_app_ui/streamlit_app.py b/search_app_ui/streamlit_app.py index db8cb41..b3c7c10 100644 --- a/search_app_ui/streamlit_app.py +++ b/search_app_ui/streamlit_app.py @@ -1,134 +1,27 @@ import streamlit as st -import typesense from datetime import datetime, time import pandas as pd import plotly.express as px from dotenv import load_dotenv -import os + +from import_data.utils.typesense_client import client +from search_app_ui.rechercher_documents import rechercher_documents +from search_app_ui.recuperer_reseaux import recuperer_reseaux # Configurer la page en mode large st.set_page_config(layout="wide") -# Forcer le thème sombre -st.markdown(""" - -""", unsafe_allow_html=True) +# Load and apply the CSS +def load_css(file_name): + with open(file_name) as f: + st.markdown(f'', unsafe_allow_html=True) -# Ajouter ce CSS pour créer une zone de résultats défilable -st.markdown(""" - -""", unsafe_allow_html=True) +# Load the CSS file +load_css('style.css') # If the CSS file is in a subdirectory, adjust the path accordingly # Charger les variables d'environnement load_dotenv() -# Initialiser le client Typesense -client = typesense.Client({ - 'nodes': [{ - 'host': 'localhost', - 'port': '8108', - 'protocol': 'http' - }], - 'api_key': os.getenv('TYPESENSE_API_KEY'), - 'connection_timeout_seconds': 2 -}) - - -def rechercher_documents(cette_requete, ces_filtres=None, facette_par=None): - parametres_recherche = { - 'q': cette_requete, - 'query_by': 'texte,embedding', - 'sort_by': '_text_match(buckets: 10):desc,creation_timestamp:desc', - "exclude_fields": "embedding", - "prefix": "false", - 'per_page': 10, - 'page': 1 - } - - if ces_filtres: - parametres_recherche['filter_by'] = ces_filtres - - if facette_par: - parametres_recherche['facet_by'] = facette_par - - st.write("Search parameters:", parametres_recherche) - - all_results = [] - try: - while True: - results = client.collections['social_media_posts'].documents.search(parametres_recherche) - all_results.extend(results['hits']) - if len(all_results) >= results['found']: - break - parametres_recherche['page'] += 1 - results['hits'] = all_results - return results - - except Exception as e: - st.error(f"Error during search: {str(e)}") # Error handling - - -# Récupérer dynamiquement les réseaux depuis Typesense -def get_networks(): - search_parameters = { - 'q': '*', - 'query_by': 'network', - 'facet_by': 'network', - 'per_page': 0 - } - try: - results = client.collections['social_media_posts'].documents.search(search_parameters) - networks = [facet['value'] for facet in results['facet_counts'][0]['counts']] - return networks - except Exception as e: - st.error(f"Erreur lors de la récupération des réseaux : {str(e)}") - return ['Facebook', 'Instagram', 'Threads', 'LinkedIn', 'WordPress'] # Valeurs par défaut en cas d'erreur - - # Interface utilisateur Streamlit st.title('Recherche dans tes contenus publiés sur le web') @@ -147,7 +40,7 @@ date_fin = col2.date_input('Date de fin', value=datetime.now()) # Filtre de réseau social et de langue col3, col4 = st.columns(2) -reseaux = get_networks() +reseaux = recuperer_reseaux() reseaux_selectionnes = col3.multiselect('Sélectionnez les réseaux sociaux', reseaux, default=reseaux[0] if reseaux else None) langues = [('fr', 'Français'), ('en', 'English')] @@ -156,105 +49,187 @@ langue_selectionnees = col4.multiselect('Sélectionnez la langue', format_func=lambda x: x, default='Français') +# Filtre sur le nombre de mots +nombre_mots = st.slider('Nombre de mots minimum', min_value=0, max_value=1000, value=100, step=10) + # Convertir les étiquettes en codes de langage selected_lang_codes = [code for code, label in langues if label in langue_selectionnees] -# Filtre sur le nombre de mots -nombre_mots = st.slider('Nombre de mots minimum', min_value=0, max_value=1000, value=100, step=10) +# Nouvelle section pour les options de recherche avancées +st.sidebar.header("Options de recherche avancées") + +# Option pour activer/désactiver les options avancées +show_advanced_options = st.sidebar.checkbox("Activer les options avancées") + +if show_advanced_options: + # Champs de recherche + query_by = st.sidebar.multiselect( + "Champs de recherche", + ["texte", "embedding"], + default=["texte", "embedding"] + ) + + # Tri + sort_options = [ + "_text_match:desc", "creation_timestamp:desc", + "_text_match:asc", "creation_timestamp:asc", + ] + nb_buckets = st.sidebar.number_input("Nombre de buckets pour le tri", min_value=1, value=10) + sort_by = st.sidebar.multiselect( + "Trier par", + sort_options, + default=["_text_match:desc", "creation_timestamp:desc"] + ) + + # Préfixe + prefix = st.sidebar.checkbox("Activer la recherche par préfixe", value=False) + + # Pagination + per_page = st.sidebar.slider("Résultats par page", min_value=1, max_value=100, value=10) + page = st.sidebar.number_input("Page", min_value=1, value=1) + +else: + # Valeurs par défaut si les options avancées ne sont pas affichées + query_by = ["texte", "embedding"] + sort_by = ["_text_match:desc", "creation_timestamp:desc"] + prefix = False + nb_buckets = 10 + per_page = 10 + page = 1 if st.button('Rechercher'): # Préparer les filtres debut_datetime = datetime.combine(date_debut, time.min) fin_datetime = datetime.combine(date_fin, time.max) filtre_date = f"creation_timestamp:[{int(debut_datetime.timestamp())}..{int(fin_datetime.timestamp())}]" - filtre_reseau = f"network:[{' '.join(reseaux_selectionnes)}]" if reseaux_selectionnes else None - filtre_langue = f"langue:[{' '.join(selected_lang_codes)}]" if selected_lang_codes else None + filtre_reseau = f"network:=[{', '.join(reseaux_selectionnes)}]" if reseaux_selectionnes else None + filtre_langue = f"langue:=[{', '.join(selected_lang_codes)}]" if selected_lang_codes else None filtre_mots = f"nombre_de_mots:[{nombre_mots}..10000]" if nombre_mots > 0 else None - filtres = ' && '.join(filter(None, [filtre_date, filtre_reseau, filtre_langue, filtre_mots])) + liste_filtres = [] + if filtre_date: + liste_filtres.append(filtre_date) + if filtre_reseau: + liste_filtres.append(filtre_reseau) + if filtre_langue: + liste_filtres.append(filtre_langue) + if filtre_mots: + liste_filtres.append(filtre_mots) + filtres = ' && '.join(liste_filtres) if liste_filtres else None # Effectuer la recherche pour tous les résultats - tous_resultats = rechercher_documents(requete, ces_filtres=filtres, facette_par='network') - nombre_total_resultats = tous_resultats['found'] + tous_resultats = rechercher_documents( + requete, + ces_filtres=filtres, + facette_par='network', + query_by=','.join(query_by), + sort_by=','.join(sort_by), + nb_buckets=nb_buckets, + prefix=prefix, + per_page=per_page, + page=page + ) + if tous_resultats: + nombre_total_resultats = tous_resultats['found'] + # Afficher le nombre total de résultats + st.subheader(f"Trouvé {nombre_total_resultats} résultats parmi {total_documents } documents indexés") + else: + nombre_total_resultats = 0 + st.subheader("Aucun résultat trouvé") - # Afficher le nombre total de résultats - st.subheader(f"Trouvé {nombre_total_resultats} résultats parmi {total_documents } documents indexés") - # Affichage des résultats (100 maximum) - st.subheader("Résultats de la recherche") - - for hit in tous_resultats['hits'][:100]: # Limite à 100 résultats - col1, col2 = st.columns([1, 4]) - - with col1: - st.markdown(f"**{hit['document']['network']}**") - st.markdown( - f"**{datetime.fromtimestamp(hit['document']['creation_timestamp']).strftime('%Y-%m-%d %H:%M:%S')}**") - st.markdown(f"**{hit['document']['nombre_de_mots']} mots**") - # Score - st.markdown(f"**Score: {hit["hybrid_search_info"]['rank_fusion_score']}**") - # Étiquettes de couleur pour les facettes - st.markdown(f""" - - {hit['document']['langue']} - - """, unsafe_allow_html=True) - - with col2: - # Boîte de texte pour le contenu - st.text_area("Contenu", hit['document']['texte'], height=150) - - # URI en dessous - if 'uri' in hit['document']: - st.markdown(f"[Lien vers le post original]({hit['document']['uri']})") - - st.markdown("---") - - # Afficher les facettes - if 'facet_counts' in tous_resultats: - facettes_reseau = {facette['value']: facette['count'] for facette in - tous_resultats['facet_counts'][0]['counts']} - st.subheader("Résultats par Réseau") - - # Graphique en camembert pour montrer la distribution des résultats par réseau social - fig = px.pie(values=list(facettes_reseau.values()), names=list(facettes_reseau.keys()), - title="Distribution par Réseau") - # Ce graphique montre la proportion de résultats pour chaque réseau social - st.plotly_chart(fig) - - # Distribution temporelle par réseau et par mois if nombre_total_resultats > 0: - st.subheader("Résultats au fil du temps par réseau (agrégation mensuelle)") + # Affichage des résultats (100 maximum) + st.subheader("Résultats de la recherche") - df_temporel = pd.DataFrame({ - 'date': [datetime.fromtimestamp(hit['document']['creation_timestamp']) for hit in tous_resultats['hits']], - 'network': [hit['document']['network'] for hit in tous_resultats['hits']] - }) + for hit in tous_resultats['hits'][:100]: # Limite à 100 résultats + col1, col2 = st.columns([1, 4]) - df_temporel['mois'] = df_temporel['date'].dt.to_period('M') - df_temporel = df_temporel.groupby(['mois', 'network']).size().reset_index(name='count') - df_temporel['mois'] = df_temporel['mois'].dt.to_timestamp() + with col1: + st.markdown(f"**{hit['document']['network']}**") + st.markdown( + f"**{datetime.fromtimestamp(hit['document']['creation_timestamp']).strftime('%Y-%m-%d %H:%M:%S')}**") + st.markdown(f"**{hit['document']['nombre_de_mots']} mots**") + # Score + if "texte" in query_by and "embedding" in query_by: + score = hit["hybrid_search_info"]['rank_fusion_score'] + score_type = "Hybrid" + elif "texte" in query_by: + score = hit["text_match_info"]['score'] + score_type = "Text" + elif "embedding" in query_by: + score = hit["vector_distance"] + score_type = "Vector" + else: + score = "N/A" + score_type = "Unknown" + + st.markdown(f"**{score_type} Score: {score}**") + # Étiquettes de couleur pour les facettes + st.markdown(f""" + + {hit['document']['langue']} + + """, unsafe_allow_html=True) - # Graphique linéaire pour montrer l'évolution du nombre de posts par réseau au fil du temps - fig = px.line(df_temporel, x='mois', y='count', color='network', - title="Distribution temporelle par réseau (agrégation mensuelle)") - fig.update_layout(xaxis_title="Mois", yaxis_title="Nombre de posts") - fig.update_xaxes(tickformat="%B %Y") - # Ce graphique permet de visualiser les tendances de publication pour chaque réseau social au fil des mois - st.plotly_chart(fig) + with col2: + # Boîte de texte pour le contenu + st.text_area("Contenu", hit['document']['texte'], height=150) - # Graphique à barres empilées pour montrer la répartition des posts par réseau pour chaque mois - fig_bar = px.bar(df_temporel, x='mois', y='count', color='network', - title="Distribution temporelle par réseau (barres empilées, agrégation mensuelle)") - fig_bar.update_layout(xaxis_title="Mois", yaxis_title="Nombre de posts") - fig_bar.update_xaxes(tickformat="%B %Y") - # Ce graphique permet de comparer facilement le volume de posts entre les différents réseaux sociaux pour chaque mois - st.plotly_chart(fig_bar) + # URI en dessous + if 'uri' in hit['document']: + st.markdown(f"[Lien vers le post original]({hit['document']['uri']})") + # Affichage du hit brut + st.subheader("Données brutes") + st.json(hit, expanded=False) - st.subheader("Tableau récapitulatif mensuel") - df_pivot = df_temporel.pivot(index='mois', columns='network', values='count').fillna(0) - df_pivot['Total'] = df_pivot.sum(axis=1) - df_pivot = df_pivot.reset_index() - df_pivot['mois'] = df_pivot['mois'].dt.strftime('%B %Y') - # Ce tableau fournit un résumé détaillé du nombre de posts par réseau social pour chaque mois - st.dataframe(df_pivot) + st.markdown("---") + + # Afficher les facettes + if 'facet_counts' in tous_resultats: + facettes_reseau = {facette['value']: facette['count'] for facette in + tous_resultats['facet_counts'][0]['counts']} + st.subheader("Résultats par Réseau") + + # Graphique en camembert pour montrer la distribution des résultats par réseau social + fig = px.pie(values=list(facettes_reseau.values()), names=list(facettes_reseau.keys()), + title="Distribution par Réseau") + # Ce graphique montre la proportion de résultats pour chaque réseau social + st.plotly_chart(fig) + + # Distribution temporelle par réseau et par mois + if nombre_total_resultats > 0: + st.subheader("Résultats au fil du temps par réseau (agrégation mensuelle)") + + df_temporel = pd.DataFrame({ + 'date': [datetime.fromtimestamp(hit['document']['creation_timestamp']) for hit in tous_resultats['hits']], + 'network': [hit['document']['network'] for hit in tous_resultats['hits']] + }) + + df_temporel['mois'] = df_temporel['date'].dt.to_period('M') + df_temporel = df_temporel.groupby(['mois', 'network']).size().reset_index(name='count') + df_temporel['mois'] = df_temporel['mois'].dt.to_timestamp() + + # Graphique linéaire pour montrer l'évolution du nombre de posts par réseau au fil du temps + fig = px.line(df_temporel, x='mois', y='count', color='network', + title="Distribution temporelle par réseau (agrégation mensuelle)") + fig.update_layout(xaxis_title="Mois", yaxis_title="Nombre de posts") + fig.update_xaxes(tickformat="%B %Y") + # Ce graphique permet de visualiser les tendances de publication pour chaque réseau social au fil des mois + st.plotly_chart(fig) + + # Graphique à barres empilées pour montrer la répartition des posts par réseau pour chaque mois + fig_bar = px.bar(df_temporel, x='mois', y='count', color='network', + title="Distribution temporelle par réseau (barres empilées, agrégation mensuelle)") + fig_bar.update_layout(xaxis_title="Mois", yaxis_title="Nombre de posts") + fig_bar.update_xaxes(tickformat="%B %Y") + # Ce graphique permet de comparer facilement le volume de posts entre les différents réseaux sociaux pour chaque mois + st.plotly_chart(fig_bar) + + st.subheader("Tableau récapitulatif mensuel") + df_pivot = df_temporel.pivot(index='mois', columns='network', values='count').fillna(0) + df_pivot['Total'] = df_pivot.sum(axis=1) + df_pivot = df_pivot.reset_index() + df_pivot['mois'] = df_pivot['mois'].dt.strftime('%B %Y') + # Ce tableau fournit un résumé détaillé du nombre de posts par réseau social pour chaque mois + st.dataframe(df_pivot) diff --git a/search_app_ui/style.css b/search_app_ui/style.css new file mode 100644 index 0000000..d645a3f --- /dev/null +++ b/search_app_ui/style.css @@ -0,0 +1,41 @@ +/* Fond principal */ +.stApp { + background-color: #0e1117; + color: #fafafa; +} + +/* Barre latérale */ +.css-1d391kg { + background-color: #262730; +} + +/* Boutons */ +.stButton>button { + color: #fafafa; + background-color: #262730; + border-color: #fafafa; +} + +/* Champs de texte */ +.stTextInput>div>div>input { + color: #fafafa; + background-color: #262730; +} + +/* Boîte de sélection */ +.stSelectbox>div>div>select { + color: #fafafa; + background-color: #262730; +} + +/* Sélection multiple */ +.stMultiSelect>div>div>select { + color: #fafafa; + background-color: #262730; +} + +/* Saisie de date */ +.stDateInput>div>div>input { + color: #fafafa; + background-color: #262730; +} \ No newline at end of file