fix: cache-busting de la galerie d apercus (C9)
This commit is contained in:
@@ -60,7 +60,7 @@
|
||||
{% if previews %}
|
||||
<div class="gallery">
|
||||
{% for n in previews %}
|
||||
<figure><img src="/p/{{ p.slug }}/previews/{{ n }}" loading="lazy">
|
||||
<figure><img src="/p/{{ p.slug }}/previews/{{ n }}?v={{ cache_v }}" loading="lazy">
|
||||
<figcaption>{{ n }}</figcaption></figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
+68
-31
@@ -191,15 +191,16 @@ async def project_page(request):
|
||||
entry = NARRATORS.get(slug)
|
||||
chat = entry["chat"] if entry else []
|
||||
# previews existantes ?
|
||||
previews = []
|
||||
pptx = info.get("dernier_pptx")
|
||||
previews, cache_v = [], 0
|
||||
pptx = _pptx_path(slug)
|
||||
if pptx:
|
||||
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews"
|
||||
prev_dir = pptx.parent / f"{pptx.stem}_previews"
|
||||
cache_v = int(pptx.stat().st_mtime)
|
||||
if prev_dir.is_dir():
|
||||
previews = sorted(p.name for p in
|
||||
prev_dir.glob("slide-*.png"))
|
||||
return render("project.html", p=info, chat_html=_chat_html(chat),
|
||||
previews=previews)
|
||||
previews=previews, cache_v=cache_v)
|
||||
|
||||
|
||||
# ── routes : chat Narrator ───────────────────────────────────────────────────
|
||||
@@ -285,28 +286,45 @@ async def formalise(request):
|
||||
entry = _narrator(slug)
|
||||
if not entry["started"]:
|
||||
return HTMLResponse('<p class="err">Aucune conversation '
|
||||
'Narrator — travaille le plan d\'abord.'
|
||||
'</p>')
|
||||
'Narrator — travaille le plan d\'abord, '
|
||||
'puis sauvegarde-le.</p>')
|
||||
proj = fac.Project(slug)
|
||||
try:
|
||||
md = entry["agent"].send(
|
||||
"formalise — produis la formalisation complète slide par "
|
||||
"slide (SLIDE N — titre, contenus, Notes éventuelles), "
|
||||
"sans commentaire autour.")
|
||||
entry["chat"].append(("system", "Formalisation demandée…"))
|
||||
# Message IDENTIQUE à /formalise du facilitator (le Narrator
|
||||
# produit un Markdown structuré #/##/### — PAS des "SLIDE N —
|
||||
# layout" : ce sont le Designer puis l'Encoder qui les créent).
|
||||
md = entry["agent"].feedback(
|
||||
"Formalise maintenant le plan compact validé en Markdown "
|
||||
"structuré complet pour le Designer (format #, ##, ###), "
|
||||
"en développant le contenu de chaque slide. Inclus les "
|
||||
"slides tiroir en les marquant [OPT-XXX].")
|
||||
entry["chat"].append(("system", "Formalisation demandée."))
|
||||
entry["chat"].append(("narrator", md))
|
||||
# Garde : le Narrator a-t-il vraiment produit un plan ?
|
||||
if md.count("#") < 3 or len(md) < 300:
|
||||
return HTMLResponse(
|
||||
'<p class="err">Le Narrator n\'a pas renvoyé un plan '
|
||||
'formalisé exploitable (réponse trop courte ou non '
|
||||
'structurée). Vérifie le plan dans le chat, ajuste, '
|
||||
'puis relance.</p>')
|
||||
proj.mark_formalised(md)
|
||||
r = api.formalise(slug, md)
|
||||
except Exception as e:
|
||||
return HTMLResponse(f'<p class="err">Échec : '
|
||||
f'{html.escape(str(e))}</p>')
|
||||
if r.get("error"):
|
||||
return HTMLResponse(f'<p class="err">'
|
||||
f'{html.escape(str(r))}</p>')
|
||||
return HTMLResponse(
|
||||
f'<p class="err">{html.escape(str(r.get("error")))}'
|
||||
f'<br><span class="hint">Le Designer/Encoder n\'a pas su '
|
||||
f'transcrire ce plan. Regarde la formalisation dans le '
|
||||
f'chat : elle doit lister les slides une à une.</span>'
|
||||
f'</p>')
|
||||
return HTMLResponse(
|
||||
f'<p class="ok">Deck généré : {r["slides"]} slides — '
|
||||
f'{html.escape(r["validation"])}<br>'
|
||||
f'<a href="/p/{slug}/download">Télécharger le PPTX</a> · '
|
||||
f'<button hx-post="/p/{slug}/preview" '
|
||||
f'hx-target="#gallery" hx-indicator="#busy">Générer les '
|
||||
f'aperçus</button></p>')
|
||||
f'<a class="btn" href="/p/{slug}/download">Télécharger le '
|
||||
f'PPTX</a> <span class="hint">puis « Générer les aperçus » '
|
||||
f'ci-dessus pour les voir.</span></p>')
|
||||
|
||||
|
||||
async def preview(request):
|
||||
@@ -325,6 +343,8 @@ async def preview(request):
|
||||
f'</figcaption></figure>'
|
||||
for i, n in enumerate(r["slides"]))
|
||||
return HTMLResponse(
|
||||
f'<p class="ok">{len(r["slides"])} aperçus générés. Coche des '
|
||||
f'slides pour les réviser.</p>'
|
||||
f'<form hx-post="/p/{slug}/revise" hx-target="#pipeline-out" '
|
||||
f'hx-indicator="#busy"><div class="gallery">{cells}</div>'
|
||||
f'<input name="instruction" placeholder="Instruction de '
|
||||
@@ -349,33 +369,45 @@ async def revise(request):
|
||||
f'{html.escape(str(r))}</p>')
|
||||
return HTMLResponse(
|
||||
f'<p class="ok">Slides {r["slides_revisees"]} révisées, deck '
|
||||
f'complet régénéré.<br><a href="/p/{slug}/download">'
|
||||
f'Télécharger</a> · <button hx-post="/p/{slug}/preview" '
|
||||
f'hx-target="#gallery" hx-indicator="#busy">Re-générer les '
|
||||
f'aperçus</button></p>')
|
||||
f'complet régénéré.<br><a class="btn" href="/p/{slug}/'
|
||||
f'download">Télécharger</a> <span class="hint">puis '
|
||||
f'« Générer les aperçus » pour revoir le deck.</span></p>')
|
||||
|
||||
|
||||
# ── routes : fichiers ────────────────────────────────────────────────────────
|
||||
|
||||
def _pptx_path(slug):
|
||||
"""Chemin ABSOLU du dernier PPTX. project_state.json stocke un
|
||||
chemin relatif à la racine du pipeline — la webapp peut avoir un
|
||||
cwd différent, on résout donc depuis BASE."""
|
||||
proj = fac.Project(slug)
|
||||
raw = (proj.load_state().get("dernier_pptx") or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
p = Path(raw)
|
||||
if not p.is_absolute():
|
||||
p = BASE / p
|
||||
return p if p.exists() else None
|
||||
|
||||
|
||||
async def preview_png(request):
|
||||
guard = require_login(request)
|
||||
if guard:
|
||||
return guard
|
||||
slug = request.path_params["slug"]
|
||||
name = request.path_params["name"]
|
||||
proj = fac.Project(slug)
|
||||
state = proj.load_state()
|
||||
pptx = state.get("dernier_pptx")
|
||||
pptx = _pptx_path(slug)
|
||||
if not pptx:
|
||||
return Response(status_code=404)
|
||||
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews"
|
||||
prev_dir = pptx.parent / f"{pptx.stem}_previews"
|
||||
try:
|
||||
png = _safe_child(prev_dir, name)
|
||||
except ValueError:
|
||||
return Response(status_code=403)
|
||||
if not png.exists():
|
||||
return Response(status_code=404)
|
||||
return FileResponse(str(png), media_type="image/png")
|
||||
return FileResponse(str(png), media_type="image/png",
|
||||
headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
async def download(request):
|
||||
@@ -383,11 +415,16 @@ async def download(request):
|
||||
if guard:
|
||||
return guard
|
||||
slug = request.path_params["slug"]
|
||||
proj = fac.Project(slug)
|
||||
pptx = proj.load_state().get("dernier_pptx")
|
||||
if not pptx or not Path(pptx).exists():
|
||||
return Response(status_code=404)
|
||||
return FileResponse(pptx, filename=Path(pptx).name)
|
||||
pptx = _pptx_path(slug)
|
||||
if not pptx:
|
||||
return HTMLResponse(
|
||||
'<p class="err">Aucun PPTX disponible pour ce projet — '
|
||||
'lance « Formaliser le deck » d\'abord.</p>',
|
||||
status_code=404)
|
||||
return FileResponse(
|
||||
str(pptx), filename=pptx.name,
|
||||
media_type="application/vnd.openxmlformats-officedocument."
|
||||
"presentationml.presentation")
|
||||
|
||||
|
||||
async def health(request):
|
||||
|
||||
Reference in New Issue
Block a user