Various HTML improvements

This commit is contained in:
sebastien committed 2026-11-03 10:02:10 +01:00
1 parent fd943696fb
commit 8c1fa0af57
16 files changed
+403 -45

No files matched your search

+4 -2
View File
@@ -26,6 +26,8 @@ def _files_digest(paths) -> str:
class AnnotationReadCache:
state_file = STATE_FILE
def __init__(self, workspace, mode: str, data: dict, group_dirs=()):
self.workspace = workspace
self.mode = mode
@@ -59,7 +61,7 @@ class AnnotationReadCache:
return False
directory = self.workspace.root / self.mode / f"Copie{copy_id}"
try:
state = read_json(directory / STATE_FILE)
state = read_json(directory / self.state_file)
return (state["signature"] == self.signatures[copy_id]
and bool(state["outputs"])
and all(Path(path).is_file() for path in state["outputs"]))
@@ -82,7 +84,7 @@ class AnnotationReadCache:
stem = destination.name.rsplit(" (", 1)[0]
outputs.extend(path for suffix in (".jpg", ".pdf")
if (path := destination / (stem + suffix)).is_file())
atomic_write_json(directory / STATE_FILE, {
atomic_write_json(directory / self.state_file, {
"signature": self.signatures[copy_id],
"outputs": [str(path) for path in outputs],
})
+14 -1
View File
@@ -38,6 +38,7 @@ def build_parser() -> argparse.ArgumentParser:
"directories, matching folders by their trailing copy ID"
),
)
parser.add_argument("--incremental", action="store_true", help="Skip unchanged return folders")
return parser
@@ -147,6 +148,7 @@ def update_named_return_answers(
def prepare_named_returns(
workspace: EvaluationWorkspace,
annotation_dir_name: str,
*, incremental: bool = False,
) -> ExitCode:
workspace.require_directories("Copies", annotation_dir_name)
workspace.return_dir.mkdir(parents=True, exist_ok=True)
@@ -198,6 +200,12 @@ def prepare_named_returns(
assigned_names.add(name)
destination = existing_returns.get(int(copy_id), workspace.return_dir / f"{safe_name} ({copy_id})")
output_name = destination.name.rsplit(" (", 1)[0]
from copienator.reviewed_cache import ReturnCache
cache = ReturnCache(workspace.root, source_folder, destination) if incremental else None
if cache and cache.unchanged():
print(f"Skipping unchanged return {destination.name}")
continue
copy_failed = False
destination.mkdir(parents=True, exist_ok=True)
try:
publish_answer_returns(workspace.root, source_folder, destination)
@@ -238,6 +246,9 @@ def prepare_named_returns(
file=sys.stderr,
)
had_errors = True
copy_failed = True
if cache and not copy_failed:
cache.remember()
unassigned = expected_names - assigned_names
if unassigned:
@@ -253,10 +264,11 @@ def run(
*,
annotation_dir: str,
update: bool = False,
incremental: bool = False,
) -> ExitCode:
if update:
return update_named_return_answers(workspace, annotation_dir)
return prepare_named_returns(workspace, annotation_dir)
return prepare_named_returns(workspace, annotation_dir, incremental=incremental)
def main(argv: Sequence[str] | None = None) -> int:
@@ -268,6 +280,7 @@ def main(argv: Sequence[str] | None = None) -> int:
workspace_from_args(args, repository=Path.cwd()),
annotation_dir=args.annotation_dir,
update=args.update,
incremental=args.incremental,
),
)
+10 -3
View File
@@ -25,16 +25,22 @@ from copienator.annotation_modifications import (
from copienator.filesystem import publish_file_sets
def run(workspace: EvaluationWorkspace, *, annotation_dir: str = "BGnot") -> ExitCode:
def run(workspace: EvaluationWorkspace, *, annotation_dir: str = "BGnot", incremental: bool = False) -> ExitCode:
workspace.require_files("labels", DRAFT)
workspace.require_directories("Copies", annotation_dir)
text = (workspace.root / DRAFT).read_text(encoding="utf-8")
instructions = read_instructions(text, annotation_dir)
validate_instructions(workspace.root, annotation_dir, instructions)
directory = workspace.root / annotation_dir
applied = directory / APPLIED
previous = read_instructions(applied.read_text(encoding="utf-8"), annotation_dir) if applied.exists() else []
if incremental and previous == instructions:
print("Skipping unchanged transfer instructions")
return ExitCode.SUCCESS
validate_instructions(workspace.root, annotation_dir, instructions)
copy_ids = sorted({item.copy_id for item in [*previous, *instructions]})
if incremental:
copy_ids = [cid for cid in copy_ids
if [i for i in previous if i.copy_id == cid] != [i for i in instructions if i.copy_id == cid]]
# Prepare every copy before publishing anything, including the applied file.
with TemporaryDirectory(prefix=".annotation-edits-", dir=directory) as temporary:
staging_root = Path(temporary)
@@ -61,12 +67,13 @@ def run(workspace: EvaluationWorkspace, *, annotation_dir: str = "BGnot") -> Exi
def build_parser():
parser = evaluation_parser("Modify reviewed answer bodies using annotation_modifications.txt")
parser.add_argument("--annotation-dir", choices=MODES, default="BGnot")
parser.add_argument("--incremental", action="store_true", help="Skip unchanged transfer instructions")
return parser
def main(argv: Sequence[str] | None = None) -> int:
return execute(build_parser(), argv, lambda args: run(
workspace_from_args(args), annotation_dir=args.annotation_dir))
workspace_from_args(args), annotation_dir=args.annotation_dir, incremental=args.incremental))
if __name__ == "__main__":
+16 -2
View File
@@ -5,9 +5,19 @@ from copienator.commands.reading_annotations import apply_actions_and_regenerate
from copienator.commands.reading_grouped_annotations import apply_actions_and_regenerate_grouped
def run(workspace: EvaluationWorkspace, mode: str):
def run(workspace: EvaluationWorkspace, mode: str, *, incremental: bool = False):
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()))
from copienator.reviewed_cache import ReviewedReadCache
cache = ReviewedReadCache(workspace, mode, directories) if incremental else None
if cache:
pending = []
for directory in directories:
if cache.unchanged(directory.name[5:]):
print(f"Skipping unchanged {directory.name}", flush=True)
else:
pending.append(directory)
directories = pending
# Validate every snapshot before starting to publish any copy.
for directory in directories:
if not (directory / FILENAME).is_file():
@@ -29,10 +39,14 @@ def run(workspace: EvaluationWorkspace, mode: str):
print(log, flush=True)
if result != ExitCode.SUCCESS:
status = ExitCode.PARTIAL
elif cache:
# Rendering can normalize the saved snapshot; fingerprint the published inputs.
ReviewedReadCache(workspace, mode, [directory]).remember([cid])
return status
def main(argv=None):
parser = evaluation_parser('Regenerate from saved reviewed inputs without rescanning handwriting')
parser.add_argument('--annotation-dir', choices=('Bnot', 'BGnot'), required=True)
return execute(parser, argv, lambda args: run(workspace_from_args(args), args.annotation_dir))
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))
+55 -10
View File
@@ -9,7 +9,7 @@ import subprocess
import sys
import threading
from copienator import atomic_write_json, read_json, utils
from copienator import EvaluationWorkspace, atomic_write_json, read_json, utils
from copienator import custom_remarks, general_remarks
from copienator.annotation_modifications import DRAFT, APPLIED, BASELINE, read_instructions, validate_instructions
from copienator.return_identity import return_folders, RETURN_FOLDER_PATTERN, set_copy_name
@@ -17,8 +17,9 @@ from copienator.score_editing import set_score
class Finalisation:
def __init__(self, root: Path, mode: str, token: str, lock_fd: int | 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.repository = (repository or Path.cwd()).resolve()
if mode not in ('Bnot', 'BGnot'):
raise ValueError('Choisissez Bnot ou BGnot.')
self.mode, self.token = mode, token
@@ -80,10 +81,13 @@ class Finalisation:
'image': (directory / f'{label}.jpg').is_file(),
'editableBody': label in base.get('images', {}),
'info': info.get(label, {})} for label in self.labels]})
names_path = EvaluationWorkspace(self.root, self.repository).names_file()
expected_names = (sorted({line.strip() for line in names_path.read_text(encoding='utf-8').splitlines()
if line.strip()}, key=str.casefold) if names_path.is_file() else None)
applied = self.root / self.mode / APPLIED
applied_text = applied.read_text() if applied.exists() else f'### Source: {self.mode}\n'
return {'evaluation': self.root.name, 'mode': self.mode, 'labels': self.labels, 'copies': copies,
'draft': self.draft(), 'applied': applied_text, 'revision': self.revision(),
'expectedNames': expected_names, 'draft': self.draft(), 'applied': applied_text, 'revision': self.revision(),
'dirty': read_json(self.dirty_path, default={}).get('dirty', False), 'job': dict(self.job)}
def save(self, data):
@@ -116,10 +120,20 @@ class Finalisation:
else:
raise ValueError('Modification inconnue.')
self.dirty_path.parent.mkdir(exist_ok=True)
atomic_write_json(self.dirty_path, {'dirty': True})
pending = read_json(self.dirty_path, default={'dirty': False})
kinds = set(pending.get('kinds', ['unknown'] if pending.get('dirty') else []))
kinds.add(kind)
render_inputs = None
if kinds == {'name'}:
render_inputs = (pending.get('render_inputs') if pending.get('kinds') == ['name']
else self._render_inputs())
pending = {'dirty': True, 'kinds': sorted(kinds)}
if kinds == {'name'}:
pending['render_inputs'] = render_inputs
atomic_write_json(self.dirty_path, pending)
return self.state()
def start_job(self, revision: str, apply_draft: bool):
def start_job(self, revision: str, apply_draft: bool, incremental: bool = True):
with self.mutex:
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.')
@@ -128,12 +142,12 @@ class Finalisation:
if not (self.root / DRAFT).exists():
(self.root / DRAFT).write_text(self.draft())
self.job = {'running': True, 'status': 'running', 'log': ''}
threading.Thread(target=self._run_job, args=(apply_draft,), daemon=True).start()
threading.Thread(target=self._run_job, args=(apply_draft, incremental), daemon=True).start()
return dict(self.job)
def _run_job(self, apply_draft):
def _run_job(self, apply_draft, incremental=True):
try:
self._regenerate(apply_draft)
self._regenerate(apply_draft, incremental)
except Exception as exc:
with self.mutex:
self.job.update(status='failed', log=self.job['log'] + '\n' + str(exc))
@@ -141,7 +155,26 @@ class Finalisation:
with self.mutex:
self.job['running'] = False
def _regenerate(self, apply_draft):
def _render_inputs(self):
from copienator.reviewed_cache import ReviewedReadCache
from copienator.review_snapshot import FILENAME
directories = self.copies()
if not directories or not all((d / FILENAME).is_file() for d in directories):
return None
cache = ReviewedReadCache(EvaluationWorkspace(self.root), self.mode, directories)
# Keep the fast path conservative when an existing rendered file disappears.
outputs = []
for directory in directories:
info = read_json(directory / 'info.json', default={})
paths = [directory / 'info.json', directory / BASELINE]
paths += [directory / f'{label}.jpg' for label, entry in info.items() if entry['present']]
paths += list(directory.glob('Concat*.*'))
if any(not path.is_file() for path in paths):
return None
outputs.extend(str(p) for p in paths)
return {'signatures': cache.signatures, 'outputs': sorted(outputs)}
def _regenerate(self, apply_draft, incremental=True):
from copienator.return_answers import load_return_answer_options
options = load_return_answer_options(self.root)
reader = 'read-grouped' if self.mode == 'BGnot' else 'read-annotations'
@@ -152,13 +185,25 @@ class Finalisation:
if apply_draft:
commands.append(['modify-annotations', str(self.root), '--annotation-dir', self.mode])
commands.append(['giving-names', str(self.root), self.mode])
pending = read_json(self.dirty_path, default={})
names_only = (incremental and not apply_draft and pending.get('kinds') == ['name']
and pending.get('render_inputs') is not None
and pending['render_inputs'] == self._render_inputs())
from copienator.review_snapshot import FILENAME
if all((directory / FILENAME).is_file() for directory in self.copies()):
if names_only:
commands = [['giving-names', str(self.root), self.mode]]
with self.mutex:
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()):
commands[0] = ['regenerate-reviewed', str(self.root), '--annotation-dir', self.mode]
elif (self.root / 'BRnot').is_dir():
with self.mutex:
self.job.update(running=False, status='failed', log='Résultat de lecture sauvegardé absent pour une évaluation avec reprise. Relisez le passage principal puis fusionnez la reprise avant de régénérer dans le navigateur. Aucun résultat n’a été remplacé.')
return
if incremental:
for command in commands:
if command[0] in ('regenerate-reviewed', 'modify-annotations', 'giving-names'):
command.append('--incremental')
env = dict(os.environ, COPIENATOR_FINALISATION_TOKEN=self.token, PYTHONUNBUFFERED='1')
from copienator import configuration
if configuration.CONFIG_PATH is not None:
+4 -3
View File
@@ -33,6 +33,7 @@ class Change(BaseModel):
class Regenerate(BaseModel):
revision: str
apply_draft: bool = False
incremental: bool = True
class Preview(BaseModel):
@@ -91,7 +92,7 @@ def create_app(service: Finalisation, browser_token: str, stop=lambda: None):
@app.post('/api/regenerate')
def regenerate(data: Regenerate):
return service.start_job(data.revision, data.apply_draft)
return service.start_job(data.revision, data.apply_draft, data.incremental)
@app.post('/api/stop')
def shutdown():
@@ -174,11 +175,11 @@ def create_app(service: Finalisation, browser_token: str, stop=lambda: None):
class WebSession:
def __init__(self, root: Path, mode: str):
def __init__(self, root: Path, mode: str, *, repository: Path | None = None):
self.lease = EvaluationLock(root)
self.lease.__enter__()
try:
self.service = Finalisation(root, mode, self.lease.token, self.lease.stream.fileno())
self.service = Finalisation(root, mode, self.lease.token, self.lease.stream.fileno(), repository=repository)
if not self.service.copies():
raise ValueError('Lisez les annotations manuscrites dans ce mode avant d’ouvrir la finalisation web.')
self.token = secrets.token_urlsafe(32)
+71
View File
@@ -0,0 +1,71 @@
"""Content-based caches for saved-review rendering and return publication."""
from dataclasses import asdict
from hashlib import sha256
import json
from pathlib import Path
from copienator import atomic_write_json, configuration, read_json, general_remarks
from copienator.annotation_read_cache import AnnotationReadCache, _files_digest
from copienator.annotation_modifications import APPLIED, read_instructions
from copienator.custom_remarks import load_remarks, copy_key
from copienator.return_answers import load_return_answer_options
from copienator.review_snapshot import FILENAME
def output_options(root):
return (load_return_answer_options(root), configuration.RETURN_ANSWERS_ENABLED,
configuration.RETURN_JPEG_ENABLED, configuration.RETURN_PDF_ENABLED)
def signature(payload):
return sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest()
class ReviewedReadCache(AnnotationReadCache):
state_file = '.reviewed-render-state.json'
def __init__(self, workspace, mode, directories):
self.workspace, self.mode, self.signatures = workspace, mode, {}
root = workspace.root
shared = [root / 'labels']
for folder in ('Text2', 'Sol2'):
shared.extend((root / folder).glob('*.pdf'))
common = (_files_digest(shared), configuration.LATEX_ANOT_BEFORE, configuration.LATEX_ANOT_AFTER)
remarks, general = load_remarks(root), general_remarks.load_remarks(root)
applied = root / mode / APPLIED
instructions = read_instructions(applied.read_text(), mode) if applied.is_file() else []
for directory in directories:
cid = directory.name[5:]
saved = read_json(directory / FILENAME)
paths = [directory / FILENAME, directory / 'score.json']
for item in saved['data'].values():
pdf = Path(item['pdf_path'])
pdf = pdf if pdf.is_absolute() else root / pdf
paths.extend((pdf, pdf.with_suffix('.layout.json')))
paths.extend(directory / note['file'] for note in saved['notes'].values() if note['file'])
self.signatures[cid] = signature((1, common, _files_digest(paths),
remarks.get(copy_key(cid), {}), general.get(copy_key(cid), []),
[asdict(i) for i in instructions if i.copy_id == cid]))
class ReturnCache:
def __init__(self, root, source, destination):
self.path = source / '.return-publication-state.json'
self.destination = destination
paths = [source / 'score.json', source / 'info.json']
paths.extend(source.glob('*.jpg'))
paths.extend(source.glob('*.pdf'))
for folder in ('Text2', 'Sol2'):
paths.extend((root / folder).glob('*.pdf'))
self.signature = signature((1, str(destination), _files_digest(paths), output_options(root)))
def unchanged(self):
try:
state = read_json(self.path)
return (state['signature'] == self.signature and bool(state['outputs'])
and all((self.destination / name).is_file() for name in state['outputs']))
except (OSError, ValueError, KeyError, TypeError):
return False
def remember(self):
outputs = [str(p.relative_to(self.destination)) for p in self.destination.rglob('*') if p.is_file()]
atomic_write_json(self.path, {'signature': self.signature, 'outputs': outputs})
@@ -1,15 +0,0 @@
(function(){const G=document.createElement("link").relList;if(G&&G.supports&&G.supports("modulepreload"))return;for(const q of document.querySelectorAll('link[rel="modulepreload"]'))y(q);new MutationObserver(q=>{for(const $ of q)if($.type==="childList")for(const rl of $.addedNodes)rl.tagName==="LINK"&&rl.rel==="modulepreload"&&y(rl)}).observe(document,{childList:!0,subtree:!0});function Y(q){const $={};return q.integrity&&($.integrity=q.integrity),q.referrerPolicy&&($.referrerPolicy=q.referrerPolicy),q.crossOrigin==="use-credentials"?$.credentials="include":q.crossOrigin==="anonymous"?$.credentials="omit":$.credentials="same-origin",$}function y(q){if(q.ep)return;q.ep=!0;const $=Y(q);fetch(q.href,$)}})();var zs={exports:{}},nn={};var lv;function ih(){if(lv)return nn;lv=1;var _=Symbol.for("react.transitional.element"),G=Symbol.for("react.fragment");function Y(y,q,$){var rl=null;if($!==void 0&&(rl=""+$),q.key!==void 0&&(rl=""+q.key),"key"in q){$={};for(var Gl in q)Gl!=="key"&&($[Gl]=q[Gl])}else $=q;return q=$.ref,{$$typeof:_,type:y,key:rl,ref:q!==void 0?q:null,props:$}}return nn.Fragment=G,nn.jsx=Y,nn.jsxs=Y,nn}var tv;function ch(){return tv||(tv=1,zs.exports=ih()),zs.exports}var d=ch(),_s={exports:{}},Z={};var uv;function fh(){if(uv)return Z;uv=1;var _=Symbol.for("react.transitional.element"),G=Symbol.for("react.portal"),Y=Symbol.for("react.fragment"),y=Symbol.for("react.strict_mode"),q=Symbol.for("react.profiler"),$=Symbol.for("react.consumer"),rl=Symbol.for("react.context"),Gl=Symbol.for("react.forward_ref"),ul=Symbol.for("react.suspense"),Dl=Symbol.for("react.memo"),R=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),M=Symbol.for("react.view_transition"),_l=Symbol.iterator;function Xl(s){return s===null||typeof s!="object"?null:(s=_l&&s[_l]||s["@@iterator"],typeof s=="function"?s:null)}var Ul={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},il=Object.assign,et={};function Ol(s,E,D){this.props=s,this.context=E,this.refs=et,this.updater=D||Ul}Ol.prototype.isReactComponent={},Ol.prototype.setState=function(s,E){if(typeof s!="object"&&typeof s!="function"&&s!=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,s,E,"setState")},Ol.prototype.forceUpdate=function(s){this.updater.enqueueForceUpdate(this,s,"forceUpdate")};function gt(){}gt.prototype=Ol.prototype;function Ql(s,E,D){this.props=s,this.context=E,this.refs=et,this.updater=D||Ul}var at=Ql.prototype=new gt;at.constructor=Ql,il(at,Ol.prototype),at.isPureReactComponent=!0;var xl=Array.isArray;function K(){}var F={H:null,A:null,T:null,S:null},Vl=Object.prototype.hasOwnProperty;function Nl(s,E,D){var j=D.ref;return{$$typeof:_,type:s,key:E,ref:j!==void 0?j:null,props:D}}function pl(s,E){return Nl(s.type,E,s.props)}function Ll(s){return typeof s=="object"&&s!==null&&s.$$typeof===_}function jt(s){var E={"=":"=0",":":"=2"};return"$"+s.replace(/[=:]/g,function(D){return E[D]})}var L=/\/+/g;function hl(s,E){return typeof s=="object"&&s!==null&&s.key!=null?jt(""+s.key):E.toString(36)}function C(s){switch(s.status){case"fulfilled":return s.value;case"rejected":throw s.reason;default:switch(typeof s.status=="string"?s.then(K,K):(s.status="pending",s.then(function(E){s.status==="pending"&&(s.status="fulfilled",s.value=E)},function(E){s.status==="pending"&&(s.status="rejected",s.reason=E)})),s.status){case"fulfilled":return s.value;case"rejected":throw s.reason}}throw s}function B(s,E,D,j,k){var al=typeof s;(al==="undefined"||al==="boolean")&&(s=null);var sl=!1;if(s===null)sl=!0;else switch(al){case"bigint":case"string":case"number":sl=!0;break;case"object":switch(s.$$typeof){case _:case G:sl=!0;break;case R:return sl=s._init,B(sl(s._payload),E,D,j,k)}}if(sl)return k=k(s),sl=j===""?"."+hl(s,0):j,xl(k)?(D="",sl!=null&&(D=sl.replace(L,"$&/")+"/"),B(k,E,D,"",function(nu){return nu})):k!=null&&(Ll(k)&&(k=pl(k,D+(k.key==null||s&&s.key===k.key?"":(""+k.key).replace(L,"$&/")+"/")+sl)),E.push(k)),1;sl=0;var x=j===""?".":j+":";if(xl(s))for(var V=0;V<s.length;V++)j=s[V],al=x+hl(j,V),sl+=B(j,E,D,al,k);else if(V=Xl(s),typeof V=="function")for(s=V.call(s),V=0;!(j=s.next()).done;)j=j.value,al=x+hl(j,V++),sl+=B(j,E,D,al,k);else if(al==="object"){if(typeof s.then=="function")return B(C(s),E,D,j,k);throw E=String(s),Error("Objects are not valid as a React child (found: "+(E==="[object Object]"?"object with keys {"+Object.keys(s).join(", ")+"}":E)+"). If you meant to render a collection of children, use an array instead.")}return sl}function X(s,E,D){if(s==null)return s;var j=[],k=0;return B(s,j,"","",function(al){return E.call(D,al,k++)}),j}function cl(s){if(s._status===-1){var E=s._result,D=E();D.then(function(j){(s._status===0||s._status===-1)&&(s._status=1,s._result=j,D.status===void 0&&(D.status="fulfilled",D.value=j))},function(j){(s._status===0||s._status===-1)&&(s._status=2,s._result=j,D.statLine truncated
at`)?" (<anonymous>)":-1<u.stack.indexOf("@")?"@unknown:0:0":""}return`
`+Vi+l+js}var Zi=!1;function Li(l,t){if(!l||Zi)return"";Zi=!0;var u=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var e={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(A){var r=A}Reflect.construct(l,[],z)}else{try{z.call()}catch(A){r=A}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(A){r=A}(z=l())&&typeof z.catch=="function"&&z.catch(function(){})}}catch(A){if(A&&r&&typeof A.stack=="string")return[A.stack,r.stack]}return[null,null]}};e.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var a=Object.getOwnPropertyDescriptor(e.DetermineComponentFrameRoot,"name");a&&a.configurable&&Object.defineProperty(e.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var n=e.DetermineComponentFrameRoot(),i=n[0],c=n[1];if(i&&c){var f=i.split(`
`),m=c.split(`
`);for(a=e=0;e<f.length&&!f[e].includes("DetermineComponentFrameRoot");)e++;for(;a<m.length&&!m[a].includes("DetermineComponentFrameRoot");)a++;if(e===f.length||a===m.length)for(e=f.length-1,a=m.length-1;1<=e&&0<=a&&f[e]!==m[a];)a--;for(;1<=e&&0<=a;e--,a--)if(f[e]!==m[a]){if(e!==1||a!==1)do if(e--,a--,0>a||f[e]!==m[a]){var S=`
`+f[e].replace(" at new "," at ");return l.displayName&&S.includes("<anonymous>")&&(S=S.replace("<anonymous>",l.displayName)),S}while(1<=e&&0<=a);break}}}finally{Zi=!1,Error.prepareStackTrace=u}return(u=l?l.displayName||l.name:"")?zu(u):""}function ov(l,t){switch(l.tag){case 26:case 27:case 5:return zu(l.type);case 16:return zu("Lazy");case 13:return l.child!==t&&t!==null?zu("Suspense Fallback"):zu("Suspense");case 19:return zu("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 zu("Activity");case 30:return zu("ViewTransition");default:return""}}function Ms(l){try{var t="",u=null;do t+=ov(l,u),u=l,l=l.return;while(l);return t}catch(e){return`
Error generating stack: `+e.message+`
`+e.stack}}var Ki=Object.prototype.hasOwnProperty,Ji=_.unstable_scheduleCallback,wi=_.unstable_cancelCallback,rv=_.unstable_shouldYield,dv=_.unstable_requestPaint,St=_.unstable_now,vv=_.unstable_getCurrentPriorityLevel,Us=_.unstable_ImmediatePriority,ps=_.unstable_UserBlockingPriority,dn=_.unstable_NormalPriority,mv=_.unstable_LowPriority,Rs=_.unstable_IdlePriority,yv=_.log,hv=_.unstable_setDisableYieldValue,da=null,bt=null;function _u(l){if(typeof yv=="function"&&hv(l),bt&&typeof bt.setStrictMode=="function")try{bt.setStrictMode(da,l)}catch{}}var Tt=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 vn=256,mn=262144,yn=4194304;function Wu(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 hn(l,t,u){var e=l.pendingLanes;if(e===0)return 0;var a=0,n=l.suspendedLanes,i=l.pingedLanes;l=l.warmLanes;var c=e&134217727;return c!==0?(e=c&~n,e!==0?a=Wu(e):(i&=c,i!==0?a=Wu(i):u||(u=c&~l,u!==0&&(a=Wu(u))))):(c=e&~n,c!==0?a=Wu(c):i!==0?a=Wu(i):u||(u=e&~l,u!==0&&(a=Wu(u)))),a===0?0:t!==0&&t!==a&&(t&n)===0&&(n=a&-a,u=t&-t,n>=u||n===32&&(u&4194048)!==0)?t:a}function va(l,t){return(l.pendingLanes&~(l.suspendedLanes&~l.pingedLanes)&t)===0}function xs(l,t){(t&8)!==0&&(t|=t&32);var u=l.entangledLanes;if(u!==0)for(l=l.entanglements,u&=t;0<u;){var e=31-Tt(u),a=1<<e;t|=l[e],u&=~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=[],u=0;31>u;u++)t.push(l);return t}function ma(l,t){l.pendingLanes|=t,t!==268435456&&(l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0)}function Ev(l,t,u,e,a,n){var i=l.pendingLanes;l.pendingLanes=u,l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0,l.expiredLanes&=u,l.entangledLanes&=u,l.errorRecoveryDisabledLanes&=u,l.shellSuspendCounter=0;var c=l.entanglements,f=l.expirationTimes,m=l.hiddenUpdates;for(u=i&~u;0<u;){var S=31-Tt(u),z=1<<S;c[S]=0,f[S]=-1;var r=m[S];if(r!==null)for(m[S]=null,S=0;S<r.length;S++){var g=r[S];g!==null&&(g.lane&=-536870913)}u&=~z}e!==0&&qs(l,e,0),n!==0&&a===0&&l.tag!==0&&(l.suspendedLanes|=n&~(i&~t))}function qs(l,t,u){l.pendingLanes|=t,l.suspendedLanes&=~t;var e=31-Tt(t);l.entangledLanes|=t,l.entanglements[e]=l.entanglements[e]|1073741824|u&261930}function Ys(l,t){var u=l.entangledLanes|=t;for(l=l.entanglements;u;){var e=31-Tt(u),a=1<<e;a&t|l[e]&t&&(l[e]|=t),u&=~a}}function Bs(l,t){var u=t&-t;return u=(u&42)!==0?1:Fi(u),(u&(l.suspendedLanes|t))!==0?0:u}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=V.p;return l!==0?l:(l=window.event,l===void 0?32:w0(l.type))}function Xs(l,t){var u=V.p;try{return V.p=l,t()}finally{V.p=u}}var iu=Math.random().toString(36).slice(2),Wl="__reactFiber$"+iu,rt="__reactProps$"+iu,Ee="__reactContainer$"+iu,Qs="__reactEvents$"+iu,zv="__reactListeners$"+iu,_v="__reactHandles$"+iu,Vs="__reactResources$"+iu,ya="__reactMarker$"+iu,gn="__reactLoad$"+iu;function Sn(l){delete l[Wl],delete l[rt],delete l[zv],delete l[_v]}function ku(l){var t;if(t=l[Wl])return t;for(var u=l.parentNode;u;){if(t=u[Ee]||u[Wl]){if(u=t.alternate,t.child!==null||u!==null&&u.child!==null)for(l=C0(l);l!==null;){if(u=l[Wl])return u;l=C0(l)}return t}l=u,u=l.parentNode}return null}function ze(l){if(l=l[Wl]||l[Ee]){var t=l.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return l}return null}function ha(l){var t=l.tag;if(t===5||t===26||t===27||t===6)return l.stateNode;throw Error(y(33))}function _e(l){var t=l[Vs];return t||(t=l[Vs]={hoistableStyles:new Map,hoistableScripts:new Map}),t}function Kl(l){l[ya]=!0}function Zs(l){l[gn]=void 0}var Ls=new Set,Ks={};function Iu(l,t){Oe(l,t),Oe(l+"Capture",t)}function Line truncated
`).replace(oy,"")}function c0(l,t){return t=i0(t),i0(l)===t}function yl(l,t,u,e,a,n){switch(u){case"children":if(typeof e=="string")t==="body"||t==="textarea"&&e===""||Ae(l,e);else if(typeof e=="number"||typeof e=="bigint")t!=="body"&&Ae(l,""+e);else return;break;case"className":Tn(l,"class",e);break;case"tabIndex":Tn(l,"tabindex",e);break;case"dir":case"role":case"viewBox":case"width":case"height":Tn(l,u,e);break;case"style":to(l,e,n);return;case"data":if(t!=="object"){Tn(l,"data",e);break}case"src":case"href":if(e===""&&(t!=="a"||u!=="href")){l.removeAttribute(u);break}if(e==null||typeof e=="function"||typeof e=="symbol"||typeof e=="boolean"){l.removeAttribute(u);break}e=En(e),l.setAttribute(u,e);break;case"action":case"formAction":if(typeof e=="function"){l.setAttribute(u,"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"&&(u==="formAction"?(t!=="input"&&yl(l,t,"name",a.name,a,null),yl(l,t,"formEncType",a.formEncType,a,null),yl(l,t,"formMethod",a.formMethod,a,null),yl(l,t,"formTarget",a.formTarget,a,null)):(yl(l,t,"encType",a.encType,a,null),yl(l,t,"method",a.method,a,null),yl(l,t,"target",a.target,a,null)));if(e==null||typeof e=="symbol"||typeof e=="boolean"){l.removeAttribute(u);break}e=En(e),l.setAttribute(u,e);break;case"onClick":e!=null&&(l.onclick=$t);return;case"onScroll":e!=null&&P("scroll",l);return;case"onScrollEnd":e!=null&&P("scrollend",l);return;case"dangerouslySetInnerHTML":if(e!=null){if(typeof e!="object"||!("__html"in e))throw Error(y(61));if(u=e.__html,u!=null){if(a.children!=null)throw Error(y(60));n?.__html!==u&&(l.innerHTML=u)}}break;case"multiple":l.multiple=e&&typeof e!="function"&&typeof e!="symbol";break;case"muted":l.muted=e&&typeof e!="function"&&typeof e!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(e==null||typeof e=="function"||typeof e=="boolean"||typeof e=="symbol"){l.removeAttribute("xlink:href");break}u=En(e),l.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",u);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":e!=null&&typeof e!="function"&&typeof e!="symbol"?l.setAttribute(u,e):l.removeAttribute(u);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":e&&typeof e!="function"&&typeof e!="symbol"?l.setAttribute(u,""):l.removeAttribute(u);break;case"capture":case"download":e===!0?l.setAttribute(u,""):e!==!1&&e!=null&&typeof e!="function"&&typeof e!="symbol"?l.setAttribute(u,e):l.removeAttribute(u);break;case"cols":case"rows":case"size":case"span":e!=null&&typeof e!="function"&&typeof e!="symbol"&&!isNaN(e)&&1<=e?l.setAttribute(u,e):l.removeAttribute(u);break;case"rowSpan":case"start":e==null||typeof e=="function"||typeof e=="symbol"||isNaN(e)?l.removeAttribute(u):l.setAttribute(u,e);break;case"popover":P("beforetoggle",l),P("toggle",l),bn(l,"popover",e);break;case"xlinkActuate":cu(l,"http://www.w3.org/1999/xlink","xlink:actuate",e);break;case"xlinkArcrole":cu(l,"http://www.w3.org/1999/xlink","xlink:arcrole",e);break;case"xlinkRole":cu(l,"http://www.w3.org/1999/xlink","xlink:role",e);break;case"xlinkShow":cu(l,"http://www.w3.org/1999/xlink","xlink:show",e);break;case"xlinkTitle":cu(l,"http://www.w3.org/1999/xlink","xlink:title",e);break;case"xlinkType":cu(l,"http://www.w3.org/1999/xlink","xlink:type",e);break;case"xmlBase":cu(l,"http://www.w3.org/XML/1998/namespace","xml:base",e);break;case"xmlLang":cu(l,"http://www.w3.org/XML/1998/namespace","xml:lang",e);break;case"xmlSpace":cu(l,"http://www.w3.org/XML/1998/namespace","xml:space",e);break;case"is":bn(l,"is",e);break;case"innerText":case"textContent":return;default:if(!(2<u.length)||u[0]!=="o"&&u[0]!=="O"||u[1]!=="n"&&u[1]!=="N")u=jv.get(u)||u,bn(l,u,e);else return}fl=!0}function Pf(l,t,u,e,a,n){switch(u){case"style":to(l,e,n);return;case"dangerouslySetInnerHTML":if(e!=null){if(typeof e!="object"||!("__html"in e))throw Error(y(61));if(u=e.__html,u!=null){if(a.children!=null)throw Error(y(60));n?.__html!==u&&(l.innerHTML=u)}}break;case"children":if(typeof e=="string")Ae(l,e);else if(typeof e=="number"||typeof e=="bigint")Ae(l,""+e);else return;break;case"onScroll":e!=null&&P("scroll",l);return;case"onScrollEnd":e!=null&&P("scrollend",l);return;case"onClick":e!=null Line truncated
`).map((O,N,s)=>O.trim()&&!O.trim().startsWith("###")?d.jsxs("div",{className:"draft-row",children:[d.jsx("code",{children:O}),d.jsx("button",{"aria-label":`Monter l’instruction ${N}`,disabled:N===0||s[N-1].startsWith("###"),onClick:()=>{const E=[...s];[E[N-1],E[N]]=[E[N],E[N-1]],Nl("draft","","",E.join(`
`))},children:"↑"}),d.jsx("button",{"aria-label":`Descendre l’instruction ${N}`,disabled:N===s.length-1||!s[N+1].trim(),onClick:()=>{const E=[...s];[E[N+1],E[N]]=[E[N],E[N+1]],Nl("draft","","",E.join(`
`))},children:"↓"}),d.jsx("button",{onClick:()=>Nl("draft","","",s.filter((E,D)=>D!==N).join(`
`)),children:"Retirer"})]},N):null)}),d.jsxs("details",{children:[d.jsx("summary",{children:"Éditer les instructions en texte"}),d.jsx("textarea",{className:"instructions","aria-label":"Brouillon des transferts",value:C,onChange:O=>Nl("draft","","",O.target.value)})]}),d.jsx("button",{disabled:!M[Gt("draft")],onClick:()=>pl([Gt("draft")]),children:"Enregistrer le brouillon"}),d.jsxs("details",{children:[d.jsx("summary",{children:"Instructions actuellement appliquées"}),d.jsx("pre",{children:_.applied})]})]}),d.jsxs("div",{className:"results",children:[d.jsxs("h2",{children:[cl.length," réponse",cl.length!==1?"s":""]}),d.jsx("span",{children:!rl&&!ul?"Vue d’ensemble · sélectionnez une copie ou un label pour cibler la vérification":"Images annotées après lecture manuscrite"})]}),d.jsx("div",{className:"answer-grid",children:cl.slice(0,R).map(({c:O,a:N})=>d.jsxs("article",{className:"answer",children:[d.jsxs("div",{className:"card-heading",children:[d.jsxs("div",{children:[d.jsxs("span",{className:"eyebrow",children:["COPIE ",O.id," · ",O.name]}),d.jsx("h3",{children:N.label})]}),d.jsx("span",{className:"badge",children:N.info.present?N.info.not_empty?N.info.touched?"Dans le PDF":"Hors PDF filtré":"Réponse vide":"Absente"})]}),d.jsx("div",{className:"image-space",children:N.image?d.jsxs("button",{className:"image-button","aria-label":`Agrandir copie ${O.id} ${N.label}`,onClick:()=>at({copy:O.id,label:N.label}),children:[d.jsx("img",{loading:"lazy",src:Gi(O.id,N.label,!1,_.revision),alt:`Réponse annotée — copie ${O.id}, ${N.label}`}),d.jsx("span",{children:"Agrandir ↗"})]}):d.jsx("p",{className:"empty",children:"Aucune image pour ce label."})}),d.jsxs("div",{className:"answer-form",children:[d.jsxs("label",{children:["Score",d.jsx("input",{inputMode:"decimal",value:Vl("score",O.id,N.label,N.score),onChange:s=>Nl("score",O.id,N.label,s.target.value)})]}),d.jsxs("label",{children:["Remarque personnelle",d.jsx("textarea",{placeholder:"Ajouter une remarque (LaTeX accepté)",value:Vl("remark",O.id,N.label,N.remark),onChange:s=>Nl("remark",O.id,N.label,s.target.value)})]})]}),d.jsxs("div",{className:"card-actions",children:[d.jsx("button",{disabled:!M[Gt("score",O.id,N.label)]&&!M[Gt("remark",O.id,N.label)],onClick:()=>pl([Gt("score",O.id,N.label),Gt("remark",O.id,N.label)]),children:"Enregistrer"}),d.jsx("button",{disabled:!N.editableBody,onClick:()=>gt({copy:O.id,label:N.label}),children:"Couper / transférer"}),d.jsx("a",{href:fn(O.id,"context",N.label),target:"_blank",rel:"noreferrer",children:"Contexte ↗"}),d.jsx("a",{href:fn(O.id,"question",N.label),target:"_blank",rel:"noreferrer",children:"Énoncé ↗"}),d.jsx("a",{href:fn(O.id,"solution",N.label),target:"_blank",rel:"noreferrer",children:"Corrigé ↗"}),d.jsx("a",{href:fn(O.id,"output"),target:"_blank",rel:"noreferrer",children:"PDF final ↗"})]})]},O.id+"|"+N.label))}),cl.length===0&&d.jsx("p",{className:"empty",children:"Aucune réponse ne correspond à ces filtres."}),cl.length>R&&d.jsx("button",{className:"load-more",onClick:()=>b(R+20),children:"Afficher les 20 suivantes"})]}),(_.job.log||_.job.running)&&d.jsxs("details",{className:"panel",open:_.job.status==="failed",children:[d.jsxs("summary",{children:["Journal de régénération · ",_.job.status==="failed"?"Échec / résultat incomplet":_.job.running?"En cours":"Terminée"]}),d.jsx("pre",{className:"log",children:_.job.log})]})]}),Ol&&d.jsx(Sh,{copy:Ol.copy,source:Ol.label,labels:_.labels,mode:_.mode,revision:_.revision,draft:C,close:()=>gt(null),accept:O=>{Nl("draft","","",O),gt(null),$("Instruction ajoutée au brouillon. Enregistrez-le avant de l’appliquer.")}}),Ql&&d.jsx(gh,{copy:Ql.copy,label:Ql.label,revision:_.revision,close:()=>at(null)})]})}function gh({copy:_,label:G,revision:Y,close:y}){const[q,$]=Sl.useState(100);return Sl.useEffect(()=>{const rl=Gl=>{Gl.key==="Escape"&&y()};return window.addEventListener("keydown",rl),()=>window.removeEventListener("keydown",rl)},[]),d.jsx("div",{className:"overlay",children:d.jsxs("section",{className:"modal viewer",role:"dialog","aria-modal":"true","aria-label":`Copie ${_} ${G}`,children:[d.jsxs("div",{className:"modal-head",children:[d.jsxs("h2",{children:["Copie ",_," · ",G]}),d.jsxs("label",{children:["Zoom ",d.jsx("input",{type:"range",min:"40",max:"200",value:q,onChange:rl=>$(Number(rl.target.value))}),q,"%"]}),d.jsx("button",{autoFocus:!0,onClick:y,children:"Fermer"})]}),d.jsx("div",{className:"zoom-scroll",children:d.jsx("img",{src:Gi(_,G,!1,Y),alt:`Copie ${_}, ${G}`,style:{width:q+"%",maxWidth:"none"}})})]})})}function Sh({copy:_,source:G,labels:Y,mode:y,revision:q,draft:$,close:rl,accept:Gl}){const[ul,Dl]=Sl.useState(Y.find(L=>L!==G)||""),[R,b]=Sl.useState("cut"),[M,_l]=Sl.useState(50),[Xl,Ul]=Sl.useState("1"),[il,et]=Sl.useState(">"),[Ol,gt]=Sl.useState(!1),[Ql,at]=Sl.useState(""),[xl,K]=Sl.useState(null),[F,Vl]=Sl.useState(!1),Nl=R==="cut"?`c{${M}}${Xl}${il}`:R,pl=`Copie${_} ${G} ${Nl} ${Ol?"|":""}${ul}${Ol?"":"|Line truncated
`+pl+`
`;Sl.useEffect(()=>K(null),[ul,R,M,Xl,il,Ol,$]);const jt=async()=>{Vl(!0),at("");try{K(await oa("preview",{copy:_,source:G,target:ul,text:Ll}))}catch(L){at(String(L))}finally{Vl(!1)}};return d.jsx("div",{className:"overlay",children:d.jsxs("section",{className:"modal",role:"dialog","aria-modal":"true","aria-label":"Couper ou transférer une réponse",children:[d.jsxs("div",{className:"modal-head",children:[d.jsxs("div",{children:[d.jsxs("span",{className:"eyebrow",children:["COPIE ",_," · ",y]}),d.jsx("h2",{children:"Couper ou transférer"})]}),d.jsx("button",{autoFocus:!0,onClick:rl,children:"Fermer"})]}),d.jsx("p",{children:"Seul le corps de la réponse est déplacé. Cliquez sur l’image source pour placer la coupe."}),Ql&&d.jsx("p",{role:"alert",className:"alert",children:Ql}),d.jsxs("div",{className:"cut-controls",children:[d.jsxs("label",{children:["Destination",d.jsx("select",{value:ul,onChange:L=>Dl(L.target.value),children:Y.filter(L=>L!==G).map(L=>d.jsx("option",{children:L},L))})]}),d.jsxs("label",{children:["Opération",d.jsxs("select",{value:R,onChange:L=>b(L.target.value),children:[d.jsx("option",{value:"cut",children:"Couper la source"}),d.jsx("option",{value:"x>",children:"Déplacer et ajouter"}),d.jsx("option",{value:"->",children:"Copier et ajouter"}),d.jsx("option",{value:"xx",children:"Déplacer et remplacer"}),d.jsx("option",{value:"-x",children:"Copier et remplacer"}),d.jsx("option",{value:"xs",children:"Vider la source"}),d.jsx("option",{value:"sx",children:"Vider la cible"}),d.jsx("option",{value:"ss",children:"Conserver les deux"})]})]}),R==="cut"&&d.jsxs(d.Fragment,{children:[d.jsxs("label",{children:["Coupe : ",M,"%",d.jsx("input",{type:"range",min:"1",max:"99",step:"0.1",value:M,onChange:L=>_l(Number(L.target.value))})]}),d.jsxs("label",{children:["Conserver à la source",d.jsxs("select",{value:Xl,onChange:L=>Ul(L.target.value),children:[d.jsx("option",{value:"1",children:"La partie haute"}),d.jsx("option",{value:"2",children:"La partie basse"})]})]}),d.jsxs("label",{children:["À la destination",d.jsxs("select",{value:il,onChange:L=>et(L.target.value),children:[d.jsx("option",{value:">",children:"Ajouter le fragment"}),d.jsx("option",{value:"x",children:"Remplacer le corps"})]})]})]}),d.jsxs("label",{className:"check",children:[d.jsx("input",{type:"checkbox",checked:Ol,onChange:L=>gt(L.target.checked)}),"Placer avant la cible"]})]}),d.jsxs("div",{className:"compare",children:[d.jsxs("div",{children:[d.jsxs("h3",{children:["Source · ",G]}),d.jsxs("div",{className:"cut-image",onClick:L=>{if(R==="cut"){const hl=L.currentTarget.getBoundingClientRect();_l(Math.min(99,Math.max(1,Math.round((L.clientY-hl.top)/hl.height*1e3)/10)))}},children:[d.jsx("img",{src:Gi(_,G,!0,q),alt:"Corps source sans en-tête"}),R==="cut"&&d.jsx("div",{className:"cut-line",style:{top:M+"%"},children:d.jsxs("span",{children:[M,"%"]})})]})]}),d.jsxs("div",{children:[d.jsxs("h3",{children:["Cible · ",ul]}),d.jsx(bh,{copy:_,label:ul,revision:q},ul)]})]}),d.jsx("code",{className:"instruction-line",children:pl}),d.jsxs("div",{className:"modal-actions",children:[d.jsx("button",{disabled:F||!ul,onClick:jt,children:F?"Calcul de l’aperçu…":"Aperçu du résultat"}),d.jsx("button",{className:"primary",disabled:!xl||F,onClick:()=>Gl(Ll),children:"Ajouter au brouillon"})]}),xl&&d.jsxs(d.Fragment,{children:[d.jsx("h3",{children:"Résultat proposé après toutes les instructions du brouillon"}),d.jsx("div",{className:"compare",children:[G,ul].map(L=>d.jsxs("div",{children:[d.jsx("h4",{children:L}),xl[L]?d.jsx("img",{src:xl[L],alt:"Aperçu "+L}):d.jsx("p",{className:"empty",children:"Corps vide"})]},L))})]}),d.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 bh({copy:_,label:G,revision:Y}){const[y,q]=Sl.useState(!1);return y?d.jsx("p",{className:"empty",children:"Pas de corps existant. Le transfert peut remplir cette cible."}):d.jsx("img",{src:Gi(_,G,!0,Y),alt:"Corps cible sans en-tête",onError:()=>q(!0)})}yh.createRoot(document.getElementById("root")).render(d.jsx(hh,{}));
@@ -0,0 +1,15 @@
(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,{}));
@@ -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
+2 -2
View File
@@ -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-B8v-OSfB.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-B6ukjHTb.css">
<!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>
<link rel="stylesheet" crossorigin href="./assets/index-NHKKhh88.css">
</head><body><div id="root"></div></body></html>
+27 -2
View File
@@ -360,7 +360,12 @@ class CopienatorApp(tk.Tk):
top, text="Ouvrir l’énoncé", command=self._open_statement_pdf
)
self.open_statement_button.grid(row=0, column=5, padx=(6, 0))
ttk.Button(top, text="Finalisation web…", command=self._open_finalisation_web).grid(row=0, column=6, padx=(6, 0))
self.finalisation_web_button = ttk.Button(
top, text="Finalisation web…", command=self._open_finalisation_web, state="disabled"
)
self.finalisation_web_button.grid(row=0, column=6, padx=(6, 0))
attach_tooltip(self.finalisation_web_button,
"Disponible après une première lecture des annotations manuscrites (Bnot ou BGnot).")
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))
@@ -538,6 +543,19 @@ class CopienatorApp(tk.Tk):
status = ttk.Label(self, textvariable=self.info_var, anchor="w", relief="sunken", padding=(6, 3))
status.grid(row=3, column=0, sticky="ew")
def _has_finalisation_outputs(self) -> bool:
evaluation = self.state_store.evaluation
if evaluation is None:
return False
# Detect actual read results, including evaluations processed through the CLI.
# Review-sheet generation alone does not create info.json.
return any(
directory.is_dir() and directory.name[5:].isdigit()
and (directory / "score.json").is_file() and (directory / "info.json").is_file()
for mode in ("Bnot", "BGnot")
for directory in (evaluation / mode).glob("Copie*")
)
def _open_finalisation_web(self) -> None:
if self.web_session is not None or self.runner.running or self.active_step_id or self.batch_monitor.active:
messagebox.showinfo("Traitement en cours", "Terminez le traitement ou la surveillance en cours avant d’ouvrir la finalisation web.")
@@ -545,6 +563,9 @@ class CopienatorApp(tk.Tk):
if not self.evaluation:
messagebox.showerror("Évaluation absente", "Chargez d’abord une évaluation.")
return
if not self._has_finalisation_outputs():
messagebox.showinfo("Finalisation web", "Exécutez d’abord « Lire les annotations manuscrites ».")
return
self._save_current_form()
dialog = tk.Toplevel(self)
dialog.title("Finalisation web")
@@ -615,7 +636,7 @@ class CopienatorApp(tk.Tk):
def start():
try:
from copienator.finalisation_server import WebSession
self.web_session = WebSession(self.evaluation, mode.get())
self.web_session = WebSession(self.evaluation, mode.get(), repository=self.repository)
except Exception as exc:
messagebox.showerror("Finalisation web", str(exc), parent=dialog)
return
@@ -2417,6 +2438,10 @@ class CopienatorApp(tk.Tk):
def _update_controls(self) -> None:
running = bool(self.active_step_id) or self.runner.running
self.finalisation_web_button.configure(
state="normal" if not running and self.web_session is None
and not self.batch_monitor.active and self._has_finalisation_outputs() else "disabled"
)
if self.current_step and self.current_step.id == "personal_read":
for button in self.review_buttons:
button.configure(state="disabled" if running or not self.state_store.evaluation else "normal")
+11
View File
@@ -59,6 +59,17 @@ l’appliquer annule le transfert correspondant. Les contraintes existantes rest
valables : une instruction par source, une cible ne peut pas être source, aucun
déplacement automatique des scores ou des en-têtes.
La case **Ignorer les copies inchangées**, cochée par défaut près du bouton de
régénération, évite de recomposer les copies et dossiers de retour dont les entrées
n’ont pas changé. Une modification de score ou de remarque ne reconstruit que la
copie concernée ; les changements de documents partagés peuvent concerner toute
la classe. Le premier passage établit les empreintes nécessaires. Les changements de noms
seuls sont suivis séparément : ils ne relancent pas la composition des corrections,
même avant ce premier passage. Enregistrer un nom renomme déjà son dossier de
retour existant et ses fichiers principaux. Les fichiers
manquants sont reconstruits et les échecs sont retentés. Décochez cette case pour
forcer une régénération complète. Le journal signale les copies ignorées.
La régénération actualise les réponses, les documents combinés et **A Rendre**
selon les options de sortie existantes, en conservant les noms personnalisés.
Le journal indique les erreurs et les résultats incomplets. La chaîne s’arrête
+162
View File
@@ -58,6 +58,25 @@ class WebTests(unittest.TestCase):
self.assertEqual(response.status_code, 200, response.text)
return response.json()
def test_names_follow_gui_repository_even_when_package_is_elsewhere(self):
repository = self.root / 'application-folder'
repository.mkdir()
(repository / 'names').write_text('Alice\nBob\n', encoding='utf-8')
service = Finalisation(self.root, 'Bnot', 'token', repository=repository)
self.assertEqual(service.state()['expectedNames'], ['Alice', 'Bob'])
(self.root / 'names').write_text('Claire\n', encoding='utf-8')
self.assertEqual(service.state()['expectedNames'], ['Claire'])
def test_expected_names_use_local_roster_and_refresh_after_name_save(self):
(self.root / 'names').write_text('Alice\nBob\nAlice\n\n', encoding='utf-8')
state = self.client.get('/api/state').json()
self.assertEqual(state['expectedNames'], ['Alice', 'Bob'])
state = self.save('name', 'Alice')
self.assertEqual(state['copies'][0]['name'], 'Alice')
self.assertEqual(state['expectedNames'], ['Alice', 'Bob'])
(self.root / 'names').write_text('', encoding='utf-8')
self.assertEqual(self.service.state()['expectedNames'], [])
def test_session_origin_and_paths(self):
outsider = TestClient(create_app(self.service, 'browser-token'))
self.assertEqual(outsider.get('/api/state').status_code, 403)
@@ -164,6 +183,35 @@ class WebTests(unittest.TestCase):
self.assertEqual(len(calls), 1)
self.assertEqual(self.service.job['status'], 'failed')
def test_names_only_skip_rendering_before_first_cache_and_after_restart(self):
from copienator.review_snapshot import stage
output = self.root / 'Bnot/Copie01'
data = {'Ex 1': {'pdf_path': self.root/'Copies/Copie01/Ex 1.pdf',
'result': {'score': 3, 'feedback': []}, 'coordinates': [0, 0]}}
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='standard', bnote={'images': []})
self.save('name', 'Alice')
self.assertFalse((output / '.reviewed-render-state.json').exists())
reopened = Finalisation(self.root, 'Bnot', 'token')
calls = []
class Process:
def __init__(self, command, **kwargs):
calls.append(command[3])
self.stdout = StringIO('done\n')
def __enter__(self): return self
def __exit__(self, *args): pass
def wait(self): return 0
with patch('copienator.finalisation.subprocess.Popen', Process):
reopened._regenerate(False)
self.assertEqual(calls, ['giving-names'])
self.save('name', 'Bob')
# A score edited outside the session must invalidate the names-only shortcut.
set_score(output/'score.json', 'Ex 1', '1')
calls.clear()
with patch('copienator.finalisation.subprocess.Popen', Process):
self.service._regenerate(False)
self.assertEqual(calls, ['regenerate-reviewed', 'giving-names'])
def test_os_lock_blocks_cli_and_releases(self):
with EvaluationLock(self.root):
with self.assertRaises(CliError):
@@ -256,6 +304,33 @@ class SnapshotTests(unittest.TestCase):
@unittest.skipUnless(os.environ.get('DISPLAY'), 'Tk requires a display')
class WebGuiTests(unittest.TestCase):
def test_web_button_requires_read_outputs_and_remains_enabled_when_stale(self):
from copienator_gui.app import CopienatorApp
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / 'Copies').mkdir()
(root / 'labels').write_text('Ex 1\n')
app = CopienatorApp(Path.cwd(), False, root)
self.addCleanup(app.destroy)
def state():
app._update_controls()
return str(app.finalisation_web_button.cget('state'))
self.assertEqual(state(), 'disabled')
output = root / 'BGnot/Copie01'
output.mkdir(parents=True)
atomic_write_json(output/'bnote.json', {'images': []})
self.assertEqual(state(), 'disabled')
seed(root, 'BGnot')
self.assertEqual(state(), 'normal')
app.state_store.update_step('read_annotations', status='stale')
self.assertEqual(state(), 'normal')
app.active_step_id = 'read_annotations'
self.assertEqual(state(), 'disabled')
app.active_step_id = None
self.assertEqual(state(), 'normal')
app.state_store.evaluation = None
self.assertEqual(state(), 'disabled')
def test_modal_lock_and_return_to_existing_workflow(self):
from copienator_gui.app import CopienatorApp
from copienator.finalisation_server import WebSession
@@ -295,3 +370,90 @@ class WebGuiTests(unittest.TestCase):
self.assertIsNone(app.web_session)
self.assertIsNone(app.grab_current())
self.assertEqual(app.state_store.step('read_annotations')['status'], 'stale')
class IncrementalReviewedTests(unittest.TestCase):
def test_only_changed_copies_render_and_missing_outputs_are_retried(self):
from copienator.commands import regenerate_reviewed, annotating
from copienator.review_snapshot import stage
from copienator.custom_remarks import set_remark
import shutil
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, 'BGnot')
pdf = root / 'Copies/Copie01/Ex 1.pdf'
pdf.write_bytes(b'answer')
data = {'Ex 1': {'pdf_path':pdf, 'result':{'score':3,'feedback':[]},'coordinates':[0,0]}}
output = root / 'BGnot/Copie01'
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='grouped')
shutil.copytree(output, root / 'BGnot/Copie02')
workspace = EvaluationWorkspace(root)
with patch.object(annotating, 'make_base_image', return_value=(Image.new('RGB',(100,100),'white'),0,0)), \
patch.object(annotating, 'compose_label_image', return_value=(Image.new('RGB',(100,120),'white'),20)) as compose, \
patch('copienator.commands.reading_grouped_annotations.get_extra_pdfs_as_images', return_value=[]):
def run(count):
compose.reset_mock()
self.assertEqual(regenerate_reviewed.run(workspace,'BGnot',incremental=True), 0)
self.assertEqual(compose.call_count,count)
run(2)
run(0)
set_score(output/'score.json','Ex 1','2')
run(1)
run(0)
set_remark(root,'01','Ex 1','Justifier.')
run(1)
set_remark(root,'01','Ex 1','')
run(1)
(output/'Ex 1.jpg').unlink()
run(1)
(root/'Text2').mkdir()
(root/'Text2/Ex 1.pdf').write_bytes(b'changed question')
run(2)
run(0)
self.assertEqual(regenerate_reviewed.run(workspace,'BGnot'),0)
self.assertEqual(compose.call_count,2)
def test_incremental_transfers_apply_changes_and_undo_without_repeating(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
workspace = EvaluationWorkspace(root)
from copienator.annotation_modifications import rebuild
with patch.object(modify_annotations, 'rebuild', wraps=rebuild) as render:
def run(text, count):
(root / DRAFT).write_text('### Source: Bnot\n' + text)
render.reset_mock()
self.assertEqual(modify_annotations.run(workspace, annotation_dir='Bnot', incremental=True), 0)
self.assertEqual(render.call_count, count)
run('Copie01 Ex 1 c{40}1> Ex 2|\n', 1)
run('Copie01 Ex 1 c{40}1> Ex 2|\n', 0)
run('Copie01 Ex 1 c{50}1> Ex 2|\n', 1)
run('', 1)
run('', 0)
self.assertEqual(Image.open(root/'Bnot/Copie01/Ex 1.jpg').height,120)
@patch('copienator.commands.giving_names.configuration.RETURN_ANSWERS_ENABLED', False)
def test_return_publication_skips_unchanged_and_repairs_missing_files(self):
import shutil
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
shutil.copytree(root/'Bnot/Copie01',root/'Bnot/Copie02')
atomic_write_json(root/'Copies/Copie02.json',{'name':'Bob'})
atomic_write_json(root/'Copies/Copie01.json',{'name':'Alice'})
workspace = EvaluationWorkspace(root)
from copienator.return_answers import publish_answer_returns
with patch.object(giving_names,'publish_answer_returns',wraps=publish_answer_returns) as publish:
def run(count):
publish.reset_mock()
self.assertEqual(giving_names.prepare_named_returns(workspace,'Bnot',incremental=True),0)
self.assertEqual(publish.call_count,count)
run(2)
run(0)
set_score(root/'Bnot/Copie01/score.json','Ex 1','1')
run(1)
run(0)
(root/'A Rendre/Bob (02)/info.json').unlink()
run(1)
self.assertTrue((root/'A Rendre/Bob (02)/info.json').is_file())
+9 -4
View File
@@ -5,7 +5,7 @@ import './style.css';
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 Job = {running:boolean;status:string;log:string};
type State = {evaluation:string;mode:string;labels:string[];copies:Copy[];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[]};
async function api(path:string, data?:unknown) {
const response=await fetch('/api/'+path, data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
@@ -22,6 +22,8 @@ function App(){
const [edits,setEdits]=useState<Record<string,Edit>>({}),[busy,setBusy]=useState(false),[applyDraft,setApplyDraft]=useState(false);
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 [onlyNameIssues,setOnlyNameIssues]=useState(false);
const [incremental,setIncremental]=useState(true);
const refresh=async()=>{try{setState(await api('state'));setError('')}catch(e){setError(String(e))}};
useEffect(()=>{void refresh()},[]);
useEffect(()=>{const protect=(e:BeforeUnloadEvent)=>{if(Object.keys(edits).length){e.preventDefault();e.returnValue=''}};window.addEventListener('beforeunload',protect);return()=>window.removeEventListener('beforeunload',protect)},[edits]);
@@ -29,7 +31,7 @@ function App(){
const value=(kind:string,copy:string,label:string,fallback:string|string[])=>edits[keyOf(kind,copy,label)]?.value??fallback;
const edit=(kind:string,copy:string,label:string,v:string|string[])=>setEdits(old=>({...old,[keyOf(kind,copy,label)]:{kind,copy,label,value:v}}));
const save=async(keys:string[])=>{if(!state)return;setBusy(true);setError('');let latest=state;try{for(const key of keys){const change=edits[key];if(!change)continue;latest=await api('change',{...change,revision:latest.revision});setState(latest);setEdits(old=>{const next={...old};delete next[key];return next})}setNotice('Modifications enregistrées.')}catch(e){setError(String(e))}finally{setBusy(false)}};
const regenerate=async()=>{if(!state)return;setBusy(true);setError('');try{const job=await api('regenerate',{revision:state.revision,apply_draft:applyDraft});setState({...state,job});setNotice('')}catch(e){setError(String(e))}finally{setBusy(false)}};
const regenerate=async()=>{if(!state)return;setBusy(true);setError('');try{const job=await api('regenerate',{revision:state.revision,apply_draft:applyDraft,incremental});setState({...state,job});setNotice('')}catch(e){setError(String(e))}finally{setBusy(false)}};
const stop=async()=>{try{await api('stop',{});setStopped(true)}catch(e){setError(String(e))}};
if(stopped)return <main className="end"><h1>Session terminée</h1><p>Vous pouvez reprendre le travail dans Copienator.</p></main>;
if(!state)return <main className="end"><h1>Copienator</h1><p>{error||'Chargement de la finalisation…'}</p><button onClick={refresh}>Réessayer</button></main>;
@@ -41,15 +43,18 @@ function App(){
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 issues=state.copies.filter(c=>c.name.toLowerCase()==='unknown'||(names.get(c.name.toLowerCase())||0)>1);
const identityMatches=matches.filter(c=>!onlyNameIssues||issues.includes(c));
const assignedNames=new Set(state.copies.map(c=>c.name.trim().toLocaleLowerCase('fr')));
const missingNames=state.expectedNames?.filter(name=>!assignedNames.has(name.trim().toLocaleLowerCase('fr')));
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>
<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>
{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>}
<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><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} 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}>
<details className="panel"><summary>Identités et remarques générales <span>{issues.length} nom(s) à vérifier</span></summary><p>Les remarques sont composées en LaTeX. Leur ordre est celui de la liste.</p>{matches.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>
<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>
+2
View File
@@ -1,3 +1,5 @@
: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:white;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:white;border-color:#205c48}.primary:hover:not(:disabled){background:#174633;color:white}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:white}.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:white;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:white;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:white;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:white;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:white;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}.compLine truncated
.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}