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