Acasset45654 commited on
Commit
d2d4d8c
·
verified ·
1 Parent(s): 4784a32

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +620 -402
app.py CHANGED
@@ -4,6 +4,54 @@ import copy
4
  import json
5
  import requests
6
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  # --- PAGE CONFIGURATION (MUST BE THE FIRST STREAMLIT COMMAND) ---
9
  # Fixed deployment issue
@@ -14,45 +62,36 @@ st.markdown("""
14
  <style>
15
  /* Cible le bouton spécifique que vous avez identifié */
16
  button[data-testid="stBaseButton-headerNoPadding"]::after {
17
- content: " Menu"; /* Le texte à ajouter */
18
- margin-left: 8px; /* Espace entre la flèche et le texte (ajustez si besoin) */
19
- font-size: 0.9em; /* Taille du texte (ajustez si besoin) */
20
- vertical-align: middle; /* Aide à l'alignement vertical avec l'icône */
21
- color: inherit; /* Hérite de la couleur du thème (bon pour thèmes clair/sombre) */
22
- font-weight: normal; /* Assure que le texte n'est pas en gras par défaut */
23
- display: inline-flex; /* Peut aider à un meilleur alignement et comportement */
24
  align-items: center;
25
  }
26
  div[data-testid="stCodeBlock"] pre,
27
- pre.st-emotion-cache-1nqbjoj /* Cible spécifique à votre HTML, attention à sa stabilité */
28
- {
29
-
30
  max-height: 520px !important;
31
  overflow-y: auto !important;
32
  font-size: 0.875em !important;
33
- /* Assurez-vous qu'il n'est pas caché par autre chose */
34
- display: block !important;
35
  visibility: visible !important;
36
  opacity: 1 !important;
37
  }
38
-
39
- /* Cible le div conteneur direct à l'intérieur de stCodeBlock s'il existe et gère le scroll */
40
  div[data-testid="stCodeBlock"] > div:first-child {
41
- /* height: 120px !important; - SUPPRIMÉ : causait des problèmes de layout */
42
- max-height: 520px !important; /* Correspond à la valeur du pre ci-dessus */
43
  overflow-y: auto !important;
44
- display: block !important;
45
  visibility: visible !important;
46
  opacity: 1 !important;
47
  }
48
-
49
- /* Si le div interne au <pre> doit gérer le scroll */
50
  pre.st-emotion-cache-1nqbjoj > div[style*="background-color: transparent;"] {
51
- height: auto !important;
52
- max-height: 100% !important;
53
  overflow-y: auto !important;
54
  }
55
- /* === NOUVELLES RÈGLES POUR L'ICÔNE DE COPIE DE ST.CODE === */
56
  button[data-testid="stCodeCopyButton"] {
57
  opacity: 0.85 !important;
58
  visibility: visible !important;
@@ -61,240 +100,88 @@ st.markdown("""
61
  border-radius: 4px !important;
62
  padding: 3px 5px !important;
63
  transition: opacity 0.15s ease-in-out, background-color 0.15s ease-in-out;
64
- /* top: 2px !important; */
65
- /* right: 2px !important; */
66
  }
67
-
68
  button[data-testid="stCodeCopyButton"]:hover {
69
  opacity: 1 !important;
70
  background-color: #e6e8eb !important;
71
  border-color: #b0b0b0 !important;
72
  }
73
-
74
  button[data-testid="stCodeCopyButton"] svg {
75
- transform: scale(1.2);
76
  vertical-align: middle;
77
  }
78
-
79
- /* === SOLUTION POUR COMPRESSION LATERALE DE LA SIDEBAR === */
80
- /* Force le contenu principal à se comprimer au lieu d'être décalé */
81
  section[data-testid="stSidebar"] {
82
  width: 31.5rem !important;
83
  min-width: 31.5rem !important;
84
  max-width: 31.5rem !important;
85
  }
86
-
87
- /* RÉDUIRE LA SIDEBAR QUAND FERMÉE */
88
  section[data-testid="stSidebar"][aria-expanded="false"] {
89
  width: 0rem !important;
90
  min-width: 0rem !important;
91
  max-width: 0rem !important;
92
  overflow: hidden !important;
93
  }
94
-
95
- /* Ajustement du conteneur principal pour la compression */
96
- .main .block-container {
97
- max-width: calc(100vw - 31.5rem) !important;
98
- width: calc(100vw - 31.5rem) !important;
99
- }
100
-
101
- /* Quand la sidebar est fermée, reprendre toute la largeur */
102
- section[data-testid="stSidebar"][aria-expanded="false"] ~ .main .block-container,
103
- section[data-testid="stSidebar"]:not([aria-expanded="true"]) ~ .main .block-container {
104
- max-width: 100vw !important;
105
- width: 100vw !important;
106
- }
107
-
108
- /* Alternative pour cibler via l'état collapsed */
109
- .main .block-container {
110
- transition: width 0.3s ease, max-width 0.3s ease !important;
111
- }
112
-
113
- /* PADDING SPECIFIQUE POUR INTERPRO1_LIGHT */
114
  [data-testid="stMainBlockContainer"] {
115
  padding-top: 3rem !important;
116
  }
117
-
118
  .css-1d391kg, .css-18e3th9 {
119
  padding-top: 3rem !important;
120
  }
121
-
122
- /* REDUCTION SPECIFIQUE POUR LA PAGE D'ACCUEIL UNIQUEMENT */
123
  h1[data-testid="stHeading"]:first-of-type {
124
  margin-top: -2rem !important;
125
  padding-top: 0rem !important;
126
  }
127
-
128
  h1:contains("Bienvenue dans votre laboratoire") {
129
  margin-top: -2rem !important;
130
  padding-top: 0rem !important;
131
  }
132
-
133
- /* Responsive: sur petits écrans, garder le comportement normal */
134
  @media (max-width: 768px) {
135
  .main .block-container {
136
  max-width: 100vw !important;
137
  width: 100vw !important;
138
  }
139
  }
140
-
141
- /* === RÈGLES SPÉCIFIQUES POUR st.code() QUI CAUSE LE PROBLÈME === */
142
- /* Forcer la compression sur TOUS les blocs de code */
143
- [data-testid="stCodeBlock"],
144
- div[data-testid="stCodeBlock"],
145
- .stCodeBlock {
146
- max-width: 100vw !important;
147
- width: 100vw !important;
148
- box-sizing: border-box !important;
149
- }
150
-
151
- /* Quand sidebar ouverte, compresser les blocs de code */
152
- section[data-testid="stSidebar"][aria-expanded="true"] ~ .main [data-testid="stCodeBlock"],
153
- section[data-testid="stSidebar"][aria-expanded="true"] ~ .main div[data-testid="stCodeBlock"],
154
- section[data-testid="stSidebar"][aria-expanded="true"] ~ .main .stCodeBlock {
155
- max-width: calc(100vw - 31.5rem) !important;
156
- width: calc(100vw - 31.5rem) !important;
157
  }
158
-
159
- /* Forcer sur les éléments internes du code block */
160
- [data-testid="stCodeBlock"] > div,
161
- [data-testid="stCodeBlock"] pre,
162
- [data-testid="stCodeBlock"] code {
163
  max-width: 100% !important;
 
 
 
 
 
 
164
  width: 100% !important;
165
- box-sizing: border-box !important;
166
- overflow-x: auto !important;
 
 
167
  }
168
-
169
- /* Quand sidebar ouverte, aussi sur les éléments internes */
170
- section[data-testid="stSidebar"][aria-expanded="true"] ~ .main [data-testid="stCodeBlock"] > div,
171
- section[data-testid="stSidebar"][aria-expanded="true"] ~ .main [data-testid="stCodeBlock"] pre,
172
- section[data-testid="stSidebar"][aria-expanded="true"] ~ .main [data-testid="stCodeBlock"] code {
173
- max-width: calc(100vw - 31.5rem) !important;
174
- width: calc(100vw - 31.5rem) !important;
175
  }
176
-
177
- </style>
178
- <script>
179
- // Force la détection de l'état de la sidebar
180
- function checkSidebarState() {
181
- const sidebar = document.querySelector('section[data-testid="stSidebar"]');
182
- const mainContent = document.querySelector('.main .block-container');
183
-
184
- if (sidebar && mainContent) {
185
- const isExpanded = sidebar.getAttribute('aria-expanded') === 'true';
186
-
187
- if (isExpanded) {
188
- mainContent.style.maxWidth = 'calc(100vw - 31.5rem)';
189
- mainContent.style.width = 'calc(100vw - 31.5rem)';
190
- } else {
191
- mainContent.style.maxWidth = '100vw';
192
- mainContent.style.width = '100vw';
193
- }
194
- }
195
-
196
- // FORCE SPÉCIAL POUR st.code() - Le vrai problème
197
- const codeBlocks = document.querySelectorAll('[data-testid="stCodeBlock"], div[data-testid="stCodeBlock"], .stCodeBlock');
198
- codeBlocks.forEach(block => {
199
- if (sidebar && sidebar.getAttribute('aria-expanded') === 'true') {
200
- // Sidebar ouverte - compresser
201
- block.style.maxWidth = 'calc(100vw - 31.5rem)';
202
- block.style.width = 'calc(100vw - 31.5rem)';
203
- block.style.boxSizing = 'border-box';
204
-
205
- // Forcer aussi sur les éléments internes
206
- const innerElements = block.querySelectorAll('div, pre, code');
207
- innerElements.forEach(inner => {
208
- inner.style.maxWidth = '100%';
209
- inner.style.width = '100%';
210
- inner.style.overflowX = 'auto';
211
- });
212
- } else {
213
- // Sidebar fermée - pleine largeur
214
- block.style.maxWidth = '100vw';
215
- block.style.width = '100vw';
216
-
217
- const innerElements = block.querySelectorAll('div, pre, code');
218
- innerElements.forEach(inner => {
219
- inner.style.maxWidth = '100%';
220
- inner.style.width = '100%';
221
- });
222
- }
223
- });
224
  }
225
-
226
- // Observer les changements
227
- const observer = new MutationObserver(checkSidebarState);
228
-
229
- // Démarrer l'observation quand le DOM est prêt
230
- document.addEventListener('DOMContentLoaded', function() {
231
- const sidebar = document.querySelector('section[data-testid="stSidebar"]');
232
- if (sidebar) {
233
- observer.observe(sidebar, { attributes: true, attributeFilter: ['aria-expanded'] });
234
- }
235
- checkSidebarState(); // Check initial state
236
- });
237
-
238
- // Observer pour changements DOM (génération de contenu)
239
- const contentObserver = new MutationObserver(function(mutations) {
240
- mutations.forEach(function(mutation) {
241
- if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
242
- // Du nouveau contenu a été ajouté, re-vérifier l'état
243
- setTimeout(checkSidebarState, 100);
244
- }
245
- });
246
- });
247
-
248
- // Observer le conteneur principal pour changements
249
- document.addEventListener('DOMContentLoaded', function() {
250
- const main = document.querySelector('.main');
251
- if (main) {
252
- contentObserver.observe(main, { childList: true, subtree: true });
253
- }
254
- });
255
-
256
- // Fallback: check périodique
257
- setInterval(checkSidebarState, 500);
258
-
259
- // Disable Enter key form submission
260
- function disableEnterSubmit() {
261
- const forms = document.querySelectorAll('form[data-testid="stForm"]');
262
- forms.forEach(form => {
263
- const inputs = form.querySelectorAll('input[type="text"], textarea');
264
- inputs.forEach(input => {
265
- input.addEventListener('keydown', function(event) {
266
- if (event.key === 'Enter' && !event.shiftKey) {
267
- event.preventDefault();
268
- return false;
269
- }
270
- });
271
- });
272
- });
273
  }
