From d812f340b8566e5008aaba911ba50dd747560cb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 18:00:50 +0000 Subject: [PATCH] CohortPanel: substrate above fires, three-state resolution The four-way resolution label (in-flight / window-open / aged-out) earned its keep in the diagnostics modal where cohorts were a flat list sorted by fired_at with no message context - the timing nuance was the only thing telling you whether the classifier had had its shot yet. Inline under the user message that triggered it, the transcript already encodes "this turn fired N exchanges ago": if a later user turn is visible the classifier had its shot, and if it didn't, the cohort is the latest one. Collapse to three states: confirmed, disconfirmed, pending. Same context shift moves substrate above the fires. The fires are the predictions that fed into producing this turn; the substrate is the worker's after-the-fact "what actually happened" summary. Reading order should be answer-then-working, not working-then- answer. Pulled the substrate block to the top of the panel and lifted it visually with a left-edge accent stripe + faintly tinted background, so the panel reads as one headline block followed by supporting detail rather than a flat list of equal- weight sections. Docs updated to match. --- docs/user/chat.md | 21 +++--- src/components/CohortPanel.svelte | 110 ++++++++++++++++-------------- 2 files changed, 71 insertions(+), 60 deletions(-) diff --git a/docs/user/chat.md b/docs/user/chat.md index 6f907f2..30908d4 100644 --- a/docs/user/chat.md +++ b/docs/user/chat.md @@ -308,18 +308,19 @@ stroke buttons under each assistant message. Click it to expand a panel anchored to that turn. The panel shows: - A header pill marking the cohort as **confirmed**, - **disconfirmed**, **waiting**, or **aged out**, depending on what - the reaction classifier did with it on the following turn. -- The **predictions that fired** for that turn, grouped by theme - (paraphrase clusters collapse into a representative with a - "+N similar" chevron). "Show all" bypasses the clustering and + **disconfirmed**, or **pending**, depending on what the reaction + classifier did with it on the following turn. +- The **substrate row** for the same round, lifted to the top of the + panel with an accent stripe because it's the worker's after-the- + fact summary of what actually happened on this turn ("user asked + X about Y, expressing Z" / "the assistant did W and it landed P"). + Includes its lifecycle state (pending assimilation, assimilated + but unembedded, or fully baked) and the round's valence reading. +- Below that, the **predictions that fired** for that turn, grouped + by theme (paraphrase clusters collapse into a representative with + a "+N similar" chevron). "Show all" bypasses the clustering and lists every fire individually. Each entry shows its tier, ranking score, valence, confidence, and health. -- The **substrate row** for the same round - the worker's structured - summary of what happened ("user asked X about Y, expressing Z" - / "the assistant did W and it landed P"), with its lifecycle - state (pending assimilation, assimilated but unembedded, or fully - baked) and the round's valence reading. The icon only appears on messages that produced at least one fire or substrate row, so cold-start messages and any turn the worker diff --git a/src/components/CohortPanel.svelte b/src/components/CohortPanel.svelte index 76b134d..df5d578 100644 --- a/src/components/CohortPanel.svelte +++ b/src/components/CohortPanel.svelte @@ -123,20 +123,18 @@ return `${sign}${v.toFixed(2)}`; } - // Three-state resolution label. Null means the reaction classifier - // hasn't scored this cohort yet - distinguish in-flight from aged- - // out by the 10-minute resolution window the classifier uses. - function resolutionLabel( - confirmed: boolean | null, - firedAtIso: string | null - ): string { + // Three-state resolution. The old four-way label (in-flight / + // window-open / aged-out) earned its keep in the diagnostics + // modal where cohorts were a flat list with no message context; + // inline under the user message that fired them, the transcript + // already encodes "this turn fired N exchanges ago" - if a later + // user turn is visible the classifier had its shot, and if it + // didn't, the cohort is the latest one. "pending" covers every + // unresolved state. + function resolutionLabel(confirmed: boolean | null): string { if (confirmed === true) return 'confirmed'; if (confirmed === false) return 'disconfirmed'; - if (!firedAtIso) return 'pending'; - const ageMs = Date.now() - new Date(firedAtIso).getTime(); - if (ageMs < 60 * 1000) return 'waiting (in-flight)'; - if (ageMs < 10 * 60 * 1000) return 'waiting (resolution window open)'; - return 'aged out (no reaction)'; + return 'pending'; } function resolutionStatusClass(confirmed: boolean | null): string { @@ -164,7 +162,7 @@ - {resolutionLabel(wasConfirmed, firedAt)} + {resolutionLabel(wasConfirmed)} {#if collapsed && !raw} @@ -188,6 +186,45 @@ {/if} + {#if substrate} + +
+
+ substrate + + {assimilationStatus(substrate)} + + {#if substrate.valence !== null} + + valence {formatValence(substrate.valence)} + + {/if} +
+ {#if substrate.situation} +

{substrate.situation}

+ {/if} + {#if substrate.outcome} +

+ Outcome: + {substrate.outcome} +

+ {/if} +
+ {/if} + {#if raw} {/if} - - {#if substrate} - -
-
- substrate - - {assimilationStatus(substrate)} - - {#if substrate.valence !== null} - - valence {formatValence(substrate.valence)} - - {/if} -
- {#if substrate.situation} -

{substrate.situation}

- {/if} - {#if substrate.outcome} -

- Outcome: - {substrate.outcome} -

- {/if} -
- {/if} {#snippet fireRow(fire: SamskaraFireDiagnosticRow)} @@ -434,12 +438,18 @@ font-weight: 700; } + /* Substrate emphasised as the headline read of the turn: left- + edge accent stripe + faintly tinted background lift it visually + above the fire list that follows. Padded inside the stripe so + the text doesn't crowd against the accent. */ .substrate-block { display: flex; flex-direction: column; gap: 0.25rem; - padding-top: 0.5rem; - border-top: 1px dashed var(--border); + padding: 0.45rem 0.6rem 0.5rem; + border-left: 3px solid var(--accent); + border-radius: 0 4px 4px 0; + background: color-mix(in srgb, var(--accent) 8%, transparent); } .substrate-head { display: flex; @@ -451,8 +461,8 @@ font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; - color: var(--muted); - font-weight: 600; + color: var(--accent); + font-weight: 700; } .substrate-meta { font-size: 0.72rem;