Improve web finalisation navigation, caching and Anot support
This commit is contained in:
1 parent
5de70dbc0d
commit
b069805988
21 files changed
+330
-49
No files matched your search
@@ -41,7 +41,8 @@ class AnnotationReadCache:
|
|||||||
common = (_files_digest(shared), load_return_answer_options(root),
|
common = (_files_digest(shared), load_return_answer_options(root),
|
||||||
configuration.RETURN_ANSWERS_ENABLED,
|
configuration.RETURN_ANSWERS_ENABLED,
|
||||||
configuration.RETURN_JPEG_ENABLED, configuration.RETURN_PDF_ENABLED,
|
configuration.RETURN_JPEG_ENABLED, configuration.RETURN_PDF_ENABLED,
|
||||||
configuration.LATEX_ANOT_BEFORE, configuration.LATEX_ANOT_AFTER)
|
getattr(configuration.LATEX_ANOT_BEFORE, "template", configuration.LATEX_ANOT_BEFORE),
|
||||||
|
configuration.LATEX_ANOT_AFTER)
|
||||||
remarks = load_remarks(root)
|
remarks = load_remarks(root)
|
||||||
general = general_remarks.load_remarks(root)
|
general = general_remarks.load_remarks(root)
|
||||||
for copy_id, labels in data.items():
|
for copy_id, labels in data.items():
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from decimal import Decimal, InvalidOperation, ROUND_CEILING
|
from decimal import Decimal, InvalidOperation, ROUND_CEILING
|
||||||
@@ -37,7 +38,7 @@ def get_rounded_score(score):
|
|||||||
"""Round score to one decimal place below (floor)."""
|
"""Round score to one decimal place below (floor)."""
|
||||||
try:
|
try:
|
||||||
val = float(score)
|
val = float(score)
|
||||||
return math.floor(val * 10) / 10
|
return math.floor(val * 10) / 10 if math.isfinite(val) else None
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -130,6 +131,7 @@ def process_images(base_dir, output_dir, *, copy_merged_histogramme=False):
|
|||||||
print(f"Error: Directory '{search_path}' not found.")
|
print(f"Error: Directory '{search_path}' not found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
exported_marks = {}
|
||||||
for student_source in sorted(path for path in search_path.iterdir() if path.is_dir()):
|
for student_source in sorted(path for path in search_path.iterdir() if path.is_dir()):
|
||||||
image_paths = sorted(student_source.glob("*.jpg"))
|
image_paths = sorted(student_source.glob("*.jpg"))
|
||||||
pdf_paths = sorted(student_source.glob("*.pdf"))
|
pdf_paths = sorted(student_source.glob("*.pdf"))
|
||||||
@@ -159,6 +161,7 @@ def process_images(base_dir, output_dir, *, copy_merged_histogramme=False):
|
|||||||
if score is None:
|
if score is None:
|
||||||
print(f"Error: Invalid score '{raw_score}' for '{student_name}'.")
|
print(f"Error: Invalid score '{raw_score}' for '{student_name}'.")
|
||||||
else:
|
else:
|
||||||
|
score_published = not image_paths
|
||||||
# 5. Process Images
|
# 5. Process Images
|
||||||
for img_path in image_paths:
|
for img_path in image_paths:
|
||||||
try:
|
try:
|
||||||
@@ -180,13 +183,20 @@ def process_images(base_dir, output_dir, *, copy_merged_histogramme=False):
|
|||||||
draw.text((x, y), text, fill=(255, 0, 0), font=font)
|
draw.text((x, y), text, fill=(255, 0, 0), font=font)
|
||||||
|
|
||||||
img.save(student_output / img_path.name)
|
img.save(student_output / img_path.name)
|
||||||
|
score_published = True
|
||||||
print(f"Processed: {student_name} -> {score}")
|
print(f"Processed: {student_name} -> {score}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing image for '{student_name}': {e}")
|
print(f"Error processing image for '{student_name}': {e}")
|
||||||
|
|
||||||
|
match = re.search(r'\((\d+)\)$', student_source.name)
|
||||||
|
if match and score_published:
|
||||||
|
exported_marks[str(int(match[1]))] = {'name': student_name, 'score': str(score)}
|
||||||
|
|
||||||
for pdf_path in pdf_paths:
|
for pdf_path in pdf_paths:
|
||||||
shutil.copy2(pdf_path, student_output / pdf_path.name)
|
shutil.copy2(pdf_path, student_output / pdf_path.name)
|
||||||
|
|
||||||
|
from copienator.final_scores import FINAL_SCORES_FILE
|
||||||
|
atomic_write_json(base_dir / FINAL_SCORES_FILE, {'version': 1, 'copies': exported_marks})
|
||||||
write_score_statistics(search_path, output_dir)
|
write_score_statistics(search_path, output_dir)
|
||||||
copy_histogram(output_dir, copy_merged_histogramme=copy_merged_histogramme)
|
copy_histogram(output_dir, copy_merged_histogramme=copy_merged_histogramme)
|
||||||
|
|
||||||
|
|||||||
@@ -516,6 +516,7 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
|
|||||||
d_notes = dict.fromkeys(all_labels, "")
|
d_notes = dict.fromkeys(all_labels, "")
|
||||||
label_images = []
|
label_images = []
|
||||||
answer_labels = []
|
answer_labels = []
|
||||||
|
reviewed_layouts = {}
|
||||||
sorted_labels = sorted(labels_data.items(), key=lambda item: natural_key(item[0]))
|
sorted_labels = sorted(labels_data.items(), key=lambda item: natural_key(item[0]))
|
||||||
|
|
||||||
for label, content in sorted_labels:
|
for label, content in sorted_labels:
|
||||||
@@ -534,7 +535,7 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
|
|||||||
result = content.get('result', {})
|
result = content.get('result', {})
|
||||||
coordinates = content.get('coordinates', (0, 0))
|
coordinates = content.get('coordinates', (0, 0))
|
||||||
d_notes[label] = str(result.get('score', 0))
|
d_notes[label] = str(result.get('score', 0))
|
||||||
final_img, _ = compose_label_image(
|
final_img, header_height = compose_label_image(
|
||||||
base_img,
|
base_img,
|
||||||
label,
|
label,
|
||||||
result,
|
result,
|
||||||
@@ -543,6 +544,8 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
|
|||||||
render_fn=render_real_latex_text,
|
render_fn=render_real_latex_text,
|
||||||
source_pdf=pdf_full_path,
|
source_pdf=pdf_full_path,
|
||||||
)
|
)
|
||||||
|
reviewed_layouts[label] = {'img': None, 'old_header_h': header_height,
|
||||||
|
'layout': final_img.info.get('annotation_layout', layout_for(pdf_full_path))}
|
||||||
final_img.save(staging / f"{label}.jpg")
|
final_img.save(staging / f"{label}.jpg")
|
||||||
if result.get('error', "") != "empty-answer":
|
if result.get('error', "") != "empty-answer":
|
||||||
label_images.append(final_img)
|
label_images.append(final_img)
|
||||||
@@ -561,7 +564,13 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
|
|||||||
canvas.paste(image, (0, current_y))
|
canvas.paste(image, (0, current_y))
|
||||||
current_y += image.height
|
current_y += image.height
|
||||||
canvas.save(staging / "Concat.jpg")
|
canvas.save(staging / "Concat.jpg")
|
||||||
|
if not problems:
|
||||||
|
from copienator.review_snapshot import stage
|
||||||
|
stage(output_dir, staging, labels_data, reviewed_layouts, engine='grouped')
|
||||||
# An all-empty copy still has valid individual return images.
|
# An all-empty copy still has valid individual return images.
|
||||||
|
if not problems:
|
||||||
|
from copienator.reviewed_cache import ReviewedReadCache
|
||||||
|
ReviewedReadCache(EvaluationWorkspace(Path(root_dir)), "Anot", [output_dir]).remember([student_id])
|
||||||
return "partial" if problems else "success"
|
return "partial" if problems else "success"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from copienator import evaluation_parser
|
|||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
parser = evaluation_parser('Open the local finalisation workspace')
|
parser = evaluation_parser('Open the local finalisation workspace')
|
||||||
parser.add_argument('--annotation-dir', choices=('BGnot', 'Bnot'), default='BGnot')
|
parser.add_argument('--annotation-dir', choices=('BGnot', 'Bnot', 'Anot'), default='BGnot')
|
||||||
parser.add_argument('--no-browser', action='store_true')
|
parser.add_argument('--no-browser', action='store_true')
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
from copienator.finalisation_server import WebSession
|
from copienator.finalisation_server import WebSession
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ def apply_actions_and_regenerate_grouped(
|
|||||||
[(label, dirty_images[label]) for label in filtered_labels],
|
[(label, dirty_images[label]) for label in filtered_labels],
|
||||||
all_labels, staging / "Concat_F.pdf", general=general,
|
all_labels, staging / "Concat_F.pdf", general=general,
|
||||||
)
|
)
|
||||||
if annotation_dir in {"Bnot", "BGnot"}:
|
if annotation_dir in {"Bnot", "BGnot", "Anot"}:
|
||||||
review_snapshot.stage(output_dir, staging, reviewed_data, label_notes,
|
review_snapshot.stage(output_dir, staging, reviewed_data, label_notes,
|
||||||
engine="grouped", selected=selected_labels,
|
engine="grouped", selected=selected_labels,
|
||||||
force_filtered=(set(filtered_labels) - selected_labels) if selected_labels else (force_filtered or ()))
|
force_filtered=(set(filtered_labels) - selected_labels) if selected_labels else (force_filtered or ()))
|
||||||
|
|||||||
@@ -8,6 +8,26 @@ from copienator.commands.reading_grouped_annotations import apply_actions_and_re
|
|||||||
def run(workspace: EvaluationWorkspace, mode: str, *, incremental: bool = False):
|
def run(workspace: EvaluationWorkspace, mode: str, *, incremental: bool = False):
|
||||||
directories = sorted((p for p in (workspace.root / mode).glob('Copie*')
|
directories = sorted((p for p in (workspace.root / mode).glob('Copie*')
|
||||||
if p.is_dir() and p.name[5:].isdigit() and (p / 'score.json').is_file()))
|
if p.is_dir() and p.name[5:].isdigit() and (p / 'score.json').is_file()))
|
||||||
|
# Legacy Anot outputs predate saved rendering inputs; create them without
|
||||||
|
# replacing any existing score or annotated image.
|
||||||
|
missing = [d for d in directories if not (d / FILENAME).is_file()]
|
||||||
|
if mode == 'Anot' and missing:
|
||||||
|
from copienator.annotation_data import load_annotation_data
|
||||||
|
from copienator.annotation_layout import layout_for
|
||||||
|
from copienator.filesystem import staged_files
|
||||||
|
from copienator.review_snapshot import stage
|
||||||
|
loaded = load_annotation_data(workspace)
|
||||||
|
if loaded.warnings:
|
||||||
|
raise ValueError("Relancez la génération Anot : " + "; ".join(loaded.warnings))
|
||||||
|
for directory in missing:
|
||||||
|
cid = directory.name[5:]
|
||||||
|
if cid not in loaded.data:
|
||||||
|
raise ValueError(f"Données Anot absentes pour {directory.name}.")
|
||||||
|
data = loaded.data[cid]
|
||||||
|
notes = {label: {'img': None, 'old_header_h': 0, 'layout': layout_for(content['pdf_path'])}
|
||||||
|
for label, content in data.items()}
|
||||||
|
with staged_files(directory) as staging:
|
||||||
|
stage(directory, staging, data, notes, engine='grouped')
|
||||||
from copienator.reviewed_cache import ReviewedReadCache
|
from copienator.reviewed_cache import ReviewedReadCache
|
||||||
cache = ReviewedReadCache(workspace, mode, directories) if incremental else None
|
cache = ReviewedReadCache(workspace, mode, directories) if incremental else None
|
||||||
if cache:
|
if cache:
|
||||||
@@ -47,6 +67,6 @@ def run(workspace: EvaluationWorkspace, mode: str, *, incremental: bool = False)
|
|||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
parser = evaluation_parser('Regenerate from saved reviewed inputs without rescanning handwriting')
|
parser = evaluation_parser('Regenerate from saved reviewed inputs without rescanning handwriting')
|
||||||
parser.add_argument('--annotation-dir', choices=('Bnot', 'BGnot'), required=True)
|
parser.add_argument('--annotation-dir', choices=('Bnot', 'BGnot', 'Anot'), required=True)
|
||||||
parser.add_argument("--incremental", action="store_true", help="Skip unchanged copies")
|
parser.add_argument("--incremental", action="store_true", help="Skip unchanged copies")
|
||||||
return execute(parser, argv, lambda args: run(workspace_from_args(args), args.annotation_dir, incremental=args.incremental))
|
return execute(parser, argv, lambda args: run(workspace_from_args(args), args.annotation_dir, incremental=args.incremental))
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Last exported overall marks, keyed by stable copy ID rather than student name."""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from copienator import read_json, configuration
|
||||||
|
from copienator.return_identity import return_folders, RETURN_FOLDER_PATTERN
|
||||||
|
|
||||||
|
FINAL_SCORES_FILE = Path('.copienator') / 'final_scores.json'
|
||||||
|
|
||||||
|
|
||||||
|
def exported_scores(root: Path, repository: Path) -> dict[str, str]:
|
||||||
|
saved = root / FINAL_SCORES_FILE
|
||||||
|
if saved.is_file():
|
||||||
|
data = read_json(saved)
|
||||||
|
return {str(int(cid)): entry['score'] for cid, entry in data.get('copies', {}).items()
|
||||||
|
if cid.isdecimal() and isinstance(entry, dict) and isinstance(entry.get('score'), str)}
|
||||||
|
|
||||||
|
# Older exports have no machine-readable overall mark. Only use their ODS
|
||||||
|
# when it predates the exported media; a newer sheet may contain other marks.
|
||||||
|
output = Path(configuration.FINAL_SCORE_OUTPUT_DIR).expanduser()
|
||||||
|
ods = Path(configuration.FINAL_SCORE_ODS_PATH).expanduser()
|
||||||
|
output = output if output.is_absolute() else repository / output
|
||||||
|
ods = ods if ods.is_absolute() else repository / ods
|
||||||
|
if not ods.is_file() or not (output / root.name).is_dir():
|
||||||
|
return {}
|
||||||
|
candidates = {}
|
||||||
|
for cid, folder in return_folders(root).items():
|
||||||
|
name = RETURN_FOLDER_PATTERN.fullmatch(folder.name)['name']
|
||||||
|
media = [p for p in (output / root.name / name).glob('*')
|
||||||
|
if p.suffix.lower() in ('.jpg', '.pdf') and p.is_file()]
|
||||||
|
if media and ods.stat().st_mtime_ns <= min(p.stat().st_mtime_ns for p in media):
|
||||||
|
candidates[str(cid)] = name
|
||||||
|
if not candidates:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
import pandas as pd
|
||||||
|
from copienator.commands.add_final_score import get_rounded_score
|
||||||
|
sheet = pd.read_excel(ods, engine='odf', header=None)
|
||||||
|
values = dict(zip(sheet[0], sheet[1]))
|
||||||
|
result = {}
|
||||||
|
for cid, name in candidates.items():
|
||||||
|
score = get_rounded_score(values.get(name))
|
||||||
|
if score is not None:
|
||||||
|
result[cid] = str(score)
|
||||||
|
return result
|
||||||
|
except (OSError, ValueError, KeyError, ImportError, OverflowError):
|
||||||
|
return {}
|
||||||
@@ -20,8 +20,8 @@ class Finalisation:
|
|||||||
def __init__(self, root: Path, mode: str, token: str, lock_fd: int | None = None, *, repository: Path | None = None):
|
def __init__(self, root: Path, mode: str, token: str, lock_fd: int | None = None, *, repository: Path | None = None):
|
||||||
self.root = root.resolve()
|
self.root = root.resolve()
|
||||||
self.repository = (repository or Path.cwd()).resolve()
|
self.repository = (repository or Path.cwd()).resolve()
|
||||||
if mode not in ('Bnot', 'BGnot'):
|
if mode not in ('Bnot', 'BGnot', 'Anot'):
|
||||||
raise ValueError('Choisissez Bnot ou BGnot.')
|
raise ValueError('Choisissez Anot, Bnot ou BGnot.')
|
||||||
self.mode, self.token = mode, token
|
self.mode, self.token = mode, token
|
||||||
self.lock_fd = lock_fd
|
self.lock_fd = lock_fd
|
||||||
self.labels = utils.read_all_labels(self.root)
|
self.labels = utils.read_all_labels(self.root)
|
||||||
@@ -66,6 +66,8 @@ class Finalisation:
|
|||||||
def state(self):
|
def state(self):
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
folders = return_folders(self.root)
|
folders = return_folders(self.root)
|
||||||
|
from copienator.final_scores import exported_scores
|
||||||
|
final_scores = exported_scores(self.root, self.repository)
|
||||||
copies = []
|
copies = []
|
||||||
for directory in self.copies():
|
for directory in self.copies():
|
||||||
cid = directory.name[5:]
|
cid = directory.name[5:]
|
||||||
@@ -75,7 +77,7 @@ class Finalisation:
|
|||||||
scores = read_json(directory / 'score.json')
|
scores = read_json(directory / 'score.json')
|
||||||
info = read_json(directory / 'info.json', default={})
|
info = read_json(directory / 'info.json', default={})
|
||||||
base = read_json(directory / BASELINE, default={})
|
base = read_json(directory / BASELINE, default={})
|
||||||
copies.append({'id': cid, 'name': name, 'general': general_remarks.remarks_for_copy(self.root, cid),
|
copies.append({'id': cid, 'name': name, 'finalScore': final_scores.get(str(int(cid))), 'general': general_remarks.remarks_for_copy(self.root, cid),
|
||||||
'answers': [{'label': label, 'score': str(scores.get(label, '')),
|
'answers': [{'label': label, 'score': str(scores.get(label, '')),
|
||||||
'remark': custom_remarks.remarks_for_copy(self.root, cid).get(label, ''),
|
'remark': custom_remarks.remarks_for_copy(self.root, cid).get(label, ''),
|
||||||
'image': (directory / f'{label}.jpg').is_file(),
|
'image': (directory / f'{label}.jpg').is_file(),
|
||||||
@@ -98,6 +100,8 @@ class Finalisation:
|
|||||||
raise ValueError('Les données ont changé. Rechargez la page avant de réessayer.')
|
raise ValueError('Les données ont changé. Rechargez la page avant de réessayer.')
|
||||||
kind, value = data['kind'], data['value']
|
kind, value = data['kind'], data['value']
|
||||||
if kind == 'draft':
|
if kind == 'draft':
|
||||||
|
if self.mode == 'Anot':
|
||||||
|
raise ValueError('Les coupes et transferts nécessitent Bnot ou BGnot.')
|
||||||
instructions = read_instructions(value, self.mode)
|
instructions = read_instructions(value, self.mode)
|
||||||
validate_instructions(self.root, self.mode, instructions)
|
validate_instructions(self.root, self.mode, instructions)
|
||||||
# Same filesystem and atomic replacement; retain the GUI-readable format.
|
# Same filesystem and atomic replacement; retain the GUI-readable format.
|
||||||
@@ -137,6 +141,8 @@ class Finalisation:
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
if self.job['running'] or self.closing or revision != self.revision():
|
if self.job['running'] or self.closing or revision != self.revision():
|
||||||
raise ValueError('Traitement déjà en cours ou données modifiées. Rechargez la page.')
|
raise ValueError('Traitement déjà en cours ou données modifiées. Rechargez la page.')
|
||||||
|
if apply_draft and self.mode == 'Anot':
|
||||||
|
raise ValueError('Les coupes et transferts nécessitent Bnot ou BGnot.')
|
||||||
if apply_draft:
|
if apply_draft:
|
||||||
validate_instructions(self.root, self.mode, read_instructions(self.draft(), self.mode))
|
validate_instructions(self.root, self.mode, read_instructions(self.draft(), self.mode))
|
||||||
if not (self.root / DRAFT).exists():
|
if not (self.root / DRAFT).exists():
|
||||||
@@ -166,7 +172,7 @@ class Finalisation:
|
|||||||
outputs = []
|
outputs = []
|
||||||
for directory in directories:
|
for directory in directories:
|
||||||
info = read_json(directory / 'info.json', default={})
|
info = read_json(directory / 'info.json', default={})
|
||||||
paths = [directory / 'info.json', directory / BASELINE]
|
paths = [directory / 'info.json', directory / (FILENAME if self.mode == 'Anot' else BASELINE)]
|
||||||
paths += [directory / f'{label}.jpg' for label, entry in info.items() if entry['present']]
|
paths += [directory / f'{label}.jpg' for label, entry in info.items() if entry['present']]
|
||||||
paths += list(directory.glob('Concat*.*'))
|
paths += list(directory.glob('Concat*.*'))
|
||||||
if any(not path.is_file() for path in paths):
|
if any(not path.is_file() for path in paths):
|
||||||
@@ -194,7 +200,7 @@ class Finalisation:
|
|||||||
commands = [['giving-names', str(self.root), self.mode]]
|
commands = [['giving-names', str(self.root), self.mode]]
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.job['log'] += 'Noms seuls modifiés : conservation des images et PDF de correction.\n'
|
self.job['log'] += 'Noms seuls modifiés : conservation des images et PDF de correction.\n'
|
||||||
elif all((directory / FILENAME).is_file() for directory in self.copies()):
|
elif self.mode == 'Anot' or all((directory / FILENAME).is_file() for directory in self.copies()):
|
||||||
commands[0] = ['regenerate-reviewed', str(self.root), '--annotation-dir', self.mode]
|
commands[0] = ['regenerate-reviewed', str(self.root), '--annotation-dir', self.mode]
|
||||||
elif (self.root / 'BRnot').is_dir():
|
elif (self.root / 'BRnot').is_dir():
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class WebSession:
|
|||||||
try:
|
try:
|
||||||
self.service = Finalisation(root, mode, self.lease.token, self.lease.stream.fileno(), repository=repository)
|
self.service = Finalisation(root, mode, self.lease.token, self.lease.stream.fileno(), repository=repository)
|
||||||
if not self.service.copies():
|
if not self.service.copies():
|
||||||
raise ValueError('Lisez les annotations manuscrites dans ce mode avant d’ouvrir la finalisation web.')
|
raise ValueError('Générez les copies Anot, ou lisez les annotations Bnot/BGnot, avant d’ouvrir la finalisation web.')
|
||||||
self.token = secrets.token_urlsafe(32)
|
self.token = secrets.token_urlsafe(32)
|
||||||
self.socket = socket.socket()
|
self.socket = socket.socket()
|
||||||
self.socket.bind(('127.0.0.1', 0))
|
self.socket.bind(('127.0.0.1', 0))
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ class ReviewedReadCache(AnnotationReadCache):
|
|||||||
shared = [root / 'labels']
|
shared = [root / 'labels']
|
||||||
for folder in ('Text2', 'Sol2'):
|
for folder in ('Text2', 'Sol2'):
|
||||||
shared.extend((root / folder).glob('*.pdf'))
|
shared.extend((root / folder).glob('*.pdf'))
|
||||||
common = (_files_digest(shared), configuration.LATEX_ANOT_BEFORE, configuration.LATEX_ANOT_AFTER)
|
common = (_files_digest(shared), getattr(configuration.LATEX_ANOT_BEFORE, "template", configuration.LATEX_ANOT_BEFORE),
|
||||||
|
configuration.LATEX_ANOT_AFTER)
|
||||||
remarks, general = load_remarks(root), general_remarks.load_remarks(root)
|
remarks, general = load_remarks(root), general_remarks.load_remarks(root)
|
||||||
applied = root / mode / APPLIED
|
applied = root / mode / APPLIED
|
||||||
instructions = read_instructions(applied.read_text(), mode) if applied.is_file() else []
|
instructions = read_instructions(applied.read_text(), mode) if applied.is_file() and mode != "Anot" else []
|
||||||
for directory in directories:
|
for directory in directories:
|
||||||
cid = directory.name[5:]
|
cid = directory.name[5:]
|
||||||
saved = read_json(directory / FILENAME)
|
saved = read_json(directory / FILENAME)
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#23323a;background:#f3f5f4;font-synthesis:none;font-size:14px}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button,a,input,select,textarea{outline-offset:3px}button{border:1px solid #c8d3ce;border-radius:7px;background:#fff;color:#244b40;padding:9px 13px;cursor:pointer;font-weight:550}button:hover:not(:disabled){background:#e9f1ed;border-color:#769b88}button:disabled{opacity:.45;cursor:default}.primary{background:#205c48;color:#fff;border-color:#205c48}.primary:hover:not(:disabled){background:#174633;color:#fff}a{color:#21654e;text-decoration:none}a:hover{text-decoration:underline}header{display:flex;align-items:center;gap:32px;padding:20px 36px;border-bottom:1px solid #dbe3de;background:#fff}.brand{display:flex;align-items:center;gap:10px;font-size:23px;font-weight:800;color:#28684f}.brand span{font-size:16px;font-weight:650}.heading{flex:1;border-left:1px solid #dbe3de;padding-left:30px}h1{font-size:22px;margin:3px 0 0}h2{font-size:20px;margin:0 0 8px}h3{font-size:19px;margin:7px 0}p{line-height:1.55;margin:8px 0;color:#56666b}.eyebrow{font-size:11px;letter-spacing:.09em;color:#5c7470;font-weight:650}main{max-width:1540px;margin:auto;padding:30px 36px 60px}.toolbar{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-bottom:24px}.filters{display:flex;gap:12px;align-items:end}label{display:flex;flex-direction:column;gap:6px;color:#405650;font-size:12px;font-weight:600}input,select,textarea{border:1px solid #c5d1cb;border-radius:6px;background:#fff;padding:9px 10px;color:#263d34;font-size:14px;min-width:0}textarea{resize:vertical;min-height:76px;line-height:1.5}input:focus,textarea:focus,select:focus{outline:2px solid #80b69c;border-color:#4d8269}input[type=checkbox]{width:16px;height:16px;accent-color:#205c48}.check{display:flex;flex-direction:row;align-items:center;max-width:310px;font-size:12px;line-height:1.4}.publish{display:flex;align-items:center;gap:16px;background:#e8f0eb;border:1px solid #d0e0d5;border-radius:10px;padding:17px 20px;margin:20px 0}.publish>div{flex:1}.publish small{display:block;margin-top:5px;color:#5b6e64}.panel{padding:18px 20px;background:#fff;border:1px solid #d9e2dd;border-radius:9px;margin-bottom:14px}summary{cursor:pointer;font-weight:600}summary span{float:right;font-size:12px;font-weight:400;color:#65766f}.identity{border-top:1px solid #e1e7e3;padding:18px 0;display:grid;grid-template-columns:280px 1fr;gap:28px}.identity-name{display:flex;flex-direction:column;gap:10px;align-items:start}.identity-name label{width:100%}.general button{margin:5px 6px 0 0}.remark-row{display:flex;align-items:center;gap:6px;margin-bottom:8px}.remark-row textarea{flex:1}.instructions{width:100%;font-family:ui-monospace,monospace;min-height:140px;margin:10px 0}.panel details{margin-top:16px}.results{display:flex;justify-content:space-between;align-items:center;margin:30px 0 12px}.results span{color:#677872;font-size:12px}.answer-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:22px}.answer{border:1px solid #d9e1dd;background:#fff;border-radius:10px;overflow:hidden}.card-heading{display:flex;justify-content:space-between;align-items:center;padding:18px 20px;gap:12px}.badge{font-size:11px;background:#edf1ee;color:#5a6e62;border-radius:4px;padding:5px 7px;white-space:nowrap}.warning{background:#fff2d8;color:#8a620b}.image-space{height:290px;overflow:hidden;background:#e7ece9;border-top:1px solid #e2e8e3;border-bottom:1px solid #e2e8e3;display:flex;justify-content:center;align-items:start;padding:12px}.image-button{position:relative;border:0;padding:0;background:#fff;width:100%;height:100%;overflow:hidden;border-radius:0}.image-button img{width:100%;display:block}.image-button span{position:absolute;bottom:8px;right:8px;background:#203d32e8;color:#fff;border-radius:5px;padding:7px 10px;font-size:12px}.answer-form{display:grid;grid-template-columns:88px 1fr;gap:15px;padding:18px 20px 10px}.card-actions{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:8px 20px 20px}.card-actions a{font-size:12px}.empty{padding:32px;color:#77877e;font-style:italic;text-align:center}.load-more{display:block;margin:24px auto}.alert{background:#fff0eb;color:#8f3827;border:1px solid #e9b6a9;border-radius:7px;padding:12px;display:flex;gap:12px;align-items:center}.alert small{font-size:12px}.notice{color:#216044}.muted{font-size:12px}fieldset{padding:0;margin:0;border:0;min-width:0}.overlay{position:fixed;inset:0;background:#142d23b8;z-index:10;display:flex;align-items:center;justify-content:center;padding:24px}.modal{width:min(1300px,100%);max-height:94vh;overflow:auto;border-radius:12px;background:#f8faf8;padding:24px;box-shadow:0 24px 80px #001c2e55}.modal-head{display:flex;justify-content:space-between;align-items:center;gap:20px}.cut-controls{display:flex;gap:15px;flex-wrap:wrap;margin:20px 0}.compare{display:grid;grid-template-columns:1fr 1fr;gap:24px}.compare>div{mLine truncated
|
:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#23323a;background:#f3f5f4;font-synthesis:none;font-size:14px}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button,a,input,select,textarea{outline-offset:3px}button{border:1px solid #c8d3ce;border-radius:7px;background:#fff;color:#244b40;padding:9px 13px;cursor:pointer;font-weight:550}button:hover:not(:disabled){background:#e9f1ed;border-color:#769b88}button:disabled{opacity:.45;cursor:default}.primary{background:#205c48;color:#fff;border-color:#205c48}.primary:hover:not(:disabled){background:#174633;color:#fff}a{color:#21654e;text-decoration:none}a:hover{text-decoration:underline}header{display:flex;align-items:center;gap:32px;padding:20px 36px;border-bottom:1px solid #dbe3de;background:#fff}.brand{display:flex;align-items:center;gap:10px;font-size:23px;font-weight:800;color:#28684f}.brand span{font-size:16px;font-weight:650}.heading{flex:1;border-left:1px solid #dbe3de;padding-left:30px}h1{font-size:22px;margin:3px 0 0}h2{font-size:20px;margin:0 0 8px}h3{font-size:19px;margin:7px 0}p{line-height:1.55;margin:8px 0;color:#56666b}.eyebrow{font-size:11px;letter-spacing:.09em;color:#5c7470;font-weight:650}main{max-width:1540px;margin:auto;padding:30px 36px 60px}.toolbar{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-bottom:24px}.filters{display:flex;gap:12px;align-items:end}label{display:flex;flex-direction:column;gap:6px;color:#405650;font-size:12px;font-weight:600}input,select,textarea{border:1px solid #c5d1cb;border-radius:6px;background:#fff;padding:9px 10px;color:#263d34;font-size:14px;min-width:0}textarea{resize:vertical;min-height:76px;line-height:1.5}input:focus,textarea:focus,select:focus{outline:2px solid #80b69c;border-color:#4d8269}input[type=checkbox]{width:16px;height:16px;accent-color:#205c48}.check{display:flex;flex-direction:row;align-items:center;max-width:310px;font-size:12px;line-height:1.4}.publish{display:flex;align-items:center;gap:16px;background:#e8f0eb;border:1px solid #d0e0d5;border-radius:10px;padding:17px 20px;margin:20px 0}.publish>div{flex:1}.publish small{display:block;margin-top:5px;color:#5b6e64}.panel{padding:18px 20px;background:#fff;border:1px solid #d9e2dd;border-radius:9px;margin-bottom:14px}summary{cursor:pointer;font-weight:600}summary span{float:right;font-size:12px;font-weight:400;color:#65766f}.identity{border-top:1px solid #e1e7e3;padding:18px 0;display:grid;grid-template-columns:280px 1fr;gap:28px}.identity-name{display:flex;flex-direction:column;gap:10px;align-items:start}.identity-name label{width:100%}.general button{margin:5px 6px 0 0}.remark-row{display:flex;align-items:center;gap:6px;margin-bottom:8px}.remark-row textarea{flex:1}.instructions{width:100%;font-family:ui-monospace,monospace;min-height:140px;margin:10px 0}.panel details{margin-top:16px}.results{display:flex;justify-content:space-between;align-items:center;margin:30px 0 12px}.results span{color:#677872;font-size:12px}.answer-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:22px}.answer{border:1px solid #d9e1dd;background:#fff;border-radius:10px;overflow:hidden}.card-heading{display:flex;justify-content:space-between;align-items:center;padding:18px 20px;gap:12px}.badge{font-size:11px;background:#edf1ee;color:#5a6e62;border-radius:4px;padding:5px 7px;white-space:nowrap}.warning{background:#fff2d8;color:#8a620b}.image-space{height:290px;overflow:hidden;background:#e7ece9;border-top:1px solid #e2e8e3;border-bottom:1px solid #e2e8e3;display:flex;justify-content:center;align-items:start;padding:12px}.image-button{position:relative;border:0;padding:0;background:#fff;width:100%;height:100%;overflow:hidden;border-radius:0}.image-button img{width:100%;display:block}.image-button span{position:absolute;bottom:8px;right:8px;background:#203d32e8;color:#fff;border-radius:5px;padding:7px 10px;font-size:12px}.answer-form{display:grid;grid-template-columns:88px 1fr;gap:15px;padding:18px 20px 10px}.card-actions{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:8px 20px 20px}.card-actions a{font-size:12px}.empty{padding:32px;color:#77877e;font-style:italic;text-align:center}.load-more{display:block;margin:24px auto}.alert{background:#fff0eb;color:#8f3827;border:1px solid #e9b6a9;border-radius:7px;padding:12px;display:flex;gap:12px;align-items:center}.alert small{font-size:12px}.notice{color:#216044}.muted{font-size:12px}fieldset{padding:0;margin:0;border:0;min-width:0}.overlay{position:fixed;inset:0;background:#142d23b8;z-index:10;display:flex;align-items:center;justify-content:center;padding:24px}.modal{width:min(1300px,100%);max-height:94vh;overflow:auto;border-radius:12px;background:#f8faf8;padding:24px;box-shadow:0 24px 80px #001c2e55}.modal-head{display:flex;justify-content:space-between;align-items:center;gap:20px}.cut-controls{display:flex;gap:15px;flex-wrap:wrap;margin:20px 0}.compare{display:grid;grid-template-columns:1fr 1fr;gap:24px}.compare>div{mLine truncated
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
(function(){const X=document.createElement("link").relList;if(X&&X.supports&&X.supports("modulepreload"))return;for(const B of document.querySelectorAll('link[rel="modulepreload"]'))h(B);new MutationObserver(B=>{for(const $ of B)if($.type==="childList")for(const sl of $.addedNodes)sl.tagName==="LINK"&&sl.rel==="modulepreload"&&h(sl)}).observe(document,{childList:!0,subtree:!0});function G(B){const $={};return B.integrity&&($.integrity=B.integrity),B.referrerPolicy&&($.referrerPolicy=B.referrerPolicy),B.crossOrigin==="use-credentials"?$.credentials="include":B.crossOrigin==="anonymous"?$.credentials="omit":$.credentials="same-origin",$}function h(B){if(B.ep)return;B.ep=!0;const $=G(B);fetch(B.href,$)}})();var zs={exports:{}},cn={};var lv;function iy(){if(lv)return cn;lv=1;var N=Symbol.for("react.transitional.element"),X=Symbol.for("react.fragment");function G(h,B,$){var sl=null;if($!==void 0&&(sl=""+$),B.key!==void 0&&(sl=""+B.key),"key"in B){$={};for(var Bl in B)Bl!=="key"&&($[Bl]=B[Bl])}else $=B;return B=$.ref,{$$typeof:N,type:h,key:sl,ref:B!==void 0?B:null,props:$}}return cn.Fragment=X,cn.jsx=G,cn.jsxs=G,cn}var tv;function cy(){return tv||(tv=1,zs.exports=iy()),zs.exports}var r=cy(),Ns={exports:{}},L={};var ev;function fy(){if(ev)return L;ev=1;var N=Symbol.for("react.transitional.element"),X=Symbol.for("react.portal"),G=Symbol.for("react.fragment"),h=Symbol.for("react.strict_mode"),B=Symbol.for("react.profiler"),$=Symbol.for("react.consumer"),sl=Symbol.for("react.context"),Bl=Symbol.for("react.forward_ref"),el=Symbol.for("react.suspense"),Al=Symbol.for("react.memo"),R=Symbol.for("react.lazy"),T=Symbol.for("react.activity"),M=Symbol.for("react.view_transition"),zl=Symbol.iterator;function Gl(o){return o===null||typeof o!="object"?null:(o=zl&&o[zl]||o["@@iterator"],typeof o=="function"?o:null)}var Dl={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},il=Object.assign,et={};function Nl(o,O,x){this.props=o,this.context=O,this.refs=et,this.updater=x||Dl}Nl.prototype.isReactComponent={},Nl.prototype.setState=function(o,O){if(typeof o!="object"&&typeof o!="function"&&o!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,o,O,"setState")},Nl.prototype.forceUpdate=function(o){this.updater.enqueueForceUpdate(this,o,"forceUpdate")};function St(){}St.prototype=Nl.prototype;function Xl(o,O,x){this.props=o,this.context=O,this.refs=et,this.updater=x||Dl}var ut=Xl.prototype=new St;ut.constructor=Xl,il(ut,Nl.prototype),ut.isPureReactComponent=!0;var Rl=Array.isArray;function K(){}var k={H:null,A:null,T:null,S:null},it=Object.prototype.hasOwnProperty;function Jl(o,O,x){var H=x.ref;return{$$typeof:N,type:o,key:O,ref:H!==void 0?H:null,props:x}}function wl(o,O){return Jl(o.type,O,o.props)}function Ml(o){return typeof o=="object"&&o!==null&&o.$$typeof===N}function at(o){var O={"=":"=0",":":"=2"};return"$"+o.replace(/[=:]/g,function(x){return O[x]})}var Z=/\/+/g;function gl(o,O){return typeof o=="object"&&o!==null&&o.key!=null?at(""+o.key):O.toString(36)}function j(o){switch(o.status){case"fulfilled":return o.value;case"rejected":throw o.reason;default:switch(typeof o.status=="string"?o.then(K,K):(o.status="pending",o.then(function(O){o.status==="pending"&&(o.status="fulfilled",o.value=O)},function(O){o.status==="pending"&&(o.status="rejected",o.reason=O)})),o.status){case"fulfilled":return o.value;case"rejected":throw o.reason}}throw o}function Q(o,O,x,H,F){var b=typeof o;(b==="undefined"||b==="boolean")&&(o=null);var _=!1;if(o===null)_=!0;else switch(b){case"bigint":case"string":case"number":_=!0;break;case"object":switch(o.$$typeof){case N:case X:_=!0;break;case R:return _=o._init,Q(_(o._payload),O,x,H,F)}}if(_)return F=F(o),_=H===""?"."+gl(o,0):H,Rl(F)?(x="",_!=null&&(x=_.replace(Z,"$&/")+"/"),Q(F,O,x,"",function(ol){return ol})):F!=null&&(Ml(F)&&(F=wl(F,x+(F.key==null||o&&o.key===F.key?"":(""+F.key).replace(Z,"$&/")+"/")+_)),O.push(F)),1;_=0;var A=H===""?".":H+":";if(Rl(o))for(var D=0;D<o.length;D++)H=o[D],b=A+gl(H,D),_+=Q(H,O,x,b,F);else if(D=Gl(o),typeof D=="function")for(o=D.call(o),D=0;!(H=o.next()).done;)H=H.value,b=A+gl(H,D++),_+=Q(H,O,x,b,F);else if(b==="object"){if(typeof o.then=="function")return Q(j(o),O,x,H,F);throw O=String(o),Error("Objects are not valid as a React child (found: "+(O==="[object Object]"?"object with keys {"+Object.keys(o).join(", ")+"}":O)+"). If you meant to render a collection of children, use an array instead.")}return _}function Y(o,O,x){if(o==null)return o;var H=[],F=0;return Q(o,H,"","",function(b){return O.call(x,b,F++)}),H}function ul(o){if(o._status===-1){var O=o._result,x=O();x.then(function(H){(o._status===0||o._status===-1)&&(o._status=1,o._result=H,x.status===void 0&&(x.status="fulfilled",x.value=H))},function(H){(o._status===0||o._status===-1)&&(o._status=2,o._result=H,x.status===void 0&&(x.status="reLine truncated
|
|
||||||
at`)?" (<anonymous>)":-1<e.stack.indexOf("@")?"@unknown:0:0":""}return`
|
|
||||||
`+Vi+l+Ds}var Zi=!1;function Li(l,t){if(!l||Zi)return"";Zi=!0;var e=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var u={DetermineComponentFrameRoot:function(){try{if(t){var z=function(){throw Error()};if(Object.defineProperty(z.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(z,[])}catch(C){var d=C}Reflect.construct(l,[],z)}else{try{z.call()}catch(C){d=C}z=!1;try{var g=Object.getOwnPropertyDescriptor(l.prototype,"props");Object.defineProperty(l.prototype,"props",{configurable:!0,set:function(){throw Error()}}),z=!0,new l}finally{z&&(g!==void 0?Object.defineProperty(l.prototype,"props",g):delete l.prototype.props)}}}else{try{throw Error()}catch(C){d=C}(z=l())&&typeof z.catch=="function"&&z.catch(function(){})}}catch(C){if(C&&d&&typeof C.stack=="string")return[C.stack,d.stack]}return[null,null]}};u.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var a=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");a&&a.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var n=u.DetermineComponentFrameRoot(),i=n[0],c=n[1];if(i&&c){var f=i.split(`
|
|
||||||
`),m=c.split(`
|
|
||||||
`);for(a=u=0;u<f.length&&!f[u].includes("DetermineComponentFrameRoot");)u++;for(;a<m.length&&!m[a].includes("DetermineComponentFrameRoot");)a++;if(u===f.length||a===m.length)for(u=f.length-1,a=m.length-1;1<=u&&0<=a&&f[u]!==m[a];)a--;for(;1<=u&&0<=a;u--,a--)if(f[u]!==m[a]){if(u!==1||a!==1)do if(u--,a--,0>a||f[u]!==m[a]){var S=`
|
|
||||||
`+f[u].replace(" at new "," at ");return l.displayName&&S.includes("<anonymous>")&&(S=S.replace("<anonymous>",l.displayName)),S}while(1<=u&&0<=a);break}}}finally{Zi=!1,Error.prepareStackTrace=e}return(e=l?l.displayName||l.name:"")?_e(e):""}function ov(l,t){switch(l.tag){case 26:case 27:case 5:return _e(l.type);case 16:return _e("Lazy");case 13:return l.child!==t&&t!==null?_e("Suspense Fallback"):_e("Suspense");case 19:return _e("SuspenseList");case 0:case 15:return Li(l.type,!1);case 11:return Li(l.type.render,!1);case 1:return Li(l.type,!0);case 31:return _e("Activity");case 30:return _e("ViewTransition");default:return""}}function Ms(l){try{var t="",e=null;do t+=ov(l,e),e=l,l=l.return;while(l);return t}catch(u){return`
|
|
||||||
Error generating stack: `+u.message+`
|
|
||||||
`+u.stack}}var Ki=Object.prototype.hasOwnProperty,Ji=N.unstable_scheduleCallback,wi=N.unstable_cancelCallback,rv=N.unstable_shouldYield,dv=N.unstable_requestPaint,Tt=N.unstable_now,vv=N.unstable_getCurrentPriorityLevel,ps=N.unstable_ImmediatePriority,Us=N.unstable_UserBlockingPriority,vn=N.unstable_NormalPriority,mv=N.unstable_LowPriority,Rs=N.unstable_IdlePriority,hv=N.log,yv=N.unstable_setDisableYieldValue,va=null,Et=null;function Oe(l){if(typeof hv=="function"&&yv(l),Et&&typeof Et.setStrictMode=="function")try{Et.setStrictMode(va,l)}catch{}}var zt=Math.clz32?Math.clz32:bv,gv=Math.log,Sv=Math.LN2;function bv(l){return l>>>=0,l===0?32:31-(gv(l)/Sv|0)|0}var mn=256,hn=262144,yn=4194304;function Ie(l){var t=l&42;if(t!==0)return t;switch(l&-l){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return l&-l;case 262144:case 524288:case 1048576:case 2097152:return l&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return l&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return l}}function gn(l,t,e){var u=l.pendingLanes;if(u===0)return 0;var a=0,n=l.suspendedLanes,i=l.pingedLanes;l=l.warmLanes;var c=u&134217727;return c!==0?(u=c&~n,u!==0?a=Ie(u):(i&=c,i!==0?a=Ie(i):e||(e=c&~l,e!==0&&(a=Ie(e))))):(c=u&~n,c!==0?a=Ie(c):i!==0?a=Ie(i):e||(e=u&~l,e!==0&&(a=Ie(e)))),a===0?0:t!==0&&t!==a&&(t&n)===0&&(n=a&-a,e=t&-t,n>=e||n===32&&(e&4194048)!==0)?t:a}function ma(l,t){return(l.pendingLanes&~(l.suspendedLanes&~l.pingedLanes)&t)===0}function xs(l,t){(t&8)!==0&&(t|=t&32);var e=l.entangledLanes;if(e!==0)for(l=l.entanglements,e&=t;0<e;){var u=31-zt(e),a=1<<u;t|=l[u],e&=~a}return t}function Tv(l,t){switch(l){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Hs(){var l=yn;return yn<<=1,(yn&62914560)===0&&(yn=4194304),l}function $i(l){for(var t=[],e=0;31>e;e++)t.push(l);return t}function ha(l,t){l.pendingLanes|=t,t!==268435456&&(l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0)}function Ev(l,t,e,u,a,n){var i=l.pendingLanes;l.pendingLanes=e,l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0,l.expiredLanes&=e,l.entangledLanes&=e,l.errorRecoveryDisabledLanes&=e,l.shellSuspendCounter=0;var c=l.entanglements,f=l.expirationTimes,m=l.hiddenUpdates;for(e=i&~e;0<e;){var S=31-zt(e),z=1<<S;c[S]=0,f[S]=-1;var d=m[S];if(d!==null)for(m[S]=null,S=0;S<d.length;S++){var g=d[S];g!==null&&(g.lane&=-536870913)}e&=~z}u!==0&&qs(l,u,0),n!==0&&a===0&&l.tag!==0&&(l.suspendedLanes|=n&~(i&~t))}function qs(l,t,e){l.pendingLanes|=t,l.suspendedLanes&=~t;var u=31-zt(t);l.entangledLanes|=t,l.entanglements[u]=l.entanglements[u]|1073741824|e&261930}function Ys(l,t){var e=l.entangledLanes|=t;for(l=l.entanglements;e;){var u=31-zt(e),a=1<<u;a&t|l[u]&t&&(l[u]|=t),e&=~a}}function Bs(l,t){var e=t&-t;return e=(e&42)!==0?1:Fi(e),(e&(l.suspendedLanes|t))!==0?0:e}function Fi(l){switch(l){case 2:l=1;break;case 8:l=4;break;case 32:l=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:l=128;break;case 268435456:l=134217728;break;default:l=0}return l}function Wi(l){return l&=-l,2<l?8<l?(l&134217727)!==0?32:268435456:8:2}function Gs(){var l=D.p;return l!==0?l:(l=window.event,l===void 0?32:w0(l.type))}function Xs(l,t){var e=D.p;try{return D.p=l,t()}finally{D.p=e}}var fe=Math.random().toString(36).slice(2),Fl="__reactFiber$"+fe,dt="__reactProps$"+fe,zu="__reactContainer$"+fe,Qs="__reactEvents$"+fe,zv="__reactListeners$"+fe,Nv="__reactHandles$"+fe,Vs="__reactResources$"+fe,ya="__reactMarker$"+fe,Sn="__reactLoad$"+fe;function bn(l){delete l[Fl],delete l[dt],delete l[zv],delete l[Nv]}function Pe(l){var t;if(t=l[Fl])return t;for(var e=l.parentNode;e;){if(t=e[zu]||e[Fl]){if(e=t.alternate,t.child!==null||e!==null&&e.child!==null)for(l=C0(l);l!==null;){if(e=l[Fl])return e;l=C0(l)}return t}l=e,e=l.parentNode}return null}function Nu(l){if(l=l[Fl]||l[zu]){var t=l.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return l}return null}function ga(l){var t=l.tag;if(t===5||t===26||t===27||t===6)return l.stateNode;throw Error(h(33))}function _u(l){var t=l[Vs];return t||(t=l[Vs]={hoistableStyles:new Map,hoistableScripts:new Map}),t}function Vl(l){l[ya]=!0}function Zs(l){l[Sn]=void 0}var Ls=new Set,Ks={};function lu(l,t){Ou(l,t),Ou(l+"Capture",t)}function
Line truncated
|
|
||||||
`).replace(oh,"")}function c0(l,t){return t=i0(t),i0(l)===t}function hl(l,t,e,u,a,n){switch(e){case"children":if(typeof u=="string")t==="body"||t==="textarea"&&u===""||Cu(l,u);else if(typeof u=="number"||typeof u=="bigint")t!=="body"&&Cu(l,""+u);else return;break;case"className":En(l,"class",u);break;case"tabIndex":En(l,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":En(l,e,u);break;case"style":to(l,u,n);return;case"data":if(t!=="object"){En(l,"data",u);break}case"src":case"href":if(u===""&&(t!=="a"||e!=="href")){l.removeAttribute(e);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){l.removeAttribute(e);break}u=zn(u),l.setAttribute(e,u);break;case"action":case"formAction":if(typeof u=="function"){l.setAttribute(e,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof n=="function"&&(e==="formAction"?(t!=="input"&&hl(l,t,"name",a.name,a,null),hl(l,t,"formEncType",a.formEncType,a,null),hl(l,t,"formMethod",a.formMethod,a,null),hl(l,t,"formTarget",a.formTarget,a,null)):(hl(l,t,"encType",a.encType,a,null),hl(l,t,"method",a.method,a,null),hl(l,t,"target",a.target,a,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){l.removeAttribute(e);break}u=zn(u),l.setAttribute(e,u);break;case"onClick":u!=null&&(l.onclick=kt);return;case"onScroll":u!=null&&P("scroll",l);return;case"onScrollEnd":u!=null&&P("scrollend",l);return;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(h(61));if(e=u.__html,e!=null){if(a.children!=null)throw Error(h(60));n?.__html!==e&&(l.innerHTML=e)}}break;case"multiple":l.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":l.muted=u&&typeof u!="function"&&typeof u!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(u==null||typeof u=="function"||typeof u=="boolean"||typeof u=="symbol"){l.removeAttribute("xlink:href");break}e=zn(u),l.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",e);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":u!=null&&typeof u!="function"&&typeof u!="symbol"?l.setAttribute(e,u):l.removeAttribute(e);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"credentialless":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":u&&typeof u!="function"&&typeof u!="symbol"?l.setAttribute(e,""):l.removeAttribute(e);break;case"capture":case"download":u===!0?l.setAttribute(e,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?l.setAttribute(e,u):l.removeAttribute(e);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?l.setAttribute(e,u):l.removeAttribute(e);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?l.removeAttribute(e):l.setAttribute(e,u);break;case"popover":P("beforetoggle",l),P("toggle",l),Tn(l,"popover",u);break;case"xlinkActuate":se(l,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":se(l,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":se(l,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":se(l,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":se(l,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":se(l,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":se(l,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":se(l,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":se(l,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":Tn(l,"is",u);break;case"innerText":case"textContent":return;default:if(!(2<e.length)||e[0]!=="o"&&e[0]!=="O"||e[1]!=="n"&&e[1]!=="N")e=Dv.get(e)||e,Tn(l,e,u);else return}cl=!0}function Pf(l,t,e,u,a,n){switch(e){case"style":to(l,u,n);return;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(h(61));if(e=u.__html,e!=null){if(a.children!=null)throw Error(h(60));n?.__html!==e&&(l.innerHTML=e)}}break;case"children":if(typeof u=="string")Cu(l,u);else if(typeof u=="number"||typeof u=="bigint")Cu(l,""+u);else return;break;case"onScroll":u!=null&&P("scroll",l);return;case"onScrollEnd":u!=null&&P("scrollend",l);return;case"onClick":u!=null
Line truncated
|
|
||||||
`).map((b,_,A)=>b.trim()&&!b.trim().startsWith("###")?r.jsxs("div",{className:"draft-row",children:[r.jsx("code",{children:b}),r.jsx("button",{"aria-label":`Monter l’instruction ${_}`,disabled:_===0||A[_-1].startsWith("###"),onClick:()=>{const D=[...A];[D[_-1],D[_]]=[D[_],D[_-1]],Z("draft","","",D.join(`
|
|
||||||
`))},children:"↑"}),r.jsx("button",{"aria-label":`Descendre l’instruction ${_}`,disabled:_===A.length-1||!A[_+1].trim(),onClick:()=>{const D=[...A];[D[_+1],D[_]]=[D[_],D[_+1]],Z("draft","","",D.join(`
|
|
||||||
`))},children:"↓"}),r.jsx("button",{onClick:()=>Z("draft","","",A.filter((D,ol)=>ol!==_).join(`
|
|
||||||
`)),children:"Retirer"})]},_):null)}),r.jsxs("details",{children:[r.jsx("summary",{children:"Éditer les instructions en texte"}),r.jsx("textarea",{className:"instructions","aria-label":"Brouillon des transferts",value:al,onChange:b=>Z("draft","","",b.target.value)})]}),r.jsx("button",{disabled:!M[Vt("draft")],onClick:()=>gl([Vt("draft")]),children:"Enregistrer le brouillon"}),r.jsxs("details",{children:[r.jsx("summary",{children:"Instructions actuellement appliquées"}),r.jsx("pre",{children:N.applied})]})]}),r.jsxs("div",{className:"results",children:[r.jsxs("h2",{children:[bt.length," réponse",bt.length!==1?"s":""]}),r.jsx("span",{children:!sl&&!el?"Vue d’ensemble · sélectionnez une copie ou un label pour cibler la vérification":"Images annotées après lecture manuscrite"})]}),r.jsx("div",{className:"answer-grid",children:bt.slice(0,R).map(({c:b,a:_})=>r.jsxs("article",{className:"answer",children:[r.jsxs("div",{className:"card-heading",children:[r.jsxs("div",{children:[r.jsxs("span",{className:"eyebrow",children:["COPIE ",b.id," · ",b.name]}),r.jsx("h3",{children:_.label})]}),r.jsx("span",{className:"badge",children:_.info.present?_.info.not_empty?_.info.touched?"Dans le PDF":"Hors PDF filtré":"Réponse vide":"Absente"})]}),r.jsx("div",{className:"image-space",children:_.image?r.jsxs("button",{className:"image-button","aria-label":`Agrandir copie ${b.id} ${_.label}`,onClick:()=>ut({copy:b.id,label:_.label}),children:[r.jsx("img",{loading:"lazy",src:Xi(b.id,_.label,!1,N.revision),alt:`Réponse annotée — copie ${b.id}, ${_.label}`}),r.jsx("span",{children:"Agrandir ↗"})]}):r.jsx("p",{className:"empty",children:"Aucune image pour ce label."})}),r.jsxs("div",{className:"answer-form",children:[r.jsxs("label",{children:["Score",r.jsx("input",{inputMode:"decimal",value:at("score",b.id,_.label,_.score),onChange:A=>Z("score",b.id,_.label,A.target.value)})]}),r.jsxs("label",{children:["Remarque personnelle",r.jsx("textarea",{placeholder:"Ajouter une remarque (LaTeX accepté)",value:at("remark",b.id,_.label,_.remark),onChange:A=>Z("remark",b.id,_.label,A.target.value)})]})]}),r.jsxs("div",{className:"card-actions",children:[r.jsx("button",{disabled:!M[Vt("score",b.id,_.label)]&&!M[Vt("remark",b.id,_.label)],onClick:()=>gl([Vt("score",b.id,_.label),Vt("remark",b.id,_.label)]),children:"Enregistrer"}),r.jsx("button",{disabled:!_.editableBody,onClick:()=>St({copy:b.id,label:_.label}),children:"Couper / transférer"}),r.jsx("a",{href:sn(b.id,"context",_.label),target:"_blank",rel:"noreferrer",children:"Contexte ↗"}),r.jsx("a",{href:sn(b.id,"question",_.label),target:"_blank",rel:"noreferrer",children:"Énoncé ↗"}),r.jsx("a",{href:sn(b.id,"solution",_.label),target:"_blank",rel:"noreferrer",children:"Corrigé ↗"}),r.jsx("a",{href:sn(b.id,"output"),target:"_blank",rel:"noreferrer",children:"PDF final ↗"})]})]},b.id+"|"+_.label))}),bt.length===0&&r.jsx("p",{className:"empty",children:"Aucune réponse ne correspond à ces filtres."}),bt.length>R&&r.jsx("button",{className:"load-more",onClick:()=>T(R+20),children:"Afficher les 20 suivantes"})]}),(N.job.log||N.job.running)&&r.jsxs("details",{className:"panel",open:N.job.status==="failed",children:[r.jsxs("summary",{children:["Journal de régénération · ",N.job.status==="failed"?"Échec / résultat incomplet":N.job.running?"En cours":"Terminée"]}),r.jsx("pre",{className:"log",children:N.job.log})]})]}),Nl&&r.jsx(Sy,{copy:Nl.copy,source:Nl.label,labels:N.labels,mode:N.mode,revision:N.revision,draft:al,close:()=>St(null),accept:b=>{Z("draft","","",b),St(null),$("Instruction ajoutée au brouillon. Enregistrez-le avant de l’appliquer.")}}),Xl&&r.jsx(gy,{copy:Xl.copy,label:Xl.label,revision:N.revision,close:()=>ut(null)})]})}function gy({copy:N,label:X,revision:G,close:h}){const[B,$]=dl.useState(100);return dl.useEffect(()=>{const sl=Bl=>{Bl.key==="Escape"&&h()};return window.addEventListener("keydown",sl),()=>window.removeEventListener("keydown",sl)},[]),r.jsx("div",{className:"overlay",children:r.jsxs("section",{className:"modal viewer",role:"dialog","aria-modal":"true","aria-label":`Copie ${N} ${X}`,children:[r.jsxs("div",{className:"modal-head",children:[r.jsxs("h2",{children:["Copie ",N," · ",X]}),r.jsxs("label",{children:["Zoom ",r.jsx("input",{type:"range",min:"40",max:"200",value:B,onChange:sl=>$(Number(sl.target.value))}),B,"%"]}),r.jsx("button",{autoFocus:!0,onClick:h,children:"Fermer"})]}),r.jsx("div",{className:"zoom-scroll",children:r.jsx("img",{src:Xi(N,X,!1,G),alt:`Copie ${N}, ${X}`,style:{width:B+"%",maxWidth:"none"}})})]})})}function Sy({copy:N,source:X,labels:G,mode:h,revision:B,draft:$,close:sl,accept:Bl}){const[el,Al]=dl.useState(G.find(Z=>Z!==X)||""),[R,T]=dl.useState("cut"),[M,zl]=dl.useState(50),[Gl,Dl]=dl.useState("1"),[il,et]=dl.useState(">"),[Nl,St]=dl.useState(!1),[Xl,ut]=dl.useState(""),[Rl,K]=dl.useState(null),[k,it]=dl.useState(!1),Jl=R==="cut"?`c{${M}}${Gl}${il}`:R,wl=`Copie${N} ${X} ${Jl} ${Nl?"|":""}${el}${Nl?"":"|"}Line truncated
|
|
||||||
`+wl+`
|
|
||||||
`;dl.useEffect(()=>K(null),[el,R,M,Gl,il,Nl,$]);const at=async()=>{it(!0),ut("");try{K(await ra("preview",{copy:N,source:X,target:el,text:Ml}))}catch(Z){ut(String(Z))}finally{it(!1)}};return r.jsx("div",{className:"overlay",children:r.jsxs("section",{className:"modal",role:"dialog","aria-modal":"true","aria-label":"Couper ou transférer une réponse",children:[r.jsxs("div",{className:"modal-head",children:[r.jsxs("div",{children:[r.jsxs("span",{className:"eyebrow",children:["COPIE ",N," · ",h]}),r.jsx("h2",{children:"Couper ou transférer"})]}),r.jsx("button",{autoFocus:!0,onClick:sl,children:"Fermer"})]}),r.jsx("p",{children:"Seul le corps de la réponse est déplacé. Cliquez sur l’image source pour placer la coupe."}),Xl&&r.jsx("p",{role:"alert",className:"alert",children:Xl}),r.jsxs("div",{className:"cut-controls",children:[r.jsxs("label",{children:["Destination",r.jsx("select",{value:el,onChange:Z=>Al(Z.target.value),children:G.filter(Z=>Z!==X).map(Z=>r.jsx("option",{children:Z},Z))})]}),r.jsxs("label",{children:["Opération",r.jsxs("select",{value:R,onChange:Z=>T(Z.target.value),children:[r.jsx("option",{value:"cut",children:"Couper la source"}),r.jsx("option",{value:"x>",children:"Déplacer et ajouter"}),r.jsx("option",{value:"->",children:"Copier et ajouter"}),r.jsx("option",{value:"xx",children:"Déplacer et remplacer"}),r.jsx("option",{value:"-x",children:"Copier et remplacer"}),r.jsx("option",{value:"xs",children:"Vider la source"}),r.jsx("option",{value:"sx",children:"Vider la cible"}),r.jsx("option",{value:"ss",children:"Conserver les deux"})]})]}),R==="cut"&&r.jsxs(r.Fragment,{children:[r.jsxs("label",{children:["Coupe : ",M,"%",r.jsx("input",{type:"range",min:"1",max:"99",step:"0.1",value:M,onChange:Z=>zl(Number(Z.target.value))})]}),r.jsxs("label",{children:["Conserver à la source",r.jsxs("select",{value:Gl,onChange:Z=>Dl(Z.target.value),children:[r.jsx("option",{value:"1",children:"La partie haute"}),r.jsx("option",{value:"2",children:"La partie basse"})]})]}),r.jsxs("label",{children:["À la destination",r.jsxs("select",{value:il,onChange:Z=>et(Z.target.value),children:[r.jsx("option",{value:">",children:"Ajouter le fragment"}),r.jsx("option",{value:"x",children:"Remplacer le corps"})]})]})]}),r.jsxs("label",{className:"check",children:[r.jsx("input",{type:"checkbox",checked:Nl,onChange:Z=>St(Z.target.checked)}),"Placer avant la cible"]})]}),r.jsxs("div",{className:"compare",children:[r.jsxs("div",{children:[r.jsxs("h3",{children:["Source · ",X]}),r.jsxs("div",{className:"cut-image",onClick:Z=>{if(R==="cut"){const gl=Z.currentTarget.getBoundingClientRect();zl(Math.min(99,Math.max(1,Math.round((Z.clientY-gl.top)/gl.height*1e3)/10)))}},children:[r.jsx("img",{src:Xi(N,X,!0,B),alt:"Corps source sans en-tête"}),R==="cut"&&r.jsx("div",{className:"cut-line",style:{top:M+"%"},children:r.jsxs("span",{children:[M,"%"]})})]})]}),r.jsxs("div",{children:[r.jsxs("h3",{children:["Cible · ",el]}),r.jsx(by,{copy:N,label:el,revision:B},el)]})]}),r.jsx("code",{className:"instruction-line",children:wl}),r.jsxs("div",{className:"modal-actions",children:[r.jsx("button",{disabled:k||!el,onClick:at,children:k?"Calcul de l’aperçu…":"Aperçu du résultat"}),r.jsx("button",{className:"primary",disabled:!Rl||k,onClick:()=>Bl(Ml),children:"Ajouter au brouillon"})]}),Rl&&r.jsxs(r.Fragment,{children:[r.jsx("h3",{children:"Résultat proposé après toutes les instructions du brouillon"}),r.jsx("div",{className:"compare",children:[X,el].map(Z=>r.jsxs("div",{children:[r.jsx("h4",{children:Z}),Rl[Z]?r.jsx("img",{src:Rl[Z],alt:"Aperçu "+Z}):r.jsx("p",{className:"empty",children:"Corps vide"})]},Z))})]}),r.jsx("p",{className:"muted",children:"Une instruction par source ; une cible ne peut pas être source. Modifiez ou retirez une instruction existante dans le panneau « Transferts et coupes »."})]})})}function by({copy:N,label:X,revision:G}){const[h,B]=dl.useState(!1);return h?r.jsx("p",{className:"empty",children:"Pas de corps existant. Le transfert peut remplir cette cible."}):r.jsx("img",{src:Xi(N,X,!0,G),alt:"Corps cible sans en-tête",onError:()=>B(!0)})}hy.createRoot(document.getElementById("root")).render(r.jsx(yy,{}));
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
(function(){const X=document.createElement("link").relList;if(X&&X.supports&&X.supports("modulepreload"))return;for(const Y of document.querySelectorAll('link[rel="modulepreload"]'))h(Y);new MutationObserver(Y=>{for(const k of Y)if(k.type==="childList")for(const ol of k.addedNodes)ol.tagName==="LINK"&&ol.rel==="modulepreload"&&h(ol)}).observe(document,{childList:!0,subtree:!0});function B(Y){const k={};return Y.integrity&&(k.integrity=Y.integrity),Y.referrerPolicy&&(k.referrerPolicy=Y.referrerPolicy),Y.crossOrigin==="use-credentials"?k.credentials="include":Y.crossOrigin==="anonymous"?k.credentials="omit":k.credentials="same-origin",k}function h(Y){if(Y.ep)return;Y.ep=!0;const k=B(Y);fetch(Y.href,k)}})();var Ns={exports:{}},fn={};var ev;function fy(){if(ev)return fn;ev=1;var T=Symbol.for("react.transitional.element"),X=Symbol.for("react.fragment");function B(h,Y,k){var ol=null;if(k!==void 0&&(ol=""+k),Y.key!==void 0&&(ol=""+Y.key),"key"in Y){k={};for(var ql in Y)ql!=="key"&&(k[ql]=Y[ql])}else k=Y;return Y=k.ref,{$$typeof:T,type:h,key:ol,ref:Y!==void 0?Y:null,props:k}}return fn.Fragment=X,fn.jsx=B,fn.jsxs=B,fn}var uv;function sy(){return uv||(uv=1,Ns.exports=fy()),Ns.exports}var o=sy(),_s={exports:{}},K={};var av;function oy(){if(av)return K;av=1;var T=Symbol.for("react.transitional.element"),X=Symbol.for("react.portal"),B=Symbol.for("react.fragment"),h=Symbol.for("react.strict_mode"),Y=Symbol.for("react.profiler"),k=Symbol.for("react.consumer"),ol=Symbol.for("react.context"),ql=Symbol.for("react.forward_ref"),cl=Symbol.for("react.suspense"),pl=Symbol.for("react.memo"),x=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),j=Symbol.for("react.view_transition"),Al=Symbol.iterator;function Vl(r){return r===null||typeof r!="object"?null:(r=Al&&r[Al]||r["@@iterator"],typeof r=="function"?r:null)}var xl={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},rl=Object.assign,nt={};function Cl(r,_,R){this.props=r,this.context=_,this.refs=nt,this.updater=R||xl}Cl.prototype.isReactComponent={},Cl.prototype.setState=function(r,_){if(typeof r!="object"&&typeof r!="function"&&r!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,r,_,"setState")},Cl.prototype.forceUpdate=function(r){this.updater.enqueueForceUpdate(this,r,"forceUpdate")};function Tt(){}Tt.prototype=Cl.prototype;function Zl(r,_,R){this.props=r,this.context=_,this.refs=nt,this.updater=R||xl}var it=Zl.prototype=new Tt;it.constructor=Zl,rl(it,Cl.prototype),it.isPureReactComponent=!0;var Yl=Array.isArray;function w(){}var ll={H:null,A:null,T:null,S:null},ft=Object.prototype.hasOwnProperty;function Rl(r,_,R){var M=R.ref;return{$$typeof:T,type:r,key:_,ref:M!==void 0?M:null,props:R}}function Ll(r,_){return Rl(r.type,_,r.props)}function Kl(r){return typeof r=="object"&&r!==null&&r.$$typeof===T}function xt(r){var _={"=":"=0",":":"=2"};return"$"+r.replace(/[=:]/g,function(R){return _[R]})}var $=/\/+/g;function zl(r,_){return typeof r=="object"&&r!==null&&r.key!=null?xt(""+r.key):_.toString(36)}function A(r){switch(r.status){case"fulfilled":return r.value;case"rejected":throw r.reason;default:switch(typeof r.status=="string"?r.then(w,w):(r.status="pending",r.then(function(_){r.status==="pending"&&(r.status="fulfilled",r.value=_)},function(_){r.status==="pending"&&(r.status="rejected",r.reason=_)})),r.status){case"fulfilled":return r.value;case"rejected":throw r.reason}}throw r}function q(r,_,R,M,I){var al=typeof r;(al==="undefined"||al==="boolean")&&(r=null);var fl=!1;if(r===null)fl=!0;else switch(al){case"bigint":case"string":case"number":fl=!0;break;case"object":switch(r.$$typeof){case T:case X:fl=!0;break;case x:return fl=r._init,q(fl(r._payload),_,R,M,I)}}if(fl)return I=I(r),fl=M===""?"."+zl(r,0):M,Yl(I)?(R="",fl!=null&&(R=fl.replace($,"$&/")+"/"),q(I,_,R,"",function(st){return st})):I!=null&&(Kl(I)&&(I=Ll(I,R+(I.key==null||r&&r.key===I.key?"":(""+I.key).replace($,"$&/")+"/")+fl)),_.push(I)),1;fl=0;var U=M===""?".":M+":";if(Yl(r))for(var Z=0;Z<r.length;Z++)M=r[Z],al=U+zl(M,Z),fl+=q(M,_,R,al,I);else if(Z=Vl(r),typeof Z=="function")for(r=Z.call(r),Z=0;!(M=r.next()).done;)M=M.value,al=U+zl(M,Z++),fl+=q(M,_,R,al,I);else if(al==="object"){if(typeof r.then=="function")return q(A(r),_,R,M,I);throw _=String(r),Error("Objects are not valid as a React child (found: "+(_==="[object Object]"?"object with keys {"+Object.keys(r).join(", ")+"}":_)+"). If you meant to render a collection of children, use an array instead.")}return fl}function V(r,_,R){if(r==null)return r;var M=[],I=0;return q(r,M,"","",function(al){return _.call(R,al,I++)}),M}function yl(r){if(r._status===-1){var _=r._result,R=_();R.then(function(M){(r._status===0||r._status===-1)&&(r._status=1,r._result=M,R.status===void 0&&(R.status="fulfilled",R.value=M))},function(M){(r._status===0||r._status===-1)&&(r._status=2,r._result=M,R.staLine truncated
|
||||||
|
at`)?" (<anonymous>)":-1<e.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||||
|
`+Zi+l+Ms}var Li=!1;function Ki(l,t){if(!l||Li)return"";Li=!0;var e=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var u={DetermineComponentFrameRoot:function(){try{if(t){var z=function(){throw Error()};if(Object.defineProperty(z.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(z,[])}catch(C){var d=C}Reflect.construct(l,[],z)}else{try{z.call()}catch(C){d=C}z=!1;try{var g=Object.getOwnPropertyDescriptor(l.prototype,"props");Object.defineProperty(l.prototype,"props",{configurable:!0,set:function(){throw Error()}}),z=!0,new l}finally{z&&(g!==void 0?Object.defineProperty(l.prototype,"props",g):delete l.prototype.props)}}}else{try{throw Error()}catch(C){d=C}(z=l())&&typeof z.catch=="function"&&z.catch(function(){})}}catch(C){if(C&&d&&typeof C.stack=="string")return[C.stack,d.stack]}return[null,null]}};u.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var a=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");a&&a.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var n=u.DetermineComponentFrameRoot(),i=n[0],c=n[1];if(i&&c){var f=i.split(`
|
||||||
|
`),m=c.split(`
|
||||||
|
`);for(a=u=0;u<f.length&&!f[u].includes("DetermineComponentFrameRoot");)u++;for(;a<m.length&&!m[a].includes("DetermineComponentFrameRoot");)a++;if(u===f.length||a===m.length)for(u=f.length-1,a=m.length-1;1<=u&&0<=a&&f[u]!==m[a];)a--;for(;1<=u&&0<=a;u--,a--)if(f[u]!==m[a]){if(u!==1||a!==1)do if(u--,a--,0>a||f[u]!==m[a]){var S=`
|
||||||
|
`+f[u].replace(" at new "," at ");return l.displayName&&S.includes("<anonymous>")&&(S=S.replace("<anonymous>",l.displayName)),S}while(1<=u&&0<=a);break}}}finally{Li=!1,Error.prepareStackTrace=e}return(e=l?l.displayName||l.name:"")?Oe(e):""}function dv(l,t){switch(l.tag){case 26:case 27:case 5:return Oe(l.type);case 16:return Oe("Lazy");case 13:return l.child!==t&&t!==null?Oe("Suspense Fallback"):Oe("Suspense");case 19:return Oe("SuspenseList");case 0:case 15:return Ki(l.type,!1);case 11:return Ki(l.type.render,!1);case 1:return Ki(l.type,!0);case 31:return Oe("Activity");case 30:return Oe("ViewTransition");default:return""}}function Us(l){try{var t="",e=null;do t+=dv(l,e),e=l,l=l.return;while(l);return t}catch(u){return`
|
||||||
|
Error generating stack: `+u.message+`
|
||||||
|
`+u.stack}}var Ji=Object.prototype.hasOwnProperty,wi=T.unstable_scheduleCallback,$i=T.unstable_cancelCallback,vv=T.unstable_shouldYield,mv=T.unstable_requestPaint,zt=T.unstable_now,hv=T.unstable_getCurrentPriorityLevel,xs=T.unstable_ImmediatePriority,Rs=T.unstable_UserBlockingPriority,mn=T.unstable_NormalPriority,yv=T.unstable_LowPriority,Hs=T.unstable_IdlePriority,gv=T.log,Sv=T.unstable_setDisableYieldValue,ma=null,Nt=null;function Ae(l){if(typeof gv=="function"&&Sv(l),Nt&&typeof Nt.setStrictMode=="function")try{Nt.setStrictMode(ma,l)}catch{}}var _t=Math.clz32?Math.clz32:Ev,bv=Math.log,Tv=Math.LN2;function Ev(l){return l>>>=0,l===0?32:31-(bv(l)/Tv|0)|0}var hn=256,yn=262144,gn=4194304;function Pe(l){var t=l&42;if(t!==0)return t;switch(l&-l){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return l&-l;case 262144:case 524288:case 1048576:case 2097152:return l&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return l&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return l}}function Sn(l,t,e){var u=l.pendingLanes;if(u===0)return 0;var a=0,n=l.suspendedLanes,i=l.pingedLanes;l=l.warmLanes;var c=u&134217727;return c!==0?(u=c&~n,u!==0?a=Pe(u):(i&=c,i!==0?a=Pe(i):e||(e=c&~l,e!==0&&(a=Pe(e))))):(c=u&~n,c!==0?a=Pe(c):i!==0?a=Pe(i):e||(e=u&~l,e!==0&&(a=Pe(e)))),a===0?0:t!==0&&t!==a&&(t&n)===0&&(n=a&-a,e=t&-t,n>=e||n===32&&(e&4194048)!==0)?t:a}function ha(l,t){return(l.pendingLanes&~(l.suspendedLanes&~l.pingedLanes)&t)===0}function qs(l,t){(t&8)!==0&&(t|=t&32);var e=l.entangledLanes;if(e!==0)for(l=l.entanglements,e&=t;0<e;){var u=31-_t(e),a=1<<u;t|=l[u],e&=~a}return t}function zv(l,t){switch(l){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Ys(){var l=gn;return gn<<=1,(gn&62914560)===0&&(gn=4194304),l}function Fi(l){for(var t=[],e=0;31>e;e++)t.push(l);return t}function ya(l,t){l.pendingLanes|=t,t!==268435456&&(l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0)}function Nv(l,t,e,u,a,n){var i=l.pendingLanes;l.pendingLanes=e,l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0,l.expiredLanes&=e,l.entangledLanes&=e,l.errorRecoveryDisabledLanes&=e,l.shellSuspendCounter=0;var c=l.entanglements,f=l.expirationTimes,m=l.hiddenUpdates;for(e=i&~e;0<e;){var S=31-_t(e),z=1<<S;c[S]=0,f[S]=-1;var d=m[S];if(d!==null)for(m[S]=null,S=0;S<d.length;S++){var g=d[S];g!==null&&(g.lane&=-536870913)}e&=~z}u!==0&&Bs(l,u,0),n!==0&&a===0&&l.tag!==0&&(l.suspendedLanes|=n&~(i&~t))}function Bs(l,t,e){l.pendingLanes|=t,l.suspendedLanes&=~t;var u=31-_t(t);l.entangledLanes|=t,l.entanglements[u]=l.entanglements[u]|1073741824|e&261930}function Gs(l,t){var e=l.entangledLanes|=t;for(l=l.entanglements;e;){var u=31-_t(e),a=1<<u;a&t|l[u]&t&&(l[u]|=t),e&=~a}}function Xs(l,t){var e=t&-t;return e=(e&42)!==0?1:Wi(e),(e&(l.suspendedLanes|t))!==0?0:e}function Wi(l){switch(l){case 2:l=1;break;case 8:l=4;break;case 32:l=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:l=128;break;case 268435456:l=134217728;break;default:l=0}return l}function ki(l){return l&=-l,2<l?8<l?(l&134217727)!==0?32:268435456:8:2}function Qs(){var l=Z.p;return l!==0?l:(l=window.event,l===void 0?32:F0(l.type))}function Vs(l,t){var e=Z.p;try{return Z.p=l,t()}finally{Z.p=e}}var oe=Math.random().toString(36).slice(2),Il="__reactFiber$"+oe,mt="__reactProps$"+oe,Nu="__reactContainer$"+oe,Zs="__reactEvents$"+oe,_v="__reactListeners$"+oe,Ov="__reactHandles$"+oe,Ls="__reactResources$"+oe,ga="__reactMarker$"+oe,bn="__reactLoad$"+oe;function Tn(l){delete l[Il],delete l[mt],delete l[_v],delete l[Ov]}function lu(l){var t;if(t=l[Il])return t;for(var e=l.parentNode;e;){if(t=e[Nu]||e[Il]){if(e=t.alternate,t.child!==null||e!==null&&e.child!==null)for(l=D0(l);l!==null;){if(e=l[Il])return e;l=D0(l)}return t}l=e,e=l.parentNode}return null}function _u(l){if(l=l[Il]||l[Nu]){var t=l.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return l}return null}function Sa(l){var t=l.tag;if(t===5||t===26||t===27||t===6)return l.stateNode;throw Error(h(33))}function Ou(l){var t=l[Ls];return t||(t=l[Ls]={hoistableStyles:new Map,hoistableScripts:new Map}),t}function $l(l){l[ga]=!0}function Ks(l){l[bn]=void 0}var Js=new Set,ws={};function tu(l,t){Au(l,t),Au(l+"Capture",t)}function
Line truncated
|
||||||
|
`).replace(dh,"")}function s0(l,t){return t=f0(t),f0(l)===t}function bl(l,t,e,u,a,n){switch(e){case"children":if(typeof u=="string")t==="body"||t==="textarea"&&u===""||ju(l,u);else if(typeof u=="number"||typeof u=="bigint")t!=="body"&&ju(l,""+u);else return;break;case"className":zn(l,"class",u);break;case"tabIndex":zn(l,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":zn(l,e,u);break;case"style":uo(l,u,n);return;case"data":if(t!=="object"){zn(l,"data",u);break}case"src":case"href":if(u===""&&(t!=="a"||e!=="href")){l.removeAttribute(e);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){l.removeAttribute(e);break}u=Nn(u),l.setAttribute(e,u);break;case"action":case"formAction":if(typeof u=="function"){l.setAttribute(e,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof n=="function"&&(e==="formAction"?(t!=="input"&&bl(l,t,"name",a.name,a,null),bl(l,t,"formEncType",a.formEncType,a,null),bl(l,t,"formMethod",a.formMethod,a,null),bl(l,t,"formTarget",a.formTarget,a,null)):(bl(l,t,"encType",a.encType,a,null),bl(l,t,"method",a.method,a,null),bl(l,t,"target",a.target,a,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){l.removeAttribute(e);break}u=Nn(u),l.setAttribute(e,u);break;case"onClick":u!=null&&(l.onclick=It);return;case"onScroll":u!=null&&ul("scroll",l);return;case"onScrollEnd":u!=null&&ul("scrollend",l);return;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(h(61));if(e=u.__html,e!=null){if(a.children!=null)throw Error(h(60));n?.__html!==e&&(l.innerHTML=e)}}break;case"multiple":l.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":l.muted=u&&typeof u!="function"&&typeof u!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(u==null||typeof u=="function"||typeof u=="boolean"||typeof u=="symbol"){l.removeAttribute("xlink:href");break}e=Nn(u),l.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",e);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":u!=null&&typeof u!="function"&&typeof u!="symbol"?l.setAttribute(e,u):l.removeAttribute(e);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"credentialless":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":u&&typeof u!="function"&&typeof u!="symbol"?l.setAttribute(e,""):l.removeAttribute(e);break;case"capture":case"download":u===!0?l.setAttribute(e,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?l.setAttribute(e,u):l.removeAttribute(e);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?l.setAttribute(e,u):l.removeAttribute(e);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?l.removeAttribute(e):l.setAttribute(e,u);break;case"popover":ul("beforetoggle",l),ul("toggle",l),En(l,"popover",u);break;case"xlinkActuate":re(l,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":re(l,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":re(l,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":re(l,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":re(l,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":re(l,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":re(l,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":re(l,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":re(l,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":En(l,"is",u);break;case"innerText":case"textContent":return;default:if(!(2<e.length)||e[0]!=="o"&&e[0]!=="O"||e[1]!=="n"&&e[1]!=="N")e=Mv.get(e)||e,En(l,e,u);else return}dl=!0}function ls(l,t,e,u,a,n){switch(e){case"style":uo(l,u,n);return;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(h(61));if(e=u.__html,e!=null){if(a.children!=null)throw Error(h(60));n?.__html!==e&&(l.innerHTML=e)}}break;case"children":if(typeof u=="string")ju(l,u);else if(typeof u=="number"||typeof u=="bigint")ju(l,""+u);else return;break;case"onScroll":u!=null&&ul("scroll",l);return;case"onScrollEnd":u!=null&&ul("scrollend",l);return;case"onClick":u
Line truncated
|
||||||
|
`).map((N,O,J)=>N.trim()&&!N.trim().startsWith("###")?o.jsxs("div",{className:"draft-row",children:[o.jsx("code",{children:N}),o.jsx("button",{"aria-label":`Monter l’instruction ${O}`,disabled:O===0||J[O-1].startsWith("###"),onClick:()=>{const L=[...J];[L[O-1],L[O]]=[L[O],L[O-1]],A("draft","","",L.join(`
|
||||||
|
`))},children:"↑"}),o.jsx("button",{"aria-label":`Descendre l’instruction ${O}`,disabled:O===J.length-1||!J[O+1].trim(),onClick:()=>{const L=[...J];[L[O+1],L[O]]=[L[O],L[O+1]],A("draft","","",L.join(`
|
||||||
|
`))},children:"↓"}),o.jsx("button",{onClick:()=>A("draft","","",J.filter((L,G)=>G!==O).join(`
|
||||||
|
`)),children:"Retirer"})]},O):null)}),o.jsxs("details",{children:[o.jsx("summary",{children:"Éditer les instructions en texte"}),o.jsx("textarea",{className:"instructions","aria-label":"Brouillon des transferts",value:Et,onChange:N=>A("draft","","",N.target.value)})]}),o.jsx("button",{disabled:!j[Zt("draft")],onClick:()=>q([Zt("draft")]),children:"Enregistrer le brouillon"}),o.jsxs("details",{children:[o.jsx("summary",{children:"Instructions actuellement appliquées"}),o.jsx("pre",{children:T.applied})]})]}),o.jsxs("div",{className:"results",children:[o.jsxs("h2",{children:[I.length," réponse",I.length!==1?"s":""]}),o.jsx("span",{children:!ol&&!cl?"Vue d’ensemble · sélectionnez une copie ou un label pour cibler la vérification":"Images annotées après lecture manuscrite"})]}),o.jsx("div",{className:"answer-grid",children:I.slice(0,x).map(({c:N,a:O})=>o.jsxs("article",{className:"answer",children:[o.jsxs("div",{className:"card-heading",children:[o.jsxs("div",{children:[o.jsxs("span",{className:"eyebrow",children:["COPIE ",N.id," · ",N.name]}),o.jsx("h3",{children:O.label})]}),o.jsx("span",{className:"badge",children:O.info.present?O.info.not_empty?O.info.touched?"Dans le PDF":"Hors PDF filtré":"Réponse vide":"Absente"})]}),o.jsx("div",{className:"image-space",children:O.image?o.jsxs("button",{className:"image-button","aria-label":`Agrandir copie ${N.id} ${O.label}`,onClick:()=>it({copy:N.id,label:O.label}),children:[o.jsx("img",{loading:"lazy",src:Qi(N.id,O.label,!1,T.revision),alt:`Réponse annotée — copie ${N.id}, ${O.label}`}),o.jsx("span",{children:"Agrandir ↗"})]}):o.jsx("p",{className:"empty",children:"Aucune image pour ce label."})}),o.jsxs("div",{className:"answer-form",children:[o.jsxs("label",{children:["Score",o.jsx("input",{inputMode:"decimal",value:zl("score",N.id,O.label,O.score),onChange:J=>A("score",N.id,O.label,J.target.value)})]}),o.jsxs("label",{children:["Remarque personnelle",o.jsx("textarea",{placeholder:"Ajouter une remarque (LaTeX accepté)",value:zl("remark",N.id,O.label,O.remark),onChange:J=>A("remark",N.id,O.label,J.target.value)})]})]}),o.jsxs("div",{className:"card-actions",children:[o.jsx("button",{disabled:!j[Zt("score",N.id,O.label)]&&!j[Zt("remark",N.id,O.label)],onClick:()=>q([Zt("score",N.id,O.label),Zt("remark",N.id,O.label)]),children:"Enregistrer"}),o.jsx("button",{disabled:!O.editableBody,onClick:()=>Tt({copy:N.id,label:O.label}),children:"Couper / transférer"}),o.jsx("a",{href:on(N.id,"context",O.label),target:"_blank",rel:"noreferrer",children:"Contexte ↗"}),o.jsx("a",{href:on(N.id,"question",O.label),target:"_blank",rel:"noreferrer",children:"Énoncé ↗"}),o.jsx("a",{href:on(N.id,"solution",O.label),target:"_blank",rel:"noreferrer",children:"Corrigé ↗"}),o.jsx("a",{href:on(N.id,"output"),target:"_blank",rel:"noreferrer",children:"PDF final ↗"})]})]},N.id+"|"+O.label))}),I.length===0&&o.jsx("p",{className:"empty",children:"Aucune réponse ne correspond à ces filtres."}),I.length>x&&o.jsx("button",{className:"load-more",onClick:()=>b(x+20),children:"Afficher les 20 suivantes"})]}),(T.job.log||T.job.running)&&o.jsxs("details",{className:"panel",open:T.job.status==="failed",children:[o.jsxs("summary",{children:["Journal de régénération · ",T.job.status==="failed"?"Échec / résultat incomplet":T.job.running?"En cours":"Terminée"]}),o.jsx("pre",{className:"log",children:T.job.log})]})]}),Cl&&o.jsx(Ty,{copy:Cl.copy,source:Cl.label,labels:T.labels,mode:T.mode,revision:T.revision,draft:Et,close:()=>Tt(null),accept:N=>{A("draft","","",N),Tt(null),k("Instruction ajoutée au brouillon. Enregistrez-le avant de l’appliquer.")}}),Zl&&o.jsx(by,{copy:Zl.copy,label:Zl.label,revision:T.revision,close:()=>it(null)})]})}function by({copy:T,label:X,revision:B,close:h}){const[Y,k]=ml.useState(100);return ml.useEffect(()=>{const ol=ql=>{ql.key==="Escape"&&h()};return window.addEventListener("keydown",ol),()=>window.removeEventListener("keydown",ol)},[]),o.jsx("div",{className:"overlay",children:o.jsxs("section",{className:"modal viewer",role:"dialog","aria-modal":"true","aria-label":`Copie ${T} ${X}`,children:[o.jsxs("div",{className:"modal-head",children:[o.jsxs("h2",{children:["Copie ",T," · ",X]}),o.jsxs("label",{children:["Zoom ",o.jsx("input",{type:"range",min:"40",max:"200",value:Y,onChange:ol=>k(Number(ol.target.value))}),Y,"%"]}),o.jsx("button",{autoFocus:!0,onClick:h,children:"Fermer"})]}),o.jsx("div",{className:"zoom-scroll",children:o.jsx("img",{src:Qi(T,X,!1,B),alt:`Copie ${T}, ${X}`,style:{width:Y+"%",maxWidth:"none"}})})]})})}function Ty({copy:T,source:X,labels:B,mode:h,revision:Y,draft:k,close:ol,accept:ql}){const[cl,pl]=ml.useState(B.find($=>$!==X)||""),[x,b]=ml.useState("cut"),[j,Al]=ml.useState(50),[Vl,xl]=ml.useState("1"),[rl,nt]=ml.useState(">"),[Cl,Tt]=ml.useState(!1),[Zl,it]=ml.useState(""),[Yl,w]=ml.useState(null),[ll,ft]=ml.useState(!1),Rl=x==="cut"?`c{${j}}${Vl}${rl}`:x,Ll=`Copie${T} ${X} ${Rl} ${Cl?"|":""}${cl}${Cl?"":"|"}`,Kl=k
Line truncated
|
||||||
|
`+Ll+`
|
||||||
|
`;ml.useEffect(()=>w(null),[cl,x,j,Vl,rl,Cl,k]);const xt=async()=>{ft(!0),it("");try{w(await va("preview",{copy:T,source:X,target:cl,text:Kl}))}catch($){it(String($))}finally{ft(!1)}};return o.jsx("div",{className:"overlay",children:o.jsxs("section",{className:"modal",role:"dialog","aria-modal":"true","aria-label":"Couper ou transférer une réponse",children:[o.jsxs("div",{className:"modal-head",children:[o.jsxs("div",{children:[o.jsxs("span",{className:"eyebrow",children:["COPIE ",T," · ",h]}),o.jsx("h2",{children:"Couper ou transférer"})]}),o.jsx("button",{autoFocus:!0,onClick:ol,children:"Fermer"})]}),o.jsx("p",{children:"Seul le corps de la réponse est déplacé. Cliquez sur l’image source pour placer la coupe."}),Zl&&o.jsx("p",{role:"alert",className:"alert",children:Zl}),o.jsxs("div",{className:"cut-controls",children:[o.jsxs("label",{children:["Destination",o.jsx("select",{value:cl,onChange:$=>pl($.target.value),children:B.filter($=>$!==X).map($=>o.jsx("option",{children:$},$))})]}),o.jsxs("label",{children:["Opération",o.jsxs("select",{value:x,onChange:$=>b($.target.value),children:[o.jsx("option",{value:"cut",children:"Couper la source"}),o.jsx("option",{value:"x>",children:"Déplacer et ajouter"}),o.jsx("option",{value:"->",children:"Copier et ajouter"}),o.jsx("option",{value:"xx",children:"Déplacer et remplacer"}),o.jsx("option",{value:"-x",children:"Copier et remplacer"}),o.jsx("option",{value:"xs",children:"Vider la source"}),o.jsx("option",{value:"sx",children:"Vider la cible"}),o.jsx("option",{value:"ss",children:"Conserver les deux"})]})]}),x==="cut"&&o.jsxs(o.Fragment,{children:[o.jsxs("label",{children:["Coupe : ",j,"%",o.jsx("input",{type:"range",min:"1",max:"99",step:"0.1",value:j,onChange:$=>Al(Number($.target.value))})]}),o.jsxs("label",{children:["Conserver à la source",o.jsxs("select",{value:Vl,onChange:$=>xl($.target.value),children:[o.jsx("option",{value:"1",children:"La partie haute"}),o.jsx("option",{value:"2",children:"La partie basse"})]})]}),o.jsxs("label",{children:["À la destination",o.jsxs("select",{value:rl,onChange:$=>nt($.target.value),children:[o.jsx("option",{value:">",children:"Ajouter le fragment"}),o.jsx("option",{value:"x",children:"Remplacer le corps"})]})]})]}),o.jsxs("label",{className:"check",children:[o.jsx("input",{type:"checkbox",checked:Cl,onChange:$=>Tt($.target.checked)}),"Placer avant la cible"]})]}),o.jsxs("div",{className:"compare",children:[o.jsxs("div",{children:[o.jsxs("h3",{children:["Source · ",X]}),o.jsxs("div",{className:"cut-image",onClick:$=>{if(x==="cut"){const zl=$.currentTarget.getBoundingClientRect();Al(Math.min(99,Math.max(1,Math.round(($.clientY-zl.top)/zl.height*1e3)/10)))}},children:[o.jsx("img",{src:Qi(T,X,!0,Y),alt:"Corps source sans en-tête"}),x==="cut"&&o.jsx("div",{className:"cut-line",style:{top:j+"%"},children:o.jsxs("span",{children:[j,"%"]})})]})]}),o.jsxs("div",{children:[o.jsxs("h3",{children:["Cible · ",cl]}),o.jsx(Ey,{copy:T,label:cl,revision:Y},cl)]})]}),o.jsx("code",{className:"instruction-line",children:Ll}),o.jsxs("div",{className:"modal-actions",children:[o.jsx("button",{disabled:ll||!cl,onClick:xt,children:ll?"Calcul de l’aperçu…":"Aperçu du résultat"}),o.jsx("button",{className:"primary",disabled:!Yl||ll,onClick:()=>ql(Kl),children:"Ajouter au brouillon"})]}),Yl&&o.jsxs(o.Fragment,{children:[o.jsx("h3",{children:"Résultat proposé après toutes les instructions du brouillon"}),o.jsx("div",{className:"compare",children:[X,cl].map($=>o.jsxs("div",{children:[o.jsx("h4",{children:$}),Yl[$]?o.jsx("img",{src:Yl[$],alt:"Aperçu "+$}):o.jsx("p",{className:"empty",children:"Corps vide"})]},$))})]}),o.jsx("p",{className:"muted",children:"Une instruction par source ; une cible ne peut pas être source. Modifiez ou retirez une instruction existante dans le panneau « Transferts et coupes »."})]})})}function Ey({copy:T,label:X,revision:B}){const[h,Y]=ml.useState(!1);return h?o.jsx("p",{className:"empty",children:"Pas de corps existant. Le transfert peut remplir cette cible."}):o.jsx("img",{src:Qi(T,X,!0,B),alt:"Corps cible sans en-tête",onError:()=>Y(!0)})}gy.createRoot(document.getElementById("root")).render(o.jsx(Sy,{}));
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<!doctype html><html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Copienator · Finalisation</title> <script type="module" crossorigin src="./assets/index-D6hPIrMO.js"></script>
|
<!doctype html><html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Copienator · Finalisation</title> <script type="module" crossorigin src="./assets/index-QxonPU25.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-NHKKhh88.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-CZ3oiwq9.css">
|
||||||
</head><body><div id="root"></div></body></html>
|
</head><body><div id="root"></div></body></html>
|
||||||
+19
-6
@@ -365,7 +365,7 @@ class CopienatorApp(tk.Tk):
|
|||||||
)
|
)
|
||||||
self.finalisation_web_button.grid(row=0, column=6, padx=(6, 0))
|
self.finalisation_web_button.grid(row=0, column=6, padx=(6, 0))
|
||||||
attach_tooltip(self.finalisation_web_button,
|
attach_tooltip(self.finalisation_web_button,
|
||||||
"Disponible après une première lecture des annotations manuscrites (Bnot ou BGnot).")
|
"Disponible après génération Anot ou lecture des annotations manuscrites Bnot/BGnot.")
|
||||||
ttk.Button(top, text="Diagnostic…", command=self._show_diagnostics).grid(row=0, column=7, padx=(6, 0))
|
ttk.Button(top, text="Diagnostic…", command=self._show_diagnostics).grid(row=0, column=7, padx=(6, 0))
|
||||||
|
|
||||||
ttk.Label(top, text="Clé Gemini").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=(7, 0))
|
ttk.Label(top, text="Clé Gemini").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=(7, 0))
|
||||||
@@ -552,7 +552,7 @@ class CopienatorApp(tk.Tk):
|
|||||||
return any(
|
return any(
|
||||||
directory.is_dir() and directory.name[5:].isdigit()
|
directory.is_dir() and directory.name[5:].isdigit()
|
||||||
and (directory / "score.json").is_file() and (directory / "info.json").is_file()
|
and (directory / "score.json").is_file() and (directory / "info.json").is_file()
|
||||||
for mode in ("Bnot", "BGnot")
|
for mode in ("Bnot", "BGnot", "Anot")
|
||||||
for directory in (evaluation / mode).glob("Copie*")
|
for directory in (evaluation / mode).glob("Copie*")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -564,7 +564,7 @@ class CopienatorApp(tk.Tk):
|
|||||||
messagebox.showerror("Évaluation absente", "Chargez d’abord une évaluation.")
|
messagebox.showerror("Évaluation absente", "Chargez d’abord une évaluation.")
|
||||||
return
|
return
|
||||||
if not self._has_finalisation_outputs():
|
if not self._has_finalisation_outputs():
|
||||||
messagebox.showinfo("Finalisation web", "Exécutez d’abord « Lire les annotations manuscrites ».")
|
messagebox.showinfo("Finalisation web", "Générez d’abord les annotations Anot, ou lisez les annotations manuscrites Bnot/BGnot.")
|
||||||
return
|
return
|
||||||
self._save_current_form()
|
self._save_current_form()
|
||||||
dialog = tk.Toplevel(self)
|
dialog = tk.Toplevel(self)
|
||||||
@@ -573,10 +573,17 @@ class CopienatorApp(tk.Tk):
|
|||||||
dialog.resizable(False, False)
|
dialog.resizable(False, False)
|
||||||
frame = ttk.Frame(dialog, padding=24)
|
frame = ttk.Frame(dialog, padding=24)
|
||||||
frame.pack(fill="both", expand=True)
|
frame.pack(fill="both", expand=True)
|
||||||
text = tk.StringVar(value="Choisissez le mode utilisé pour lire les annotations manuscrites.")
|
text = tk.StringVar(value="Choisissez le dossier des copies générées ou relues.")
|
||||||
ttk.Label(frame, textvariable=text, wraplength=500).pack(pady=(0, 12))
|
ttk.Label(frame, textvariable=text, wraplength=500).pack(pady=(0, 12))
|
||||||
mode = tk.StringVar(value=annotation_mode_for_reader(self.state_store.step("read_annotations")))
|
preferred_mode = annotation_mode_for_reader(self.state_store.step("read_annotations"))
|
||||||
choice = ttk.Combobox(frame, textvariable=mode, values=("BGnot", "Bnot"), state="readonly")
|
ready_modes = [mode for mode in ANNOTATION_DIRECTORIES
|
||||||
|
if any((d / "score.json").is_file() and (d / "info.json").is_file()
|
||||||
|
for d in (self.evaluation / mode).glob("Copie*"))]
|
||||||
|
annotation = self.state_store.step("annotation")
|
||||||
|
if annotation.get("last_run_variant", annotation.get("variant")) == "simple" and "Anot" in ready_modes:
|
||||||
|
preferred_mode = "Anot"
|
||||||
|
mode = tk.StringVar(value=preferred_mode if preferred_mode in ready_modes else ready_modes[0])
|
||||||
|
choice = ttk.Combobox(frame, textvariable=mode, values=ready_modes, state="readonly")
|
||||||
choice.pack(fill="x", pady=8)
|
choice.pack(fill="x", pady=8)
|
||||||
url = tk.StringVar()
|
url = tk.StringVar()
|
||||||
link = ttk.Entry(frame, textvariable=url, state="readonly", width=65)
|
link = ttk.Entry(frame, textvariable=url, state="readonly", width=65)
|
||||||
@@ -597,6 +604,12 @@ class CopienatorApp(tk.Tk):
|
|||||||
reader = self.state_store.step("read_annotations")
|
reader = self.state_store.step("read_annotations")
|
||||||
values = dict(reader.get("values", {}))
|
values = dict(reader.get("values", {}))
|
||||||
values["update_score"] = True
|
values["update_score"] = True
|
||||||
|
if session.service.mode == "Anot":
|
||||||
|
if data["dirty"] or session.service.job["status"] != "idle":
|
||||||
|
self.state_store.invalidate_after(self._progression_ids("annotation"), "annotation")
|
||||||
|
self.state_store.update_step("annotation", variant="simple", status="stale" if data["dirty"] else "success")
|
||||||
|
self.state_store.update_step("giving_names", status="stale" if data["dirty"] else "detected")
|
||||||
|
else:
|
||||||
if data["dirty"] or session.service.job["status"] != "idle":
|
if data["dirty"] or session.service.job["status"] != "idle":
|
||||||
self.state_store.invalidate_after(self._progression_ids("read_annotations"), "read_annotations")
|
self.state_store.invalidate_after(self._progression_ids("read_annotations"), "read_annotations")
|
||||||
self.state_store.update_step("read_annotations", variant="standard" if session.service.mode == "Bnot" else "grouped")
|
self.state_store.update_step("read_annotations", variant="standard" if session.service.mode == "Bnot" else "grouped")
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ Elle fonctionne sur cet ordinateur, sans hébergement ni nouvel appel à Gemini.
|
|||||||
|
|
||||||
## Ouvrir
|
## Ouvrir
|
||||||
|
|
||||||
Après **Lire les annotations manuscrites**, cliquez sur **Finalisation web…**
|
Après génération des annotations **Anot**, ou après **Lire les annotations
|
||||||
en haut du GUI, choisissez **Bnot** ou **BGnot**, puis **Ouvrir**. Le GUI reste
|
manuscrites** pour **Bnot/BGnot**, cliquez sur **Finalisation web…** en haut du GUI,
|
||||||
|
choisissez le dossier disponible, puis **Ouvrir**. Le GUI reste
|
||||||
verrouillé par une fenêtre modale tant que le serveur fonctionne. Le bouton
|
verrouillé par une fenêtre modale tant que le serveur fonctionne. Le bouton
|
||||||
**Arrêter le serveur et reprendre** ferme la session et recharge les éditeurs.
|
**Arrêter le serveur et reprendre** ferme la session et recharge les éditeurs.
|
||||||
Fermer seulement l’onglet du navigateur ne ferme pas le serveur.
|
Fermer seulement l’onglet du navigateur ne ferme pas le serveur.
|
||||||
@@ -26,7 +27,10 @@ restant sur disque n’empêche pas la prochaine ouverture.
|
|||||||
|
|
||||||
## Vérifier et modifier
|
## Vérifier et modifier
|
||||||
|
|
||||||
- **Copie** seule : toutes les réponses de cette copie.
|
- **Copie** seule : toutes les réponses de cette copie. Le champ **Nom** propose
|
||||||
|
une complétion ; vous pouvez chercher une partie du nom, sans tenir compte des
|
||||||
|
accents ou des majuscules. Choisir un nom renseigne l’identifiant de copie, et
|
||||||
|
inversement. Les suggestions indiquent l’identifiant pour distinguer les doublons.
|
||||||
- **Label** seul : toutes les copies pour ce label.
|
- **Label** seul : toutes les copies pour ce label.
|
||||||
- Les deux : leur intersection. Sans filtre, la page présente toutes les réponses
|
- Les deux : leur intersection. Sans filtre, la page présente toutes les réponses
|
||||||
par lots de vingt, avec chargement différé des images.
|
par lots de vingt, avec chargement différé des images.
|
||||||
@@ -45,6 +49,19 @@ les modifications avant de régénérer. Si une autre page a changé les fichier
|
|||||||
une sauvegarde périmée est refusée ; **Recharger les données** actualise les
|
une sauvegarde périmée est refusée ; **Recharger les données** actualise les
|
||||||
valeurs enregistrées en conservant les saisies locales à réexaminer.
|
valeurs enregistrées en conservant les saisies locales à réexaminer.
|
||||||
|
|
||||||
|
## Score final exporté
|
||||||
|
|
||||||
|
Après **Ajouter le score final**, sélectionner une copie par identifiant ou nom
|
||||||
|
fait apparaître son **Score final exporté** au-dessus des réponses. Cette note
|
||||||
|
est celle du dernier export, arrondie comme sur le JPEG ; modifier les scores par
|
||||||
|
question ne la recalcule pas. Le passage enregistre désormais les notes dans
|
||||||
|
`.copienator/final_scores.json`, par identifiant, afin de conserver leur association
|
||||||
|
après un changement de nom.
|
||||||
|
|
||||||
|
Pour les anciens exports, la note peut être récupérée du fichier ODS uniquement
|
||||||
|
si ce fichier n’est pas plus récent que les documents exportés. Sinon, relancez
|
||||||
|
**Ajouter le score final** pour enregistrer la note de façon explicite.
|
||||||
|
|
||||||
## Brouillon, application et régénération
|
## Brouillon, application et régénération
|
||||||
|
|
||||||
**Ajouter au brouillon** ne modifie pas les images. **Enregistrer le brouillon**
|
**Ajouter au brouillon** ne modifie pas les images. **Enregistrer le brouillon**
|
||||||
@@ -106,8 +123,11 @@ repassez par **Lire les annotations manuscrites** dans le workflow existant avan
|
|||||||
de revenir au navigateur. `regenerate-reviewed` réutilise l’interprétation
|
de revenir au navigateur. `regenerate-reviewed` réutilise l’interprétation
|
||||||
sauvegardée ; il ne recherche pas de nouvelles annotations.
|
sauvegardée ; il ne recherche pas de nouvelles annotations.
|
||||||
|
|
||||||
Anot reste géré par le workflow existant. La finalisation web concerne Bnot et
|
Le mode **Anot** donne accès aux noms, scores, remarques et à leur régénération
|
||||||
BGnot, les deux modes qui disposent de corps de réponses éditables.
|
dès la génération initiale, sans lecture manuscrite intermédiaire. Les coupes et
|
||||||
|
transferts restent réservés à **Bnot/BGnot**, qui disposent des limites exactes
|
||||||
|
des corps de réponses relus. Les anciens résultats Anot peuvent créer leur état
|
||||||
|
de rendu lors de la première régénération, sans remplacer leurs scores enregistrés.
|
||||||
|
|
||||||
## Développement
|
## Développement
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ class AddFinalScoreTests(unittest.TestCase):
|
|||||||
self.assertFalse((student / "answers" / "001 - Ex 1.jpg").exists())
|
self.assertFalse((student / "answers" / "001 - Ex 1.jpg").exists())
|
||||||
self.assertEqual((output / "histogramme.pdf").read_bytes(), b"histogram")
|
self.assertEqual((output / "histogramme.pdf").read_bytes(), b"histogram")
|
||||||
self.assertEqual(read_json(output / "stat.json"), {"Ex 1": "4.0"})
|
self.assertEqual(read_json(output / "stat.json"), {"Ex 1": "4.0"})
|
||||||
|
self.assertEqual(read_json(root / '.copienator/final_scores.json'),
|
||||||
|
{'version': 1, 'copies': {'1': {'name': 'Student', 'score': '12.3'}}})
|
||||||
|
|
||||||
def test_omits_answers_directory_when_it_was_not_generated(self):
|
def test_omits_answers_directory_when_it_was_not_generated(self):
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
import pandas as pd
|
||||||
|
from copienator import atomic_write_json
|
||||||
|
from copienator.final_scores import exported_scores
|
||||||
|
|
||||||
|
|
||||||
|
class ExportedFinalScoresTests(unittest.TestCase):
|
||||||
|
def test_legacy_export_uses_only_sheet_predating_export(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)/'Interro'
|
||||||
|
(root/'A Rendre/Alice (01)').mkdir(parents=True)
|
||||||
|
ods = root/'marks.ods'
|
||||||
|
ods.write_bytes(b'sheet')
|
||||||
|
image = root/'output/Interro/Alice/Alice.jpg'
|
||||||
|
image.parent.mkdir(parents=True)
|
||||||
|
image.write_bytes(b'export')
|
||||||
|
os.utime(ods, ns=(1000000,1000000))
|
||||||
|
os.utime(image, ns=(2000000,2000000))
|
||||||
|
with patch('copienator.final_scores.configuration.FINAL_SCORE_OUTPUT_DIR',root/'output'), \
|
||||||
|
patch('copienator.final_scores.configuration.FINAL_SCORE_ODS_PATH',ods), \
|
||||||
|
patch('pandas.read_excel',return_value=pd.DataFrame({0:['Alice'],1:[12.39]})) as read:
|
||||||
|
self.assertEqual(exported_scores(root,root), {'1':'12.3'})
|
||||||
|
os.utime(ods, ns=(3000000,3000000))
|
||||||
|
read.reset_mock()
|
||||||
|
self.assertEqual(exported_scores(root,root), {})
|
||||||
|
read.assert_not_called()
|
||||||
|
|
||||||
|
def test_no_final_mark_before_export_and_saved_marks_do_not_read_live_sheet(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root=Path(temporary)
|
||||||
|
with patch('copienator.final_scores.configuration.FINAL_SCORE_OUTPUT_DIR',root/'output'), \
|
||||||
|
patch('copienator.final_scores.configuration.FINAL_SCORE_ODS_PATH',root/'missing.ods'), \
|
||||||
|
patch('pandas.read_excel') as read:
|
||||||
|
self.assertEqual(exported_scores(root,root), {})
|
||||||
|
atomic_write_json(root/'.copienator/final_scores.json',
|
||||||
|
{'version':1,'copies':{'01':{'score':'0.0','name':'Alice'}}})
|
||||||
|
self.assertEqual(exported_scores(root,root), {'1':'0.0'})
|
||||||
|
read.assert_not_called()
|
||||||
@@ -212,6 +212,14 @@ class WebTests(unittest.TestCase):
|
|||||||
self.service._regenerate(False)
|
self.service._regenerate(False)
|
||||||
self.assertEqual(calls, ['regenerate-reviewed', 'giving-names'])
|
self.assertEqual(calls, ['regenerate-reviewed', 'giving-names'])
|
||||||
|
|
||||||
|
def test_exported_final_score_is_bound_to_copy_id_after_rename(self):
|
||||||
|
self.assertIsNone(self.client.get('/api/state').json()['copies'][0]['finalScore'])
|
||||||
|
atomic_write_json(self.root/'.copienator/final_scores.json',
|
||||||
|
{'version':1,'copies':{'1':{'name':'Old name','score':'12.3'}}})
|
||||||
|
self.assertEqual(self.client.get('/api/state').json()['copies'][0]['finalScore'], '12.3')
|
||||||
|
self.save('name', 'New name')
|
||||||
|
self.assertEqual(self.client.get('/api/state').json()['copies'][0]['finalScore'], '12.3')
|
||||||
|
|
||||||
def test_os_lock_blocks_cli_and_releases(self):
|
def test_os_lock_blocks_cli_and_releases(self):
|
||||||
with EvaluationLock(self.root):
|
with EvaluationLock(self.root):
|
||||||
with self.assertRaises(CliError):
|
with self.assertRaises(CliError):
|
||||||
@@ -277,7 +285,7 @@ class SnapshotTests(unittest.TestCase):
|
|||||||
from copienator.commands import regenerate_reviewed
|
from copienator.commands import regenerate_reviewed
|
||||||
from copienator.commands import annotating
|
from copienator.commands import annotating
|
||||||
from copienator.custom_remarks import set_remark
|
from copienator.custom_remarks import set_remark
|
||||||
for mode in ('Bnot', 'BGnot'):
|
for mode in ('Bnot', 'BGnot', 'Anot'):
|
||||||
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as temporary:
|
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as temporary:
|
||||||
root = Path(temporary)
|
root = Path(temporary)
|
||||||
seed(root, mode)
|
seed(root, mode)
|
||||||
@@ -331,6 +339,27 @@ class WebGuiTests(unittest.TestCase):
|
|||||||
app.state_store.evaluation = None
|
app.state_store.evaluation = None
|
||||||
self.assertEqual(state(), 'disabled')
|
self.assertEqual(state(), 'disabled')
|
||||||
|
|
||||||
|
def test_anot_generation_enables_button_and_selects_anot(self):
|
||||||
|
from copienator_gui.app import CopienatorApp
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
seed(root, 'Anot')
|
||||||
|
app = CopienatorApp(Path.cwd(), False, root)
|
||||||
|
self.addCleanup(app.destroy)
|
||||||
|
app._update_controls()
|
||||||
|
self.assertEqual(str(app.finalisation_web_button.cget('state')), 'normal')
|
||||||
|
app._open_finalisation_web()
|
||||||
|
dialog = app.grab_current()
|
||||||
|
def descendants(widget):
|
||||||
|
for child in widget.winfo_children():
|
||||||
|
yield child
|
||||||
|
yield from descendants(child)
|
||||||
|
combo = next(w for w in descendants(dialog) if w.winfo_class() == 'TCombobox')
|
||||||
|
self.assertEqual(combo.get(), 'Anot')
|
||||||
|
self.assertEqual(tuple(combo.cget('values')), ('Anot',))
|
||||||
|
dialog.grab_release()
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
def test_modal_lock_and_return_to_existing_workflow(self):
|
def test_modal_lock_and_return_to_existing_workflow(self):
|
||||||
from copienator_gui.app import CopienatorApp
|
from copienator_gui.app import CopienatorApp
|
||||||
from copienator.finalisation_server import WebSession
|
from copienator.finalisation_server import WebSession
|
||||||
@@ -457,3 +486,66 @@ class IncrementalReviewedTests(unittest.TestCase):
|
|||||||
(root/'A Rendre/Bob (02)/info.json').unlink()
|
(root/'A Rendre/Bob (02)/info.json').unlink()
|
||||||
run(1)
|
run(1)
|
||||||
self.assertTrue((root/'A Rendre/Bob (02)/info.json').is_file())
|
self.assertTrue((root/'A Rendre/Bob (02)/info.json').is_file())
|
||||||
|
|
||||||
|
|
||||||
|
class CrossProcessCacheTests(unittest.TestCase):
|
||||||
|
def test_reviewed_cache_survives_worker_restart_and_selects_only_two_score_edits(self):
|
||||||
|
from copienator.review_snapshot import stage
|
||||||
|
import shutil
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
seed(root, 'BGnot')
|
||||||
|
output = root/'BGnot/Copie01'
|
||||||
|
data = {'Ex 1': {'pdf_path':root/'Copies/Copie01/Ex 1.pdf',
|
||||||
|
'result':{'score':3,'feedback':[]},'coordinates':[0,0]}}
|
||||||
|
with staged_files(output) as staging:
|
||||||
|
stage(output, staging, data, {}, engine='grouped')
|
||||||
|
for cid in ('02','03','04'):
|
||||||
|
shutil.copytree(output,root/'BGnot'/f'Copie{cid}')
|
||||||
|
script = '''
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
from copienator import EvaluationWorkspace, configuration
|
||||||
|
from copienator.reviewed_cache import ReviewedReadCache
|
||||||
|
from string import Template
|
||||||
|
configuration.LATEX_ANOT_BEFORE = Template(sys.argv[3])
|
||||||
|
root=Path(sys.argv[1])
|
||||||
|
directories=sorted((root/'BGnot').glob('Copie*'))
|
||||||
|
cache=ReviewedReadCache(EvaluationWorkspace(root),'BGnot',directories)
|
||||||
|
if sys.argv[2]=='remember':
|
||||||
|
cache.remember([d.name[5:] for d in directories])
|
||||||
|
print(json.dumps([d.name for d in directories if not cache.unchanged(d.name[5:])]))
|
||||||
|
'''
|
||||||
|
def run(action='check', template='Stable ${width_in} template'):
|
||||||
|
return __import__('json').loads(subprocess.check_output(
|
||||||
|
[sys.executable,'-c',script,str(root),action,template],text=True))
|
||||||
|
self.assertEqual(run('remember'), [])
|
||||||
|
self.assertEqual(run(), [])
|
||||||
|
set_score(root/'BGnot/Copie01/score.json','Ex 1','1')
|
||||||
|
set_score(root/'BGnot/Copie03/score.json','Ex 1','2')
|
||||||
|
self.assertEqual(run(), ['Copie01','Copie03'])
|
||||||
|
self.assertEqual(run('remember'), [])
|
||||||
|
self.assertEqual(run(), [])
|
||||||
|
self.assertEqual(run(template='Changed template'), ['Copie01','Copie02','Copie03','Copie04'])
|
||||||
|
|
||||||
|
def test_legacy_reader_cache_template_is_stable_across_workers(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
seed(root)
|
||||||
|
atomic_write_json(root/'Bnot/Copie01/review_snapshot.json', {})
|
||||||
|
script = '''
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
from string import Template
|
||||||
|
from copienator import EvaluationWorkspace, configuration
|
||||||
|
from copienator.annotation_read_cache import AnnotationReadCache
|
||||||
|
configuration.LATEX_ANOT_BEFORE=Template('Stable ${width_in} template')
|
||||||
|
root=Path(sys.argv[1])
|
||||||
|
data={'01':{'Ex 1':{'pdf_path':root/'Copies/Copie01/Ex 1.pdf'}}}
|
||||||
|
cache=AnnotationReadCache(EvaluationWorkspace(root),'Bnot',data)
|
||||||
|
if sys.argv[2]=='remember': cache.remember(['01'])
|
||||||
|
print(json.dumps(cache.unchanged('01')))
|
||||||
|
'''
|
||||||
|
for action in ('remember','check'):
|
||||||
|
result=subprocess.check_output([sys.executable,'-c',script,str(root),action],text=True)
|
||||||
|
self.assertEqual(result.strip(),'true')
|
||||||
+20
-5
@@ -3,7 +3,7 @@ import {createRoot} from 'react-dom/client';
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
type Answer = {label:string;score:string;remark:string;image:boolean;editableBody:boolean;info:{present?:boolean;not_empty?:boolean;touched?:boolean}};
|
type Answer = {label:string;score:string;remark:string;image:boolean;editableBody:boolean;info:{present?:boolean;not_empty?:boolean;touched?:boolean}};
|
||||||
type Copy = {id:string;name:string;general:string[];answers:Answer[]};
|
type Copy = {id:string;name:string;finalScore?:string|null;general:string[];answers:Answer[]};
|
||||||
type Job = {running:boolean;status:string;log:string};
|
type Job = {running:boolean;status:string;log:string};
|
||||||
type State = {evaluation:string;mode:string;labels:string[];copies:Copy[];expectedNames:string[]|null;draft:string;applied:string;revision:string;dirty:boolean;job:Job};
|
type State = {evaluation:string;mode:string;labels:string[];copies:Copy[];expectedNames:string[]|null;draft:string;applied:string;revision:string;dirty:boolean;job:Job};
|
||||||
type Edit = {kind:string;copy:string;label:string;value:string|string[]};
|
type Edit = {kind:string;copy:string;label:string;value:string|string[]};
|
||||||
@@ -15,6 +15,8 @@ async function api(path:string, data?:unknown) {
|
|||||||
const imageUrl=(copy:string,label:string,body=false,revision='')=>'/api/image?'+new URLSearchParams({copy,label,body:String(body),v:revision});
|
const imageUrl=(copy:string,label:string,body=false,revision='')=>'/api/image?'+new URLSearchParams({copy,label,body:String(body),v:revision});
|
||||||
const documentUrl=(copy:string,kind:string,label='')=>'/api/document?'+new URLSearchParams({copy,kind,label});
|
const documentUrl=(copy:string,kind:string,label='')=>'/api/document?'+new URLSearchParams({copy,kind,label});
|
||||||
const keyOf=(kind:string,copy='',label='')=>JSON.stringify([kind,copy,label]);
|
const keyOf=(kind:string,copy='',label='')=>JSON.stringify([kind,copy,label]);
|
||||||
|
const searchName=(text:string)=>text.normalize('NFD').replace(/\p{M}/gu,'').toLocaleLowerCase('fr').trim();
|
||||||
|
const copyName=(copy:Copy)=>`${copy.name} · copie ${copy.id}`;
|
||||||
|
|
||||||
function App(){
|
function App(){
|
||||||
const [state,setState]=useState<State|null>(null),[error,setError]=useState(''),[notice,setNotice]=useState('');
|
const [state,setState]=useState<State|null>(null),[error,setError]=useState(''),[notice,setNotice]=useState('');
|
||||||
@@ -23,6 +25,7 @@ function App(){
|
|||||||
const [cut,setCut]=useState<{copy:string;label:string}|null>(null),[zoom,setZoom]=useState<{copy:string;label:string}|null>(null);
|
const [cut,setCut]=useState<{copy:string;label:string}|null>(null),[zoom,setZoom]=useState<{copy:string;label:string}|null>(null);
|
||||||
const [stopped,setStopped]=useState(false);
|
const [stopped,setStopped]=useState(false);
|
||||||
const [onlyNameIssues,setOnlyNameIssues]=useState(false);
|
const [onlyNameIssues,setOnlyNameIssues]=useState(false);
|
||||||
|
const [nameQuery,setNameQuery]=useState('');
|
||||||
const [incremental,setIncremental]=useState(true);
|
const [incremental,setIncremental]=useState(true);
|
||||||
const refresh=async()=>{try{setState(await api('state'));setError('')}catch(e){setError(String(e))}};
|
const refresh=async()=>{try{setState(await api('state'));setError('')}catch(e){setError(String(e))}};
|
||||||
useEffect(()=>{void refresh()},[]);
|
useEffect(()=>{void refresh()},[]);
|
||||||
@@ -39,7 +42,18 @@ function App(){
|
|||||||
const pending=Object.keys(edits).length;
|
const pending=Object.keys(edits).length;
|
||||||
const draft=value('draft','','',state.draft) as string;
|
const draft=value('draft','','',state.draft) as string;
|
||||||
const draftDiff=state.draft.trim()!==state.applied.trim();
|
const draftDiff=state.draft.trim()!==state.applied.trim();
|
||||||
const matches=state.copies.filter(c=>!copyFilter||Number(c.id)===Number(copyFilter));
|
const selectedCopy=copyFilter?state.copies.find(c=>Number(c.id)===Number(copyFilter)):undefined;
|
||||||
|
const nameChoices=[...state.copies].sort((a,b)=>a.name.localeCompare(b.name,'fr')||Number(a.id)-Number(b.id));
|
||||||
|
const selectName=(text:string)=>{
|
||||||
|
setLimit(20);
|
||||||
|
const selection=state.copies.find(c=>copyName(c)===text);
|
||||||
|
const exact=state.copies.filter(c=>searchName(c.name)===searchName(text));
|
||||||
|
const copy=selection||(exact.length===1?exact[0]:undefined);
|
||||||
|
setCopyFilter(copy?.id||'');
|
||||||
|
setNameQuery(copy?'':text);
|
||||||
|
};
|
||||||
|
const matches=state.copies.filter(c=>(!copyFilter||Number(c.id)===Number(copyFilter))
|
||||||
|
&&(!nameQuery||searchName(c.name).includes(searchName(nameQuery))));
|
||||||
const cards=matches.flatMap(c=>c.answers.filter(a=>!labelFilter||a.label===labelFilter).map(a=>({c,a})));
|
const cards=matches.flatMap(c=>c.answers.filter(a=>!labelFilter||a.label===labelFilter).map(a=>({c,a})));
|
||||||
const names=new Map<string,number>();state.copies.forEach(c=>names.set(c.name.toLowerCase(),(names.get(c.name.toLowerCase())||0)+1));
|
const names=new Map<string,number>();state.copies.forEach(c=>names.set(c.name.toLowerCase(),(names.get(c.name.toLowerCase())||0)+1));
|
||||||
const issues=state.copies.filter(c=>c.name.toLowerCase()==='unknown'||(names.get(c.name.toLowerCase())||0)>1);
|
const issues=state.copies.filter(c=>c.name.toLowerCase()==='unknown'||(names.get(c.name.toLowerCase())||0)>1);
|
||||||
@@ -49,13 +63,14 @@ function App(){
|
|||||||
return <>
|
return <>
|
||||||
<header inert={!!cut||!!zoom}><div className="brand">C<span>Copienator</span></div><div className="heading"><div className="eyebrow">FINALISATION · {state.mode}</div><h1>{state.evaluation}</h1></div><button className="quiet" disabled={disabled||!!pending} onClick={stop}>Fermer et reprendre dans le GUI</button></header>
|
<header inert={!!cut||!!zoom}><div className="brand">C<span>Copienator</span></div><div className="heading"><div className="eyebrow">FINALISATION · {state.mode}</div><h1>{state.evaluation}</h1></div><button className="quiet" disabled={disabled||!!pending} onClick={stop}>Fermer et reprendre dans le GUI</button></header>
|
||||||
<main inert={!!cut||!!zoom}>
|
<main inert={!!cut||!!zoom}>
|
||||||
<section className="toolbar"><div><h2>Vérifier les copies</h2><p>Retrouvez une copie, une question, ou leur intersection.</p></div><div className="filters"><label>Copie<input type="text" inputMode="numeric" list="copies" placeholder="Toutes les copies" value={copyFilter} onChange={e=>{setCopyFilter(e.target.value.replace(/\D/g,''));setLimit(20)}}/></label><datalist id="copies">{state.copies.map(c=><option key={c.id} value={c.id}>{c.name}</option>)}</datalist><label>Label<select value={labelFilter} onChange={e=>{setLabelFilter(e.target.value);setLimit(20)}}><option value="">Tous les labels</option>{state.labels.map(l=><option key={l}>{l}</option>)}</select></label><button onClick={()=>{setCopyFilter('');setLabelFilter('');setLimit(20)}}>Réinitialiser</button></div></section>
|
<section className="toolbar"><div><h2>Vérifier les copies</h2><p>Retrouvez une copie, une question, ou leur intersection.</p></div><div className="filters"><label>Copie<input type="text" inputMode="numeric" list="copies" placeholder="Toutes les copies" value={copyFilter} onChange={e=>{setCopyFilter(e.target.value.replace(/\D/g,''));setNameQuery('');setLimit(20)}}/></label><datalist id="copies">{state.copies.map(c=><option key={c.id} value={c.id}>{c.name}</option>)}</datalist><label>Nom<input type="text" list="copy-names" placeholder="Rechercher un nom…" autoComplete="off" value={selectedCopy?copyName(selectedCopy):nameQuery} onChange={e=>selectName(e.target.value)}/></label><datalist id="copy-names">{nameChoices.filter(c=>!nameQuery||searchName(c.name).includes(searchName(nameQuery))).map(c=><option key={c.id} value={copyName(c)}/>)}</datalist><label>Label<select value={labelFilter} onChange={e=>{setLabelFilter(e.target.value);setLimit(20)}}><option value="">Tous les labels</option>{state.labels.map(l=><option key={l}>{l}</option>)}</select></label><button onClick={()=>{setCopyFilter('');setNameQuery('');setLabelFilter('');setLimit(20)}}>Réinitialiser</button></div></section>
|
||||||
|
{matches.length===1&&(copyFilter||nameQuery)&&matches[0].finalScore!=null&&<div className="final-score" role="status"><span>Copie {matches[0].id} · {matches[0].name}</span><strong>Score final exporté : {matches[0].finalScore.replace('.', ',')}</strong></div>}
|
||||||
{error&&<div role="alert" className="alert">{error}<button onClick={refresh}>Recharger les données</button><small>Les saisies non enregistrées restent dans la page.</small></div>}
|
{error&&<div role="alert" className="alert">{error}<button onClick={refresh}>Recharger les données</button><small>Les saisies non enregistrées restent dans la page.</small></div>}
|
||||||
{notice&&<p role="status" className="notice">{notice}</p>}
|
{notice&&<p role="status" className="notice">{notice}</p>}
|
||||||
<section className="publish"><div><strong>{state.job.running?'Régénération en cours…':pending?`${pending} modification(s) non enregistrée(s)`:state.dirty?'Enregistré · sorties à régénérer':'Modifications enregistrées'}</strong><small>Les sorties comprennent les images, PDF et le dossier A Rendre configurés.</small></div><button disabled={disabled||!pending} onClick={()=>save(Object.keys(edits))}>Tout enregistrer</button>{pending>0&&<button disabled={disabled} onClick={()=>setEdits({})}>Annuler les saisies</button>}<label className="check"><input type="checkbox" checked={applyDraft} disabled={disabled} onChange={e=>setApplyDraft(e.target.checked)}/>Appliquer aussi le brouillon de transferts{draftDiff?' (différent du résultat actuel)':''}</label><label className="check"><input type="checkbox" checked={incremental} disabled={disabled} onChange={e=>setIncremental(e.target.checked)}/>Ignorer les copies inchangées</label><button className="primary" disabled={disabled||!!pending} onClick={regenerate}>Régénérer les sorties</button></section>
|
<section className="publish"><div><strong>{state.job.running?'Régénération en cours…':pending?`${pending} modification(s) non enregistrée(s)`:state.dirty?'Enregistré · sorties à régénérer':'Modifications enregistrées'}</strong><small>Les sorties comprennent les images, PDF et le dossier A Rendre configurés.</small></div><button disabled={disabled||!pending} onClick={()=>save(Object.keys(edits))}>Tout enregistrer</button>{pending>0&&<button disabled={disabled} onClick={()=>setEdits({})}>Annuler les saisies</button>}<label className="check"><input type="checkbox" checked={applyDraft} disabled={disabled||state.mode==='Anot'} onChange={e=>setApplyDraft(e.target.checked)}/>Appliquer aussi le brouillon de transferts{draftDiff?' (différent du résultat actuel)':''}</label><label className="check"><input type="checkbox" checked={incremental} disabled={disabled} onChange={e=>setIncremental(e.target.checked)}/>Ignorer les copies inchangées</label><button className="primary" disabled={disabled||!!pending} onClick={regenerate}>Régénérer les sorties</button></section>
|
||||||
<fieldset disabled={disabled}>
|
<fieldset disabled={disabled}>
|
||||||
<details className="panel"><summary>Identités et remarques générales <span>{issues.length} nom(s) à vérifier</span></summary><div className="identity-overview"><strong>Noms manquants{missingNames?` (${missingNames.length})`:''}</strong>{missingNames===undefined?<p>Liste des noms attendus introuvable (fichier names).</p>:missingNames.length?<ul className="missing-names">{missingNames.map(name=><li key={name}>{name}</li>)}</ul>:<p>Tous les noms attendus sont attribués.</p>}<label className="check"><input type="checkbox" checked={onlyNameIssues} onChange={e=>setOnlyNameIssues(e.target.checked)}/>Afficher uniquement les copies « Nom à vérifier »</label></div><p>Les remarques sont composées en LaTeX. Leur ordre est celui de la liste.</p>{identityMatches.length===0&&<p>Aucune copie ne correspond aux filtres sélectionnés.</p>}{identityMatches.map(c=>{const general=value('general',c.id,'',c.general) as string[];const changeGeneral=(next:string[])=>edit('general',c.id,'',next);return <section className="identity" key={c.id}><div className="identity-name"><strong>Copie {c.id}</strong><label>Nom<input value={value('name',c.id,'',c.name) as string} onChange={e=>edit('name',c.id,'',e.target.value)}/></label>{issues.includes(c)&&<span className="badge warning">Nom à vérifier</span>}<a target="_blank" rel="noreferrer" href={documentUrl(c.id,'original')}>Voir l’originale ↗</a><button disabled={!edits[keyOf('name',c.id)]} onClick={()=>save([keyOf('name',c.id)])}>Enregistrer le nom</button></div><div className="general">{general.map((text,i)=><div className="remark-row" key={i}><textarea aria-label={`Remarque générale ${i+1}, copie ${c.id}`} value={text} onChange={e=>changeGeneral(general.map((v,j)=>i===j?e.target.value:v))}/><button disabled={i===0} aria-label="Monter" onClick={()=>{const a=[...general];[a[i-1],a[i]]=[a[i],a[i-1]];changeGeneral(a)}}>↑</button><button disabled={i===general.length-1} aria-label="Descendre" onClick={()=>{const a=[...general];[a[i+1],a[i]]=[a[i],a[i+1]];changeGeneral(a)}}>↓</button><button onClick={()=>changeGeneral(general.filter((_,j)=>j!==i))}>Supprimer</button></div>)}<button onClick={()=>changeGeneral([...general,''])}>+ Remarque générale</button><button disabled={!edits[keyOf('general',c.id)]} onClick={()=>save([keyOf('general',c.id)])}>Enregistrer les remarques</button></div></section>})}</details>
|
<details className="panel"><summary>Identités et remarques générales <span>{issues.length} nom(s) à vérifier</span></summary><div className="identity-overview"><strong>Noms manquants{missingNames?` (${missingNames.length})`:''}</strong>{missingNames===undefined?<p>Liste des noms attendus introuvable (fichier names).</p>:missingNames.length?<ul className="missing-names">{missingNames.map(name=><li key={name}>{name}</li>)}</ul>:<p>Tous les noms attendus sont attribués.</p>}<label className="check"><input type="checkbox" checked={onlyNameIssues} onChange={e=>setOnlyNameIssues(e.target.checked)}/>Afficher uniquement les copies « Nom à vérifier »</label></div><p>Les remarques sont composées en LaTeX. Leur ordre est celui de la liste.</p>{identityMatches.length===0&&<p>Aucune copie ne correspond aux filtres sélectionnés.</p>}{identityMatches.map(c=>{const general=value('general',c.id,'',c.general) as string[];const changeGeneral=(next:string[])=>edit('general',c.id,'',next);return <section className="identity" key={c.id}><div className="identity-name"><strong>Copie {c.id}</strong><label>Nom<input value={value('name',c.id,'',c.name) as string} onChange={e=>edit('name',c.id,'',e.target.value)}/></label>{issues.includes(c)&&<span className="badge warning">Nom à vérifier</span>}<a target="_blank" rel="noreferrer" href={documentUrl(c.id,'original')}>Voir l’originale ↗</a><button disabled={!edits[keyOf('name',c.id)]} onClick={()=>save([keyOf('name',c.id)])}>Enregistrer le nom</button></div><div className="general">{general.map((text,i)=><div className="remark-row" key={i}><textarea aria-label={`Remarque générale ${i+1}, copie ${c.id}`} value={text} onChange={e=>changeGeneral(general.map((v,j)=>i===j?e.target.value:v))}/><button disabled={i===0} aria-label="Monter" onClick={()=>{const a=[...general];[a[i-1],a[i]]=[a[i],a[i-1]];changeGeneral(a)}}>↑</button><button disabled={i===general.length-1} aria-label="Descendre" onClick={()=>{const a=[...general];[a[i+1],a[i]]=[a[i],a[i+1]];changeGeneral(a)}}>↓</button><button onClick={()=>changeGeneral(general.filter((_,j)=>j!==i))}>Supprimer</button></div>)}<button onClick={()=>changeGeneral([...general,''])}>+ Remarque générale</button><button disabled={!edits[keyOf('general',c.id)]} onClick={()=>save([keyOf('general',c.id)])}>Enregistrer les remarques</button></div></section>})}</details>
|
||||||
<details className="panel"><summary>Transferts et coupes <span>{draftDiff?'Brouillon non appliqué':'Brouillon identique aux modifications appliquées'}</span></summary><p>Les en-têtes et scores restent à leur label. Enregistrer un brouillon ne l’applique pas. Pour annuler un transfert, retirez sa ligne puis régénérez en cochant « Appliquer aussi le brouillon ».</p><div className="draft-list">{draft.split('\n').map((line,index,lines)=>line.trim()&&!line.trim().startsWith('###')?<div className="draft-row" key={index}><code>{line}</code><button aria-label={`Monter l’instruction ${index}`} disabled={index===0||lines[index-1].startsWith('###')} onClick={()=>{const next=[...lines];[next[index-1],next[index]]=[next[index],next[index-1]];edit('draft','','',next.join('\n'))}}>↑</button><button aria-label={`Descendre l’instruction ${index}`} disabled={index===lines.length-1||!lines[index+1].trim()} onClick={()=>{const next=[...lines];[next[index+1],next[index]]=[next[index],next[index+1]];edit('draft','','',next.join('\n'))}}>↓</button><button onClick={()=>edit('draft','','',lines.filter((_,i)=>i!==index).join('\n'))}>Retirer</button></div>:null)}</div><details><summary>Éditer les instructions en texte</summary><textarea className="instructions" aria-label="Brouillon des transferts" value={draft} onChange={e=>edit('draft','','',e.target.value)}/></details><button disabled={!edits[keyOf('draft')]} onClick={()=>save([keyOf('draft')])}>Enregistrer le brouillon</button><details><summary>Instructions actuellement appliquées</summary><pre>{state.applied}</pre></details></details>
|
{state.mode!=='Anot'&&<details className="panel"><summary>Transferts et coupes <span>{draftDiff?'Brouillon non appliqué':'Brouillon identique aux modifications appliquées'}</span></summary><p>Les en-têtes et scores restent à leur label. Enregistrer un brouillon ne l’applique pas. Pour annuler un transfert, retirez sa ligne puis régénérez en cochant « Appliquer aussi le brouillon ».</p><div className="draft-list">{draft.split('\n').map((line,index,lines)=>line.trim()&&!line.trim().startsWith('###')?<div className="draft-row" key={index}><code>{line}</code><button aria-label={`Monter l’instruction ${index}`} disabled={index===0||lines[index-1].startsWith('###')} onClick={()=>{const next=[...lines];[next[index-1],next[index]]=[next[index],next[index-1]];edit('draft','','',next.join('\n'))}}>↑</button><button aria-label={`Descendre l’instruction ${index}`} disabled={index===lines.length-1||!lines[index+1].trim()} onClick={()=>{const next=[...lines];[next[index+1],next[index]]=[next[index],next[index+1]];edit('draft','','',next.join('\n'))}}>↓</button><button onClick={()=>edit('draft','','',lines.filter((_,i)=>i!==index).join('\n'))}>Retirer</button></div>:null)}</div><details><summary>Éditer les instructions en texte</summary><textarea className="instructions" aria-label="Brouillon des transferts" value={draft} onChange={e=>edit('draft','','',e.target.value)}/></details><button disabled={!edits[keyOf('draft')]} onClick={()=>save([keyOf('draft')])}>Enregistrer le brouillon</button><details><summary>Instructions actuellement appliquées</summary><pre>{state.applied}</pre></details></details>}
|
||||||
<div className="results"><h2>{cards.length} réponse{cards.length!==1?'s':''}</h2><span>{!copyFilter&&!labelFilter?'Vue d’ensemble · sélectionnez une copie ou un label pour cibler la vérification':'Images annotées après lecture manuscrite'}</span></div>
|
<div className="results"><h2>{cards.length} réponse{cards.length!==1?'s':''}</h2><span>{!copyFilter&&!labelFilter?'Vue d’ensemble · sélectionnez une copie ou un label pour cibler la vérification':'Images annotées après lecture manuscrite'}</span></div>
|
||||||
<div className="answer-grid">{cards.slice(0,limit).map(({c,a})=><article className="answer" key={c.id+'|'+a.label}><div className="card-heading"><div><span className="eyebrow">COPIE {c.id} · {c.name}</span><h3>{a.label}</h3></div><span className="badge">{!a.info.present?'Absente':!a.info.not_empty?'Réponse vide':a.info.touched?'Dans le PDF':'Hors PDF filtré'}</span></div><div className="image-space">{a.image?<button className="image-button" aria-label={`Agrandir copie ${c.id} ${a.label}`} onClick={()=>setZoom({copy:c.id,label:a.label})}><img loading="lazy" src={imageUrl(c.id,a.label,false,state.revision)} alt={`Réponse annotée — copie ${c.id}, ${a.label}`}/><span>Agrandir ↗</span></button>:<p className="empty">Aucune image pour ce label.</p>}</div><div className="answer-form"><label>Score<input inputMode="decimal" value={value('score',c.id,a.label,a.score) as string} onChange={e=>edit('score',c.id,a.label,e.target.value)}/></label><label>Remarque personnelle<textarea placeholder="Ajouter une remarque (LaTeX accepté)" value={value('remark',c.id,a.label,a.remark) as string} onChange={e=>edit('remark',c.id,a.label,e.target.value)}/></label></div><div className="card-actions"><button disabled={!edits[keyOf('score',c.id,a.label)]&&!edits[keyOf('remark',c.id,a.label)]} onClick={()=>save([keyOf('score',c.id,a.label),keyOf('remark',c.id,a.label)])}>Enregistrer</button><button disabled={!a.editableBody} onClick={()=>setCut({copy:c.id,label:a.label})}>Couper / transférer</button><a href={documentUrl(c.id,'context',a.label)} target="_blank" rel="noreferrer">Contexte ↗</a><a href={documentUrl(c.id,'question',a.label)} target="_blank" rel="noreferrer">Énoncé ↗</a><a href={documentUrl(c.id,'solution',a.label)} target="_blank" rel="noreferrer">Corrigé ↗</a><a href={documentUrl(c.id,'output')} target="_blank" rel="noreferrer">PDF final ↗</a></div></article>)}</div>
|
<div className="answer-grid">{cards.slice(0,limit).map(({c,a})=><article className="answer" key={c.id+'|'+a.label}><div className="card-heading"><div><span className="eyebrow">COPIE {c.id} · {c.name}</span><h3>{a.label}</h3></div><span className="badge">{!a.info.present?'Absente':!a.info.not_empty?'Réponse vide':a.info.touched?'Dans le PDF':'Hors PDF filtré'}</span></div><div className="image-space">{a.image?<button className="image-button" aria-label={`Agrandir copie ${c.id} ${a.label}`} onClick={()=>setZoom({copy:c.id,label:a.label})}><img loading="lazy" src={imageUrl(c.id,a.label,false,state.revision)} alt={`Réponse annotée — copie ${c.id}, ${a.label}`}/><span>Agrandir ↗</span></button>:<p className="empty">Aucune image pour ce label.</p>}</div><div className="answer-form"><label>Score<input inputMode="decimal" value={value('score',c.id,a.label,a.score) as string} onChange={e=>edit('score',c.id,a.label,e.target.value)}/></label><label>Remarque personnelle<textarea placeholder="Ajouter une remarque (LaTeX accepté)" value={value('remark',c.id,a.label,a.remark) as string} onChange={e=>edit('remark',c.id,a.label,e.target.value)}/></label></div><div className="card-actions"><button disabled={!edits[keyOf('score',c.id,a.label)]&&!edits[keyOf('remark',c.id,a.label)]} onClick={()=>save([keyOf('score',c.id,a.label),keyOf('remark',c.id,a.label)])}>Enregistrer</button><button disabled={!a.editableBody} onClick={()=>setCut({copy:c.id,label:a.label})}>Couper / transférer</button><a href={documentUrl(c.id,'context',a.label)} target="_blank" rel="noreferrer">Contexte ↗</a><a href={documentUrl(c.id,'question',a.label)} target="_blank" rel="noreferrer">Énoncé ↗</a><a href={documentUrl(c.id,'solution',a.label)} target="_blank" rel="noreferrer">Corrigé ↗</a><a href={documentUrl(c.id,'output')} target="_blank" rel="noreferrer">PDF final ↗</a></div></article>)}</div>
|
||||||
{cards.length===0&&<p className="empty">Aucune réponse ne correspond à ces filtres.</p>}{cards.length>limit&&<button className="load-more" onClick={()=>setLimit(limit+20)}>Afficher les 20 suivantes</button>}
|
{cards.length===0&&<p className="empty">Aucune réponse ne correspond à ces filtres.</p>}{cards.length>limit&&<button className="load-more" onClick={()=>setLimit(limit+20)}>Afficher les 20 suivantes</button>}
|
||||||
|
|||||||
@@ -3,3 +3,7 @@
|
|||||||
.draft-list{margin:16px 0}.draft-row{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid #e2e8e3}.draft-row code{flex:1;overflow-wrap:anywhere}.draft-row button{padding:6px 9px}.publish>div{min-width:190px}@media(max-width:1000px){.publish>div{flex:1 0 100%}}
|
.draft-list{margin:16px 0}.draft-row{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid #e2e8e3}.draft-row code{flex:1;overflow-wrap:anywhere}.draft-row button{padding:6px 9px}.publish>div{min-width:190px}@media(max-width:1000px){.publish>div{flex:1 0 100%}}
|
||||||
|
|
||||||
.identity-overview{margin-top:18px;padding:16px;background:#f3f6f4;border-radius:7px}.identity-overview .check{max-width:none;margin-top:14px}.missing-names{display:flex;flex-wrap:wrap;gap:6px 24px;padding-left:20px;margin:10px 0 16px}
|
.identity-overview{margin-top:18px;padding:16px;background:#f3f6f4;border-radius:7px}.identity-overview .check{max-width:none;margin-top:14px}.missing-names{display:flex;flex-wrap:wrap;gap:6px 24px;padding-left:20px;margin:10px 0 16px}
|
||||||
|
|
||||||
|
.filters{flex-wrap:wrap;justify-content:flex-end}.filters input[list="copy-names"]{width:245px}
|
||||||
|
|
||||||
|
.final-score{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:16px 20px;border:1px solid #b9d4c4;background:#e8f0eb;border-radius:9px;margin:16px 0}.final-score strong{font-size:20px;color:#205c48}
|
||||||
Reference in new issue
Block a user