274
-
275
- // Apply on DOM ready and on content changes
276
- document.addEventListener('DOMContentLoaded', disableEnterSubmit);
277
-
278
- // Observer for new forms being added dynamically
279
- const formObserver = new MutationObserver(function(mutations) {
280
- mutations.forEach(function(mutation) {
281
- if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
282
- setTimeout(disableEnterSubmit, 100);
283
- }
284
- });
285
- });
286
-
287
- // Start observing
288
- document.addEventListener('DOMContentLoaded', function() {
289
- const body = document.querySelector('body');
290
- if (body) {
291
- formObserver.observe(body, { childList: true, subtree: true });
292
- }
293
- });
294
-
295
- // Apply periodically as fallback
296
- setInterval(disableEnterSubmit, 1000);
297
- </script>
298
  """, unsafe_allow_html=True)
299
 
300
  # --- Initial Data Structure & Constants ---
@@ -541,39 +428,58 @@ def save_to_local_file(new_content_json_string):
541
  st.error(f"Erreur de sauvegarde du fichier local: {e}")
542
  return False
543
 
544
- def save_editable_prompts_to_local():
 
545
  if 'editable_prompts' in st.session_state:
546
  data_to_save = _preprocess_for_saving(st.session_state.editable_prompts)
547
  try:
548
  json_string = json.dumps(data_to_save, indent=4, ensure_ascii=False)
549
- if save_to_local_file(json_string):
550
- st.toast("💾 Données sauvegardées localement!", icon="💾")
551
- else:
552
- st.warning("Sauvegarde locale échouée.")
 
 
 
 
 
 
 
 
 
 
 
553
  except Exception as e: # pragma: no cover
554
- st.error(f"Erreur préparation données pour sauvegarde locale: {e}")
555
-
556
- def load_editable_prompts_from_local():
557
- raw_content = get_local_file_content()
 
 
 
 
 
558
  if raw_content:
559
  try:
560
  loaded_data = json.loads(raw_content)
561
  if not loaded_data or not isinstance(loaded_data, dict):
562
- raise ValueError("Contenu fichier local vide ou mal structuré.")
563
  return _postprocess_after_loading(loaded_data)
564
  except (json.JSONDecodeError, TypeError, ValueError) as e:
565
- st.info(f"Erreur chargement fichier local ('{str(e)[:50]}...'). Initialisation avec modèles par défaut.")
566
  else:
567
- st.info("Fichier local vide ou inaccessible. Initialisation avec modèles par défaut.")
568
  initial_data = copy.deepcopy(INITIAL_PROMPT_TEMPLATES)
569
  if raw_content is None or raw_content == "{}":
570
  data_to_save_init = _preprocess_for_saving(initial_data)
571
  try:
572
  json_string_init = json.dumps(data_to_save_init, indent=4, ensure_ascii=False)
573
- if save_to_local_file(json_string_init):
574
- st.info("Modèles par défaut sauvegardés localement pour initialisation.")
 
 
575
  except Exception as e: # pragma: no cover
576
- st.error(f"Erreur sauvegarde initiale locale: {e}")
577
  return initial_data
578
 
579
  # --- ACCESS CODE VERIFICATION ---
@@ -636,7 +542,7 @@ if not st.session_state.access_granted:
636
  st.markdown("""
637
  <p style="font-style: italic; color: #666; font-size: 0.9rem; margin-top: 1.5rem; text-align: center;">
638
  Pour demander un accès, contactez votre référent métier ou l'administrateur de l'application :
639
- <a href="mailto:arthur.causse-prestataire@laposte.fr" style="color: #0066cc;">arthur.causse-prestataire@laposte.fr</a>
640
  </p>
