MMOON commited on
Commit
bb5b32f
·
verified ·
1 Parent(s): 33a1b2c

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +466 -130
src/streamlit_app.py CHANGED
@@ -1,4 +1,4 @@
1
- # streamlit_app_enhanced.py
2
  import streamlit as st
3
  import requests
4
  from bs4 import BeautifulSoup
@@ -7,25 +7,32 @@ import pandas as pd
7
  from datetime import datetime
8
  import urllib.parse
9
  import time
 
 
10
 
11
  # --- Configuration ---
12
  CODEX_CATEGORIES = {
13
  'codes': {
14
  'name': 'Codes de Pratique (CXC)',
15
  'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/codes-of-practice/fr/',
16
- 'prefix': 'CXC'
 
 
17
  },
18
  'standards': {
19
  'name': 'Normes (CXS)',
20
  'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/list-standards/fr/',
21
- 'prefix': 'CXS'
 
 
22
  },
23
  'guidelines': {
24
  'name': 'Directives (CXG)',
25
  'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/guidelines/fr/',
26
- 'prefix': 'CXG'
 
 
27
  }
28
- # 'misc' peut être ajouté si nécessaire
29
  }
30
 
31
  HEADERS = {
@@ -33,12 +40,94 @@ HEADERS = {
33
  }
34
  TIMEOUT = 30
35
  BASE_URL = "https://www.fao.org"
36
- # --- Fin Configuration ---
37
 
38
  # Configuration de la page
39
- st.set_page_config(page_title="Codex Alimentarius Monitor", page_icon="📋", layout="wide")
 
 
 
 
 
40
 
41
- @st.cache_data(ttl=1800) # 30 minutes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def extract_documents_from_url(url, category_key):
43
  """
44
  Fonction pour extraire les documents d'une catégorie Codex.
@@ -46,7 +135,6 @@ def extract_documents_from_url(url, category_key):
46
  """
47
  category_info = CODEX_CATEGORIES[category_key]
48
  category_name = category_info['name']
49
- st.info(f"Extraction de {category_name}...")
50
 
51
  documents = []
52
  seen_codes = set()
@@ -58,19 +146,15 @@ def extract_documents_from_url(url, category_key):
58
 
59
  tables = soup.find_all('table')
60
  if not tables:
61
- st.warning(f"Aucun tableau trouvé pour {category_name}.")
62
  return documents
63
 
64
  for table in tables:
65
  rows = table.find_all('tr')
66
  for row in rows:
67
  cells = row.find_all(['td', 'th'])
68
- # Vérifier s'il y a au moins 5 cellules (données + cellule PDF)
69
  if len(cells) >= 5:
70
- # Extraire les données de base (cellules 1 à 4)
71
  cell_texts = [cell.get_text(strip=True) for cell in cells[:4]]
72
  code_candidate = cell_texts[0] if cell_texts else ""
73
- # Pattern pour le préfixe de la catégorie
74
  prefix = category_info['prefix']
75
  code_match = re.match(rf'^({prefix})\s+([\w\-R]*\d+(?:-\d+)?[R]?)$', code_candidate)
76
 
@@ -90,29 +174,21 @@ def extract_documents_from_url(url, category_key):
90
  except ValueError:
91
  year = 0
92
 
93
- # --- EXTRACTION DU LIEN PDF DIRECTEMENT DU HREF ---
94
- # Le lien PDF est dans la 5ème cellule (index 4)
95
  pdf_cell = cells[4]
96
  pdf_url = None
97
-
98
- # Trouver le premier lien <a> dans cette cellule qui contient 'pdf'
99
  link_tag = pdf_cell.find('a', href=re.compile(r'.*\.pdf', re.IGNORECASE))
100
  if link_tag:
101
  href = link_tag.get('href')
102
  if href:
103
- # 1. Décoder les entités HTML (&amp; -> &)
104
  decoded_href = urllib.parse.unquote(href)
105
-
106
- # 2. Construire l'URL absolue
107
  pdf_url = urllib.parse.urljoin(BASE_URL, decoded_href)
108
 
109
- # Si aucun lien PDF n'a été trouvé, lien de recherche
110
  if not pdf_url:
111
  pdf_url = f"https://www.fao.org/fao-who-codexalimentarius/search/en/?q={full_code.replace(' ', '%20')}"
112
 
113
- # --- Logique de nouveauté ---
114
- is_new = year >= datetime.now().year - 1 # Considéré nouveau si dans les 2 dernières années
115
- is_updated = year == datetime.now().year # Mis à jour cette année
116
 
117
  documents.append({
118
  'code': full_code,
@@ -123,151 +199,411 @@ def extract_documents_from_url(url, category_key):
123
  'category_name': category_name,
124
  'pdf_url': pdf_url,
125
  'is_new': is_new,
126
- 'is_updated': is_updated
 
 
127
  })
128
- st.success(f"Extraction terminée pour {category_name}. {len(documents)} documents trouvés.")
129
  return documents
130
 
131
  except Exception as e:
132
  st.error(f"Erreur lors de l'extraction de {category_name} : {e}")
133
  return []
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  # Initialisation de l'état de session
136
  if 'documents' not in st.session_state:
137
  st.session_state.documents = []
138
  st.session_state.last_update = None
139
 
140
- # --- Interface Utilisateur ---
141
- st.title("📋 Moniteur Codex Alimentarius Amélioré")
142
- st.markdown("Extraction et affichage des documents CXC, CXS et CXG avec liens de téléchargement.")
 
 
 
 
 
 
143
 
144
- # Barre latérale
145
  with st.sidebar:
146
- st.header("🎛️ Contrôles")
147
 
148
- if st.button("🔄 Charger/Mettre à jour les Documents", type="primary"):
149
- with st.spinner("Extraction en cours..."):
150
- all_documents = []
151
- progress_bar = st.progress(0)
152
- status_text = st.empty()
153
-
154
- for i, (cat_key, cat_info) in enumerate(CODEX_CATEGORIES.items()):
155
- status_text.info(f"Extraction de {cat_info['name']}...")
156
- docs = extract_documents_from_url(cat_info['url'], cat_key)
157
- all_documents.extend(docs)
158
- progress_bar.progress((i + 1) / len(CODEX_CATEGORIES))
159
- time.sleep(0.5) # Petite pause
160
-
161
- status_text.empty()
162
- st.session_state.documents = all_documents
163
- st.session_state.last_update = datetime.now()
164
- st.success("✅ Chargement terminé!")
165
-
166
- if st.session_state.last_update:
167
- st.caption(f"Dernière mise à jour : {st.session_state.last_update.strftime('%d/%m/%Y à %H:%M:%S')}")
 
168
 
 
 
 
169
  st.divider()
170
- st.header("🔍 Filtres")
171
 
 
172
  if st.session_state.documents:
173
- df_all = pd.DataFrame(st.session_state.documents)
174
 
175
- # Filtre par catégorie
176
- categories = ['Toutes'] + list(df_all['category_name'].unique())
177
- selected_category = st.selectbox("Catégorie:", categories)
 
 
 
 
 
 
 
 
178
 
179
- # Filtre par comité
180
- committees = ['Tous'] + sorted(df_all['committee'].unique())
181
- selected_committee = st.selectbox("Comité:", committees)
 
 
 
182
 
183
- # Filtre par nouveauté
184
- st.subheader("Statut")
185
- filter_new = st.checkbox("Nouveaux (2 dernières années)")
186
- filter_updated = st.checkbox("Mis à jour cette année")
 
 
 
 
187
 
188
- # Recherche textuelle
189
- search_term = st.text_input("🔍 Recherche (code ou titre):")
190
 
191
- # Application des filtres
192
- filtered_df = df_all.copy()
193
 
194
- if selected_category != 'Toutes':
195
- filtered_df = filtered_df[filtered_df['category_name'] == selected_category]
 
196
 
197
- if selected_committee != 'Tous':
198
- filtered_df = filtered_df[filtered_df['committee'] == selected_committee]
 
199
 
200
- if filter_new:
201
- filtered_df = filtered_df[filtered_df['is_new']]
202
 
203
- if filter_updated:
204
- filtered_df = filtered_df[filtered_df['is_updated']]
205
 
206
- if search_term:
207
- filtered_df = filtered_df[
208
- filtered_df['title'].str.contains(search_term, case=False, na=False) |
209
- filtered_df['code'].str.contains(search_term, case=False, na=False)
210
- ]
211
-
212
- # Stocker le DataFrame filtré dans st.session_state pour l'utiliser ailleurs
213
- st.session_state.filtered_df = filtered_df
214
- else:
215
- st.info("Chargez les documents pour activer les filtres.")
216
 
217
- # Afficher les documents
 
 
 
 
 
 
 
 
 
 
218
  if st.session_state.documents:
219
  df_display = st.session_state.get('filtered_df', pd.DataFrame(st.session_state.documents))
220
-
221
- # Trier par année (desc) puis code
222
  df_display = df_display.sort_values(by=['year', 'code'], ascending=[False, True]).reset_index(drop=True)
223
 
224
- st.divider()
225
- st.subheader(f"📚 Documents ({len(df_display)} trouvés)")
226
 
227
- # Affichage dans des "cartes" pour chaque document
228
- for index, doc in df_display.iterrows():
229
- with st.container(border=True):
230
- col1, col2 = st.columns([3, 1])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
  with col1:
233
- # Titre et code
234
- badge_html = f"<strong>{doc['code']}</strong> "
235
- if doc['is_new']:
236
- badge_html += "<span style='background-color: #90EE90; color: black; padding: 2px 6px; border-radius: 4px; font-size: 0.8em; margin-left: 5px;'>NOUVEAU</span>"
237
- if doc['is_updated']:
238
- badge_html += "<span style='background-color: #87CEEB; color: black; padding: 2px 6px; border-radius: 4px; font-size: 0.8em; margin-left: 5px;'>MIS À JOUR</span>"
239
- badge_html += f" <span style='background-color: #D3D3D3; color: black; padding: 2px 6px; border-radius: 4px; font-size: 0.8em; margin-left: 5px;'>{doc['category_name']}</span>"
240
- st.markdown(badge_html, unsafe_allow_html=True)
241
-
242
- st.markdown(f"**{doc['title']}**")
243
- st.caption(f"🏢 Comité: {doc['committee']} | 📅 Année: {doc['year']}")
244
 
245
  with col2:
246
- # Lien de téléchargement
247
- st.link_button("📄 Télécharger le PDF", doc['pdf_url'], type="primary", use_container_width=True)
 
 
 
 
 
 
248
 
249
- st.divider()
250
-
251
- # Option d'export
252
- st.divider()
253
- st.subheader("💾 Exporter les données filtrées")
254
- col1, col2 = st.columns(2)
255
- with col1:
256
- csv = df_display.to_csv(index=False, sep=';')
257
- st.download_button(
258
- label="📄 Télécharger en CSV",
259
- data=csv,
260
- file_name=f"codex_documents_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
261
- mime="text/csv",
262
- )
263
- with col2:
264
- json_str = df_display.to_json(orient='records', indent=2)
265
- st.download_button(
266
- label="📋 Télécharger en JSON",
267
- data=json_str,
268
- file_name=f"codex_documents_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
269
- mime="application/json",
270
- )
271
 
272
  else:
273
- st.info("👈 Cliquez sur le bouton 'Charger/Mettre à jour les Documents' dans la barre latérale pour démarrer.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # codex_app_improved.py
2
  import streamlit as st
3
  import requests
4
  from bs4 import BeautifulSoup
 
7
  from datetime import datetime
8
  import urllib.parse
9
  import time
10
+ import plotly.express as px
11
+ import plotly.graph_objects as go
12
 
13
  # --- Configuration ---
14
  CODEX_CATEGORIES = {
15
  'codes': {
16
  'name': 'Codes de Pratique (CXC)',
17
  'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/codes-of-practice/fr/',
18
+ 'prefix': 'CXC',
19
+ 'icon': '📋',
20
+ 'color': '#FF6B6B'
21
  },
22
  'standards': {
23
  'name': 'Normes (CXS)',
24
  'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/list-standards/fr/',
25
+ 'prefix': 'CXS',
26
+ 'icon': '⚖️',
27
+ 'color': '#4ECDC4'
28
  },
29
  'guidelines': {
30
  'name': 'Directives (CXG)',
31
  'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/guidelines/fr/',
32
+ 'prefix': 'CXG',
33
+ 'icon': '📖',
34
+ 'color': '#45B7D1'
35
  }
 
36
  }
37
 
38
  HEADERS = {
 
40
  }
41
  TIMEOUT = 30
42
  BASE_URL = "https://www.fao.org"
 
43
 
44
  # Configuration de la page
45
+ st.set_page_config(
46
+ page_title="Codex Alimentarius Monitor",
47
+ page_icon="🔬",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded"
50
+ )
51
 
52
+ # CSS personnalisé pour améliorer le design
53
+ st.markdown("""
54
+ <style>
55
+ .main-header {
56
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
57
+ padding: 2rem;
58
+ border-radius: 10px;
59
+ margin-bottom: 2rem;
60
+ color: white;
61
+ text-align: center;
62
+ }
63
+ .metric-container {
64
+ background: white;
65
+ padding: 1.5rem;
66
+ border-radius: 10px;
67
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
68
+ text-align: center;
69
+ margin-bottom: 1rem;
70
+ }
71
+ .category-card {
72
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
73
+ padding: 1.5rem;
74
+ border-radius: 10px;
75
+ border-left: 5px solid;
76
+ margin-bottom: 1rem;
77
+ transition: transform 0.2s;
78
+ }
79
+ .category-card:hover {
80
+ transform: translateY(-2px);
81
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
82
+ }
83
+ .document-card {
84
+ background: white;
85
+ padding: 1.5rem;
86
+ border-radius: 10px;
87
+ border: 1px solid #e0e0e0;
88
+ margin-bottom: 1rem;
89
+ transition: all 0.2s;
90
+ }
91
+ .document-card:hover {
92
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
93
+ border-color: #667eea;
94
+ }
95
+ .status-badge {
96
+ padding: 0.25rem 0.75rem;
97
+ border-radius: 20px;
98
+ font-size: 0.8rem;
99
+ font-weight: bold;
100
+ margin: 0.25rem;
101
+ display: inline-block;
102
+ }
103
+ .badge-new {
104
+ background-color: #d4edda;
105
+ color: #155724;
106
+ border: 1px solid #c3e6cb;
107
+ }
108
+ .badge-updated {
109
+ background-color: #cce5ff;
110
+ color: #004085;
111
+ border: 1px solid #b3d9ff;
112
+ }
113
+ .badge-category {
114
+ background-color: #f8f9fa;
115
+ color: #495057;
116
+ border: 1px solid #dee2e6;
117
+ }
118
+ .filter-section {
119
+ background: #f8f9fa;
120
+ padding: 1rem;
121
+ border-radius: 10px;
122
+ margin-bottom: 1rem;
123
+ }
124
+ .sidebar .stSelectbox > div > div > div {
125
+ background-color: white;
126
+ }
127
+ </style>
128
+ """, unsafe_allow_html=True)
129
+
130
+ @st.cache_data(ttl=1800)
131
  def extract_documents_from_url(url, category_key):
132
  """
133
  Fonction pour extraire les documents d'une catégorie Codex.
 
135
  """
136
  category_info = CODEX_CATEGORIES[category_key]
137
  category_name = category_info['name']
 
138
 
139
  documents = []
140
  seen_codes = set()
 
146
 
147
  tables = soup.find_all('table')
148
  if not tables:
 
149
  return documents
150
 
151
  for table in tables:
152
  rows = table.find_all('tr')
153
  for row in rows:
154
  cells = row.find_all(['td', 'th'])
 
155
  if len(cells) >= 5:
 
156
  cell_texts = [cell.get_text(strip=True) for cell in cells[:4]]
157
  code_candidate = cell_texts[0] if cell_texts else ""
 
158
  prefix = category_info['prefix']
159
  code_match = re.match(rf'^({prefix})\s+([\w\-R]*\d+(?:-\d+)?[R]?)$', code_candidate)
160
 
 
174
  except ValueError:
175
  year = 0
176
 
177
+ # Extraction du lien PDF
 
178
  pdf_cell = cells[4]
179
  pdf_url = None
 
 
180
  link_tag = pdf_cell.find('a', href=re.compile(r'.*\.pdf', re.IGNORECASE))
181
  if link_tag:
182
  href = link_tag.get('href')
183
  if href:
 
184
  decoded_href = urllib.parse.unquote(href)
 
 
185
  pdf_url = urllib.parse.urljoin(BASE_URL, decoded_href)
186
 
 
187
  if not pdf_url:
188
  pdf_url = f"https://www.fao.org/fao-who-codexalimentarius/search/en/?q={full_code.replace(' ', '%20')}"
189
 
190
+ is_new = year >= datetime.now().year - 1
191
+ is_updated = year == datetime.now().year
 
192
 
193
  documents.append({
194
  'code': full_code,
 
199
  'category_name': category_name,
200
  'pdf_url': pdf_url,
201
  'is_new': is_new,
202
+ 'is_updated': is_updated,
203
+ 'icon': category_info['icon'],
204
+ 'color': category_info['color']
205
  })
206
+
207
  return documents
208
 
209
  except Exception as e:
210
  st.error(f"Erreur lors de l'extraction de {category_name} : {e}")
211
  return []
212
 
213
+ def create_dashboard_metrics(df):
214
+ """Créer des métriques pour le dashboard"""
215
+ if df.empty:
216
+ return
217
+
218
+ col1, col2, col3, col4 = st.columns(4)
219
+
220
+ with col1:
221
+ st.markdown("""
222
+ <div class="metric-container">
223
+ <h2 style="color: #667eea; margin: 0;">📊</h2>
224
+ <h3 style="margin: 0.5rem 0;">{}</h3>
225
+ <p style="color: #666; margin: 0;">Total Documents</p>
226
+ </div>
227
+ """.format(len(df)), unsafe_allow_html=True)
228
+
229
+ with col2:
230
+ new_docs = len(df[df['is_new']])
231
+ st.markdown("""
232
+ <div class="metric-container">
233
+ <h2 style="color: #28a745; margin: 0;">✨</h2>
234
+ <h3 style="margin: 0.5rem 0;">{}</h3>
235
+ <p style="color: #666; margin: 0;">Nouveaux</p>
236
+ </div>
237
+ """.format(new_docs), unsafe_allow_html=True)
238
+
239
+ with col3:
240
+ updated_docs = len(df[df['is_updated']])
241
+ st.markdown("""
242
+ <div class="metric-container">
243
+ <h2 style="color: #17a2b8; margin: 0;">🔄</h2>
244
+ <h3 style="margin: 0.5rem 0;">{}</h3>
245
+ <p style="color: #666; margin: 0;">Mis à jour</p>
246
+ </div>
247
+ """.format(updated_docs), unsafe_allow_html=True)
248
+
249
+ with col4:
250
+ categories = df['category_name'].nunique()
251
+ st.markdown("""
252
+ <div class="metric-container">
253
+ <h2 style="color: #ffc107; margin: 0;">📂</h2>
254
+ <h3 style="margin: 0.5rem 0;">{}</h3>
255
+ <p style="color: #666; margin: 0;">Catégories</p>
256
+ </div>
257
+ """.format(categories), unsafe_allow_html=True)
258
+
259
+ def create_category_overview(df):
260
+ """Créer un aperçu des catégories"""
261
+ if df.empty:
262
+ return
263
+
264
+ st.subheader("📋 Aperçu par Catégorie")
265
+
266
+ category_stats = df.groupby(['category_name', 'category']).agg({
267
+ 'code': 'count',
268
+ 'is_new': 'sum',
269
+ 'is_updated': 'sum'
270
+ }).reset_index()
271
+
272
+ for _, row in category_stats.iterrows():
273
+ category_key = row['category']
274
+ category_info = CODEX_CATEGORIES[category_key]
275
+
276
+ st.markdown(f"""
277
+ <div class="category-card" style="border-left-color: {category_info['color']};">
278
+ <div style="display: flex; justify-content: space-between; align-items: center;">
279
+ <div>
280
+ <h4 style="margin: 0; color: #333;">
281
+ {category_info['icon']} {row['category_name']}
282
+ </h4>
283
+ <p style="margin: 0.5rem 0; color: #666;">
284
+ {int(row['code'])} documents •
285
+ {int(row['is_new'])} nouveaux •
286
+ {int(row['is_updated'])} mis à jour
287
+ </p>
288
+ </div>
289
+ <div style="font-size: 2rem; opacity: 0.3;">
290
+ {category_info['icon']}
291
+ </div>
292
+ </div>
293
+ </div>
294
+ """, unsafe_allow_html=True)
295
+
296
+ def create_visualization(df):
297
+ """Créer des visualisations"""
298
+ if df.empty:
299
+ return
300
+
301
+ st.subheader("📊 Visualisations")
302
+
303
+ col1, col2 = st.columns(2)
304
+
305
+ with col1:
306
+ # Graphique par catégorie
307
+ category_counts = df.groupby(['category_name']).size().reset_index(name='count')
308
+ fig_pie = px.pie(
309
+ category_counts,
310
+ values='count',
311
+ names='category_name',
312
+ title="Répartition par Catégorie",
313
+ color_discrete_sequence=['#FF6B6B', '#4ECDC4', '#45B7D1']
314
+ )
315
+ fig_pie.update_layout(height=400)
316
+ st.plotly_chart(fig_pie, use_container_width=True)
317
+
318
+ with col2:
319
+ # Graphique par année
320
+ year_counts = df[df['year'] > 2000].groupby('year').size().reset_index(name='count')
321
+ fig_bar = px.bar(
322
+ year_counts,
323
+ x='year',
324
+ y='count',
325
+ title="Documents par Année",
326
+ color='count',
327
+ color_continuous_scale='viridis'
328
+ )
329
+ fig_bar.update_layout(height=400)
330
+ st.plotly_chart(fig_bar, use_container_width=True)
331
+
332
+ def display_documents_grid(df):
333
+ """Afficher les documents dans une grille"""
334
+ if df.empty:
335
+ st.info("Aucun document trouvé avec les filtres sélectionnés.")
336
+ return
337
+
338
+ # Pagination
339
+ docs_per_page = 10
340
+ total_pages = (len(df) - 1) // docs_per_page + 1
341
+
342
+ if 'current_page' not in st.session_state:
343
+ st.session_state.current_page = 1
344
+
345
+ col1, col2, col3 = st.columns([1, 2, 1])
346
+ with col2:
347
+ page = st.selectbox(
348
+ "Page",
349
+ range(1, total_pages + 1),
350
+ index=st.session_state.current_page - 1,
351
+ key="page_selector"
352
+ )
353
+ st.session_state.current_page = page
354
+
355
+ # Documents pour la page actuelle
356
+ start_idx = (page - 1) * docs_per_page
357
+ end_idx = start_idx + docs_per_page
358
+ page_docs = df.iloc[start_idx:end_idx]
359
+
360
+ st.markdown(f"**Affichage de {start_idx + 1}-{min(end_idx, len(df))} sur {len(df)} documents**")
361
+
362
+ for _, doc in page_docs.iterrows():
363
+ # Badges de statut
364
+ badges_html = f"<span class='status-badge badge-category'>{doc['icon']} {doc['category_name']}</span>"
365
+ if doc['is_new']:
366
+ badges_html += "<span class='status-badge badge-new'>✨ NOUVEAU</span>"
367
+ if doc['is_updated']:
368
+ badges_html += "<span class='status-badge badge-updated'>🔄 MIS À JOUR</span>"
369
+
370
+ st.markdown(f"""
371
+ <div class="document-card">
372
+ <div style="display: flex; justify-content: space-between; align-items: start;">
373
+ <div style="flex: 1;">
374
+ <div style="margin-bottom: 0.5rem;">
375
+ {badges_html}
376
+ </div>
377
+ <h4 style="margin: 0.5rem 0; color: #333;">
378
+ {doc['code']} - {doc['title']}
379
+ </h4>
380
+ <p style="color: #666; margin: 0;">
381
+ 🏢 Comité: {doc['committee']} • 📅 Année: {doc['year']}
382
+ </p>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ """, unsafe_allow_html=True)
387
+
388
+ col1, col2 = st.columns([3, 1])
389
+ with col2:
390
+ st.link_button(
391
+ "📄 Télécharger PDF",
392
+ doc['pdf_url'],
393
+ type="primary",
394
+ use_container_width=True
395
+ )
396
+
397
  # Initialisation de l'état de session
398
  if 'documents' not in st.session_state:
399
  st.session_state.documents = []
400
  st.session_state.last_update = None
401
 
402
+ # Interface utilisateur principale
403
+ st.markdown("""
404
+ <div class="main-header">
405
+ <h1 style="margin: 0;">🔬 Codex Alimentarius Monitor</h1>
406
+ <p style="margin: 0.5rem 0 0 0; opacity: 0.9;">
407
+ Exploration et suivi des normes alimentaires internationales
408
+ </p>
409
+ </div>
410
+ """, unsafe_allow_html=True)
411
 
412
+ # Barre latérale améliorée
413
  with st.sidebar:
414
+ st.markdown("### 🎛️ Centre de Contrôle")
415
 
416
+ # Section de chargement des données
417
+ with st.container():
418
+ st.markdown("#### 📊 Données")
419
+ if st.button("🔄 Actualiser les Documents", type="primary", use_container_width=True):
420
+ with st.spinner("🔍 Extraction en cours..."):
421
+ all_documents = []
422
+ progress_bar = st.progress(0)
423
+ status_container = st.container()
424
+
425
+ for i, (cat_key, cat_info) in enumerate(CODEX_CATEGORIES.items()):
426
+ with status_container:
427
+ st.info(f"{cat_info['icon']} Extraction de {cat_info['name']}...")
428
+ docs = extract_documents_from_url(cat_info['url'], cat_key)
429
+ all_documents.extend(docs)
430
+ progress_bar.progress((i + 1) / len(CODEX_CATEGORIES))
431
+ time.sleep(0.5)
432
+
433
+ st.session_state.documents = all_documents
434
+ st.session_state.last_update = datetime.now()
435
+ status_container.empty()
436
+ st.success("✅ Données actualisées!")
437
 
438
+ if st.session_state.last_update:
439
+ st.caption(f"🕒 Dernière mise à jour: {st.session_state.last_update.strftime('%d/%m/%Y à %H:%M')}")
440
+
441
  st.divider()
 
442
 
443
+ # Filtres avancés
444
  if st.session_state.documents:
445
+ st.markdown("#### 🔍 Filtres Avancés")
446
 
447
+ with st.container():
448
+ df_all = pd.DataFrame(st.session_state.documents)
449
+
450
+ # Filtre par catégorie avec icônes
451
+ categories = ['🌐 Toutes les catégories'] + [f"{CODEX_CATEGORIES[cat]['icon']} {name}"
452
+ for cat, name in df_all.groupby('category')['category_name'].first().items()]
453
+ selected_category = st.selectbox("Catégorie:", categories)
454
+
455
+ # Filtre par comité
456
+ committees = ['🏢 Tous les comités'] + sorted([f"🏢 {c}" for c in df_all['committee'].unique()])
457
+ selected_committee = st.selectbox("Comité:", committees)
458
 
459
+ # Filtres de statut
460
+ col1, col2 = st.columns(2)
461
+ with col1:
462
+ filter_new = st.checkbox("✨ Nouveaux")
463
+ with col2:
464
+ filter_updated = st.checkbox("🔄 Mis à jour")
465
 
466
+ # Filtre par année
467
+ years = sorted(df_all[df_all['year'] > 0]['year'].unique(), reverse=True)
468
+ if years:
469
+ year_range = st.select_slider(
470
+ "📅 Période:",
471
+ options=years,
472
+ value=(years[-1], years[0])
473
+ )
474
 
475
+ # Recherche textuelle
476
+ search_term = st.text_input("🔍 Recherche:", placeholder="Code ou titre...")
477
 
478
+ # Application des filtres
479
+ filtered_df = df_all.copy()
480
 
481
+ if not selected_category.startswith('🌐'):
482
+ cat_name = selected_category.split(' ', 1)[1]
483
+ filtered_df = filtered_df[filtered_df['category_name'] == cat_name]
484
 
485
+ if not selected_committee.startswith('🏢 Tous'):
486
+ committee_name = selected_committee.split(' ', 1)[1]
487
+ filtered_df = filtered_df[filtered_df['committee'] == committee_name]
488
 
489
+ if filter_new:
490
+ filtered_df = filtered_df[filtered_df['is_new']]
491
 
492
+ if filter_updated:
493
+ filtered_df = filtered_df[filtered_df['is_updated']]
494
 
495
+ if 'year_range' in locals() and years:
496
+ filtered_df = filtered_df[
497
+ (filtered_df['year'] >= year_range[0]) &
498
+ (filtered_df['year'] <= year_range[1])
499
+ ]
 
 
 
 
 
500
 
501
+ if search_term:
502
+ filtered_df = filtered_df[
503
+ filtered_df['title'].str.contains(search_term, case=False, na=False) |
504
+ filtered_df['code'].str.contains(search_term, case=False, na=False)
505
+ ]
506
+
507
+ st.session_state.filtered_df = filtered_df
508
+
509
+ st.markdown(f"**📊 {len(filtered_df)} documents trouvés**")
510
+
511
+ # Contenu principal
512
  if st.session_state.documents:
513
  df_display = st.session_state.get('filtered_df', pd.DataFrame(st.session_state.documents))
 
 
514
  df_display = df_display.sort_values(by=['year', 'code'], ascending=[False, True]).reset_index(drop=True)
515
 
516
+ # Onglets pour différentes vues
517
+ tab1, tab2, tab3, tab4 = st.tabs(["📊 Dashboard", "📋 Aperçu", "📈 Analyses", "📄 Documents"])
518
 
519
+ with tab1:
520
+ create_dashboard_metrics(df_display)
521
+ create_category_overview(df_display)
522
+
523
+ with tab2:
524
+ st.subheader("📋 Résumé Exécutif")
525
+ if not df_display.empty:
526
+ col1, col2 = st.columns(2)
527
+ with col1:
528
+ st.markdown("##### 📊 Statistiques Générales")
529
+ st.write(f"• **Total documents**: {len(df_display)}")
530
+ st.write(f"• **Nouveaux documents**: {len(df_display[df_display['is_new']])}")
531
+ st.write(f"• **Mis à jour cette année**: {len(df_display[df_display['is_updated']])}")
532
+ st.write(f"• **Période couverte**: {df_display['year'].min()} - {df_display['year'].max()}")
533
+
534
+ with col2:
535
+ st.markdown("##### 🏢 Top 5 Comités")
536
+ top_committees = df_display['committee'].value_counts().head().reset_index()
537
+ for _, row in top_committees.iterrows():
538
+ st.write(f"• **{row['committee']}**: {row['count']} documents")
539
+
540
+ with tab3:
541
+ create_visualization(df_display)
542
+
543
+ with tab4:
544
+ st.subheader(f"📄 Documents ({len(df_display)} résultats)")
545
+ display_documents_grid(df_display)
546
+
547
+ # Options d'export
548
+ if not df_display.empty:
549
+ st.divider()
550
+ st.subheader("💾 Exporter les Données")
551
+ col1, col2, col3 = st.columns(3)
552
 
553
  with col1:
554
+ csv = df_display.to_csv(index=False, sep=';')
555
+ st.download_button(
556
+ label="📊 Télécharger CSV",
557
+ data=csv,
558
+ file_name=f"codex_documents_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
559
+ mime="text/csv",
560
+ use_container_width=True
561
+ )
 
 
 
562
 
563
  with col2:
564
+ json_str = df_display.to_json(orient='records', indent=2)
565
+ st.download_button(
566
+ label="📋 Télécharger JSON",
567
+ data=json_str,
568
+ file_name=f"codex_documents_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
569
+ mime="application/json",
570
+ use_container_width=True
571
+ )
572
 
573
+ with col3:
574
+ # Export Excel (nécessiterait openpyxl)
575
+ st.button("📈 Exporter Excel", disabled=True, help="Fonctionnalité à venir")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
  else:
578
+ # Page d'accueil avec instructions
579
+ col1, col2, col3 = st.columns([1, 2, 1])
580
+ with col2:
581
+ st.markdown("""
582
+ <div style="text-align: center; padding: 3rem; background: #f8f9fa; border-radius: 10px; margin: 2rem 0;">
583
+ <h2 style="color: #667eea;">🚀 Commencer l'Exploration</h2>
584
+ <p style="color: #666; margin: 1rem 0;">
585
+ Cliquez sur le bouton "Actualiser les Documents" dans la barre latérale
586
+ pour charger les dernières données du Codex Alimentarius.
587
+ </p>
588
+ <div style="margin: 2rem 0;">
589
+ <p style="font-size: 3rem; margin: 0;">📊</p>
590
+ </div>
591
+ </div>
592
+ """, unsafe_allow_html=True)
593
+
594
+ # Informations sur l'application
595
+ with st.expander("ℹ️ À propos de cette application"):
596
+ st.markdown("""
597
+ **Codex Alimentarius Monitor** vous permet de:
598
+
599
+ - 📊 **Explorer** les normes alimentaires internationales
600
+ - 🔍 **Rechercher** et filtrer les documents par catégorie, comité, année
601
+ - 📈 **Visualiser** les tendances et statistiques
602
+ - 📄 **Télécharger** les documents PDF officiels
603
+ - 💾 **Exporter** les données pour analyse externe
604
+
605
+ **Catégories disponibles:**
606
+ - 📋 **Codes de Pratique (CXC)** - Procédures et bonnes pratiques
607
+ - ⚖️ **Normes (CXS)** - Standards alimentaires officiels
608
+ - 📖 **Directives (CXG)** - Lignes directrices et recommandations
609
+ """)