diff --git "a/refApp.py" "b/refApp.py" new file mode 100644--- /dev/null +++ "b/refApp.py" @@ -0,0 +1,1718 @@ +import streamlit as st +from datetime import datetime, date +import copy +import json +import requests +import os + +# --- PAGE CONFIGURATION (MUST BE THE FIRST STREAMLIT COMMAND) --- +# Fixed deployment issue +st.set_page_config(layout="wide", page_title="đ ïž Le laboratoire des Prompts IA", initial_sidebar_state="collapsed" ) + +# --- CUSTOM CSS FOR SIDEBAR TOGGLE TEXT --- +st.markdown(""" + + +""", unsafe_allow_html=True) + +# --- Initial Data Structure & Constants --- +CURRENT_YEAR = datetime.now().year + +# Variable de contrĂŽle pour la section de paramĂ©trage - changez Ă True pour rĂ©activer +SHOW_CONFIG_SECTION = True +GIST_DATA_FILENAME = "prompt_templates_data_v3.json" +LOCAL_DATA_FILENAME = "prompt_templates_data_v3.json" + +# --- Function to load prompt templates from files --- +def load_prompt_template(filename): + """Load prompt template from file, with fallback to basic template if file not found.""" + try: + with open(filename, 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + st.warning(f"Fichier de template '{filename}' non trouvĂ©. Utilisation d'un template de base.") + return "# MISSION\nVous ĂȘtes un assistant IA. Veuillez traiter la demande suivante: {problematique}" + except Exception as e: + st.error(f"Erreur lors du chargement du template '{filename}': {e}") + return "# MISSION\nVous ĂȘtes un assistant IA. Veuillez traiter la demande suivante: {problematique}" + +# Load prompt templates from external files +META_PROMPT_FOR_EXTERNAL_LLM_TEMPLATE = load_prompt_template("AIBprompt_creation_template.md") +META_PROMPT_FOR_LLM_AMELIORATION_TEMPLATE = load_prompt_template("AIBprompt_improvement_template.md") + +ASSISTANT_FORM_VARIABLES = [ + {"name": "problematique", "label": "DĂ©crivez le besoin ou la tĂąche que le prompt cible doit rĂ©soudre :", "type": "text_area", "default": "", "height": 100}, + {"name": "doc_source", "label": "Quel(s) types de document(s) sont nĂ©cessaire pour la rĂ©alisation de votre besoin ? (e.g. PDF, e-mail, texte brut -laisser vide si non pertinent-) :", "type": "text_input", "default": ""}, + {"name": "elements_specifiques_a_extraire", "label": "Quelles sont les informations spĂ©cifiques que vous souhaitez identifier / gĂ©nĂ©rer ? (e.g. l'ensemble des ID clients, les clauses du contrat) :", "type": "text_area", "default": "", "height": 100}, + {"name": "format_sortie_desire", "label": "Optionnel : sous quel format voulez vous que le prompt produise une rĂ©ponse ? (e.g. un texte de deux pages, une liste Ă puces) :", "type": "text_area", "default": "", "height": 75}, + {"name": "public_cible_reponse", "label": "Optionnel : pour quel public cible s'adressera le rĂ©sultat du prompt ? (e.g. des profils techniques, le grand public) :", "type": "text_input", "default": ""}, +] + +def get_default_dates(): + now_iso = datetime.now().isoformat() + return now_iso, now_iso + +INITIAL_PROMPT_TEMPLATES = { + "Propositions commerciales": {}, "Ateliers mĂ©tiers": {}, "Project Management": {} +} +for family, use_cases in INITIAL_PROMPT_TEMPLATES.items(): # Initial cleanup + if isinstance(use_cases, dict): + for uc_name, uc_config in use_cases.items(): + if "is_favorite" in uc_config: # pragma: no cover + del uc_config["is_favorite"] + +# --- Utility Functions (User's original versions, with height fix in _postprocess_after_loading) --- +def parse_default_value(value_str, var_type): + if not value_str: + if var_type == "number_input": return 0.0 + if var_type == "date_input": return datetime.now().date() + return "" + if var_type == "number_input": + try: return float(value_str) + except ValueError: return 0.0 + elif var_type == "date_input": + try: return datetime.strptime(value_str, "%Y-%m-%d").date() + except (ValueError, TypeError): + return value_str if isinstance(value_str, date) else datetime.now().date() + return value_str + +def _preprocess_for_saving(data_to_save): + processed_data = copy.deepcopy(data_to_save) + for family_name in list(processed_data.keys()): + use_cases_in_family = processed_data[family_name] + if not isinstance(use_cases_in_family, dict): # pragma: no cover + st.error(f"DonnĂ©es corrompues (famille non-dict): '{family_name}'. Suppression.") + del processed_data[family_name] + continue + for use_case_name in list(use_cases_in_family.keys()): + config = use_cases_in_family[use_case_name] + if not isinstance(config, dict): # pragma: no cover + st.error(f"DonnĂ©es corrompues (cas d'usage non-dict): '{use_case_name}' dans '{family_name}'. Suppression.") + del processed_data[family_name][use_case_name] + continue + if not isinstance(config.get("variables"), list): + config["variables"] = [] + for var_info in config.get("variables", []): + if isinstance(var_info, dict): + if var_info.get("type") == "date_input" and isinstance(var_info.get("default"), date): + var_info["default"] = var_info["default"].strftime("%Y-%m-%d") + if var_info.get("type") == "number_input": + if "default" in var_info and var_info["default"] is not None: + var_info["default"] = float(var_info["default"]) + if "min_value" in var_info and var_info["min_value"] is not None: + var_info["min_value"] = float(var_info["min_value"]) + if "max_value" in var_info and var_info["max_value"] is not None: + var_info["max_value"] = float(var_info["max_value"]) + if "step" in var_info and var_info["step"] is not None: + var_info["step"] = float(var_info["step"]) + else: + var_info["step"] = 1.0 + # Ensure height for text_area is an int if it exists (it should be already from other functions) + if var_info.get("type") == "text_area": + if "height" in var_info and var_info["height"] is not None: + try: + var_info["height"] = int(var_info["height"]) + except (ValueError, TypeError): # pragma: no cover + var_info["height"] = 100 # Should not happen if data is clean + + config.setdefault("tags", []) + if "is_favorite" in config: # pragma: no cover + del config["is_favorite"] + config.setdefault("usage_count", 0) + config.setdefault("created_at", datetime.now().isoformat()) + config.setdefault("updated_at", datetime.now().isoformat()) + return processed_data + +def _postprocess_after_loading(loaded_data): # User's trusted version + height fix + processed_data = copy.deepcopy(loaded_data) + now_iso = datetime.now().isoformat() + for family_name in list(processed_data.keys()): + use_cases_in_family = processed_data[family_name] + if not isinstance(use_cases_in_family, dict): # pragma: no cover + st.warning(f"DonnĂ©es corrompues (famille non-dict): '{family_name}'. IgnorĂ©e.") + del processed_data[family_name] + continue + for use_case_name in list(use_cases_in_family.keys()): + config = use_cases_in_family[use_case_name] + if not isinstance(config, dict): # pragma: no cover + st.warning(f"DonnĂ©es corrompues (cas d'usage non-dict): '{use_case_name}' dans '{family_name}'. IgnorĂ©.") + del processed_data[family_name][use_case_name] + continue + if not isinstance(config.get("variables"), list): + config["variables"] = [] + for var_info in config.get("variables", []): + if isinstance(var_info, dict): + if var_info.get("type") == "date_input" and isinstance(var_info.get("default"), str): + try: + var_info["default"] = datetime.strptime(var_info["default"], "%Y-%m-%d").date() + except ValueError: + var_info["default"] = datetime.now().date() + if var_info.get("type") == "number_input": + if "default" in var_info and var_info["default"] is not None: + var_info["default"] = float(var_info["default"]) + else: + var_info["default"] = 0.0 + if "min_value" in var_info and var_info["min_value"] is not None: + var_info["min_value"] = float(var_info["min_value"]) + if "max_value" in var_info and var_info["max_value"] is not None: + var_info["max_value"] = float(var_info["max_value"]) + if "step" in var_info and var_info["step"] is not None: + var_info["step"] = float(var_info["step"]) + else: + var_info["step"] = 1.0 + + # --- ADDED ROBUST HEIGHT VALIDATION --- + if var_info.get("type") == "text_area": + height_val = var_info.get("height") + if height_val is not None: + try: + h = int(height_val) + if h >= 68: + var_info["height"] = h + else: + var_info["height"] = 68 # Set to minimum if too small + # st.warning(f"Hauteur pour '{var_info.get('name', 'N/A')}' ajustĂ©e Ă 68px (minimum).") + except (ValueError, TypeError): + var_info["height"] = 100 # Default if invalid + # If height_val was None, 'height' key might not be in var_info, or it's None. + # The st.text_area widget call will handle None by using its internal default. + # Or we can explicitly set a default: + # else: + # var_info["height"] = 100 # Default if not present + + config.setdefault("tags", []) + if "is_favorite" in config: # pragma: no cover + del config["is_favorite"] + config.setdefault("usage_count", 0) + config.setdefault("created_at", now_iso) + config.setdefault("updated_at", now_iso) + if not isinstance(config.get("tags"), list): config["tags"] = [] + return processed_data + +# --- NEW: Simplified function to prepare newly injected use case config --- +def _prepare_newly_injected_use_case_config(uc_config_from_json): + prepared_config = copy.deepcopy(uc_config_from_json) + now_iso_created, now_iso_updated = get_default_dates() + + prepared_config["created_at"] = now_iso_created + prepared_config["updated_at"] = now_iso_updated + prepared_config["usage_count"] = 0 + + if "template" not in prepared_config or not isinstance(prepared_config["template"], str): # pragma: no cover + prepared_config["template"] = "" + st.warning(f"Cas d'usage injectĂ© '{uc_config_from_json.get('name', 'INCONNU')}' sans template valide. Template initialisĂ© Ă vide.") + + # Ensure description field exists + if "description" not in prepared_config or not isinstance(prepared_config["description"], str): + prepared_config["description"] = "" + + if not isinstance(prepared_config.get("variables"), list): + prepared_config["variables"] = [] + + for var_info in prepared_config.get("variables", []): # Ensure height is valid for text_area + if isinstance(var_info, dict) and var_info.get("type") == "text_area": + height_val = var_info.get("height") + if height_val is not None: + try: + h = int(height_val) + if h >= 68: var_info["height"] = h + else: var_info["height"] = 68 + except (ValueError, TypeError): + var_info["height"] = 100 # Default if invalid type + # If 'height' key is missing, it's fine; widget will use its default. + + if not isinstance(prepared_config.get("tags"), list): + prepared_config["tags"] = [] + else: + prepared_config["tags"] = sorted(list(set(str(tag).strip() for tag in prepared_config["tags"] if str(tag).strip()))) + + if "is_favorite" in prepared_config: # pragma: no cover + del prepared_config["is_favorite"] + + # Ajout pour la gestion des notes + # if "ratings" not in prepared_config or not isinstance(prepared_config["ratings"], list): + # prepared_config["ratings"] = [] + # if "average_rating" not in prepared_config: + # prepared_config["average_rating"] = 0.0 + return prepared_config + +# --- Gist Interaction Functions (User's original versions) --- +def get_local_file_content(): + try: + if os.path.exists(LOCAL_DATA_FILENAME): + with open(LOCAL_DATA_FILENAME, 'r', encoding='utf-8') as f: + content = f.read() + return content if content.strip() else "{}" + else: + st.info(f"Fichier '{LOCAL_DATA_FILENAME}' non trouvĂ©. Initialisation.") + return "{}" + except Exception as e: # pragma: no cover + st.error(f"Erreur de lecture du fichier local: {e}") + return None + +def save_to_local_file(new_content_json_string): + try: + with open(LOCAL_DATA_FILENAME, 'w', encoding='utf-8') as f: + f.write(new_content_json_string) + return True + except Exception as e: # pragma: no cover + st.error(f"Erreur de sauvegarde du fichier local: {e}") + return False + +def save_editable_prompts_to_local(): + if 'editable_prompts' in st.session_state: + data_to_save = _preprocess_for_saving(st.session_state.editable_prompts) + try: + json_string = json.dumps(data_to_save, indent=4, ensure_ascii=False) + if save_to_local_file(json_string): + st.toast("đŸ DonnĂ©es sauvegardĂ©es localement!", icon="đŸ") + else: + st.warning("Sauvegarde locale Ă©chouĂ©e.") + except Exception as e: # pragma: no cover + st.error(f"Erreur prĂ©paration donnĂ©es pour sauvegarde locale: {e}") + +def load_editable_prompts_from_local(): + raw_content = get_local_file_content() + if raw_content: + try: + loaded_data = json.loads(raw_content) + if not loaded_data or not isinstance(loaded_data, dict): + raise ValueError("Contenu fichier local vide ou mal structurĂ©.") + return _postprocess_after_loading(loaded_data) + except (json.JSONDecodeError, TypeError, ValueError) as e: + st.info(f"Erreur chargement fichier local ('{str(e)[:50]}...'). Initialisation avec modĂšles par dĂ©faut.") + else: + st.info("Fichier local vide ou inaccessible. Initialisation avec modĂšles par dĂ©faut.") + initial_data = copy.deepcopy(INITIAL_PROMPT_TEMPLATES) + if raw_content is None or raw_content == "{}": + data_to_save_init = _preprocess_for_saving(initial_data) + try: + json_string_init = json.dumps(data_to_save_init, indent=4, ensure_ascii=False) + if save_to_local_file(json_string_init): + st.info("ModĂšles par dĂ©faut sauvegardĂ©s localement pour initialisation.") + except Exception as e: # pragma: no cover + st.error(f"Erreur sauvegarde initiale locale: {e}") + return initial_data + +# --- ACCESS CODE VERIFICATION --- +# Initialize access control session state +if 'access_granted' not in st.session_state: + st.session_state.access_granted = False +if 'access_code_input' not in st.session_state: + st.session_state.access_code_input = "" + +# Access code popup +if not st.session_state.access_granted: + # Simple CSS for background styling + st.markdown(""" + + """, unsafe_allow_html=True) + + # No vertical spacing to align at top + + # Create the access form + with st.container(): + st.markdown(""" +
+ Veuillez entrer le code d'accĂšs pour utiliser l'application +
++ Pour demander un accÚs, contactez votre référent métier ou l'administrateur de l'application : + arthur.causse-prestataire@laposte.fr +
+ """, unsafe_allow_html=True) + + # Stop execution here if access not granted + st.stop() + +# --- Session State Initialization --- +if 'editable_prompts' not in st.session_state: + st.session_state.editable_prompts = load_editable_prompts_from_local() +if 'view_mode' not in st.session_state: + st.session_state.view_mode = "accueil" # Nouvelle vue par dĂ©faut + +if 'library_selected_family_for_display' not in st.session_state: + available_families = list(st.session_state.editable_prompts.keys()) + st.session_state.library_selected_family_for_display = available_families[0] if available_families else None + +if 'family_selector_edition' not in st.session_state: + available_families = list(st.session_state.editable_prompts.keys()) + st.session_state.family_selector_edition = available_families[0] if available_families else None +if 'use_case_selector_edition' not in st.session_state: st.session_state.use_case_selector_edition = None +if 'editing_variable_info' not in st.session_state: st.session_state.editing_variable_info = None +if 'show_create_new_use_case_form' not in st.session_state: st.session_state.show_create_new_use_case_form = False +if 'force_select_family_name' not in st.session_state: st.session_state.force_select_family_name = None +if 'force_select_use_case_name' not in st.session_state: st.session_state.force_select_use_case_name = None +if 'confirming_delete_details' not in st.session_state: st.session_state.confirming_delete_details = None +if 'confirming_delete_family_name' not in st.session_state: st.session_state.confirming_delete_family_name = None +if 'library_search_term' not in st.session_state: st.session_state.library_search_term = "" +if 'library_selected_tags' not in st.session_state: st.session_state.library_selected_tags = [] +# Track previous filter values for change detection +if 'previous_search_term' not in st.session_state: st.session_state.previous_search_term = "" +if 'previous_selected_tags' not in st.session_state: st.session_state.previous_selected_tags = [] +if 'variable_type_to_create' not in st.session_state: st.session_state.variable_type_to_create = None +if 'active_generated_prompt' not in st.session_state: st.session_state.active_generated_prompt = "" +if 'duplicating_use_case_details' not in st.session_state: st.session_state.duplicating_use_case_details = None +if 'go_to_config_section' not in st.session_state: st.session_state.go_to_config_section = False + +# Generator session state variables +if 'generator_selected_family' not in st.session_state: st.session_state.generator_selected_family = None +if 'generator_selected_use_case' not in st.session_state: st.session_state.generator_selected_use_case = None + +if 'injection_selected_family' not in st.session_state: + st.session_state.injection_selected_family = None +if 'injection_json_text' not in st.session_state: + st.session_state.injection_json_text = "" + +if 'assistant_form_values' not in st.session_state: + st.session_state.assistant_form_values = {var['name']: var['default'] for var in ASSISTANT_FORM_VARIABLES} +if 'generated_meta_prompt_for_llm' not in st.session_state: + st.session_state.generated_meta_prompt_for_llm = "" + +# NOUVELLES CLĂS POUR L'ASSISTANT UNIFIĂ +if 'assistant_mode' not in st.session_state: + st.session_state.assistant_mode = "creation" # Modes possibles: "creation", "amelioration" +if 'assistant_existing_prompt_value' not in st.session_state: + st.session_state.assistant_existing_prompt_value = "" + +# --- Sidebar Navigation with Tabs --- +st.sidebar.header("Menu Principal") +tab_bibliotheque, tab_injection = st.sidebar.tabs([ + "đ BibliothĂšque", + "đĄ Assistant" +]) + + +# --- Tab: BibliothĂšque (Sidebar content) --- +with tab_bibliotheque: + st.subheader("Explorer la BibliothĂšque de Prompts") + search_col, filter_tag_col = st.columns(2) + with search_col: + st.session_state.library_search_term = st.text_input( + "đ Rechercher par mot-clĂ©:", + value=st.session_state.get("library_search_term", ""), + placeholder="Nom, template, variable..." + ) + + all_tags_list = sorted(list(set(tag for family in st.session_state.editable_prompts.values() for uc in family.values() for tag in uc.get("tags", [])))) + with filter_tag_col: + st.session_state.library_selected_tags = st.multiselect( + "đ·ïž Filtrer par Tags:", + options=all_tags_list, + default=st.session_state.get("library_selected_tags", []) + ) + + # Detect filter changes and auto-redirect to global search results + current_search_term = st.session_state.get("library_search_term", "").strip() + current_selected_tags = st.session_state.get("library_selected_tags", []) + + # Check if filters have changed + search_changed = current_search_term != st.session_state.get("previous_search_term", "") + tags_changed = current_selected_tags != st.session_state.get("previous_selected_tags", []) + + # If filters changed and we have active filters, redirect to global search + if (search_changed or tags_changed) and (current_search_term or current_selected_tags): + # Update previous values + st.session_state.previous_search_term = current_search_term + st.session_state.previous_selected_tags = current_selected_tags.copy() + # Redirect to global search page + if st.session_state.view_mode != "select_family_for_library": + st.session_state.view_mode = "select_family_for_library" + st.rerun() + # Update previous values even when filters are cleared + elif not current_search_term and not current_selected_tags: + st.session_state.previous_search_term = current_search_term + st.session_state.previous_selected_tags = current_selected_tags.copy() + + st.markdown("---") + + if not st.session_state.editable_prompts or not any(st.session_state.editable_prompts.values()): + st.info("La bibliothĂšque est vide. Ajoutez des prompts via l'onglet 'Ădition'.") + else: + sorted_families_bib = sorted(list(st.session_state.editable_prompts.keys())) + + if not st.session_state.get('library_selected_family_for_display') or \ + st.session_state.library_selected_family_for_display not in sorted_families_bib: + st.session_state.library_selected_family_for_display = sorted_families_bib[0] if sorted_families_bib else None + + st.write("SĂ©lectionner un mĂ©tier Ă afficher :") + for family_name_bib in sorted_families_bib: + button_key = f"lib_family_btn_{family_name_bib.replace(' ', '_').replace('&', '_')}" + is_selected_family = (st.session_state.library_selected_family_for_display == family_name_bib) + if st.button( + family_name_bib, + key=button_key, + use_container_width=True, + type="primary" if is_selected_family else "secondary" + ): + if st.session_state.library_selected_family_for_display != family_name_bib: + st.session_state.library_selected_family_for_display = family_name_bib + st.session_state.view_mode = "library" + st.rerun() + st.markdown("---") + +# --- Tab: Injection (Sidebar content) --- +with tab_injection: + st.subheader("Assistant & Injection") + st.markdown("Utilisez l'assistant pour prĂ©parer un prompt systĂšme ou injectez des cas d'usage en format JSON.") + # MODIFICATION DU BOUTON EXISTANT + if st.button("âš Assistant Prompt SystĂšme", key="start_assistant_unified_btn", use_container_width=True): # Nom du bouton mis Ă jour + st.session_state.view_mode = "assistant_creation" + # RĂ©initialiser au mode "creation" par dĂ©faut et vider les champs des deux modes potentiels + st.session_state.assistant_mode = "creation" + st.session_state.assistant_form_values = {var['name']: var['default'] for var in ASSISTANT_FORM_VARIABLES} + st.session_state.assistant_existing_prompt_value = "" + st.session_state.generated_meta_prompt_for_llm = "" # Le mĂ©ta-prompt gĂ©nĂ©rĂ© est commun + st.rerun() + + if st.button("đ Injecter JSON Manuellement", key="start_manual_injection_btn", use_container_width=True): + st.session_state.view_mode = "inject_manual" + st.session_state.injection_selected_family = None + st.session_state.injection_json_text = "" + st.session_state.generated_meta_prompt_for_llm = "" # Aussi rĂ©initialiser ici + st.rerun() + +# --- Main Display Area --- +# Handle force selection after injection +if st.session_state.get('force_select_family_name'): + st.session_state.family_selector_edition = st.session_state.force_select_family_name + st.session_state.force_select_family_name = None + +if st.session_state.get('force_select_use_case_name'): + st.session_state.use_case_selector_edition = st.session_state.force_select_use_case_name + st.session_state.force_select_use_case_name = None + +final_selected_family_edition = st.session_state.get('family_selector_edition') +final_selected_use_case_edition = st.session_state.get('use_case_selector_edition') +library_family_to_display = st.session_state.get('library_selected_family_for_display') + +# NOUVELLE SECTION POUR LA PAGE D'ACCUEIL +if st.session_state.view_mode == "accueil": + st.header("Bienvenue dans votre laboratoire des prompts IA ! đĄ") + st.caption(f"Créé par le pĂŽle Data / IA") + st.markdown(""" + Vous ĂȘtes au bon endroit pour maĂźtriser l'art de "parler" aux Intelligences Artificielles (IA) et obtenir d'elles exactement ce dont vous avez besoin ! + + **Qu'est-ce qu'un "prompt" ?** + Imaginez donner des instructions Ă un assistant virtuel intelligent, mais qui a besoin de consignes claires. Un "prompt", c'est simplement cette instruction, cette question ou cette consigne que vous formulez Ă l'IA. + Plus votre instruction est bien pensĂ©e, plus l'IA vous fournira une rĂ©ponse utile et pertinente. + + **Que pouvez-vous faire avec cette application ?** + + Cet outil est conçu pour vous simplifier la vie, que vous soyez novice ou plus expĂ©rimentĂ© : + + * **DĂ©couvrir et utiliser des modĂšles d'instructions prĂȘts Ă l'emploi** : Explorez une collection de "prompts" dĂ©jĂ conçus pour diverses tĂąches (comme rĂ©diger un email, rĂ©sumer un document, analyser une situation, etc.). Vous pourrez les utiliser tels quels ou les adapter facilement. + * **CrĂ©er vos propres instructions sur mesure** : Vous avez une idĂ©e prĂ©cise en tĂȘte ? Notre assistant vous guide pas Ă pas pour construire le "prompt" parfait, mĂȘme si vous n'avez aucune connaissance technique. L'objectif est de transformer votre besoin en une instruction claire pour l'IA. + * **Organiser et amĂ©liorer vos instructions** : Conservez vos meilleurs "prompts", modifiez-les et perfectionnez-les au fil du temps. + + En bref, cet outil vous aide Ă formuler les meilleures demandes possibles aux IA pour qu'elles deviennent de vĂ©ritables alliĂ©es dans votre travail ou vos projets. + """) + + cols_accueil = st.columns(2) + with cols_accueil[0]: + if st.button("đ Je souhaite utiliser / modifier un prompt existant", use_container_width=True, type="primary"): + st.session_state.view_mode = "select_family_for_library" + st.rerun() + with cols_accueil[1]: + if st.button("âš Je souhaite crĂ©er un prompt Ă partir de mon besoin", use_container_width=True, type="primary"): + st.session_state.view_mode = "assistant_creation" + # RĂ©initialiser les valeurs du formulaire de l'assistant et le prompt gĂ©nĂ©rĂ© + st.session_state.assistant_form_values = {var['name']: var['default'] for var in ASSISTANT_FORM_VARIABLES} + st.session_state.generated_meta_prompt_for_llm = "" + st.rerun() + +elif st.session_state.view_mode == "select_family_for_library": + if st.button("âŹ ïž Retour Ă l'accueil", key="back_to_accueil_from_select_family"): + st.session_state.view_mode = "accueil" + st.rerun() + + # Check if global filters are active + search_term_lib = st.session_state.get("library_search_term", "").strip().lower() + selected_tags_lib = st.session_state.get("library_selected_tags", []) + has_active_filters = bool(search_term_lib or selected_tags_lib) + + if has_active_filters: + st.header("đ RĂ©sultats de recherche dans tous les mĂ©tiers") + if search_term_lib and selected_tags_lib: + st.markdown(f"Recherche: **{search_term_lib}** | Tags: **{', '.join(selected_tags_lib)}**") + elif search_term_lib: + st.markdown(f"Recherche: **{search_term_lib}**") + elif selected_tags_lib: + st.markdown(f"Tags: **{', '.join(selected_tags_lib)}**") + st.markdown("---") + + # Global filtering across all families + global_filtered_results = {} + for family_name, use_cases in st.session_state.editable_prompts.items(): + family_filtered_use_cases = {} + for uc_name, uc_config in use_cases.items(): + # Apply same filtering logic as in library view + match_search = True + if search_term_lib: + match_search = (search_term_lib in uc_name.lower() or + search_term_lib in uc_config.get("template", "").lower() or + any(search_term_lib in var.get("name","").lower() or search_term_lib in var.get("label","").lower() + for var in uc_config.get("variables", [])) or + any(search_term_lib in tag.lower() for tag in uc_config.get("tags", []))) + match_tags = True + if selected_tags_lib: + match_tags = all(tag in uc_config.get("tags", []) for tag in selected_tags_lib) + + if match_search and match_tags: + family_filtered_use_cases[uc_name] = uc_config + + if family_filtered_use_cases: + global_filtered_results[family_name] = family_filtered_use_cases + + if not global_filtered_results: + st.info("Aucun prompt ne correspond Ă vos critĂšres de recherche dans tous les mĂ©tiers.") + else: + # Display results grouped by family + for family_name, filtered_use_cases in sorted(global_filtered_results.items()): + st.subheader(f"đč {family_name}") + sorted_use_cases = sorted(filtered_use_cases.keys()) + for uc_name in sorted_use_cases: + uc_config = filtered_use_cases[uc_name] + exp_title = f"{uc_name}" + if uc_config.get("usage_count", 0) > 0: + exp_title += f" (UtilisĂ© {uc_config.get('usage_count')} fois)" + + with st.expander(exp_title, expanded=False): + # Display tags in the same format as standard library view + tags_display = uc_config.get("tags", []) + if tags_display: + st.markdown(f"**Tags :** {', '.join([f'`{tag}`' for tag in tags_display])}") + + # Display creation and modification dates + created_at_str = uc_config.get('created_at', get_default_dates()[0]) + updated_at_str = uc_config.get('updated_at', get_default_dates()[1]) + st.caption(f"Créé le : {datetime.fromisoformat(created_at_str).strftime('%d/%m/%Y %H:%M')} | ModifiĂ© le : {datetime.fromisoformat(updated_at_str).strftime('%d/%m/%Y %H:%M')}") + + # Three buttons in columns - same format as standard library view + col_btn_lib1, col_btn_lib2, col_btn_lib3 = st.columns(3) + with col_btn_lib1: + if st.button(f"âïž Utiliser ce prompt", key=f"global_lib_use_{family_name.replace(' ', '_')}_{uc_name.replace(' ', '_')}", use_container_width=True): + st.session_state.view_mode = "generator" + st.session_state.generator_selected_family = family_name + st.session_state.generator_selected_use_case = uc_name + st.session_state.active_generated_prompt = "" + st.rerun() + with col_btn_lib2: + if st.button(f"đ Dupliquer ce prompt", key=f"global_lib_duplicate_{family_name.replace(' ', '_')}_{uc_name.replace(' ', '_')}", use_container_width=True): + st.session_state.duplicating_use_case_details = { + "family": family_name, + "use_case": uc_name + } + st.rerun() + with col_btn_lib3: + if st.button(f"đïž Supprimer ce prompt", key=f"global_lib_delete_{family_name.replace(' ', '_')}_{uc_name.replace(' ', '_')}", use_container_width=True): + st.session_state.confirming_delete_details = { + "family": family_name, + "use_case": uc_name + } + st.rerun() + st.markdown("---") + + # Clear filters option + if st.button("đïž Effacer les filtres", type="secondary"): + st.session_state.library_search_term = "" + st.session_state.library_selected_tags = [] + st.rerun() + + else: + st.header("đ Explorer les prompts par mĂ©tier") + st.markdown("Cliquez sur le nom d'un mĂ©tier pour afficher les prompts associĂ©s.") + st.markdown("---") + + available_families = list(st.session_state.editable_prompts.keys()) + + if not available_families: + st.info("Aucun mĂ©tier de prompts n'a Ă©tĂ© créé pour le moment.") + st.markdown("Vous pouvez en crĂ©er via l'onglet **Ădition** dans le menu latĂ©ral (accessible via l'icĂŽne Menu en haut Ă gauche).") + st.markdown("---") + + else: + sorted_families = sorted(available_families) + + # Vous pouvez ajuster le nombre de colonnes si vous avez beaucoup de familles + num_cols = 3 + cols = st.columns(num_cols) + for i, family_name in enumerate(sorted_families): + with cols[i % num_cols]: + if st.button(f"{family_name}", key=f"select_family_for_lib_btn_{family_name}", use_container_width=True, help=f"Voir les prompts du mĂ©tier '{family_name}'"): + st.session_state.library_selected_family_for_display = family_name + st.session_state.view_mode = "library" # Redirige vers la bibliothĂšque avec la famille sĂ©lectionnĂ©e + st.rerun() + + st.markdown("---") + +elif st.session_state.view_mode == "library": + if st.button("âŹ ïž Retour Ă la sĂ©lection des mĂ©tiers", key="back_to_select_family_from_library"): + st.session_state.view_mode = "select_family_for_library" + st.rerun() + if not library_family_to_display: + st.info("Veuillez sĂ©lectionner un mĂ©tier dans la barre latĂ©rale (onglet BibliothĂšque) pour afficher les prompts.") + available_families_main_display = list(st.session_state.editable_prompts.keys()) + if available_families_main_display: + st.session_state.library_selected_family_for_display = available_families_main_display[0] + st.rerun() + elif not any(st.session_state.editable_prompts.values()): + st.warning("Aucun mĂ©tier de cas d'usage n'est configurĂ©e. CrĂ©ez-en via l'onglet 'Ădition'.") + elif library_family_to_display in st.session_state.editable_prompts: + st.header(f"BibliothĂšque - mĂ©tier : {library_family_to_display}") + use_cases_in_family_display = st.session_state.editable_prompts[library_family_to_display] + filtered_use_cases = {} + search_term_lib = st.session_state.get("library_search_term", "").strip().lower() + selected_tags_lib = st.session_state.get("library_selected_tags", []) + if use_cases_in_family_display: + for uc_name, uc_config in use_cases_in_family_display.items(): + match_search = True + if search_term_lib: + match_search = (search_term_lib in uc_name.lower() or + search_term_lib in uc_config.get("template", "").lower() or + any(search_term_lib in var.get("name","").lower() or search_term_lib in var.get("label","").lower() + for var in uc_config.get("variables", [])) or + any(search_term_lib in tag.lower() for tag in uc_config.get("tags", []))) + match_tags = True + if selected_tags_lib: match_tags = all(tag in uc_config.get("tags", []) for tag in selected_tags_lib) + if match_search and match_tags: filtered_use_cases[uc_name] = uc_config + if not filtered_use_cases: + if not use_cases_in_family_display: st.info(f"Le mĂ©tier '{library_family_to_display}' ne contient actuellement aucun prompt.") + else: st.info("Aucun prompt ne correspond Ă vos critĂšres de recherche/filtre dans cette mĂ©tier.") + else: + # Gestion de la duplication de cas d'usage + if st.session_state.duplicating_use_case_details and \ + st.session_state.duplicating_use_case_details["family"] == library_family_to_display: + + original_uc_name_for_dup = st.session_state.duplicating_use_case_details["use_case"] + original_family_name_for_dup = st.session_state.duplicating_use_case_details["family"] + + st.markdown(f"### đ Dupliquer '{original_uc_name_for_dup}' (depuis: {original_family_name_for_dup})") + + form_key_duplicate = f"form_duplicate_lib_{original_family_name_for_dup.replace(' ','_')}_{original_uc_name_for_dup.replace(' ','_')}" + with st.form(key=form_key_duplicate): + available_families_list = list(st.session_state.editable_prompts.keys()) + try: + default_family_idx = available_families_list.index(original_family_name_for_dup) + except ValueError: + default_family_idx = 0 + + selected_target_family_for_duplicate = st.selectbox( + "Choisir la famille de destination pour la copie :", + options=available_families_list, + index=default_family_idx, + key=f"target_family_dup_select_{form_key_duplicate}" + ) + + suggested_new_name_base = f"{original_uc_name_for_dup} (copie)" + suggested_new_name = suggested_new_name_base + temp_copy_count = 1 + while suggested_new_name in st.session_state.editable_prompts.get(selected_target_family_for_duplicate, {}): + suggested_new_name = f"{suggested_new_name_base} {temp_copy_count}" + temp_copy_count += 1 + + new_duplicated_uc_name_input = st.text_input( + "Nouveau nom pour le cas d'usage dupliquĂ©:", + value=suggested_new_name, + key=f"new_dup_name_input_{form_key_duplicate}" + ) + + submitted_duplicate_form = st.form_submit_button("â Confirmer la Duplication", use_container_width=True) + + if submitted_duplicate_form: + new_uc_name_val_from_form = new_duplicated_uc_name_input.strip() + target_family_on_submit = selected_target_family_for_duplicate + + if not new_uc_name_val_from_form: + st.error("Le nom du nouveau cas d'usage ne peut pas ĂȘtre vide.") + elif new_uc_name_val_from_form in st.session_state.editable_prompts.get(target_family_on_submit, {}): + st.error(f"Un cas d'usage nommĂ© '{new_uc_name_val_from_form}' existe dĂ©jĂ dans la famille '{target_family_on_submit}'.") + else: + current_prompt_config = st.session_state.editable_prompts[original_family_name_for_dup][original_uc_name_for_dup] + st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form] = copy.deepcopy(current_prompt_config) + now_iso_dup_create, now_iso_dup_update = get_default_dates() + st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["created_at"] = now_iso_dup_create + st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["updated_at"] = now_iso_dup_update + st.session_state.editable_prompts[target_family_on_submit][new_uc_name_val_from_form]["usage_count"] = 0 + save_editable_prompts_to_local() + 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}'.") + + st.session_state.duplicating_use_case_details = None + if target_family_on_submit != library_family_to_display: + st.session_state.library_selected_family_for_display = target_family_on_submit + st.rerun() + + cancel_key_duplicate = f"cancel_dup_process_lib_{original_family_name_for_dup.replace(' ','_')}_{original_uc_name_for_dup.replace(' ','_')}" + if st.button("â Annuler la Duplication", key=cancel_key_duplicate, use_container_width=True): + st.session_state.duplicating_use_case_details = None + st.rerun() + + st.markdown("---") + + # Gestion de la suppression de cas d'usage + if st.session_state.confirming_delete_details and \ + st.session_state.confirming_delete_details["family"] == library_family_to_display: + + details = st.session_state.confirming_delete_details + st.warning(f"â ïž Supprimer '{details['use_case']}' de '{details['family']}' ? Action irrĂ©versible.") + + c1_del_uc, c2_del_uc, _ = st.columns([1,1,3]) + if c1_del_uc.button(f"Oui, supprimer '{details['use_case']}'", key=f"del_yes_lib_{details['family']}_{details['use_case']}", type="primary"): + 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}'.") + st.session_state.confirming_delete_details = None + st.rerun() + + if c2_del_uc.button("Non, annuler", key=f"del_no_lib_{details['family']}_{details['use_case']}"): + st.session_state.confirming_delete_details = None + st.rerun() + + st.markdown("---") + + sorted_use_cases_display = sorted(list(filtered_use_cases.keys())) + for use_case_name_display in sorted_use_cases_display: + prompt_config_display = filtered_use_cases[use_case_name_display] + template_display = prompt_config_display.get("template", "_Template non dĂ©fini._") + exp_title = f"{use_case_name_display}" + if prompt_config_display.get("usage_count", 0) > 0: exp_title += f" (UtilisĂ© {prompt_config_display.get('usage_count')} fois)" + with st.expander(exp_title, expanded=False): + + tags_display = prompt_config_display.get("tags", []) + if tags_display: st.markdown(f"**Tags :** {', '.join([f'`{tag}`' for tag in tags_display])}") + created_at_str = prompt_config_display.get('created_at', get_default_dates()[0]) + updated_at_str = prompt_config_display.get('updated_at', get_default_dates()[1]) + st.caption(f"Créé le : {datetime.fromisoformat(created_at_str).strftime('%d/%m/%Y %H:%M')} | ModifiĂ© le : {datetime.fromisoformat(updated_at_str).strftime('%d/%m/%Y %H:%M')}") + + col_btn_lib1, col_btn_lib2, col_btn_lib3 = st.columns(3) + with col_btn_lib1: + if st.button(f"âïž Utiliser ce prompt", key=f"main_lib_use_{library_family_to_display.replace(' ', '_')}_{use_case_name_display.replace(' ', '_')}", use_container_width=True): + st.session_state.view_mode = "generator"; st.session_state.generator_selected_family = library_family_to_display; st.session_state.generator_selected_use_case = use_case_name_display; st.session_state.active_generated_prompt = ""; st.rerun() + with col_btn_lib2: + if st.button(f"đ Dupliquer ce prompt", key=f"main_lib_duplicate_{library_family_to_display.replace(' ', '_')}_{use_case_name_display.replace(' ', '_')}", use_container_width=True): + st.session_state.duplicating_use_case_details = { + "family": library_family_to_display, + "use_case": use_case_name_display + } + st.rerun() + with col_btn_lib3: + if st.button(f"đïž Supprimer ce prompt", key=f"main_lib_delete_{library_family_to_display.replace(' ', '_')}_{use_case_name_display.replace(' ', '_')}", use_container_width=True): + st.session_state.confirming_delete_details = { + "family": library_family_to_display, + "use_case": use_case_name_display + } + st.rerun() + else: + st.info("Aucun mĂ©tier n'est actuellement sĂ©lectionnĂ©e dans la bibliothĂšque ou le mĂ©tier sĂ©lectionnĂ© n'existe plus.") + available_families_check = list(st.session_state.editable_prompts.keys()) + if not available_families_check : st.warning("La bibliothĂšque est entiĂšrement vide. Veuillez crĂ©er des mĂ©tiers et des prompts.") + +elif st.session_state.view_mode == "edit": + 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 + if st.button(f"âŹ ïž Retour Ă la bibliothĂšque ({current_family_of_edited_prompt or 'MĂ©tier'})", key="back_to_library_from_edit"): + if current_family_of_edited_prompt: + st.session_state.library_selected_family_for_display = current_family_of_edited_prompt + st.session_state.view_mode = "library" + st.rerun() + 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.") + 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.") + 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]: + current_prompt_config = st.session_state.editable_prompts[final_selected_family_edition][final_selected_use_case_edition] + st.header(f"Cas d'usage: {final_selected_use_case_edition}") + 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]) + 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')}") + # Afficher la description si elle existe + description = current_prompt_config.get("description", "").strip() + if description: + st.markdown(f"*{description}*") + gen_form_values = {} + with st.form(key=f"gen_form_{final_selected_family_edition}_{final_selected_use_case_edition}"): + st.markdown("**Remplissez le formulaire ci-dessous pour ajouter du contexte Ă votre prompt :**") + 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.") + variables_for_form = current_prompt_config.get("variables", []) + if not isinstance(variables_for_form, list): variables_for_form = [] + cols_per_row = 2 if len(variables_for_form) > 1 else 1 + var_chunks = [variables_for_form[i:i + cols_per_row] for i in range(0, len(variables_for_form), cols_per_row)] + for chunk in var_chunks: + cols = st.columns(len(chunk)) + for i, var_info in enumerate(chunk): + with cols[i]: + 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") + 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) + elif var_type == "selectbox": + opts = var_info.get("options", []); idx = 0 + if opts: + try: idx = opts.index(field_default) if field_default in opts else 0 + except ValueError: idx = 0 # pragma: no cover + gen_form_values[var_info["name"]] = st.selectbox(var_info["label"], options=opts, index=idx, key=widget_key) + elif var_type == "date_input": + val_date = field_default if isinstance(field_default, date) else datetime.now().date() + gen_form_values[var_info["name"]] = st.date_input(var_info["label"], value=val_date, key=widget_key) + elif var_type == "number_input": + 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") + val_num_gen = float(current_value_default_gen) if isinstance(current_value_default_gen, (int, float)) else 0.0 + 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 + if min_val_gen is not None and val_num_gen < min_val_gen: val_num_gen = min_val_gen + if max_val_gen is not None and val_num_gen > max_val_gen: val_num_gen = max_val_gen + 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") + elif var_type == "text_area": + height_val = var_info.get("height") + final_height = None + if height_val is not None: + try: + h = int(height_val) + if h >= 68: final_height = h + else: final_height = 68 + except (ValueError, TypeError): final_height = None + else: final_height = None + gen_form_values[var_info["name"]] = st.text_area(var_info["label"], value=str(field_default or ""), height=final_height, key=widget_key) + if st.form_submit_button("đ GĂ©nĂ©rer Prompt"): + processed_values_for_template = {} + for k, v_val in gen_form_values.items(): # gen_form_values vient de votre formulaire + if v_val is None: + # On ne met pas les valeurs None dans le dictionnaire, + # donc les placeholders correspondants ne seront pas remplacĂ©s (comportement original) + continue + + if isinstance(v_val, date): + processed_values_for_template[k] = v_val.strftime("%d/%m/%Y") + elif isinstance(v_val, float): # On regroupe ici tous les traitements pour les floats + if v_val.is_integer(): # Si le float est un entier (ex: 50.0) + processed_values_for_template[k] = str(int(v_val)) # Convertir en "50" + else: # S'il s'agit d'un float avec des dĂ©cimales (ex: 0.125000...) + processed_values_for_template[k] = f"{v_val:.2f}" # Formater avec 2 dĂ©cimales + else: # Pour tous les autres types (str, bool, etc. qui ne sont ni date ni float) + processed_values_for_template[k] = str(v_val) + + final_vals_for_prompt = processed_values_for_template # final_vals_for_prompt contient maintenant des chaĂźnes + + try: + prompt_template_content = current_prompt_config.get("template", "") + processed_template = prompt_template_content + + # 1. Remplacer les variables connues par Streamlit (celles du formulaire) + # Trier par longueur de clĂ© (descendant) pour Ă©viter les substitutions partielles + # (ex: remplacer {jour_semaine} avant {jour}) + sorted_vars_for_formatting = sorted(final_vals_for_prompt.items(), key=lambda item: len(item[0]), reverse=True) + + for var_name, var_value in sorted_vars_for_formatting: + placeholder_streamlit = f"{{{var_name}}}" + # Remplacer uniquement les placeholders exacts et simples + processed_template = processed_template.replace(placeholder_streamlit, str(var_value)) + + # 2. Convertir les doubles accolades (pour le LLM final) en simples accolades + # Ceci suppose que le template original (venant du JSON) utilisait bien {{...}} + # pour les placeholders destinĂ©s au LLM final. + formatted_template_content = processed_template.replace("{{", "{").replace("}}", "}") + + use_case_title = final_selected_use_case_edition + generated_prompt = f"Sujet : {use_case_title}\n{formatted_template_content}" + st.session_state.active_generated_prompt = generated_prompt + st.success("Prompt gĂ©nĂ©rĂ© avec succĂšs!") + st.balloons() + current_prompt_config["usage_count"] = current_prompt_config.get("usage_count", 0) + 1 + current_prompt_config["updated_at"] = datetime.now().isoformat() + save_editable_prompts_to_local() + + except Exception as e: # Garder un catch-all pour les erreurs imprĂ©vues + st.error(f"Erreur inattendue lors de la gĂ©nĂ©ration du prompt : {e}") # pragma: no cover + st.session_state.active_generated_prompt = f"ERREUR INATTENDUE - TEMPLATE ORIGINAL :\n---\n{prompt_template_content}" # pragma: no cover + st.markdown("---") + if st.session_state.active_generated_prompt: + st.subheader("â Prompt GĂ©nĂ©rĂ© (Ă©ditable):") + 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") + if edited_prompt_value != st.session_state.active_generated_prompt: + st.session_state.active_generated_prompt = edited_prompt_value # pragma: no cover + col_caption, col_indicator = st.columns([1.8, 0.2]) # Ajustez les proportions si nĂ©cessaire + with col_caption: + st.caption("Prompt gĂ©nĂ©rĂ© (pour relecture et copie manuelle) :") + with col_indicator: + st.markdown("