641
  """, unsafe_allow_html=True)
642
 
@@ -645,7 +551,7 @@ if not st.session_state.access_granted:
645
 
646
  # --- Session State Initialization ---
647
  if 'editable_prompts' not in st.session_state:
648
- st.session_state.editable_prompts = load_editable_prompts_from_local()
649
  if 'view_mode' not in st.session_state:
650
  st.session_state.view_mode = "accueil" # Nouvelle vue par défaut
651
 
@@ -695,12 +601,12 @@ if 'assistant_existing_prompt_value' not in st.session_state:
695
 
696
  # --- Sidebar Navigation with Tabs ---
697
  st.sidebar.header("Menu Principal")
698
- tab_bibliotheque, tab_injection = st.sidebar.tabs([
699
  "📚 Bibliothèque",
 
700
  "💡 Assistant"
701
  ])
702
 
703
-
704
  # --- Tab: Bibliothèque (Sidebar content) ---
705
  with tab_bibliotheque:
706
  st.subheader("Explorer la Bibliothèque de Prompts")
@@ -769,6 +675,233 @@ with tab_bibliotheque:
769
  st.rerun()
770
  st.markdown("---")
771
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  # --- Tab: Injection (Sidebar content) ---
773
  with tab_injection:
774
  st.subheader("Assistant & Injection")
@@ -800,8 +933,6 @@ if st.session_state.get('force_select_use_case_name'):
800
  st.session_state.use_case_selector_edition = st.session_state.force_select_use_case_name
801
  st.session_state.force_select_use_case_name = None
802
 
803
- final_selected_family_edition = st.session_state.get('family_selector_edition')
804
- final_selected_use_case_edition = st.session_state.get('use_case_selector_edition')
805
  library_family_to_display = st.session_state.get('library_selected_family_for_display')
806
 
807
  # NOUVELLE SECTION POUR LA PAGE D'ACCUEIL
@@ -1052,7 +1183,7 @@ elif st.session_state.view_mode == "library":
1052
  st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["created_at"] = now_iso_dup_create
1053
  st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["updated_at"] = now_iso_dup_update
1054
  st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["usage_count"] = 0
1055
- save_editable_prompts_to_local()
1056
  st.success(f"Cas d'usage '{original_uc_name_for_dup}' dupliqué en '{new_uc_name_val_from_form}' dans la famille '{target_family_on_submit}'.")
1057
 
1058
  st.session_state.duplicating_use_case_details = None
@@ -1079,7 +1210,7 @@ elif st.session_state.view_mode == "library":
1079
  deleted_uc_name_for_msg = details['use_case']
1080
  deleted_uc_fam_for_msg = details['family']
1081
  del st.session_state.editable_prompts[details["family"]][details["use_case"]]
1082
- save_editable_prompts_to_local()
1083
  st.success(f"'{deleted_uc_name_for_msg}' supprimé de '{deleted_uc_fam_for_msg}'.")
1084
  st.session_state.confirming_delete_details = None
1085
  st.rerun()
@@ -1127,165 +1258,6 @@ elif st.session_state.view_mode == "library":
1127
  available_families_check = list(st.session_state.editable_prompts.keys())
1128
  if not available_families_check : st.warning("La bibliothèque est entièrement vide. Veuillez créer des métiers et des prompts.")
1129
 
1130
- elif st.session_state.view_mode == "edit":
1131
- current_family_of_edited_prompt = st.session_state.get('family_selector_edition') # ou 'métier_selector_edition' si vous avez renommé cette clé de session_state
1132
- if st.button(f"⬅️ Retour à la bibliothèque ({current_family_of_edited_prompt or 'Métier'})", key="back_to_library_from_edit"):
1133
- if current_family_of_edited_prompt:
1134
- st.session_state.library_selected_family_for_display = current_family_of_edited_prompt
1135
- st.session_state.view_mode = "library"
1136
- st.rerun()
1137
- if not final_selected_family_edition : st.info("Sélectionnez un métier dans la barre latérale (onglet Édition) ou créez-en un pour commencer.")
1138
- elif not final_selected_use_case_edition: st.info(f"Sélectionnez un cas d'usage dans le métier '{final_selected_family_edition}' ou créez-en un nouveau pour commencer.")
1139
- elif final_selected_family_edition in st.session_state.editable_prompts and final_selected_use_case_edition in st.session_state.editable_prompts[final_selected_family_edition]:
1140
- current_prompt_config = st.session_state.editable_prompts[final_selected_family_edition][final_selected_use_case_edition]
1141
- st.header(f"Cas d'usage: {final_selected_use_case_edition}")
1142
- created_at_str_edit = current_prompt_config.get('created_at', get_default_dates()[0]); updated_at_str_edit = current_prompt_config.get('updated_at', get_default_dates()[1])
1143
- st.caption(f"Métier : {final_selected_family_edition} | Utilisé {current_prompt_config.get('usage_count', 0)} fois. Créé le : {datetime.fromisoformat(created_at_str_edit).strftime('%d/%m/%Y')}, Modifié le : {datetime.fromisoformat(updated_at_str_edit).strftime('%d/%m/%Y')}")
1144
- # Afficher la description si elle existe
1145
- description = current_prompt_config.get("description", "").strip()
1146
- if description:
1147
- st.markdown(f"*{description}*")
1148
- gen_form_values = {}
1149
- with st.form(key=f"gen_form_{final_selected_family_edition}_{final_selected_use_case_edition}"):
1150
- st.markdown("**Remplissez le formulaire ci-dessous pour ajouter du contexte à votre prompt :**")
1151
- if not current_prompt_config.get("variables"): st.info("Ce cas d'usage n'a pas de variables configurées pour la génération.")
1152
- variables_for_form = current_prompt_config.get("variables", [])
1153
- if not isinstance(variables_for_form, list): variables_for_form = []
1154
- cols_per_row = 2 if len(variables_for_form) > 1 else 1
1155
- var_chunks = [variables_for_form[i:i + cols_per_row] for i in range(0, len(variables_for_form), cols_per_row)]
1156
- for chunk in var_chunks:
1157
- cols = st.columns(len(chunk))
1158
- for i, var_info in enumerate(chunk):
1159
- with cols[i]:
1160
- widget_key = f"gen_input_{final_selected_family_edition}_{final_selected_use_case_edition}_{var_info['name']}"; field_default = var_info.get("default"); var_type = var_info.get("type")
1161
- if var_type == "text_input": gen_form_values[var_info["name"]] = st.text_input(var_info["label"], value=str(field_default or ""), key=widget_key)
1162
- elif var_type == "selectbox":
1163
- opts = var_info.get("options", []); idx = 0
1164
- if opts:
1165
- try: idx = opts.index(field_default) if field_default in opts else 0
1166
- except ValueError: idx = 0 # pragma: no cover
1167
- gen_form_values[var_info["name"]] = st.selectbox(var_info["label"], options=opts, index=idx, key=widget_key)
1168
- elif var_type == "date_input":
1169
- val_date = field_default if isinstance(field_default, date) else datetime.now().date()
1170
- gen_form_values[var_info["name"]] = st.date_input(var_info["label"], value=val_date, key=widget_key)
1171
- elif var_type == "number_input":
1172
- current_value_default_gen = var_info.get("default"); min_val_config_gen = var_info.get("min_value"); max_val_config_gen = var_info.get("max_value"); step_config_gen = var_info.get("step")
1173
- val_num_gen = float(current_value_default_gen) if isinstance(current_value_default_gen, (int, float)) else 0.0
1174
- min_val_gen = float(min_val_config_gen) if min_val_config_gen is not None else None; max_val_gen = float(max_val_config_gen) if max_val_config_gen is not None else None; step_val_gen = float(step_config_gen) if step_config_gen is not None else 1.0
1175
- if min_val_gen is not None and val_num_gen < min_val_gen: val_num_gen = min_val_gen
1176
- if max_val_gen is not None and val_num_gen > max_val_gen: val_num_gen = max_val_gen
1177
- gen_form_values[var_info["name"]] = st.number_input(var_info["label"], value=val_num_gen, min_value=min_val_gen,max_value=max_val_gen, step=step_val_gen, key=widget_key, format="%.2f")
1178
- elif var_type == "text_area":
1179
- height_val = var_info.get("height")
1180
- final_height = None
1181
- if height_val is not None:
1182
- try:
1183
- h = int(height_val)
1184
- if h >= 68: final_height = h
1185
- else: final_height = 68
1186
- except (ValueError, TypeError): final_height = None
1187
- else: final_height = None
1188
- gen_form_values[var_info["name"]] = st.text_area(var_info["label"], value=str(field_default or ""), height=final_height, key=widget_key)
1189
- if st.form_submit_button("🚀 Générer Prompt"):
1190
- processed_values_for_template = {}
1191
- for k, v_val in gen_form_values.items(): # gen_form_values vient de votre formulaire
1192
- if v_val is None:
1193
- # On ne met pas les valeurs None dans le dictionnaire,
1194
- # donc les placeholders correspondants ne seront pas remplacés (comportement original)
1195
- continue
1196
-
1197
- if isinstance(v_val, date):
1198
- processed_values_for_template[k] = v_val.strftime("%d/%m/%Y")
1199
- elif isinstance(v_val, float): # On regroupe ici tous les traitements pour les floats
1200
- if v_val.is_integer(): # Si le float est un entier (ex: 50.0)
1201
- processed_values_for_template[k] = str(int(v_val)) # Convertir en "50"
1202
- else: # S'il s'agit d'un float avec des décimales (ex: 0.125000...)
1203
- processed_values_for_template[k] = f"{v_val:.2f}" # Formater avec 2 décimales
1204
- else: # Pour tous les autres types (str, bool, etc. qui ne sont ni date ni float)
1205
- processed_values_for_template[k] = str(v_val)
1206
-
1207
- final_vals_for_prompt = processed_values_for_template # final_vals_for_prompt contient maintenant des chaînes
1208
-
1209
- try:
1210
- prompt_template_content = current_prompt_config.get("template", "")
1211
- processed_template = prompt_template_content
1212
-
1213
- # 1. Remplacer les variables connues par Streamlit (celles du formulaire)
1214
- # Trier par longueur de clé (descendant) pour éviter les substitutions partielles
1215
- # (ex: remplacer {jour_semaine} avant {jour})
1216
- sorted_vars_for_formatting = sorted(final_vals_for_prompt.items(), key=lambda item: len(item[0]), reverse=True)
1217
-
1218
- for var_name, var_value in sorted_vars_for_formatting:
1219
- placeholder_streamlit = f"{{{var_name}}}"
1220
- # Remplacer uniquement les placeholders exacts et simples
1221
- processed_template = processed_template.replace(placeholder_streamlit, str(var_value))
1222
-
1223
- # 2. Convertir les doubles accolades (pour le LLM final) en simples accolades
1224
- # Ceci suppose que le template original (venant du JSON) utilisait bien {{...}}
1225
- # pour les placeholders destinés au LLM final.
1226
- formatted_template_content = processed_template.replace("{{", "{").replace("}}", "}")
1227
-
1228
- use_case_title = final_selected_use_case_edition
1229
- generated_prompt = f"Sujet : {use_case_title}\n{formatted_template_content}"
1230
- st.session_state.active_generated_prompt = generated_prompt
1231
- st.success("Prompt généré avec succès!")
1232
- st.balloons()
1233
- current_prompt_config["usage_count"] = current_prompt_config.get("usage_count", 0) + 1
1234
- current_prompt_config["updated_at"] = datetime.now().isoformat()
1235
- save_editable_prompts_to_local()
1236
-
1237
- except Exception as e: # Garder un catch-all pour les erreurs imprévues
1238
- st.error(f"Erreur inattendue lors de la génération du prompt : {e}") # pragma: no cover
1239
- st.session_state.active_generated_prompt = f"ERREUR INATTENDUE - TEMPLATE ORIGINAL :\n---\n{prompt_template_content}" # pragma: no cover
1240
- st.markdown("---")
1241
- if st.session_state.active_generated_prompt:
1242
- st.subheader("✅ Prompt Généré (éditable):")
1243
- edited_prompt_value = st.text_area("Prompt:", value=st.session_state.active_generated_prompt, height=200, key=f"editable_generated_prompt_output_{final_selected_family_edition}_{final_selected_use_case_edition}", label_visibility="collapsed")
1244
- if edited_prompt_value != st.session_state.active_generated_prompt:
1245
- st.session_state.active_generated_prompt = edited_prompt_value # pragma: no cover
1246
- col_caption, col_indicator = st.columns([1.8, 0.2]) # Ajustez les proportions si nécessaire
1247
- with col_caption:
1248
- st.caption("Prompt généré (pour relecture et copie manuelle) :")
1249
- with col_indicator:
1250
- st.markdown("<div style='color:red; text-align:right; font-size:0.9em; padding-right:0.9em;'>Copier ici : 👇</div>", unsafe_allow_html=True)
1251
-
1252
-
1253
- if st.session_state.active_generated_prompt:
1254
- st.code(st.session_state.active_generated_prompt, language='markdown', line_numbers=True)
1255
- else:
1256
- st.markdown("*Aucun prompt généré à afficher.*")
1257
-
1258
- st.markdown("---") # Un petit séparateur
1259
-
1260
- prompt_text_escaped_for_js = json.dumps(st.session_state.active_generated_prompt)
1261
-
1262
- should_expand_config = SHOW_CONFIG_SECTION and st.session_state.get('go_to_config_section', False)
1263
- if SHOW_CONFIG_SECTION:
1264
- with st.expander(f"⚙️ Paramétrage du Prompt: {final_selected_use_case_edition}", expanded=should_expand_config):
1265
- st.info("Section de paramétrage temporairement désactivée.")
1266
-
1267
- if st.session_state.get('go_to_config_section'):
1268
- st.session_state.go_to_config_section = False
1269
-
1270
- if st.session_state.confirming_delete_details and st.session_state.confirming_delete_details["family"] == final_selected_family_edition and st.session_state.confirming_delete_details["use_case"] == final_selected_use_case_edition:
1271
- details = st.session_state.confirming_delete_details; st.warning(f"Supprimer '{details['use_case']}' de '{details['family']}' ? Action irréversible.")
1272
- c1_del_uc, c2_del_uc, _ = st.columns([1,1,3])
1273
- if c1_del_uc.button(f"Oui, supprimer '{details['use_case']}'", key=f"del_yes_{details['family']}_{details['use_case']}", type="primary"):
1274
- deleted_uc_name_for_msg = details['use_case']; deleted_uc_fam_for_msg = details['family']; del st.session_state.editable_prompts[details["family"]][details["use_case"]]; save_editable_prompts_to_local(); st.success(f"'{deleted_uc_name_for_msg}' supprimé de '{deleted_uc_fam_for_msg}'.")
1275
- st.session_state.confirming_delete_details = None; st.session_state.force_select_family_name = deleted_uc_fam_for_msg; st.session_state.force_select_use_case_name = None
1276
- if st.session_state.editing_variable_info and st.session_state.editing_variable_info.get("family") == deleted_uc_fam_for_msg and st.session_state.editing_variable_info.get("use_case") == deleted_uc_name_for_msg: st.session_state.editing_variable_info = None # pragma: no cover
1277
- st.session_state.active_generated_prompt = ""; st.session_state.variable_type_to_create = None; st.session_state.view_mode = "edit"; st.rerun()
1278
- if c2_del_uc.button("Non, annuler", key=f"del_no_{details['family']}_{details['use_case']}"): st.session_state.confirming_delete_details = None; st.rerun()
1279
- st.markdown("---")
1280
-
1281
- else:
1282
- if not final_selected_family_edition:
1283
- st.info("Veuillez sélectionner un métier dans la barre latérale (onglet Édition) pour commencer.")
1284
- elif not final_selected_use_case_edition:
1285
- st.info(f"Veuillez sélectionner un cas d'usage pour le métier '{final_selected_family_edition}' ou en créer un.")
1286
- else:
1287
- st.warning(f"Le cas d'usage '{final_selected_use_case_edition}' dans le métier '{final_selected_family_edition}' semble introuvable. Il a peut-être été supprimé. Veuillez vérifier vos sélections.")
1288
- st.session_state.use_case_selector_edition = None # pragma: no cover
1289
 
1290
  elif st.session_state.view_mode == "generator":
1291
  generator_family = st.session_state.get('generator_selected_family')
@@ -1442,7 +1414,7 @@ elif st.session_state.view_mode == "generator":
1442
  st.balloons()
1443
  current_prompt_config["usage_count"] = current_prompt_config.get("usage_count", 0) + 1
1444
  current_prompt_config["updated_at"] = datetime.now().isoformat()
1445
- save_editable_prompts_to_local()
1446
 
1447
  except Exception as e:
1448
  st.error(f"Erreur inattendue lors de la génération du prompt : {e}")
@@ -1476,7 +1448,253 @@ elif st.session_state.view_mode == "generator":
1476
  should_expand_config = SHOW_CONFIG_SECTION and st.session_state.get('go_to_config_section', False)
1477
  if SHOW_CONFIG_SECTION:
1478
  with st.expander(f"⚙️ Paramétrage du Prompt: {generator_use_case}", expanded=should_expand_config):
1479
- st.info("Section de paramétrage temporairement désactivée.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1480
 
1481
  if st.session_state.get('go_to_config_section'):
1482
  st.session_state.go_to_config_section = False
@@ -1552,11 +1770,11 @@ elif st.session_state.view_mode == "inject_manual":
1552
  if first_new_uc_name is None:
1553
  first_new_uc_name = uc_name_stripped
1554
  if successful_injections:
1555
- save_editable_prompts_to_local()
1556
  st.success(f"{len(successful_injections)} cas d'usage injectés avec succès dans '{target_family_name}': {', '.join(successful_injections)}")
1557
  st.session_state.injection_json_text = ""
1558
  if first_new_uc_name:
1559
- st.session_state.view_mode = "edit"
1560
  st.session_state.force_select_family_name = target_family_name
1561
  st.session_state.force_select_use_case_name = first_new_uc_name
1562
  st.session_state.go_to_config_section = True
@@ -1610,7 +1828,7 @@ elif st.session_state.view_mode == "assistant_creation": # Cette vue gère maint
1610
  st.rerun()
1611
 
1612
  if st.session_state.assistant_mode == "creation":
1613
- st.markdown("Décrivez votre besoin pour que l'assistant génère une instruction détaillée. Vous donnerez cette instruction à LaPoste GPT qui, en retour, produira les éléments de votre cas d'usage (prompt système, variables, etc.).")
1614
  with st.form(key="assistant_creation_form_std"):
1615
  # Initialiser current_form_input_values avec les valeurs de session_state ou les valeurs par défaut
1616
  # pour que les champs du formulaire soient pré-remplis correctement.
@@ -1659,7 +1877,7 @@ elif st.session_state.view_mode == "assistant_creation": # Cette vue gère maint
1659
  st.session_state.generated_meta_prompt_for_llm = ""
1660
 
1661
  elif st.session_state.assistant_mode == "amelioration":
1662
- st.markdown("Collez votre prompt existant. L'assistant générera une instruction pour LaPoste GPT afin de transformer votre prompt en un cas d'usage structuré et améliorable pour cette application.")
1663
  with st.form(key="assistant_amelioration_form_unified"):
1664
  # Utilise la valeur de session_state pour ce champ
1665
  prompt_existant_input_val = st.text_area(
@@ -1693,14 +1911,14 @@ elif st.session_state.view_mode == "assistant_creation": # Cette vue gère maint
1693
  if st.session_state.generated_meta_prompt_for_llm:
1694
  col_subheader_assist, col_indicator_assist = st.columns([0.85, 0.15])
1695
  with col_subheader_assist:
1696
- st.subheader("📋 Instruction Générée (à coller dans LaPosteGPT) :")
1697
  with col_indicator_assist:
1698
  st.markdown("<div style='color:red; text-align:right; font-size:0.9em; padding-top:1.9em;padding-right:0.9em;'>Copier ici : 👇</div>", unsafe_allow_html=True)
1699
 
1700
  st.code(st.session_state.generated_meta_prompt_for_llm, language='markdown', line_numbers=True)
1701
  st.caption("<span style='color:gray; font-size:0.9em;'>Utilisez l'icône en haut à droite du bloc de code pour copier l'instruction.</span>", unsafe_allow_html=True)
1702
  st.markdown("---")
1703
- st.info("Une fois que LaPoste GPT (ou votre LLM externe) a généré le JSON basé sur cette instruction, copiez ce JSON et utilisez le bouton \"💉 Injecter JSON Manuellement\" (disponible aussi dans l'onglet Assistant du menu) pour l'ajouter à votre atelier.")
1704
  if st.button("💉 Injecter JSON Manuellement", key="prepare_inject_from_assistant_unified_btn", use_container_width=True, type="primary"):
1705
  st.session_state.view_mode = "inject_manual"
1706
  st.session_state.injection_selected_family = None
@@ -1709,10 +1927,10 @@ elif st.session_state.view_mode == "assistant_creation": # Cette vue gère maint
1709
  st.rerun()
1710
  if not any(st.session_state.editable_prompts.values()): # pragma: no cover
1711
  st.warning("Aucun groupement de cas d'usage métier n'est configurée. Veuillez en créer une via l'onglet 'Édition' ou vérifier votre Gist.")
1712
- elif st.session_state.view_mode not in ["library", "edit", "inject_manual", "assistant_creation"]: # pragma: no cover
1713
- st.session_state.view_mode = "library" if list(st.session_state.editable_prompts.keys()) else "edit"
1714
  st.rerun()
1715
 
1716
  # --- Sidebar Footer ---
1717
  st.sidebar.markdown("---")
1718
- st.sidebar.info(f"Générateur v3.3.6 - © {CURRENT_YEAR} La Poste (démo)")
 
4
  import json
5
  import requests
6
  import os
7
+ # --- Hugging Face Hub integration ---
8
+ try:
9
+ from huggingface_hub import HfApi, hf_hub_download
10
+ HF_HUB_AVAILABLE = True
11
+ except ImportError:
12
+ HF_HUB_AVAILABLE = False
13
+
14
+ HF_REPO_ID = "AIB-Research/bibliPrompt"
15
+ HF_JSON_FILENAME = "prompt_templates_data_v3.json"
16
+ HF_TOKEN = os.environ.get("HF_TOKEN", None)
17
+
18
+ # --- Save to Hugging Face Space ---
19
+ def save_to_hf_space(json_path, repo_id=HF_REPO_ID, token=HF_TOKEN):
20
+ if not HF_HUB_AVAILABLE:
21
+ st.warning("huggingface_hub n'est pas installé. Sauvegarde sur HF désactivée.")
22
+ return False
23
+ try:
24
+ api = HfApi()
25
+ api.upload_file(
26
+ path_or_fileobj=json_path,
27
+ path_in_repo=HF_JSON_FILENAME,
28
+ repo_id=repo_id,
29
+ repo_type="space",
30
+ token=token
31
+ )
32
+ st.toast("💾 Données sauvegardées sur Hugging Face Space!", icon="🤗")
33
+ return True
34
+ except Exception as e:
35
+ st.warning(f"Erreur lors de la sauvegarde sur Hugging Face: {e}")
36
+ return False
37
+
38
+ # --- Load from Hugging Face Space ---
39
+ def load_from_hf_space(repo_id=HF_REPO_ID, token=HF_TOKEN):
40
+ if not HF_HUB_AVAILABLE:
41
+ st.warning("huggingface_hub n'est pas installé. Chargement depuis HF désactivé.")
42
+ return None
43
+ try:
44
+ local_path = hf_hub_download(
45
+ repo_id=repo_id,
46
+ filename=HF_JSON_FILENAME,
47
+ repo_type="space",
48
+ token=token
49
+ )
50
+ with open(local_path, "r", encoding="utf-8") as f:
51
+ return f.read()
52
+ except Exception as e:
53
+ st.warning(f"Erreur lors du chargement depuis Hugging Face: {e}")
54
+ return None
55
 
56
  # --- PAGE CONFIGURATION (MUST BE THE FIRST STREAMLIT COMMAND) ---
57
  # Fixed deployment issue
 
62
  <style>
63
  /* Cible le bouton spécifique que vous avez identifié */
64
  button[data-testid="stBaseButton-headerNoPadding"]::after {
65
+ content: " Menu";
66
+ margin-left: 8px;
67
+ font-size: 0.9em;
68
+ vertical-align: middle;
69
+ color: inherit;
70
+ font-weight: normal;
71
+ display: inline-flex;
72
  align-items: center;
73
  }
74
  div[data-testid="stCodeBlock"] pre,
75
+ pre.st-emotion-cache-1nqbjoj {
 
 
76
  max-height: 520px !important;
77
  overflow-y: auto !important;
78
  font-size: 0.875em !important;
79
+ display: block !important;
 
80
  visibility: visible !important;
81
  opacity: 1 !important;
82
  }
 
 
83
  div[data-testid="stCodeBlock"] > div:first-child {
84
+ max-height: 520px !important;
 
85
  overflow-y: auto !important;
86
+ display: block !important;
87
  visibility: visible !important;
88
  opacity: 1 !important;
89
  }
 
 
90
  pre.st-emotion-cache-1nqbjoj > div[style*="background-color: transparent;"] {
91
+ height: auto !important;
92
+ max-height: 100% !important;
93
  overflow-y: auto !important;
94
  }
 
95
  button[data-testid="stCodeCopyButton"] {
96
  opacity: 0.85 !important;
97
  visibility: visible !important;
 
100
  border-radius: 4px !important;
101
  padding: 3px 5px !important;
102
  transition: opacity 0.15s ease-in-out, background-color 0.15s ease-in-out;
 
 
103
  }
 
104
  button[data-testid="stCodeCopyButton"]:hover {
105
  opacity: 1 !important;
106
  background-color: #e6e8eb !important;
107
  border-color: #b0b0b0 !important;
108
  }
 
109
  button[data-testid="stCodeCopyButton"] svg {
110
+ transform: scale(1.2);
111
  vertical-align: middle;
112
  }
 
 
 
113
  section[data-testid="stSidebar"] {
114
  width: 31.5rem !important;
115
  min-width: 31.5rem !important;
116
  max-width: 31.5rem !important;
117
  }
 
 
118
  section[data-testid="stSidebar"][aria-expanded="false"] {
119
  width: 0rem !important;
120
  min-width: 0rem !important;
121
  max-width: 0rem !important;
122
  overflow: hidden !important;
123
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  [data-testid="stMainBlockContainer"] {
125
  padding-top: 3rem !important;
126
  }
 
127
  .css-1d391kg, .css-18e3th9 {
128
  padding-top: 3rem !important;
129
  }
 
 
130
  h1[data-testid="stHeading"]:first-of-type {
131
  margin-top: -2rem !important;
132
  padding-top: 0rem !important;
133
  }
 
134
  h1:contains("Bienvenue dans votre laboratoire") {
135
  margin-top: -2rem !important;
136
  padding-top: 0rem !important;
137
  }
 
 
138
  @media (max-width: 768px) {
139
  .main .block-container {
140
  max-width: 100vw !important;
141
  width: 100vw !important;
142
  }
143
  }
144
+ div[data-testid="stExpander"] div[data-testid="stCodeBlock"] {
145
+ margin-top: 0.1rem !important;
146
+ margin-bottom: 0.15rem !important;
147
+ padding-top: 0.1rem !important;
148
+ padding-bottom: 0.1rem !important;
149
+ }
150
+ div[data-testid="stExpander"] div[data-testid="stCodeBlock"] pre {
151
+ padding-top: 0.2rem !important;
152
+ padding-bottom: 0.2rem !important;
153
+ line-height: 1.1 !important;
154
+ font-size: 0.85em !important;
155
+ margin: 0 !important;
 
 
 
 
 
156
  }
157
+ div[data-testid="stExpander"] {
158
+ width: 100% !important;
 
 
 
159
  max-width: 100% !important;
160
+ overflow: visible !important;
161
+ }
162
+ div[data-testid="stExpander"] > div:first-child {
163
+ overflow: visible !important;
164
+ }
165
+ div[data-testid="stExpander"] .streamlit-expanderContent {
166
  width: 100% !important;
167
+ max-width: 100% !important;
168
+ overflow: visible !important;
169
+ position: relative !important;
170
+ transform: translateZ(0) !important;
171
  }
172
+ .main .block-container {
173
+ overflow-x: hidden !important;
174
+ overflow-y: auto !important;
 
 
 
 
175
  }
176
+ div[data-testid="stExpander"] form {
177
+ max-width: 100% !important;
178
+ overflow-x: hidden !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  }
180
+ div[data-testid="stExpander"] div[data-testid="column"] {
181
+ min-width: 0 !important;
182
+ overflow: hidden !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  }
184
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  """, unsafe_allow_html=True)
186
 
