{# FE2-3: /corpus/{doi} — per-paper detail page. Context: paper — dict from _get_paper_row(): doi, title, authors (list), year, journal, source_type, zotero_key, note_path, extraction (dict). breakdown — dict from _get_score_breakdown() ({doi, run_id, breakdown:{...}}) or None. All paper / extraction content is rendered through Jinja autoescape ONLY — never |safe. The score_decomposition.html partial expects a different shape (paper.score_breakdown_json string + paper.score), so it is NOT reused here; a small table is rendered from breakdown.breakdown instead. #} {% extends "base.html" %} {% block title %}{{ paper.title or "(untitled)" }} — Corpus — lit-monitor{% endblock %} {% block content %}
{# --- Extraction -------------------------------------------------------- #}

Extraction

{% if paper.extraction and paper.extraction.get("_overall_confidence") is not none %}

Overall confidence: {{ "%.3f" | format(paper.extraction.get("_overall_confidence") | float) }}

{% endif %} {# Collect non-underscore keys; "_"-prefixed keys (e.g. _overall_confidence) are metadata, surfaced separately above and skipped here. #} {% set _visible = [] %} {% for key, value in paper.extraction.items() %} {% if not key.startswith("_") %}{% set _ = _visible.append((key, value)) %}{% endif %} {% endfor %} {% if _visible %}
{% for key, value in _visible %} {# Plain Jinja autoescape renders any injected markup inert as text (e.g. becomes <img onerror=...>). Do NOT strip tags here: that would delete legitimate scientific text containing angle brackets (p<0.05, n>3, IC50 < 5 uM). No |safe anywhere. #}
{{ key }}
{{ value }}
{% endfor %}
{% else %}

No extraction fields stored for this paper.

{% endif %}
{# --- Score decomposition (only when a breakdown exists) ---------------- #} {% if breakdown and breakdown.breakdown %}

Score decomposition

{% for signal, score in breakdown.breakdown.items() %} {% endfor %}
SignalScore
{{ signal }}{{ "%.3f" | format(score | float) }}
{% endif %} {# --- Related work + knowledge graph (FE2-4: lazy HTMX fragments) -------- #} {# Both load AFTER the page paints (hx-trigger="load") so the detail view renders instantly; the graph-backed fragments stream in once ready. The DOI is embedded literally in the path so the {doi:path} fragment routes match (no urlencode on the path — double-encoding "/" → "%2F" breaks the route). #} {% if paper.doi %}

Loading knowledge graph…

{% else %}
{% endif %} {# --- Actions (mirror _partials/paper_card.html P8 idiom) --------------- #} {% if paper.doi %} {# DOI is embedded literally in the URL path — no urlencode — so the {doi:path} route (which accepts "10.x/y" verbatim) matches. Double-encoding would yield "%2F" in the path and break the route. #}

Actions

{% endif %}
{% endblock %}