// The insights panel: what the last turn sent, and what it cost. import { useEffect, useState } from 'react' import { api } from '../../../api' import { SECTION_LABELS, pctLabel, sectionColor } from '../format' import { CacheReport, TokenBreakdown, WorldStateReport } from '../reports' function InsightsPanel({ advId, inspectActionId, onClearInspect, refreshKey }) { const [report, setReport] = useState(null) const [error, setError] = useState(null) useEffect(() => { let stale = false // a slow earlier request must not clobber a newer one setError(null) const load = inspectActionId ? api.getActionContext(advId, inspectActionId) : api.getAdventureContext(advId) load .then((r) => { if (!stale) setReport(r) }) .catch((err) => { if (!stale) { setReport(null); setError(err.message) } }) return () => { stale = true } }, [advId, inspectActionId, refreshKey]) if (error) return
{error}
if (!report) return
Loading…
const { tokens, cards, history, sections } = report const overBudget = tokens.total > tokens.budget // The per-section sum, not tokens.total: the total also counts the separators // between sections, and percentages have to add up to 100 for the reader. const sectionTotal = sections.reduce((n, s) => n + s.tokens, 0) const jumpToSection = (i) => { document.getElementById(`ctx-sec-${i}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }) } return (
{inspectActionId ? (
Snapshot of a past turn
) : (
What will be sent on the next turn
)}
{tokens.total} / {tokens.budget} tokens {/* M6: the reply has to fit in the same window, so say how much of it is being held back for one. Without this the reader can see that the prompt fits and still get a truncated turn. */} {tokens.output_reserve > 0 && ( · {tokens.output_reserve} reserved for the reply )}
History: {history.included} of {history.total} actions in context {history.total > history.included && ' (older history trimmed)'} {history.oldest_truncated && ' — oldest entry cut mid-text'}
{cards.length > 0 && (
{cards.map((c, i) => (
▸ {c.name || '(unnamed card)'} triggered on “{c.keyword}” {!c.included && ' — dropped (over card budget)'}
))}
)} {report.memories && (
{report.memories.error && (
⚠ Memory bank: {report.memories.error}
)} {report.memories.used?.map((m, i) => (
▸ memory retrieved ({m.pinned ? 'pinned' : `similarity ${m.similarity.toFixed(2)}`}) {/* M6: what weight it carries, and where it came from. An inference must not read like a record. */} {m.authority === 'heuristic' ? · inferred : · from the story} {m.source?.depth != null && ( {' '}· turn {m.source.source_start === m.source.source_end ? m.source.depth : `${m.source.source_start}–${m.source.source_end}`} )} : {m.text.length > 90 ? m.text.slice(0, 90) + '…' : m.text}
))} {report.memories.considered != null && (
{report.memories.considered} memories eligible on this line of the story.
)} {!report.memories.error && report.memories.used?.length === 0 && (
Memory bank on — no memories retrieved.
)}
)} {/* M6: which summary was used, and what stretch of story it covers, so "what history did that summary cover?" is answerable here. */} {report.summary ? (
▸ summary in use {' '}· covers {report.summary.source_start != null ? `turns ${report.summary.source_start}–${report.summary.source_end}` : `up to turn ${report.summary.source_end ?? report.summary.depth}`} {report.summary.trigger === 'manual' ? ' · written by you' : ' · generated'}
) : (
▸ no summary is eligible for this point in the story.
)} {/* M6: a dead memory bank used to be invisible. It is not any more. */} {report.derived?.some((d) => d.status === 'failed') && (
{report.derived.filter((d) => d.status === 'failed').map((d) => (
⚠ Background {d.kind} work is failing ({d.failures} {d.failures === 1 ? ' attempt' : ' attempts'}): {d.detail}
))}
The story itself is unaffected — this only stops new {' '} {report.derived.filter((d) => d.status === 'failed') .map((d) => d.kind).join(' and ')} work being written.
)}
{sections.map((s, i) => (
{SECTION_LABELS[s.label] || s.label} {s.tokens} tok {sectionTotal > 0 && ` · ${pctLabel((s.tokens / sectionTotal) * 100)}`}
{s.text}
))} {report.raw_output && (
Raw AI output (before state block was stripped)
{report.raw_output}
)}
) } export { InsightsPanel }