187
  # --- Initial Data Structure & Constants ---
 
428
  st.error(f"Erreur de sauvegarde du fichier local: {e}")
429
  return False
430
 
431
+ # --- Unified save/load functions ---
432
+ def save_editable_prompts_to_local_and_hf():
433
  if 'editable_prompts' in st.session_state:
434
  data_to_save = _preprocess_for_saving(st.session_state.editable_prompts)
435
  try:
436
  json_string = json.dumps(data_to_save, indent=4, ensure_ascii=False)
437
+ local_ok = save_to_local_file(json_string)
438
+ hf_ok = False
439
+ # Save to HF Space if possible
440
+ with open(LOCAL_DATA_FILENAME, 'w', encoding='utf-8') as f:
441
+ f.write(json_string)
442
+ if HF_HUB_AVAILABLE:
443
+ hf_ok = save_to_hf_space(LOCAL_DATA_FILENAME)
444
+ if local_ok and hf_ok:
445
+ st.toast("💾 Données sauvegardées localement et sur Hugging Face!", icon="💾")
446
+ elif local_ok:
447
+ st.toast("💾 Données sauvegardées localement!", icon="💾")
448
+ elif hf_ok:
449
+ st.toast("💾 Données sauvegardées sur Hugging Face!", icon="🤗")
450
+ else:
451
+ st.warning("Sauvegarde échouée.")
452
  except Exception as e: # pragma: no cover
