{# FI-2: read-only /insights page — learning state, cluster weights, server-SVG signal mix, recent events. Supersedes the old /feedback page. #} {% extends "base.html" %} {% block title %}Insights — lit-monitor{% endblock %} {# CU-2: Chart.js via CDN, loaded on this page only. Charts degrade gracefully to the data tables below if the CDN is blocked (site.js no-ops without window.Chart). #} {% block head %}{% endblock %} {% block content %}
{# ----------------------------------------------------------------- #} {# 1. Learning state #} {# ----------------------------------------------------------------- #}

Learning state

{% if learning.available %}
Feedback events
{{ learning.n_events }}
Soft-gate
{{ "%.2f" | format(learning.soft_gate | float) }} {% if learning.inert %} INERT — below cold-start floor (10) {% endif %}
Interest-vector dim
{{ learning.dim }}
Computed at
{{ learning.computed_at }}
{% if learning.inert %}

The interest vector exists but is gated to zero influence until at least 10 feedback events accumulate (cold-start protection).

{% endif %} {% else %}

The engine hasn't learned anything yet — give feedback (save / dismiss) on discoveries to build an interest vector.

{% endif %} {# FI-3: top library papers aligned with the interest vector. Lazy HTMX load — the fragment replaces this container's own contents on page load so the page renders instantly. #}
{# ----------------------------------------------------------------- #} {# 2. Cluster weights #} {# ----------------------------------------------------------------- #}

Cluster weights

{% if clusters.clusters %}

Per-theme atrophy weight (1.0 = full attention; lower = de-emphasised by your dismissals). Weights never decay below the floor of {{ "%.2f" | format(clusters.floor | float) }}.

{# CU-2: Chart.js bar chart of per-theme weights. JSON island carries the labels + values; site.js builds the chart, reading colors from the palette tokens. The table below remains the exact-values + degradation fallback. #} {% set _cw_labels = [] %} {% set _cw_data = [] %} {% for c in clusters.clusters %} {% set _ = _cw_labels.append(c.name) %} {% set _ = _cw_data.append(c.weight | float) %} {% endfor %}
{% for c in clusters.clusters %} {% endfor %}
ThemeWeightAt floor?
{{ c.name }} {{ "%.2f" | format(c.weight | float) }} {% if c.at_floor %}at floor {% else %}—{% endif %}
{% else %}

No themes yet — run a brain-build to cluster papers.

{% endif %} {# ----------------------------------------------------------------- #} {# 3. Signal mix (server-rendered inline SVG; no client-side chart lib) #} {# ----------------------------------------------------------------- #}

Signal mix

{% set _sig_rows = [] %} {% set _sig_max = namespace(v=0) %} {% for signal, count in summary.items() | sort %} {% set _ = _sig_rows.append({"label": signal, "count": count}) %} {% if (count | int) > _sig_max.v %}{% set _sig_max.v = count | int %}{% endif %} {% endfor %} {% if _sig_rows and _sig_max.v > 0 %} {# CU-2: Chart.js bar chart + JSON data island; table kept beside it. #} {% set _sig_labels = [] %} {% set _sig_data = [] %} {% for row in _sig_rows %} {% set _ = _sig_labels.append(row.label) %} {% set _ = _sig_data.append(row.count | int) %} {% endfor %}
{% for row in _sig_rows %} {% endfor %}
SignalCount
{{ row.label }}{{ row.count }}
{% else %}

No feedback events yet — give feedback (save / dismiss) on discoveries to build the signal mix.

{% endif %} {# ----------------------------------------------------------------- #} {# 3b. Feedback by source (FI-6; server-rendered inline SVG) #} {# ----------------------------------------------------------------- #}

Feedback by source

{% set _src_rows = [] %} {% set _src_max = namespace(v=0) %} {# Sort descending by count so the heaviest source leads the chart. #} {% for source, count in summary_by_source.items() | sort(attribute='1', reverse=true) %} {% set _ = _src_rows.append({"label": source, "count": count}) %} {% if (count | int) > _src_max.v %}{% set _src_max.v = count | int %}{% endif %} {% endfor %} {% if _src_rows and _src_max.v > 0 %}

Where each feedback signal came from (e.g. discovery cards, the themes page, or an implicit Zotero save). Open-ended; unknown groups events recorded without a source tag.

{# CU-2: Chart.js bar chart + JSON data island; table kept beside it. #} {% set _src_labels = [] %} {% set _src_data = [] %} {% for row in _src_rows %} {% set _ = _src_labels.append(row.label) %} {% set _ = _src_data.append(row.count | int) %} {% endfor %}
{% for row in _src_rows %} {% endfor %}
By source, all-time totals.
SourceCount
{{ row.label }}{{ row.count }}
{% else %}

No feedback events yet — no source breakdown to show.

{% endif %} {# ----------------------------------------------------------------- #} {# 4. Recent events #} {# ----------------------------------------------------------------- #}

Recent events

{% if recent %} {% for ev in recent %} {% endfor %}
# DOI Signal Weight Rating Source Created
{{ ev.id }} {% if ev.doi %} {{ ev.doi }} {% else %}—{% endif %} {{ ev.signal_type }} {{ "%.2f" | format(ev.weight | float) }} {% if ev.rating %}{{ ev.rating }}{% else %}—{% endif %} {% if ev.source %}{{ ev.source }}{% else %}—{% endif %} {% if ev.created_at %}{{ ev.created_at }}{% else %}—{% endif %}
{% elif events_total %}

No events on this page.

{% else %}

No recent events yet — give feedback (save / dismiss / rate) on discoveries to populate this log.

{% endif %} {# AR-7: corpus-style prev/next pagination for recent events (page size 100). Rendered whenever any events exist (not gated on the current slice) so an over-the-end offset still shows Prev to navigate back. Prev is hidden at offset 0; Next is hidden on the last page. #} {% if events_total %}
{{ events_offset + 1 }}–{{ events_offset + recent|length }} of {{ events_total }} {% if events_has_prev %} ← Prev {% endif %} {% if events_has_next %} Next → {% endif %}
{% endif %} {# FI-3: feedback timeline chart. Lazy HTMX load — the fragment replaces this container's own contents on page load so the page renders instantly. #}
{% endblock %}