from __future__ import annotations import tkinter as tk from pathlib import Path from tempfile import TemporaryDirectory from tkinter import messagebox, ttk from copienator import CliError, utils from copienator.annotation_modifications import ( DRAFT, baseline, body_image, edited_bodies, normalize_copy, read_instructions, validate_instructions, ) from copienator.commands.resolve_manual import ManualInstruction from copienator.platform import open_path from .cut_helper import CutHelper from .manual_resolution import ManualResolutionPanel HELP = """Ajoutez une copie, un label source et un label cible. Source, Cible et Cut montrent uniquement le corps de la réponse avec ses annotations conservées, sans note, commentaire global, contexte, énoncé ou corrigé. Les en-têtes restent associés à leur label ; les scores ne changent pas. Format : Copie43 Label source OP Label cible| -> ajoute la source à la cible ; x> la déplace ; -x copie en remplaçant la cible ; xx déplace en remplaçant. ss conserve les deux ; sx vide la cible ; xs vide la source. c{43}1> conserve les premiers 43 % de la source et ajoute le reste à la cible ; c{43}2x conserve la fin et remplace la cible par le début. Le pourcentage porte uniquement sur le corps de la réponse. |Cible place le fragment avant la cible ; Cible| le place après. Une instruction par source. Une cible ne peut pas également être source. Plusieurs sources peuvent alimenter une cible dans l’ordre du fichier. Une cible sans image est vide. Cut affiche une commande à recopier dans l’éditeur. Enregistrez puis Recharger. Aperçu du résultat montre les corps après toutes les instructions enregistrées. Make modifications applique uniquement le fichier enregistré. Chaque application repart des images originales relues : modifier ou retirer une ligne puis appliquer remplace ou annule son effet. Les lignes ### sont des commentaires ; conservez la ligne ### Source: BGnot ou Bnot. Le fichier est conservé. Une nouvelle lecture des annotations réapplique les dernières instructions appliquées, jamais un brouillon. Relancez ensuite la préparation de A Rendre (sans l’option limitée à answers) pour actualiser tous les livrables.""" def annotation_mode_for_reader(reader): if reader.get("last_run_variant", reader.get("variant")) == "standard": return "Bnot" values = reader.get("last_run_values", reader.get("values", {})) if values.get("refaire") and values.get("annotation_dir") == "Bnot": return "Bnot" return "BGnot" class AnnotationModificationsPanel(ManualResolutionPanel): def __init__(self, parent, get_evaluation, get_mode): self.get_mode = get_mode self.previews = TemporaryDirectory(prefix="copienator-body-preview-") super().__init__(parent, get_evaluation, help_text=HELP, show_provenance=False) self.bind("", self._cleanup, add="+") form = ttk.Frame(self) form.pack(fill="x", before=self.editor_button.master, pady=5) self.copy_var, self.source_var, self.target_var = (tk.StringVar() for _ in range(3)) ttk.Label(form, text="Copie").grid(row=0, column=0) ttk.Entry(form, textvariable=self.copy_var, width=9).grid(row=1, column=0, padx=3) self.label_choices = [] for column, title, variable in ((1, "Label source", self.source_var), (2, "Label cible", self.target_var)): ttk.Label(form, text=title).grid(row=0, column=column) combo = ttk.Combobox(form, textvariable=variable, width=22) combo.grid(row=1, column=column, padx=3) self.label_choices.append(combo) ttk.Button(form, text="Ajouter", command=self.add_instruction).grid(row=1, column=3, padx=5) ttk.Button(self.editor_button.master, text="Aperçu du résultat", command=self.preview_result).pack(side="left") self.reload() def _cleanup(self, event): if event.widget is self: self.previews.cleanup() def manual_path(self): evaluation = self.get_evaluation() return evaluation / DRAFT if evaluation else None def _read(self): path = self.manual_path() if path is None: raise CliError("Chargez une évaluation.") text = path.read_text(encoding="utf-8") if path.exists() else f"### Source: {self.get_mode()}\n" return path, text, read_instructions(text, self.get_mode()) def reload(self): super().reload() self.editor_button.configure(state="normal" if self.manual_path() else "disabled") try: path, _, instructions = self._read() validate_instructions(path.parent, self.get_mode(), instructions) for combo in getattr(self, "label_choices", []): combo.configure(values=utils.read_all_labels(path.parent)) except (CliError, OSError, ValueError, KeyError) as exc: self.status.configure(text=str(exc)) def open_editor(self): try: path = self.manual_path() if path is None: raise CliError("Chargez une évaluation.") if not path.exists(): path.write_text(f"### Source: {self.get_mode()}\n", encoding="utf-8") open_path(path) except (CliError, OSError, ValueError, RuntimeError) as exc: messagebox.showerror("Ouverture du fichier", str(exc)) def add_instruction(self): try: path, text, instructions = self._read() copy_id = normalize_copy(path.parent, self.copy_var.get()) source, target = self.source_var.get().strip(), self.target_var.get().strip() item = ManualInstruction(copy_id, source, "->", target, False) validate_instructions(path.parent, self.get_mode(), [*instructions, item]) text = text.rstrip() + f"\nCopie{copy_id} {source} -> {target}|\n" path.write_text(text, encoding="utf-8") self.reload() except (CliError, OSError, ValueError, KeyError) as exc: messagebox.showerror("Instruction invalide", str(exc)) def _show(self, image, name): path = Path(self.previews.name) / f"{name}.png" image.save(path) open_path(path) def open_pdf(self, evaluation, copy_id, label): try: self._show(body_image(evaluation / self.get_mode() / f"Copie{copy_id}", label), f"Copie{copy_id} - {label} - corps") except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc: messagebox.showinfo("Aperçu du corps annoté", str(exc)) def open_cut(self, evaluation, instruction): def accepted(operator): target = "|" + instruction.new_label if instruction.pipe_first else instruction.new_label + "|" self.cut_result.set(f"Copie{instruction.copy_id} {instruction.old_label} {operator} {target}") try: source = body_image(evaluation / self.get_mode() / f"Copie{instruction.copy_id}", instruction.old_label) initial = (*instruction.cut, instruction.operator[-1]) if instruction.cut else None CutHelper(self, source, accepted, initial) except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc: messagebox.showerror("Coupe du corps annoté", str(exc)) def preview_result(self): try: path, _, instructions = self._read() validate_instructions(path.parent, self.get_mode(), instructions) # Preview just the requested copy, avoiding a window for every copy. copy_id = normalize_copy(path.parent, self.copy_var.get()) selected = [i for i in instructions if i.copy_id == copy_id] if not selected: raise CliError("Aucune instruction pour cette copie. Saisissez son numéro dans Copie.") directory = path.parent / self.get_mode() / f"Copie{copy_id}" _, bodies, changed = edited_bodies(directory, baseline(directory), selected) for label in sorted(changed): if label in bodies: self._show(bodies[label], f"Copie{copy_id} - {label} - résultat") emptied = sorted(changed - bodies.keys()) if emptied: messagebox.showinfo("Réponses vidées", ", ".join(emptied)) except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc: messagebox.showerror("Aperçu du résultat", str(exc))