453
+ st.error(f"Erreur préparation données pour sauvegarde: {e}")
454
+
455
+ def load_editable_prompts_from_local_or_hf():
456
+ # Try HF first, fallback to local
457
+ raw_content = None
458
+ if HF_HUB_AVAILABLE:
459
+ raw_content = load_from_hf_space()
460
+ if not raw_content:
461
+ raw_content = get_local_file_content()
462
  if raw_content:
463
  try:
464
  loaded_data = json.loads(raw_content)
465
  if not loaded_data or not isinstance(loaded_data, dict):
466
+ raise ValueError("Contenu fichier vide ou mal structuré.")
467
  return _postprocess_after_loading(loaded_data)
468
  except (json.JSONDecodeError, TypeError, ValueError) as e:
469
+ st.info(f"Erreur chargement fichier ('{str(e)[:50]}...'). Initialisation avec modèles par défaut.")
470
  else:
471
+ st.info("Fichier vide ou inaccessible. Initialisation avec modèles par défaut.")
472
  initial_data = copy.deepcopy(INITIAL_PROMPT_TEMPLATES)
473
  if raw_content is None or raw_content == "{}":
474
  data_to_save_init = _preprocess_for_saving(initial_data)
475
  try:
476
  json_string_init = json.dumps(data_to_save_init, indent=4, ensure_ascii=False)
477
+ save_to_local_file(json_string_init)
478
+ if HF_HUB_AVAILABLE:
479
+ save_to_hf_space(LOCAL_DATA_FILENAME)
480
+ st.info("Modèles par défaut sauvegardés pour initialisation.")
481
  except Exception as e: # pragma: no cover
482
+ st.error(f"Erreur sauvegarde initiale: {e}")
483
  return initial_data
484
 
485
  # --- ACCESS CODE VERIFICATION ---
 
542
  st.markdown("""
543
  <p style="font-style: italic; color: #666; font-size: 0.9rem; margin-top: 1.5rem; text-align: center;">
544
  Pour demander un accès, contactez votre référent métier ou l'administrateur de l'application :
545
+ <a href="mailto:arthur.causse@aibuilders.ai" style="color: #0066cc;">arthur.causse@aibuilders.ai</a>
546
  </p>
547
  """, unsafe_allow_html=True)
548
 
 
551
 
552
  # --- Session State Initialization ---
553
  if 'editable_prompts' not in st.session_state:
554
+ st.session_state.editable_prompts = load_editable_prompts_from_local_or_hf()
555
  if 'view_mode' not in st.session_state:
556
  st.session_state.view_mode = "accueil" # Nouvelle vue par défaut
557
 
 
601
 
602
  # --- Sidebar Navigation with Tabs ---
603
  st.sidebar.header("Menu Principal")
604
+ tab_bibliotheque, tab_edition_generation, tab_injection = st.sidebar.tabs([
605
  "📚 Bibliothèque",
606
+ "✍️ Édition",
607
  "💡 Assistant"
608
  ])
609
 
 
610
  # --- Tab: Bibliothèque (Sidebar content) ---
611
  with tab_bibliotheque:
612
  st.subheader("Explorer la Bibliothèque de Prompts")
 
675
  st.rerun()
676
  st.markdown("---")
677
 
678
+ # --- Tab: Édition (Sidebar content) ---
679
+ with tab_edition_generation:
680
+ st.subheader("Explorateur de Prompts")
681
+ available_families = list(st.session_state.editable_prompts.keys())
682
+ default_family_idx_edit = 0
683
+ current_family_for_edit = st.session_state.get('family_selector_edition')
684
+
685
+ if st.session_state.force_select_family_name and st.session_state.force_select_family_name in available_families:
686
+ current_family_for_edit = st.session_state.force_select_family_name
687
+ st.session_state.family_selector_edition = current_family_for_edit
688
+ elif current_family_for_edit and current_family_for_edit in available_families:
689
+ pass
690
+ elif available_families:
691
+ current_family_for_edit = available_families[0]
692
+ st.session_state.family_selector_edition = current_family_for_edit
693
+ else:
694
+ current_family_for_edit = None
695
+ st.session_state.family_selector_edition = None
696
+
697
+ if current_family_for_edit and current_family_for_edit in available_families:
698
+ default_family_idx_edit = available_families.index(current_family_for_edit)
699
+ elif available_families:
700
+ default_family_idx_edit = 0
701
+ current_family_for_edit = available_families[0]
702
+ st.session_state.family_selector_edition = current_family_for_edit
703
+ else:
704
+ default_family_idx_edit = 0
705
+
706
+ if not available_families:
707
+ st.info("Aucune famille de métier de cas d'usage. Créez-en une via les options ci-dessous.")
708
+ else:
709
+ prev_family_selection_edit = st.session_state.get('family_selector_edition')
710
+ selected_family_ui_edit = st.selectbox(
711
+ "Métier :",
712
+ options=available_families,
713
+ index=default_family_idx_edit,
714
+ key='family_selectbox_widget_edit',
715
+ help="Sélectionnez une métier pour voir ses cas d'usage."
716
+ )
717
+ if st.session_state.family_selector_edition != selected_family_ui_edit :
718
+ st.session_state.family_selector_edition = selected_family_ui_edit
719
+
720
+ if prev_family_selection_edit != selected_family_ui_edit:
721
+ st.session_state.use_case_selector_edition = None
722
+ st.session_state.force_select_use_case_name = None
723
+ st.session_state.view_mode = "generator"
724
+ st.session_state.active_generated_prompt = ""
725
+ st.session_state.variable_type_to_create = None
726
+ st.session_state.editing_variable_info = None
727
+ st.rerun()
728
+
729
+ current_selected_family_for_edit_logic = st.session_state.get('family_selector_edition')
730
+ use_cases_in_current_family_edit_options = []
731
+ if current_selected_family_for_edit_logic and current_selected_family_for_edit_logic in st.session_state.editable_prompts:
732
+ use_cases_in_current_family_edit_options = sorted(list(st.session_state.editable_prompts[current_selected_family_for_edit_logic].keys()))
733
+
734
+ if use_cases_in_current_family_edit_options:
735
+ default_uc_idx_edit = 0
736
+ current_uc_for_edit = st.session_state.get('use_case_selector_edition')
737
+
738
+ if st.session_state.force_select_use_case_name and st.session_state.force_select_use_case_name in use_cases_in_current_family_edit_options:
739
+ current_uc_for_edit = st.session_state.force_select_use_case_name
740
+ elif current_uc_for_edit and current_uc_for_edit in use_cases_in_current_family_edit_options:
741
+ pass
742
+ elif use_cases_in_current_family_edit_options:
743
+ current_uc_for_edit = use_cases_in_current_family_edit_options[0]
744
+
745
+ st.session_state.use_case_selector_edition = current_uc_for_edit
746
+
747
+ if current_uc_for_edit and current_uc_for_edit in use_cases_in_current_family_edit_options:
748
+ default_uc_idx_edit = use_cases_in_current_family_edit_options.index(current_uc_for_edit)
749
+
750
+ prev_uc_selection_edit = st.session_state.get('use_case_selector_edition')
751
+ selected_use_case_ui_edit = st.radio(
752
+ "Cas d'usage :",
753
+ options=use_cases_in_current_family_edit_options,
754
+ index=default_uc_idx_edit,
755
+ key='use_case_radio_widget_edit',
756
+ help="Sélectionnez un cas d'usage pour générer un prompt ou le paramétrer."
757
+ )
758
+ if st.session_state.use_case_selector_edition != selected_use_case_ui_edit:
759
+ st.session_state.use_case_selector_edition = selected_use_case_ui_edit
760
+
761
+ if prev_uc_selection_edit != selected_use_case_ui_edit:
762
+ st.session_state.view_mode = "generator"
763
+ st.session_state.active_generated_prompt = ""
764
+ st.session_state.variable_type_to_create = None
765
+ st.session_state.editing_variable_info = None
766
+ st.rerun()
767
+
768
+ elif current_selected_family_for_edit_logic:
769
+ st.info(f"Aucun cas d'usage dans '{current_selected_family_for_edit_logic}'. Créez-en un.")
770
+ st.session_state.use_case_selector_edition = None
771
+
772
+ if st.session_state.force_select_family_name: st.session_state.force_select_family_name = None
773
+ if st.session_state.force_select_use_case_name: st.session_state.force_select_use_case_name = None
774
+ st.markdown("---")
775
+
776
+ with st.expander("🗂️ Gérer les familles de prompts par métier", expanded=False):
777
+ with st.form("new_family_form_sidebar", clear_on_submit=True):
778
+ new_family_name = st.text_input("Nom du nouveau métier:", key="new_fam_name_sidebar")
779
+ submitted_new_family = st.form_submit_button("➕ Créer métier")
780
+ if submitted_new_family and new_family_name.strip():
781
+ if new_family_name.strip() in st.session_state.editable_prompts:
782
+ st.error(f"Le métier '{new_family_name.strip()}' existe déjà.")
783
+ else:
784
+ st.session_state.editable_prompts[new_family_name.strip()] = {}
785
+ save_editable_prompts_to_local_and_hf()
786
+ st.success(f"Métier '{new_family_name.strip()}' créée.")
787
+ st.session_state.force_select_family_name = new_family_name.strip()
788
+ st.session_state.use_case_selector_edition = None
789
+ st.session_state.view_mode = "generator"
790
+ st.rerun()
791
+ elif submitted_new_family:
792
+ st.error("Le nom du métier ne peut pas être vide.")
793
+
794
+ if available_families and current_selected_family_for_edit_logic :
795
+ st.markdown("---")
796
+ with st.form("rename_family_form_sidebar"):
797
+ st.write(f"Renommer le métier : **{current_selected_family_for_edit_logic}**")
798
+ renamed_family_name_input = st.text_input("Nouveau nom :", value=current_selected_family_for_edit_logic, key="ren_fam_name_sidebar")
799
+ submitted_rename_family = st.form_submit_button("✏️ Renommer")
800
+ if submitted_rename_family and renamed_family_name_input.strip():
801
+ renamed_family_name = renamed_family_name_input.strip()
802
+ if renamed_family_name == current_selected_family_for_edit_logic:
803
+ st.info("Le nouveau nom est identique à l'ancien.")
804
+ elif renamed_family_name in st.session_state.editable_prompts:
805
+ st.error(f"Un métier nommé '{renamed_family_name}' existe déjà.")
806
+ else:
807
+ st.session_state.editable_prompts[renamed_family_name] = st.session_state.editable_prompts.pop(current_selected_family_for_edit_logic)
808
+ save_editable_prompts_to_local_and_hf()
809
+ st.success(f"Métier '{current_selected_family_for_edit_logic}' renommé en '{renamed_family_name}'.")
810
+ st.session_state.force_select_family_name = renamed_family_name
811
+ if st.session_state.library_selected_family_for_display == current_selected_family_for_edit_logic:
812
+ st.session_state.library_selected_family_for_display = renamed_family_name
813
+ st.session_state.view_mode = "generator"
814
+ st.rerun()
815
+ elif submitted_rename_family:
816
+ st.error("Le nouveau nom du métier ne peut pas être vide.")
817
+
818
+ st.markdown("---")
819
+ st.write(f"Supprimer le métier : **{current_selected_family_for_edit_logic}**")
820
+ if st.session_state.confirming_delete_family_name == current_selected_family_for_edit_logic:
821
+ st.warning(f"Supprimer '{current_selected_family_for_edit_logic}' et tous ses cas d'usage ? Action irréversible.")
822
+
823
+ _text_confirm_delete = f"Oui, supprimer définitivement '{current_selected_family_for_edit_logic}'"
824
+ if st.button(_text_confirm_delete, type="primary", key=f"confirm_del_fam_sb_{current_selected_family_for_edit_logic}", use_container_width=True):
825
+ deleted_fam_name = current_selected_family_for_edit_logic
826
+ del st.session_state.editable_prompts[current_selected_family_for_edit_logic]
827
+ save_editable_prompts_to_local_and_hf()
828
+ st.success(f"Métier '{deleted_fam_name}' supprimée.")
829
+ st.session_state.confirming_delete_family_name = None
830
+ st.session_state.family_selector_edition = None
831
+ st.session_state.use_case_selector_edition = None
832
+ if st.session_state.library_selected_family_for_display == deleted_fam_name:
833
+ st.session_state.library_selected_family_for_display = None
834
+ st.session_state.view_mode = "library"
835
+ st.rerun()
836
+
837
+ if st.button("Non, annuler la suppression", key=f"cancel_del_fam_sb_{current_selected_family_for_edit_logic}", use_container_width=True):
838
+ st.session_state.confirming_delete_family_name = None
839
+ st.session_state.view_mode = "generator"
840
+ st.rerun()
841
+ else:
842
+ if st.button(f"🗑️ Supprimer le métier Sélectionnée", key=f"del_fam_btn_sb_{current_selected_family_for_edit_logic}"):
843
+ st.session_state.confirming_delete_family_name = current_selected_family_for_edit_logic
844
+ st.session_state.view_mode = "generator"
845
+ st.rerun()
846
+ elif not available_families:
847
+ st.caption("Créez un métier pour pouvoir le gérer.")
848
+ else:
849
+ st.caption("Sélectionnez un métier (ci-dessus) pour le gérer.")
850
+
851
+ st.markdown("---")
852
+
853
+ with st.expander("➕ Créer un Cas d'Usage", expanded=st.session_state.get('show_create_new_use_case_form', False)):
854
+ if not available_families:
855
+ st.caption("Veuillez d'abord créer une famille de métier pour y ajouter des cas d'usage.")
856
+ else:
857
+ if st.button("Afficher/Masquer Formulaire de Création de Cas d'Usage", key="toggle_create_uc_form_in_exp"):
858
+ st.session_state.show_create_new_use_case_form = not st.session_state.get('show_create_new_use_case_form', False)
859
+ st.rerun()
860
+
861
+ if st.session_state.get('show_create_new_use_case_form', False):
862
+ with st.form("new_use_case_form_in_exp", clear_on_submit=True):
863
+ default_create_family_idx_tab = 0
864
+ if current_selected_family_for_edit_logic and current_selected_family_for_edit_logic in available_families:
865
+ default_create_family_idx_tab = available_families.index(current_selected_family_for_edit_logic)
866
+
867
+ uc_parent_family = st.selectbox(
868
+ "Métier parent du nouveau cas d'usage:",
869
+ options=available_families,
870
+ index=default_create_family_idx_tab,
871
+ key="new_uc_parent_fam_in_exp"
872
+ )
873
+ uc_name_input = st.text_input("Nom du Nouveau Cas d'Usage:", key="new_uc_name_in_exp")
874
+ uc_description_input = st.text_area("Description du cas d'usage:", height=100, key="new_uc_desc_in_exp", value="")
875
+ uc_template_input = st.text_area("Template Initial du Cas d'Usage:", height=150, key="new_uc_template_in_exp", value="Nouveau prompt...")
876
+ submitted_new_uc = st.form_submit_button("Créer Cas d'Usage")
877
+
878
+ if submitted_new_uc:
879
+ parent_family_val = uc_parent_family
880
+ uc_name_val = uc_name_input.strip()
881
+ uc_template_val = uc_template_input
882
+ uc_description_val = uc_description_input.strip()
883
+
884
+ if not uc_name_val:
885
+ st.error("Le nom du cas d'usage ne peut pas être vide.")
886
+ elif uc_name_val in st.session_state.editable_prompts.get(parent_family_val, {}):
887
+ st.error(f"Le cas d'usage '{uc_name_val}' existe déjà dans le métier '{parent_family_val}'.")
888
+ else:
889
+ now_iso_create, now_iso_update = get_default_dates()
890
+ st.session_state.editable_prompts[parent_family_val][uc_name_val] = {
891
+ "description": uc_description_val,
892
+ "template": uc_template_val or "Nouveau prompt...",
893
+ "variables": [], "tags": [],
894
+ "usage_count": 0, "created_at": now_iso_create, "updated_at": now_iso_update
895
+ }
896
+ save_editable_prompts_to_local_and_hf()
897
+ st.success(f"Cas d'usage '{uc_name_val}' créé avec succès dans '{parent_family_val}'.")
898
+ st.session_state.show_create_new_use_case_form = False
899
+ st.session_state.force_select_family_name = parent_family_val
900
+ st.session_state.force_select_use_case_name = uc_name_val
901
+ st.session_state.view_mode = "generator"
902
+ st.session_state.active_generated_prompt = ""
903
+ st.rerun()
904
+
905
  # --- Tab: Injection (Sidebar content) ---
