"""Onglet « Configuration » de la GUI V6 (G4 — alignement maquette). Sous-navigation Réglages / Masquage / Partage / Règles (cf. maquette). Le sous-onglet Réglages édite un :class:`ConfigState` partagé (profil, NER local, moteurs). Les autres sous-onglets reprennent le style maquette (cartes denses). Aucune logique de détection ; ``config_defaults.py`` n'est pas modifié. """ from __future__ import annotations from pathlib import Path from tkinter import filedialog import customtkinter as ctk from gui_v6 import theme as theme_mod from gui_v6 import ui_kit from gui_v6.config_state import ConfigState, default_profile_key, list_profile_keys _SUBTABS = [ ("reg", "⚙️ Réglages"), ("msk", "🎭 Masquage"), ("shr", "🔄 Partage"), ("rul", "🛡️ Règles 2"), ] _DETECTION_OPTIONS = [ ("Noms et prénoms", "Gazetteers INSEE · CamemBERT"), ("Dates de naissance", "Uniquement la date de naissance"), ("Établissements", "Répertoire FINESS + contexte"), ("Adresses et codes postaux", ""), ("N° sécurité sociale", ""), ("Téléphones et e-mails", ""), ("N° adhérent mutuelle", ""), ] _PRESERVE_TERMS = ["FUROSEMIDE", "rééducation fonctionnelle", "classification internationale"] _MASK_TERMS = ["CHUXX"] CONFIG_MOCKUP_SECTIONS = { "reglages": [ "Profil métier", "Moteurs NER", "Données à détecter", "Termes à toujours conserver", "Termes à toujours masquer", ], "masquage": [ "Couleur de masquage (PDF)", "Style des marqueurs (texte)", "Épaisseur du masque", "Codes de remplacement", "Masques de zones fixes", ], "partage": ["Exporter la configuration", "Importer une configuration"], "regles": ["Règles actives", "Testeur de règle"], } class ConfigTab(ctk.CTkFrame): def __init__(self, master, state: ConfigState | None = None, palette: dict | None = None, **kwargs): self._p = palette or theme_mod.get_palette(theme_mod.DEFAULT_THEME) super().__init__(master, fg_color=self._p["bg"], **kwargs) self._state = state if state is not None else ConfigState() self._sub = "reg" self._sub_buttons: dict = {} self._build() @property def state(self) -> ConfigState: return self._state def _build(self) -> None: p = self._p bar = ctk.CTkFrame(self, fg_color="transparent") bar.pack(fill="x", padx=14, pady=(14, 4)) for key, label in _SUBTABS: btn = ctk.CTkButton( bar, text=label, command=lambda k=key: self._show_sub(k), fg_color="transparent", hover_color=p["card_border"], text_color=p["primary"] if key == self._sub else p["text_dim"], font=ui_kit.font(13, "bold" if key == self._sub else "normal"), corner_radius=0, width=10, ) btn.pack(side="left", padx=3) self._sub_buttons[key] = btn self._body = ctk.CTkFrame(self, fg_color="transparent") self._body.pack(fill="both", expand=True) self._show_sub("reg") def _show_sub(self, key: str) -> None: self._sub = key p = self._p for k, btn in self._sub_buttons.items(): btn.configure(text_color=p["primary"] if k == key else p["text_dim"], font=ui_kit.font(13, "bold" if k == key else "normal")) for w in self._body.winfo_children(): w.destroy() {"reg": self._build_reglages, "msk": self._build_masquage, "shr": self._build_partage, "rul": self._build_regles}[key]() # -- Réglages (câblé ConfigState) ------------------------------------- def _build_reglages(self) -> None: p = self._p cols = self._two_columns(self._body) left, right = cols prof = ui_kit.Card(left, p, title="🗂️ Profil métier") prof.pack(fill="x", pady=7) row = ctk.CTkFrame(prof, fg_color="transparent") row.pack(fill="x", padx=16, pady=(0, 10)) profiles = list_profile_keys() current = self._state.profile or default_profile_key() or (profiles[0] if profiles else "") self._state.profile = current or None ctk.CTkLabel(row, text="Profil :", text_color=p["text"], font=ui_kit.font(13)).pack(side="left", padx=(0, 8)) menu = ctk.CTkOptionMenu(row, values=profiles or ["(aucun profil)"], command=self._on_profile, fg_color=p["btn_sec_bg"], button_color=p["primary"], text_color=p["text"]) if current: menu.set(current) menu.pack(side="left") outrow = ctk.CTkFrame(prof, fg_color="transparent") outrow.pack(fill="x", padx=16, pady=(0, 14)) ui_kit.secondary_button(outrow, p, "📁 Dossier de sortie…", command=self._pick_output).pack(side="left", padx=(0, 8)) self._out_label = ctk.CTkLabel(outrow, text=str(self._state.output_dir or "(défaut anonymise/)"), text_color=p["text_muted"], font=ui_kit.font(12)) self._out_label.pack(side="left") ner = ui_kit.Card(left, p, title="🧠 Moteurs NER") ner.pack(fill="x", pady=7) self._tog_ner = ui_kit.ToggleRow(ner, p, "CamemBERT-bio ⚡", "~10 ms/doc · F1 = 0.963", value=self._state.use_local_ner, command=self._on_ner) self._tog_ner.pack(fill="x", padx=16, pady=2) self._tog_eds = ui_kit.ToggleRow(ner, p, "EDS-Pseudo PRÉCIS", "~200 ms/doc · médical français", value=self._state.enable_eds, command=self._on_eds) self._tog_eds.pack(fill="x", padx=16, pady=2) self._tog_gli = ui_kit.ToggleRow(ner, p, "GLiNER OPTIONNEL", "~95 ms/doc · vote croisé", value=self._state.enable_gliner, command=self._on_gliner) self._tog_gli.pack(fill="x", padx=16, pady=(2, 14)) det = ui_kit.Card(right, p, title="🔍 Données à détecter") det.pack(fill="x", pady=7) for label, hint in _DETECTION_OPTIONS: ui_kit.ToggleRow(det, p, label, hint, value=True).pack(fill="x", padx=16, pady=2) ctk.CTkFrame(det, fg_color="transparent", height=8).pack() keep = ui_kit.Card(right, p, title="✅ Termes à toujours conserver") keep.pack(fill="x", pady=7) self._note(keep, "Ces termes ne seront jamais masqués, même s'ils ressemblent à un nom propre.") self._tag_editor(keep, "Ex : FUROSEMIDE…", _PRESERVE_TERMS, "keep") mask = ui_kit.Card(right, p, title="🚫 Termes à toujours masquer") mask.pack(fill="x", pady=7) self._note(mask, "Ces termes seront toujours masqués, même sans contexte médical autour.") self._tag_editor(mask, "Ex : CHUXX, Dr Dupont…", _MASK_TERMS, "mask") def _build_masquage(self) -> None: p = self._p cols = self._two_columns(self._body) left, right = cols color = ui_kit.Card(left, p, title="⬛ Couleur de masquage (PDF)") color.pack(fill="x", pady=7) self._note(color, "Couleur des rectangles dans le PDF final.") swatches = ctk.CTkFrame(color, fg_color="transparent") swatches.pack(fill="x", padx=16, pady=(0, 14)) for color_value in ["#000000", "#1a1a2e", "#374151", "#92400e", "#1e3a5f"]: ctk.CTkButton( swatches, text="", width=30, height=30, fg_color=color_value, hover_color=color_value, border_color=p["primary"] if color_value == "#000000" else p["card_border"], border_width=3 if color_value == "#000000" else 1, corner_radius=6, command=lambda: None, ).pack(side="left", padx=(0, 8)) style = ui_kit.Card(left, p, title="🏷️ Style des marqueurs (texte)") style.pack(fill="x", pady=7) self._mask_style_var = ctk.StringVar(value="brackets") for label, value, preview in [ ("Crochets", "brackets", "[NOM]"), ("Étoiles", "stars", "***"), ("Noirci", "blackout", "████"), ]: row = ctk.CTkFrame(style, fg_color="transparent") row.pack(fill="x", padx=16, pady=2) ctk.CTkRadioButton( row, text=f"{label} — {preview}", variable=self._mask_style_var, value=value, command=self._update_mask_preview, text_color=p["text"], fg_color=p["primary"], hover_color=p["primary_dim"], font=ui_kit.font(13), ).pack(anchor="w") self._mask_preview = ctk.CTkLabel( style, text="Patient [NOM], né le [DATE_NAISSANCE]", text_color=p["text_dim"], fg_color=p["divider"], corner_radius=6, font=ui_kit.font(13), anchor="w", ) self._mask_preview.pack(fill="x", padx=16, pady=(8, 14), ipady=8) thick = ui_kit.Card(right, p, title="📐 Épaisseur du masque") thick.pack(fill="x", pady=7) self._note(thick, "Marge autour du texte masqué, en points.") self._slider_row(thick, "Marge horizontale", 2) self._slider_row(thick, "Marge verticale", 1) ui_kit.ToggleRow(thick, p, "Coins arrondis", "", value=False).pack(fill="x", padx=16, pady=(2, 14)) codes = ui_kit.Card(right, p, title="🔒 Codes de remplacement") codes.pack(fill="x", pady=7) for k, v in [ ("Nom/Prénom", "[NOM]"), ("Date naissance", "[DATE_NAISSANCE]"), ("Établissement", "[ETABLISSEMENT]"), ("Adresse", "[ADRESSE]"), ("Téléphone", "[TEL]"), ("N° sécu", "[NIR]"), ("IPP", "[IPP]"), ("Email", "[EMAIL]"), ]: r = ctk.CTkFrame(codes, fg_color="transparent") r.pack(fill="x", padx=16, pady=1) ctk.CTkLabel(r, text=k, text_color=p["text_muted"], font=ui_kit.font(12)).pack(side="left") ctk.CTkLabel(r, text=v, text_color=p["primary"], font=ui_kit.font(12, "bold")).pack(side="right") ctk.CTkFrame(codes, fg_color="transparent", height=8).pack() editor = ui_kit.Card(self._body, p, title="🏠 Masques de zones fixes (logos, en-têtes)") editor.pack(fill="x", padx=14, pady=7) top = ctk.CTkFrame(editor, fg_color="transparent") top.pack(fill="x", padx=16, pady=(0, 8)) ctk.CTkLabel( top, text="Dessinez des rectangles sur un PDF modèle pour masquer systématiquement les logos, en-têtes ou zones fixes.", text_color=p["text_dim"], font=ui_kit.font(12), justify="left", wraplength=610, ).pack(side="left", fill="x", expand=True) ui_kit.primary_button(top, p, "✏ Ouvrir l'éditeur de masques", command=lambda: None).pack(side="right", padx=(8, 0)) toolbar = ctk.CTkFrame(editor, fg_color=p["divider"], border_color=p["card_border"], border_width=1, corner_radius=8) toolbar.pack(fill="x", padx=16, pady=(0, 10)) for label in ["📄 Ouvrir PDF…", "−", "100%", "+", "💾 Sauver", "📁 Charger", "🗑 Effacer page"]: ui_kit.secondary_button(toolbar, p, label, command=lambda: None).pack(side="left", padx=4, pady=8) canvas = ctk.CTkFrame(editor, fg_color=p["divider"], border_color=p["card_border"], border_width=1, corner_radius=8, height=150) canvas.pack(fill="x", padx=16, pady=(0, 12)) canvas.pack_propagate(False) ctk.CTkLabel( canvas, text="📄\nOuvrez un PDF pour commencer à dessiner des zones de masquage.\nCliquez-glissez pour tracer un rectangle.", text_color=p["text_muted"], font=ui_kit.font(12), justify="center", ).pack(expand=True) def _build_partage(self) -> None: p = self._p export = ui_kit.Card(self._body, p, title="📤 Exporter la configuration") export.pack(fill="x", padx=14, pady=7) self._note(export, "Génère un fichier .json avec vos listes, à envoyer par e-mail à d'autres établissements.") ui_kit.secondary_button(export, p, "⬇ Exporter (.json)", command=lambda: None).pack(anchor="w", padx=16, pady=(0, 14)) import_card = ui_kit.Card(self._body, p, title="📥 Importer une configuration") import_card.pack(fill="x", padx=14, pady=7) self._note(import_card, "Importez un fichier reçu. Vos réglages locaux ne seront pas supprimés.") ui_kit.secondary_button(import_card, p, "⬆ Importer (.json)", command=lambda: None).pack(anchor="w", padx=16, pady=(0, 14)) def _build_regles(self) -> None: p = self._p card = ui_kit.Card(self._body, p, title="🛡️ Règles actives") card.pack(fill="x", padx=14, pady=7) self._note(card, "Ces règles adaptent le moteur à votre établissement. Chaque règle est validée avant activation.") headers = ctk.CTkFrame(card, fg_color="transparent") headers.pack(fill="x", padx=16, pady=(0, 4)) for text, width in [("Label", 190), ("Type", 80), ("Cible → Résultat", 210), ("Statut", 70), ("", 70)]: ctk.CTkLabel(headers, text=text.upper(), width=width, anchor="w", text_color=p["text_muted"], font=ui_kit.font(10, "bold")).pack(side="left") for row in [ ("Masquer le sigle CHUXX", "exact", "CHUXX → [MASK]", "Actif"), ("Préserver “classification internationale”", "preserve", "conservé tel quel", "Actif"), ("Identifier N° 1234567", "norm-id", "N° 1234567 → [NDA]", "Candidat"), ]: self._rule_row(card, row) actions = ctk.CTkFrame(card, fg_color="transparent") actions.pack(fill="x", padx=16, pady=(10, 14)) ui_kit.primary_button(actions, p, "+ Nouvelle règle", command=lambda: None).pack(side="left", padx=(0, 8)) ui_kit.secondary_button(actions, p, "🔄 Recharger", command=lambda: None).pack(side="left") sim = ui_kit.Card(self._body, p, title="🧪 Testeur de règle") sim.pack(fill="x", padx=14, pady=7) ctk.CTkLabel(sim, text="Texte de test", text_color=p["text_muted"], font=ui_kit.font(12)).pack(anchor="w", padx=16) txt = ctk.CTkTextbox(sim, height=78, fg_color=p["divider"], text_color=p["text"], border_color=p["card_border"], border_width=1) txt.pack(fill="x", padx=16, pady=(5, 10)) txt.insert("1.0", "Compte rendu CHUXX, patient N° 1234567.") btns = ctk.CTkFrame(sim, fg_color="transparent") btns.pack(fill="x", padx=16, pady=(0, 14)) ui_kit.primary_button(btns, p, "▶ Tester", command=lambda: None).pack(side="left", padx=(0, 8)) ui_kit.secondary_button(btns, p, "✖ Fermer", command=lambda: None).pack(side="left") # -- callbacks -------------------------------------------------------- def _on_profile(self, value: str) -> None: self._state.profile = value def _on_ner(self) -> None: self._state.use_local_ner = self._tog_ner.get() def _on_eds(self) -> None: self._state.enable_eds = self._tog_eds.get() def _on_gliner(self) -> None: self._state.enable_gliner = self._tog_gli.get() def _pick_output(self) -> None: path = filedialog.askdirectory(title="Dossier de sortie") if path: self._state.output_dir = Path(path) self._out_label.configure(text=str(self._state.output_dir)) # -- helpers UI ------------------------------------------------------- def _two_columns(self, parent) -> tuple[ctk.CTkFrame, ctk.CTkFrame]: row = ctk.CTkFrame(parent, fg_color="transparent") row.pack(fill="both", expand=True, padx=14) left = ctk.CTkFrame(row, fg_color="transparent") right = ctk.CTkFrame(row, fg_color="transparent") left.pack(side="left", fill="both", expand=True, padx=(0, 7)) right.pack(side="left", fill="both", expand=True, padx=(7, 0)) return left, right def _note(self, parent, text: str) -> None: p = self._p ctk.CTkLabel( parent, text=text, text_color=p["text_muted"], fg_color=p["divider"], corner_radius=4, font=ui_kit.font(11), anchor="w", justify="left", wraplength=330, ).pack(fill="x", padx=16, pady=(0, 10), ipady=5) def _tag_editor(self, parent, placeholder: str, terms: list[str], kind: str) -> None: p = self._p row = ctk.CTkFrame(parent, fg_color="transparent") row.pack(fill="x", padx=16, pady=(0, 8)) entry = ctk.CTkEntry( row, placeholder_text=placeholder, fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"], height=32, ) entry.pack(side="left", fill="x", expand=True, padx=(0, 8)) ui_kit.secondary_button(row, p, "+ Ajouter", command=lambda: None).pack(side="right") cloud = ctk.CTkFrame(parent, fg_color="transparent") cloud.pack(fill="x", padx=16, pady=(0, 14)) tag_color = p["success"] if kind == "keep" else p["primary"] line = ctk.CTkFrame(cloud, fg_color="transparent") line.pack(fill="x", anchor="w") used_width = 0 for term in terms: estimated_width = min(230, max(88, len(term) * 8 + 34)) if used_width and used_width + estimated_width > 360: line = ctk.CTkFrame(cloud, fg_color="transparent") line.pack(fill="x", anchor="w") used_width = 0 ctk.CTkLabel( line, text=f"{term[:30]}{'…' if len(term) > 30 else ''} ×", width=estimated_width, text_color=tag_color, fg_color=p["btn_sec_bg"], corner_radius=99, font=ui_kit.font(12), ).pack(side="left", padx=(0, 6), pady=3, ipadx=4, ipady=3) used_width += estimated_width + 6 def _slider_row(self, parent, label: str, value: int) -> None: p = self._p row = ctk.CTkFrame(parent, fg_color="transparent") row.pack(fill="x", padx=16, pady=3) ctk.CTkLabel(row, text=label, text_color=p["text"], font=ui_kit.font(13)).pack(side="left") slider = ctk.CTkSlider(row, from_=0, to=6, number_of_steps=6, progress_color=p["primary"], width=125) slider.set(value) slider.pack(side="right") def _update_mask_preview(self) -> None: value = self._mask_style_var.get() if value == "stars": text = "Patient ***, né le ***" elif value == "blackout": text = "Patient ████, né le ████" else: text = "Patient [NOM], né le [DATE_NAISSANCE]" self._mask_preview.configure(text=text) def _rule_row(self, parent, values: tuple[str, str, str, str]) -> None: p = self._p label, rule_type, target, status = values row = ctk.CTkFrame(parent, fg_color="transparent") row.pack(fill="x", padx=16, pady=1) ctk.CTkLabel(row, text=label, width=190, anchor="w", text_color=p["text"], font=ui_kit.font(12)).pack(side="left") ctk.CTkLabel(row, text=rule_type, width=80, anchor="w", text_color=p["text_dim"], font=ui_kit.font(11)).pack(side="left") ctk.CTkLabel(row, text=target, width=210, anchor="w", text_color=p["primary"], font=ui_kit.font(12, "bold")).pack(side="left") color = p["success"] if status == "Actif" else p["warning"] ctk.CTkLabel(row, text=status, width=70, anchor="w", text_color=color, font=ui_kit.font(11, "bold")).pack(side="left") ui_kit.secondary_button(row, p, "▶ Tester", command=lambda: None).pack(side="left")