906
  with tab_injection:
907
  st.subheader("Assistant & Injection")
 
933
  st.session_state.use_case_selector_edition = st.session_state.force_select_use_case_name
934
  st.session_state.force_select_use_case_name = None
935
 
 
 
936
  library_family_to_display = st.session_state.get('library_selected_family_for_display')
937
 
938
  # NOUVELLE SECTION POUR LA PAGE D'ACCUEIL
 
1183
  st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["created_at"] = now_iso_dup_create
1184
  st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["updated_at"] = now_iso_dup_update
1185
  st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["usage_count"] = 0
1186
+ save_editable_prompts_to_local_and_hf()
1187
  st.success(f"Cas d'usage '{original_uc_name_for_dup}' dupliqué en '{new_uc_name_val_from_form}' dans la famille '{target_family_on_submit}'.")
1188
 
1189
  st.session_state.duplicating_use_case_details = None
 
1210
  deleted_uc_name_for_msg = details['use_case']
1211
  deleted_uc_fam_for_msg = details['family']
1212
  del st.session_state.editable_prompts[details["family"]][details["use_case"]]
1213
+ save_editable_prompts_to_local_and_hf()
1214
  st.success(f"'{deleted_uc_name_for_msg}' supprimé de '{deleted_uc_fam_for_msg}'.")
1215
  st.session_state.confirming_delete_details = None
1216
  st.rerun()
 
1258
  available_families_check = list(st.session_state.editable_prompts.keys())
1259
  if not available_families_check : st.warning("La bibliothèque est entièrement vide. Veuillez créer des métiers et des prompts.")
1260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1261
 
1262
  elif st.session_state.view_mode == "generator":
1263
  generator_family = st.session_state.get('generator_selected_family')
 
1414
  st.balloons()
1415
  current_prompt_config["usage_count"] = current_prompt_config.get("usage_count", 0) + 1
1416
  current_prompt_config["updated_at"] = datetime.now().isoformat()
1417
+ save_editable_prompts_to_local_and_hf()
1418
 
1419
  except Exception as e:
1420
  st.error(f"Erreur inattendue lors de la génération du prompt : {e}")
 
1448
  should_expand_config = SHOW_CONFIG_SECTION and st.session_state.get('go_to_config_section', False)
1449
  if SHOW_CONFIG_SECTION:
1450
  with st.expander(f"⚙️ Paramétrage du Prompt: {generator_use_case}", expanded=should_expand_config):
1451
+ st.subheader("Template du Prompt")
1452
+ safe_family_key_part = str(generator_family).replace(' ', '_').replace('.', '_').replace('{', '_').replace('}', '_').replace('(', '_').replace(')', '_')
1453
+ safe_uc_key_part = str(generator_use_case).replace(' ', '_').replace('.', '_').replace('{', '_').replace('}', '_').replace('(', '_').replace(')', '_')
1454
+ template_text_area_key = f"template_text_area_{safe_family_key_part}_{safe_uc_key_part}"; new_tpl = st.text_area("Template:", value=current_prompt_config.get('template', ''), height=200, key=template_text_area_key)
1455
+ # Description
1456
+ new_desc = st.text_area("Description (aide pour l'utilisateur):", value=current_prompt_config.get('description', ''), height=100, key=f"desc_text_area_{safe_family_key_part}_{safe_uc_key_part}")
1457
+ st.markdown("##### Variables disponibles à insérer :"); variables_config = current_prompt_config.get('variables', [])
1458
+ if not variables_config: st.caption("Aucune variable définie pour ce prompt. Ajoutez-en ci-dessous.")
1459
+ else:
1460
+ for var_info in variables_config:
1461
+ if 'name' in var_info:
1462
+ st.code(f"{{{var_info['name']}}}", language=None)
1463
+ st.caption("Survolez une variable ci-dessus et cliquez sur l'icône qui apparaît pour la copier.")
1464
+ save_template_button_key = f"save_template_button_{safe_family_key_part}_{safe_uc_key_part}"
1465
+ if st.button("Sauvegarder Template & Description", key=save_template_button_key):
1466
+ current_prompt_config['template'] = new_tpl
1467
+ current_prompt_config['description'] = new_desc
1468
+ current_prompt_config["updated_at"] = datetime.now().isoformat()
1469
+ save_editable_prompts_to_local_and_hf()
1470
+ st.success("Template et Description sauvegardés!");
1471
+ st.rerun()
1472
+ st.markdown("---"); st.subheader("Variables du Prompt"); current_variables_list = current_prompt_config.get('variables', [])
1473
+ if not current_variables_list: st.info("Aucune variable définie.")
1474
+ else: pass
1475
+ for idx, var_data in enumerate(list(current_variables_list)):
1476
+ var_id_for_key = var_data.get('name', f"varidx{idx}").replace(" ", "_"); action_key_prefix = f"var_action_{generator_family.replace(' ','_')}_{generator_use_case.replace(' ','_')}_{var_id_for_key}"
1477
+ col_info, col_up, col_down, col_edit, col_delete = st.columns([3, 0.5, 0.5, 0.8, 0.8])
1478
+ with col_info: st.markdown(f"**{idx + 1}. {var_data.get('name', 'N/A')}** ({var_data.get('label', 'N/A')})\n*Type: `{var_data.get('type', 'N/A')}`*")
1479
+ with col_up:
1480
+ disable_up_button = (idx == 0)
1481
+ if st.button("↑", key=f"{action_key_prefix}_up", help="Monter cette variable", disabled=disable_up_button, use_container_width=True): current_variables_list[idx], current_variables_list[idx-1] = current_variables_list[idx-1], current_variables_list[idx]; current_prompt_config["variables"] = current_variables_list; current_prompt_config["updated_at"] = datetime.now().isoformat(); save_editable_prompts_to_local_and_hf(); st.session_state.editing_variable_info = None; st.session_state.variable_type_to_create = None; st.rerun()
1482
+ with col_down:
1483
+ disable_down_button = (idx == len(current_variables_list) - 1)
1484
+ if st.button("↓", key=f"{action_key_prefix}_down", help="Descendre cette variable", disabled=disable_down_button, use_container_width=True): current_variables_list[idx], current_variables_list[idx+1] = current_variables_list[idx+1], current_variables_list[idx]; current_prompt_config["variables"] = current_variables_list; current_prompt_config["updated_at"] = datetime.now().isoformat(); save_editable_prompts_to_local_and_hf(); st.session_state.editing_variable_info = None; st.session_state.variable_type_to_create = None; st.rerun()
1485
+ with col_edit:
1486
+ if st.button("Modifier", key=f"{action_key_prefix}_edit", use_container_width=True): st.session_state.editing_variable_info = { "family": generator_family, "use_case": generator_use_case, "index": idx, "data": copy.deepcopy(var_data) }; st.session_state.variable_type_to_create = var_data.get('type'); st.rerun()
1487
+ with col_delete:
1488
+ if st.button("Suppr.", key=f"{action_key_prefix}_delete", type="secondary", use_container_width=True): variable_name_to_delete = current_variables_list.pop(idx).get('name', 'Variable inconnue'); current_prompt_config["variables"] = current_variables_list; current_prompt_config["updated_at"] = datetime.now().isoformat(); save_editable_prompts_to_local_and_hf(); st.success(f"Variable '{variable_name_to_delete}' supprimée."); st.session_state.editing_variable_info = None; st.session_state.variable_type_to_create = None; st.rerun()
1489
+ st.markdown("---"); st.subheader("Ajouter une Variable"); is_editing_var = False; variable_data_for_form = {"name": "", "label": "", "type": "", "options": "", "default": ""}
1490
+ if st.session_state.editing_variable_info and st.session_state.editing_variable_info.get("family") == generator_family and st.session_state.editing_variable_info.get("use_case") == generator_use_case:
1491
+ edit_var_idx = st.session_state.editing_variable_info["index"]
1492
+ if edit_var_idx < len(current_prompt_config.get('variables',[])):
1493
+ is_editing_var = True; current_editing_data_snapshot = current_prompt_config['variables'][edit_var_idx]; variable_data_for_form.update(copy.deepcopy(current_editing_data_snapshot))
1494
+ if isinstance(variable_data_for_form.get("options"), list): variable_data_for_form["options"] = ", ".join(map(str, variable_data_for_form["options"]))
1495
+ raw_def_edit_form = variable_data_for_form.get("default")
1496
+ if isinstance(raw_def_edit_form, date): variable_data_for_form["default"] = raw_def_edit_form.strftime("%Y-%m-%d")
1497
+ elif raw_def_edit_form is not None: variable_data_for_form["default"] = str(raw_def_edit_form)
1498
+ else: variable_data_for_form["default"] = ""
1499
+ else: st.session_state.editing_variable_info = None; st.session_state.variable_type_to_create = None; st.warning("La variable que vous tentiez de modifier n'existe plus. Annulation de l'édition."); st.rerun() # pragma: no cover
1500
+ if not is_editing_var and st.session_state.variable_type_to_create is None:
1501
+ st.markdown("##### 1. Choisissez le type de variable à créer :"); variable_types_map = { "Zone de texte (courte)": "text_input", "Liste choix": "selectbox", "Date": "date_input", "Nombre": "number_input", "Zone de texte (longue)": "text_area" }; num_type_buttons = len(variable_types_map); cols_type_buttons = st.columns(min(num_type_buttons, 5)); button_idx = 0
1502
+ for btn_label, type_val in variable_types_map.items():
1503
+ if cols_type_buttons[button_idx % len(cols_type_buttons)].button(btn_label, key=f"btn_type_{type_val}_{generator_use_case.replace(' ','_')}", use_container_width=True): st.session_state.variable_type_to_create = type_val; st.rerun()
1504
+ button_idx += 1
1505
+ st.markdown("---")
1506
+ if st.session_state.variable_type_to_create:
1507
+ current_type_for_form = st.session_state.variable_type_to_create
1508
+ variable_types_map_display = {
1509
+ "text_input": "Zone de texte (courte)", "selectbox": "Liste choix",
1510
+ "date_input": "Date", "number_input": "Nombre", "text_area": "Zone de texte (longue)"
1511
+ }
1512
+ readable_type = variable_types_map_display.get(current_type_for_form, "Type Inconnu")
1513
+ form_title = f"Modifier Variable : {variable_data_for_form.get('name','N/A')} ({readable_type})" if is_editing_var else f"Nouvelle Variable : {readable_type}"
1514
+ st.markdown(f"##### 2. Configurez la variable")
1515
+
1516
+ form_key_suffix = f"_edit_{st.session_state.editing_variable_info['index']}" if is_editing_var and st.session_state.editing_variable_info else "_create"
1517
+ form_var_specific_key = f"form_var_{current_type_for_form}_{generator_use_case.replace(' ','_')}{form_key_suffix}"
1518
+
1519
+ with st.form(key=form_var_specific_key, clear_on_submit=(not is_editing_var)):
1520
+ st.subheader(form_title)
1521
+ var_name_input_form = st.text_input(
1522
+ "Nom technique (ex : nom_client. Sans espaces, accents ou caractères spéciaux)",
1523
+ value=variable_data_for_form.get("name", ""),
1524
+ key=f"{form_var_specific_key}_name",
1525
+ disabled=is_editing_var
1526
+ )
1527
+ var_label_input_form = st.text_input(
1528
+ "Label pour l'utilisateur (description affichée)",
1529
+ value=variable_data_for_form.get("label", ""),
1530
+ key=f"{form_var_specific_key}_label"
1531
+ )
1532
+ var_options_str_input_form = ""
1533
+ if current_type_for_form == "selectbox":
1534
+ var_options_str_input_form = st.text_input(
1535
+ "Options (séparées par une virgule)",
1536
+ value=variable_data_for_form.get("options", ""),
1537
+ key=f"{form_var_specific_key}_options"
1538
+ )
1539
+ date_hint = " (Format AAAA-MM-JJ)" if current_type_for_form == "date_input" else ""
1540
+ var_default_val_str_input_form = st.text_input(
1541
+ f"Valeur par défaut{date_hint}",
1542
+ value=str(variable_data_for_form.get("default", "")),
1543
+ key=f"{form_var_specific_key}_default"
1544
+ )
1545
+
1546
+ min_val_input_form, max_val_input_form, step_val_input_form, height_val_input_form = None, None, None, None
1547
+ if current_type_for_form == "number_input":
1548
+ num_cols_var_form = st.columns(3)
1549
+ min_val_edit_default = variable_data_for_form.get("min_value")
1550
+ max_val_edit_default = variable_data_for_form.get("max_value")
1551
+ step_val_edit_default = variable_data_for_form.get("step", 1.0)
1552
+ min_val_input_form = num_cols_var_form[0].number_input("Valeur minimale (optionnel)", value=float(min_val_edit_default) if min_val_edit_default is not None else None, format="%g", key=f"{form_var_specific_key}_min")
1553
+ max_val_input_form = num_cols_var_form[1].number_input("Valeur maximale (optionnel)", value=float(max_val_edit_default) if max_val_edit_default is not None else None, format="%g", key=f"{form_var_specific_key}_max")
1554
+ step_val_input_form = num_cols_var_form[2].number_input("Pas (incrément)", value=float(step_val_edit_default), format="%g", min_value=1e-9, key=f"{form_var_specific_key}_step")
1555
+ if current_type_for_form == "text_area":
1556
+ height_val_input_form = st.number_input("Hauteur de la zone de texte (pixels)", value=int(variable_data_for_form.get("height", 100)), min_value=68, step=25, key=f"{form_var_specific_key}_height")
1557
+
1558
+ submit_button_label_form = "Sauvegarder Modifications" if is_editing_var else "Ajouter Variable"
1559
+ submitted_specific_var_form = st.form_submit_button(submit_button_label_form)
1560
+
1561
+ if submitted_specific_var_form:
1562
+ var_name_val_submit = var_name_input_form.strip()
1563
+ if not var_name_val_submit or not var_label_input_form.strip(): st.error("Le nom technique et le label de la variable sont requis.")
1564
+ elif not var_name_val_submit.isidentifier(): st.error("Nom technique invalide. Utilisez lettres, chiffres, underscores. Ne pas commencer par un chiffre. Ne pas utiliser de mot-clé Python.")
1565
+ elif current_type_for_form == "selectbox" and not [opt.strip() for opt in var_options_str_input_form.split(',') if opt.strip()]: st.error("Pour une variable de type 'Liste choix', au moins une option est requise.")
1566
+ else:
1567
+ new_var_data_to_submit = { "name": var_name_val_submit, "label": var_label_input_form.strip(), "type": current_type_for_form }; parsed_def_val_submit = parse_default_value(var_default_val_str_input_form.strip(), current_type_for_form)
1568
+ if current_type_for_form == "selectbox":
1569
+ options_list_submit = [opt.strip() for opt in var_options_str_input_form.split(',') if opt.strip()]; new_var_data_to_submit["options"] = options_list_submit
1570
+ if options_list_submit:
1571
+ if parsed_def_val_submit not in options_list_submit: st.warning(f"La valeur par défaut '{parsed_def_val_submit}' n'est pas dans la liste d'options. La première option '{options_list_submit[0]}' sera utilisée comme défaut."); new_var_data_to_submit["default"] = options_list_submit[0]
1572
+ else: new_var_data_to_submit["default"] = parsed_def_val_submit
1573
+ else: new_var_data_to_submit["default"] = ""
1574
+ else: new_var_data_to_submit["default"] = parsed_def_val_submit
1575
+ if current_type_for_form == "number_input":
1576
+ if min_val_input_form is not None: new_var_data_to_submit["min_value"] = float(min_val_input_form)
1577
+ if max_val_input_form is not None: new_var_data_to_submit["max_value"] = float(max_val_input_form)
1578
+ if step_val_input_form is not None: new_var_data_to_submit["step"] = float(step_val_input_form)
1579
+ else: new_var_data_to_submit["step"] = 1.0
1580
+ if current_type_for_form == "text_area" and height_val_input_form is not None:
1581
+ new_var_data_to_submit["height"] = int(height_val_input_form)
1582
+
1583
+ can_proceed_with_save = True; target_vars_list = current_prompt_config.get('variables', [])
1584
+ if is_editing_var:
1585
+ idx_to_edit_submit_form = st.session_state.editing_variable_info["index"]; target_vars_list[idx_to_edit_submit_form] = new_var_data_to_submit; st.success(f"Variable '{var_name_val_submit}' mise à jour avec succès."); st.session_state.editing_variable_info = None; st.session_state.variable_type_to_create = None
1586
+ else:
1587
+ existing_var_names_in_uc = [v['name'] for v in target_vars_list]
1588
+ if var_name_val_submit in existing_var_names_in_uc: st.error(f"Une variable avec le nom technique '{var_name_val_submit}' existe déjà pour ce cas d'usage."); can_proceed_with_save = False
1589
+ else: target_vars_list.append(new_var_data_to_submit); st.success(f"Variable '{var_name_val_submit}' ajoutée avec succès.")
1590
+ if can_proceed_with_save:
1591
+ current_prompt_config["variables"] = target_vars_list; current_prompt_config["updated_at"] = datetime.now().isoformat(); save_editable_prompts_to_local_and_hf()
1592
+ if not is_editing_var: st.session_state.variable_type_to_create = None
1593
+ st.rerun()
1594
+
1595
+ cancel_button_label_form = "Annuler Modification" if is_editing_var else "Changer de Type / Annuler Création"
1596
+ cancel_btn_key = f"cancel_var_action_btn_{form_var_specific_key}_outside"
1597
+
1598
+ if st.button(cancel_button_label_form, key=cancel_btn_key, help="Réinitialise le formulaire de variable."):
1599
+ st.session_state.variable_type_to_create = None
1600
+ if is_editing_var:
1601
+ st.session_state.editing_variable_info = None
1602
+ st.rerun()
1603
+ st.markdown("---"); st.subheader("🏷️ Tags"); current_tags_str = ", ".join(current_prompt_config.get("tags", []))
1604
+ new_tags_str_input = st.text_input("Tags (séparés par des virgules):", value=current_tags_str, key=f"tags_input_{generator_family}_{generator_use_case}")
1605
+ if st.button("Sauvegarder Tags", key=f"save_tags_btn_{generator_family}_{generator_use_case}"): current_prompt_config["tags"] = sorted(list(set(t.strip() for t in new_tags_str_input.split(',') if t.strip()))); current_prompt_config["updated_at"] = datetime.now().isoformat(); save_editable_prompts_to_local_and_hf(); st.success("Tags sauvegardés!"); st.rerun()
1606
+
1607
+ st.markdown("---")
1608
+ st.subheader("Actions sur le Cas d'Usage")
1609
+
1610
+ if st.session_state.duplicating_use_case_details and \
1611
+ st.session_state.duplicating_use_case_details["family"] == generator_family and \
1612
+ st.session_state.duplicating_use_case_details["use_case"] == generator_use_case:
1613
+
1614
+ original_uc_name_for_dup_form = st.session_state.duplicating_use_case_details["use_case"]
1615
+ original_family_name_for_dup = st.session_state.duplicating_use_case_details["family"]
1616
+ st.markdown(f"#### Dupliquer '{original_uc_name_for_dup_form}' (depuis: {original_family_name_for_dup})")
1617
+
1618
+ form_key_duplicate = f"form_duplicate_name_{original_family_name_for_dup.replace(' ','_')}_{original_uc_name_for_dup_form.replace(' ','_')}"
1619
+ with st.form(key=form_key_duplicate):
1620
+ available_families_list = list(st.session_state.editable_prompts.keys())
1621
+ try:
1622
+ default_family_idx = available_families_list.index(original_family_name_for_dup)
1623
+ except ValueError:
1624
+ default_family_idx = 0
1625
+
1626
+ selected_target_family_for_duplicate = st.selectbox(
1627
+ "Choisir la famille de destination pour la copie :",
1628
+ options=available_families_list,
1629
+ index=default_family_idx,
1630
+ key=f"target_family_dup_select_{form_key_duplicate}"
1631
+ )
1632
+
1633
+ suggested_new_name_base = f"{original_uc_name_for_dup_form} (copie)"
1634
+ suggested_new_name = suggested_new_name_base
1635
+ temp_copy_count = 1
1636
+ while suggested_new_name in st.session_state.editable_prompts.get(selected_target_family_for_duplicate, {}):
1637
+ suggested_new_name = f"{suggested_new_name_base} {temp_copy_count}"
1638
+ temp_copy_count += 1
1639
+
1640
+ new_duplicated_uc_name_input = st.text_input(
1641
+ "Nouveau nom pour le cas d'usage dupliqué:",
1642
+ value=suggested_new_name,
1643
+ key=f"new_dup_name_input_{form_key_duplicate}"
1644
+ )
1645
+
1646
+ submitted_duplicate_form = st.form_submit_button("✅ Confirmer la Duplication", use_container_width=True)
1647
+
1648
+ if submitted_duplicate_form:
1649
+ new_uc_name_val_from_form = new_duplicated_uc_name_input.strip()
1650
+ target_family_on_submit = selected_target_family_for_duplicate
1651
+
1652
+ if not new_uc_name_val_from_form:
1653
+ st.error("Le nom du nouveau cas d'usage ne peut pas être vide.")
1654
+ elif new_uc_name_val_from_form in st.session_state.editable_prompts.get(target_family_on_submit, {}):
1655
+ st.error(f"Un cas d'usage nommé '{new_uc_name_val_from_form}' existe déjà dans la famille '{target_family_on_submit}'.")
1656
+ else:
1657
+ st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form] = copy.deepcopy(current_prompt_config)
1658
+ now_iso_dup_create, now_iso_dup_update = get_default_dates()
1659
+ st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["created_at"] = now_iso_dup_create
1660
+ st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["updated_at"] = now_iso_dup_update
1661
+ st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["usage_count"] = 0
1662
+ save_editable_prompts_to_local_and_hf()
1663
+ st.success(f"Cas d'usage '{original_uc_name_for_dup_form}' dupliqué en '{new_uc_name_val_from_form}' dans la famille '{target_family_on_submit}'.")
1664
+
1665
+ st.session_state.duplicating_use_case_details = None
1666
+ st.session_state.force_select_family_name = target_family_on_submit
1667
+ st.session_state.force_select_use_case_name = new_uc_name_val_from_form
1668
+ st.session_state.active_generated_prompt = ""
1669
+ st.session_state.variable_type_to_create = None
1670
+ st.session_state.editing_variable_info = None
1671
+ st.session_state.go_to_config_section = True
1672
+ st.rerun()
1673
+
1674
+ cancel_key_duplicate = f"cancel_dup_process_{original_family_name_for_dup.replace(' ','_')}_{original_uc_name_for_dup_form.replace(' ','_')}"
1675
+ if st.button("❌ Annuler la Duplication", key=cancel_key_duplicate, use_container_width=True):
1676
+ st.session_state.duplicating_use_case_details = None
1677
+ st.rerun()
1678
+ else:
1679
+ action_cols_manage = st.columns(2)
1680
+ with action_cols_manage[0]:
1681
+ dup_key_init = f"initiate_dup_uc_btn_{generator_family.replace(' ','_')}_{generator_use_case.replace(' ','_')}"
1682
+ if st.button("🔄 Dupliquer ce Cas d'Usage", key=dup_key_init, use_container_width=True):
1683
+ st.session_state.duplicating_use_case_details = {
1684
+ "family": generator_family,
1685
+ "use_case": generator_use_case
1686
+ }
1687
+ st.session_state.go_to_config_section = True
1688
+ st.rerun()
1689
+
1690
+ with action_cols_manage[1]:
1691
+ del_uc_key_exp_main = f"del_uc_btn_exp_main_{generator_family.replace(' ','_')}_{generator_use_case.replace(' ','_')}"
1692
+ is_confirming_this_uc_delete_main = bool(st.session_state.confirming_delete_details and \
1693
+ st.session_state.confirming_delete_details.get("family") == generator_family and \
1694
+ st.session_state.confirming_delete_details.get("use_case") == generator_use_case)
1695
+ if st.button("🗑️ Supprimer Cas d'Usage", key=del_uc_key_exp_main, type="secondary", disabled=is_confirming_this_uc_delete_main, use_container_width=True):
1696
+ st.session_state.confirming_delete_details = {"family": generator_family, "use_case": generator_use_case}
1697
+ st.rerun()
1698
 
1699
  if st.session_state.get('go_to_config_section'):
1700
  st.session_state.go_to_config_section = False
 
1770
  if first_new_uc_name is None:
1771
  first_new_uc_name = uc_name_stripped
1772
  if successful_injections:
1773
+ save_editable_prompts_to_local_and_hf()
1774
  st.success(f"{len(successful_injections)} cas d'usage injectés avec succès dans '{target_family_name}': {', '.join(successful_injections)}")
1775
  st.session_state.injection_json_text = ""
1776
  if first_new_uc_name:
1777
+ st.session_state.view_mode = "generator"
1778
  st.session_state.force_select_family_name = target_family_name
1779
  st.session_state.force_select_use_case_name = first_new_uc_name
1780
  st.session_state.go_to_config_section = True
 
1828
  st.rerun()
1829
 
1830
  if st.session_state.assistant_mode == "creation":
1831
+ st.markdown("Décrivez votre besoin pour que l'assistant génère une instruction détaillée. Vous donnerez cette instruction à votre LLM qui, en retour, produira les éléments de votre cas d'usage (prompt système, variables, etc.).")
1832
  with st.form(key="assistant_creation_form_std"):
1833
  # Initialiser current_form_input_values avec les valeurs de session_state ou les valeurs par défaut
1834
  # pour que les champs du formulaire soient pré-remplis correctement.
 
1877
  st.session_state.generated_meta_prompt_for_llm = ""
1878
 
1879
  elif st.session_state.assistant_mode == "amelioration":
1880
+ st.markdown("Collez votre prompt existant. L'assistant générera une instruction pour votre LLM afin de transformer votre prompt en un cas d'usage structuré et améliorable pour cette application.")
1881
  with st.form(key="assistant_amelioration_form_unified"):
1882
  # Utilise la valeur de session_state pour ce champ
1883
  prompt_existant_input_val = st.text_area(
 
1911
  if st.session_state.generated_meta_prompt_for_llm:
1912
  col_subheader_assist, col_indicator_assist = st.columns([0.85, 0.15])
1913
  with col_subheader_assist:
1914
+ st.subheader("📋 Instruction Générée (à coller dans votre LLM) :")
1915
  with col_indicator_assist:
1916
  st.markdown("<div style='color:red; text-align:right; font-size:0.9em; padding-top:1.9em;padding-right:0.9em;'>Copier ici : 👇</div>", unsafe_allow_html=True)
1917
 
1918
  st.code(st.session_state.generated_meta_prompt_for_llm, language='markdown', line_numbers=True)
1919
  st.caption("<span style='color:gray; font-size:0.9em;'>Utilisez l'icône en haut à droite du bloc de code pour copier l'instruction.</span>", unsafe_allow_html=True)
1920
  st.markdown("---")
1921
+ st.info("Une fois que votre LLM externe a généré le JSON basé sur cette instruction, copiez ce JSON et utilisez le bouton \"💉 Injecter JSON Manuellement\" (disponible aussi dans l'onglet Assistant du menu) pour l'ajouter à votre atelier.")
1922
  if st.button("💉 Injecter JSON Manuellement", key="prepare_inject_from_assistant_unified_btn", use_container_width=True, type="primary"):
1923
  st.session_state.view_mode = "inject_manual"
1924
  st.session_state.injection_selected_family = None
 
1927
  st.rerun()
1928
  if not any(st.session_state.editable_prompts.values()): # pragma: no cover
1929
  st.warning("Aucun groupement de cas d'usage métier n'est configurée. Veuillez en créer une via l'onglet 'Édition' ou vérifier votre Gist.")
1930
+ elif st.session_state.view_mode not in ["library", "generator", "inject_manual", "assistant_creation"]: # pragma: no cover
1931
+ st.session_state.view_mode = "library" if list(st.session_state.editable_prompts.keys()) else "generator"
1932
  st.rerun()
1933
 
1934
  # --- Sidebar Footer ---
1935
  st.sidebar.markdown("---")
1936
+ st.sidebar.info(f"Générateur v3.3.6 - © {CURRENT_YEAR} AIBuilders (démo)")