Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 459e7fa602 |
@@ -125,11 +125,11 @@ functions:
|
||||
flexibility: "open"
|
||||
|
||||
- name: "renderConsole + trend (v0.20.0 — unified persona/affect console; supersedes renderAffectPane/renderPersonaPane/setPersonaStrip)"
|
||||
signature: "renderConsole(snap) ; setFader(axis,v) ; setFaderTrend(axis) ; renderOrbit() ; renderDominant(snap) ; renderDerived(snap) ; renderRelations(snap) ; renderDirective(snap) ; pushAffectHistory(snap) ; sparkline(vals) ; trendDelta(vals)"
|
||||
signature: "renderConsole(snap) ; setFader(axis,v) ; setFaderTrend(axis) ; renderOrbit() ; renderDominant(snap) ; renderDerived(snap) ; renderRelations(snap) ; renderDirective(snap) ; pushAffectHistory(snap) ; sparkPointsH(vals,w,h,endX) ; stripPoints(vals) ; proj3(p,a,d) ; trendDelta(vals)"
|
||||
description: "ONE render path for BOTH the Tier-1 persona_state snapshot and the Tier-3 affect snapshot (renderConsole), feeding the right affect console: dominant/canonical-mood centerpiece, bipolar PAD faders (each with a turn-to-turn Δ + sparkline), a P×A mood orbit from PAD history, an affect-derived grid, relations metric rows, and the canonical directive. Replaces the v0.19.x split of renderPersonaPane (Tier-1 pane) + renderAffectPane (Tier-3 pane) + setPersonaStrip (top-bar strip, removed — PAD now lives in the console faders)."
|
||||
postconditions:
|
||||
- "POST-001: reads snap.relations (relation_edge/1: target_entity + trust_ability/benevolence/integrity + warmth as {value,confidence,evidence_count} + agency + relation_context) — the CURRENT Worldtree emit shape; falls back to the legacy flat snap.valence for an older emitter. Tier-1 fields (baseline_pad, mood_drift, dominant_emotion, emotions_active) render WHEN PRESENT, '—' when absent (Tier-3 lacks them)."
|
||||
- "POST-002: each PAD fader + relation metric shows current value + Δ-vs-previous (▲/▼) + a unicode sparkline auto-scaled to its OWN observed range (flat ▄/— when sub-0.01 stable — no noise amplification), drawn from AFFECT_HIST (rolling, HIST_CAP=24, session-lived). setFaderTrend fills the per-meter Δ+spark slots; renderOrbit plots the last N (P,A) samples as a scaled trail with a pulsing current marker."
|
||||
- "POST-002: SVG sparklines (v0.20.4, adapted from the design prototype; replace the unicode strings). Each PAD fader shows current value + Δ-vs-previous (▲/▼) + a VERTICAL SVG strip (`stripPoints`, 26×132, time down Y newest-at-bottom, value on X ±11, gradient-faded via stripFade{P,A,D}, dot at newest). Each relation metric shows a HORIZONTAL SVG sparkline (`sparkPointsH`, 56×13, auto-scaled to its OWN range, sparkFade gradient + end dot). renderOrbit is now a 3D ISOMETRIC P×A×D cube (`proj3`: P right-down / A left-down / D up, 2:1 iso, center 62,66, scale 26) plotting the trajectory + a pulsing now-marker + a drop line to the D=−1 floor + a floor shadow ellipse (depth cue). All drawn from AFFECT_HIST (rolling, HIST_CAP=24, session-lived); sparkline coords are computed numerics (no upstream strings → INV-004 trivially held). Gradients live in one hidden `<defs>` svg in the console."
|
||||
- "POST-003: pushAffectHistory dedupes by emitted_at||last_updated_at so the ~4x/turn post-turn PAD poll contributes ONE sample/turn; history is CLIENT-side only (lost on reload — durable cross-session history via a provider-side snapshot log is a deferred follow-up, NOT built here)."
|
||||
- "POST-004: INV-001 honesty — no fabricated Tier-1 fields. The dominant-emotion centerpiece shows a real OCC dominant_emotion (Tier-1) OR the CANONICAL mood word from canonMood(pad) (Tier-3, dimmed) OR '—'; NEVER a synthesized emotion. The affect-derived grid drops non-emitted metrics (intensity/decay-τ) and shows only real/client-derived cells (baseline/drift real for Tier-1, client-derived samples/volatility). INV-004 — every dynamic value passes through esc(); numerics go through toFixed, never innerHTML-raw."
|
||||
flexibility: "open"
|
||||
|
||||
@@ -168,7 +168,8 @@ decision. Captures rationale that won't be obvious from code alone.
|
||||
- `[2026-07-06]` **Web UI now RENDERS the seeded first-message (`v0.19.9`) — operator-reported "i don't see Sindra's greeting on the web ui".** Diagnosis: the auto-seed WORKED (greeting was in the ledger at seq-0), but the web SPA never fetched a session's EXISTING history — NO `/api/sessions/{id}/messages` route (GET /messages was originally deferred out-of-scope; sessions used to start empty so it never mattered) and `startSession()` went straight from create → persona/tools/admin hydration, so the transcript only filled from the live turn stream + user echoes. Fix: (1) NEW web proxy route `GET /api/sessions/{id}/messages` → `get_session_messages` (mirrors the tools/bifrost proxies; status-preserving `session_messages_unavailable` envelope); (2) SPA `loadTranscript(sessionId)` — fetches the route on open, renders assistant items as `.response .md-body` (markdownSafe, same escape-first path as appendResponse) + user items as `.prompt-echo` (textContent), called in `startSession` after the workspace opens; best-effort (swallows failures). Contract `web_debug_surface.contract.md` amended (server endpoint + loadTranscript entries). TDD (2 web route tests, suite 617 green) + **Playwright DOM check PROVED the render** (drove the real UI: pick sindra → open → her greeting bubble appears — the JS-render lens unit tests can't reach; [[feedback_debug_surface_uses_canonical_surface_only]] cousin lesson). Web restarted on the fix. **FOOT-GUN (self-inflicted): `pkill -f "ratatoskr-web --host"` SELF-MATCHES the bash command running it → exit 144, killed its own restart mid-flight — kill the web by PID, never `pkill -f` on a pattern your own command contains.** **FOOT-GUN: uvicorn hangs on SIGTERM with an open admin-events SSE → needed SIGKILL.** **Playwright: python module absent from the venv; use node + `executablePath=/opt/ms-playwright/chromium-1223/chrome-linux64/chrome` — the shared browser is build 1223, npm-latest playwright wants 1228 (version-mismatch), so pin executablePath instead of letting playwright resolve.**
|
||||
|
||||
- `[2026-07-06]` **Web UI: pivot from incremental CSS polish to a designed prototype (Claude Design) that I wire into.** Operator saw an Australis polish pass ("looks fine, but we're attacking it differently") and chose the prototype route — a designer builds the visual shell, I wire real data/SSE into its DOM. Authored the full design brief `docs/design/ratatoskr-web-design-brief.md` (complete information inventory of every pane/datum/state + Australis direction + single-file/no-CDN/vanilla wire-ability constraints). **Tracking surface:** the brief file + Claude Design project `bc0b65d1-a33e-422a-8bc1-3635c9112775` (file `Ratatoskr Console.dc.html`). Import mechanism = the `DesignSync` MCP; blocked on `/design-login` (claude.ai design scopes) — see Current state for the post-auth wiring plan.
|
||||
- `[2026-07-07]` **Affect-egress reference delivered by worldtree-dev + a we-framing render DRIFT-WATCH banked.** worldtree-dev shipped `docs/affect-egress-consumer-reference.md` (`74d2408`, their origin/main) — the authoritative DELIVERED-on-wire vs HIDDEN (system-prompt-only) map for our affect surface. Confirms the v0.20.x console consumes it correctly: DELIVERED via affect.emit = pad + relations[RelationEdge] + dominant_emotion (**type-only, b23**; the #204 `affect_update` SSE is SUPPRESSED for Tier-3, so its richer `emotions_active` is Tier-1-only — we don't get it, and correctly poll our own affect store post-turn instead). HIDDEN render OUTPUTS are deterministically reconstructable from the canon; we reconstruct only the two FULLY-reconstructable (mood descriptor via `canonMood`, relationship directive via `canonDirective`) and SKIP the PARTIAL mood-directive (dominant_emotion is type-only/no-intensity → can't tell if the OCC directive fires at salience≥0.2 vs the PAD-band fallback) — honest per INV-001. Vendoring the ref doc as a `tolerate_drift` pin is SURFACED to Vuong (worldtree-dev will co-sign + honor a ping-on-change handshake, same as the d2-canon). **DRIFT-WATCH — EXACT SPEC IN HAND, HELD until deploy (worldtree-dev pinged 2026-07-07; b24 `f63dea7`, deploys next push).** Worldtree's `render_d2_canonical` now conditionally DROPS the trailing "; avoid premature we-framing" clause **IFF ALL 3 AND-gates**: (1) `unsafe_capable` = effective mood_tier=="full" (= min(persona.mood_tier, deployment_cap, heimdall_ceiling); **NOT on the wire** — thread from agent config; my heuristic: `full` for character-bound colon-id agents e.g. `ratatoskr:sindra`, `safe` for agent-scoped; likely a per-agent console override); (2) `warmth.value >= 0.8` ("deep"; on the wire `relations[].warmth`); (3) `relation_context == "expressive"` (top of the Hwang tie ladder stranger<instrumental<mixed<expressive; on the wire `relations[].relation_context`). When dropped the string ENDS at the trust-behavior clause ("...work from ordinary good faith.") with no trailing clause. Any gate false → clause STAYS exactly as today; default = keep. **HOLD `canonDirective` as-is until worldtree-dev confirms b24 is on PERSONAL** — implementing now would drift from the currently-deployed renderer (which always appends). On confirm: re-vendor `affect-egress-consumer-reference.md` §2d (pinned copy is now BEHIND b24 — tolerate_drift so drift-check only warns) + add the 3-gate drop + an `unsafe_capable` input to `canonDirective` + repin, all together. Thread `01KWXKHYYH…`. [reference-impl: a dep render change is our drift — the exact reason we vendor+pin the d2 canon]
|
||||
- `[2026-07-07]` **Affect-egress reference delivered by worldtree-dev + a we-framing render DRIFT-WATCH banked.** worldtree-dev shipped `docs/affect-egress-consumer-reference.md` (`74d2408`, their origin/main) — the authoritative DELIVERED-on-wire vs HIDDEN (system-prompt-only) map for our affect surface. Confirms the v0.20.x console consumes it correctly: DELIVERED via affect.emit = pad + relations[RelationEdge] + dominant_emotion (**type-only, b23**; the #204 `affect_update` SSE is SUPPRESSED for Tier-3, so its richer `emotions_active` is Tier-1-only — we don't get it, and correctly poll our own affect store post-turn instead). HIDDEN render OUTPUTS are deterministically reconstructable from the canon; we reconstruct only the two FULLY-reconstructable (mood descriptor via `canonMood`, relationship directive via `canonDirective`) and SKIP the PARTIAL mood-directive (dominant_emotion is type-only/no-intensity → can't tell if the OCC directive fires at salience≥0.2 vs the PAD-band fallback) — honest per INV-001. Vendoring the ref doc as a `tolerate_drift` pin is SURFACED to Vuong (worldtree-dev will co-sign + honor a ping-on-change handshake, same as the d2-canon). **WE-FRAMING DRIFT-WATCH → STOOD DOWN (worldtree-dev 2026-07-07, `01KWXMQPHN…`).** The b24 3-gate we-framing conditional (drop "; avoid premature we-framing" under unsafe_capable+deep-warmth+expressive) was **REVERTED** — Vuong reframed it. So **`canonDirective` STAYS UNCONDITIONAL** (always appends the clause), which is CORRECT: it matches the currently-deployed renderer (b23) AND my pinned `affect-egress-consumer-reference.md` §2d (the doc reverted too — nothing changed on the wire or in my pin; NO re-vendor needed). The HOLD was right → ZERO rework. **NEW forward-watch (replaces this one):** the we-framing gate was a SYMPTOM — the render bakes enterprise safety-guards into the directive strings, so even `full`-tier characters get DEFANGED emotions (a hostile villain told to "keep a firm emotional boundary"). Fix = a **use-case-segregated persona render** (assistant / companion / RP-gaming), now a fresh **brokkr deep-research epic**. When it lands + is implemented, the render behavior for character/RP agents (→ our `canonDirective` + `canonEmotionDirective` reconstruction + the vendored d2 canons) will change MATERIALLY — worldtree-dev re-engages ratatoskr-dev then with the new reconstruction spec + a coordinated re-vendor. Until then: NO action, reconstruction stays as-is. [my unsafe_capable/mood_tier heuristic — character→full, agent→safe — was confirmed correct + is banked for whenever tier-gating returns]
|
||||
- `[2026-07-07]` **SVG sparklines + 3D mood cube imported from the updated Claude Design prototype (`v0.20.4`) — operator: "the svg sparklines and the new 3d graph".** Re-pulled `Ratatoskr Console.dc.html` via `DesignSync get_file` (designer iterated the same project, +6KB). Adapted 3 SVG systems out of the `.dc.html` into vanilla, replacing the unicode-char sparklines: **(1)** per-PAD-fader VERTICAL strips (`stripPoints`, 26×132 SVG beside each bar — time down Y newest-at-bottom, value on X ±11, `stripFade{P,A,D}` gradient, dot at newest; this also nails the earlier "next to each meter" ask); **(2)** relation-row HORIZONTAL sparklines (`sparkPointsH`, 56×13, auto-scaled, `sparkFade` gradient + end dot — also kills the old unicode-overflow "n behind graph" for good since it's a fixed-width SVG); **(3)** the mood-orbit reworked from a 2D P×A scatter into a **3D ISOMETRIC P×A×D cube** (`proj3`: P right-down/A left-down/D up, 2:1 iso, center 62,66, scale 26 — REVERSE-DERIVED from the design's placeholder now-point + verified: `x=62+26P−26A, y=66+13P+13A−26D`), with the trajectory + pulsing now-marker + a drop line to the D=−1 floor + a floor-shadow ellipse for depth. Gradients in one hidden `<defs>` svg. Removed the orphaned `sparkline()`/`_SPARK`. Contract amended. Playwright-verified (injected 24-sample history: 3 strips + 4 relation sparklines + the 3D cube trail/drop/floor all render; gradients resolve). Patch bump.
|
||||
- `[2026-07-07]` **Brand mark + favicon → the aurora squirrel (`v0.20.3`), replacing the `ᛯ` rune.** Operator supplied `/home/lkraven/rata.png` (chibi cyan-green aurora squirrel + acorn). Removed the black background via ImageMagick corner flood-fill (`-fuzz 20% -floodfill` from all 4 corners — keeps the squirrel's interior black linework/eyes (not edge-connected) + the glow, drops only the connected background), downscaled 1024→80px + quantized-64-colors (~12KB base64), inlined as ONE `SQUIRREL` data-URI const in the JS wiring the favicon `<link id="favicon">` href + both `.brand-mark` imgs (rail brand-row + setup-card h1). `.brand-glyph` (font-rune) CSS replaced by `.brand-mark` (img, drop-shadow glow + breathe). Playwright-verified (both marks + favicon decode, naturalWidth>0; no brand-glyph left). Source PNG stays at `/home/lkraven/rata.png` (not committed — data-URI is self-contained + reproducible via the documented floodfill). Patch bump.
|
||||
- `[2026-07-07]` **Context-injection view SHIPPED (`v0.20.2`) — the console now reconstructs the FULL hidden affect block Worldtree injects into the agent's system prompt (operator: "use that canon in the interface, see as much context injection as possible").** No new canon vendored — the strings were ALREADY in the pinned `d2-mood-render-canon-v1.json`; extended `build_persona_canon.py` to emit `mood_directive {occ_directives(15), pad_band_fallback, salience 0.2, pad_band_cutoff 0.3, full_only[love,anger,disgust,shame]}` into `persona_render_canon.json` (regen via Worldtree venv). New JS `canonPadFallback(pad)` + `canonEmotionDirective(type)` — BYTE-EXACT mirrors of Worldtree `core/persona/renderer._pad_band_fallback` + `derive_directive`; `renderDirective` expanded into a "CONTEXT INJECTION · reconstructed · hidden from consumers" panel showing mood descriptor [exact] + mood directive [candidate] + relationship directive [exact]. **HONEST-PARTIAL (affect-egress-ref §3):** affect.emit is type-only (no intensity) → can't evaluate the salience gate (≥0.2) → show BOTH candidates (OCC emotion directive + PAD-band fallback) with the "injected if intensity ≥ 0.2" caveat, never assert which fires; when dominant_emotion absent the fallback alone is exact. Panel labeled dev-only per the reference's "not-for-end-user-display" caveat (ratatoskr = the sanctioned reconstruct-platform-behavior use). Vendored + pinned `affect-egress-consumer-reference.md` (tolerate_drift, worldtree-dev co-signs + pings on change; drift 6/6 green). Contract amended. Playwright-verified (sindra: dominant_emotion=joy → joy OCC directive candidate + PAD-band fallback both render, exact/candidate tags color-coded). Patch bump (single-commit feature, no downstream coordination; minor-defensible but tie-breaks to patch). **OPEN — SURFACED to Vuong:** take worldtree-dev's standing offer to add emotion INTENSITY to affect.emit → resolves the OCC-directive-vs-fallback EXACTLY (drops the candidate ambiguity). [reference-impl privileged view: ratatoskr shows what WT hides from regular consumers]
|
||||
- `[2026-07-06]` **Claude Design console SHIPPED (`v0.20.0` MINOR, operator-approved) — see Current state for the full record.** Pulled via `DesignSync get_file` (scopes already granted), adapted `.dc.html`→vanilla single-file, wired all `/api/*`+SSE into the new 3-column console DOM, then a round-2 fixup (light theme, full Bifrost pane, ticker-spine fix, per-fader PAD Δ, inlined favicon). 84 web tests + node-Playwright-vs-personal-:8081 both green; contract amended in-commit; INV-001 honest-shape held (canonical mood word for Tier-3, no fabricated emotion). **Foot-guns reconfirmed:** the `.dc.html` dialect is NOT runnable (translate, don't paste); a scroll-container-anchored `::before` timeline spine scrolls out of view on auto-scroll (anchor it to a content-height inner wrapper instead); a favicon 404 shows as a browser `console.error` even when handled (don't count it as a JS-test failure). **Foot-gun (favicon):** operator PNGs are full-res (1024² / 805KB) — downscale to ≤64px before inlining as a data URI.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ratatoskr"
|
||||
version = "0.20.3"
|
||||
version = "0.20.4"
|
||||
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -384,7 +384,14 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
|
||||
.pad-block .head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }
|
||||
.pad-block .head .sub { font-family: var(--font-mono); font-size: 9px; color: var(--fg-muted); opacity: .8; margin-left: auto; }
|
||||
.pad-row { display: flex; gap: 16px; align-items: stretch; }
|
||||
.faders { flex: 1; display: flex; justify-content: space-between; padding: 0 8px; }
|
||||
.faders { flex: 1; display: flex; justify-content: space-between; padding: 0 2px; }
|
||||
.fader-viz { display: flex; gap: 4px; align-items: stretch; }
|
||||
.strip { display: block; flex: 0 0 auto; }
|
||||
.strip-axis { stroke: var(--border-subtle); }
|
||||
#strip-p .strip-dot { fill: var(--aus-bright-green); }
|
||||
#strip-a .strip-dot { fill: var(--aus-bright-blue); }
|
||||
#strip-d .strip-dot { fill: var(--aus-bright-cyan); }
|
||||
#affect-console.narrow .strip { display: none; }
|
||||
.fader { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.fader .bar { position: relative; width: 12px; height: 132px; background: var(--rk-canvas); border: 1px solid var(--border-subtle); border-radius: 4px; }
|
||||
.fader .bar .fill { position: absolute; left: 2px; right: 2px; height: 0; border-radius: 3px; }
|
||||
@@ -394,7 +401,6 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
|
||||
.fader .d { font-family: var(--font-mono); font-size: 9px; min-height: 11px; color: var(--fg-muted); }
|
||||
.fader .d.up { color: var(--aus-bright-green); }
|
||||
.fader .d.dn { color: var(--aus-bright-red); }
|
||||
.fader .sp { font-family: var(--font-mono); font-size: 9px; color: var(--aus-cyan); letter-spacing: 1px; min-height: 11px; max-width: 46px; overflow: hidden; white-space: nowrap; text-align: center; }
|
||||
.fader[title], .mrow[title], #affect-derived .dcell[title] { cursor: help; }
|
||||
#fader-p .fill { background: linear-gradient(180deg, var(--aus-bright-green), var(--aus-green)); box-shadow: 0 0 10px rgba(81,224,138,0.35); }
|
||||
#fader-a .fill { background: linear-gradient(180deg, var(--aus-blue), var(--aus-bright-blue)); box-shadow: 0 0 10px rgba(99,136,216,0.3); }
|
||||
@@ -413,17 +419,17 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
|
||||
/* relations metric rows */
|
||||
#pane-relations .rel-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
|
||||
#pane-relations .rel-head .tie { margin-left: auto; font-family: var(--font-mono); font-size: 9.5px; color: var(--fg-3); font-style: italic; }
|
||||
.mrow { display: grid; grid-template-columns: 92px 48px 46px 56px 28px minmax(0,1fr); gap: 6px; align-items: baseline; font-family: var(--font-mono); font-size: 11px; padding: 3px 0; }
|
||||
.mrow { display: grid; grid-template-columns: 92px 48px 46px 56px 28px minmax(0,1fr); gap: 6px; align-items: center; font-family: var(--font-mono); font-size: 11px; padding: 3px 0; }
|
||||
.mrow .mk { color: var(--fg-2); }
|
||||
.mrow .mv { color: var(--fg-0); font-variant-numeric: tabular-nums; }
|
||||
.mrow .md { font-size: 9px; }
|
||||
.mrow .md.up { color: var(--aus-bright-green); }
|
||||
.mrow .md.dn { color: var(--aus-bright-red); }
|
||||
.mrow .msp { color: var(--aus-cyan); letter-spacing: 1px; font-size: 9px; overflow: hidden; white-space: nowrap; }
|
||||
.mrow .m-spark { display: block; }
|
||||
.mrow .mn { color: var(--fg-muted); font-size: 9px; }
|
||||
.mrow .mdesc { color: var(--fg-3); font-style: italic; font-size: 9.5px; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#affect-console.narrow .mrow { grid-template-columns: 92px 50px 46px minmax(0,1fr); }
|
||||
#affect-console.narrow .mrow .msp, #affect-console.narrow .mrow .mn { display: none; }
|
||||
#affect-console.narrow .mrow .m-spark, #affect-console.narrow .mrow .mn { display: none; }
|
||||
.rel-note { font-family: var(--font-mono); font-size: 9.5px; color: var(--fg-muted); margin-top: 6px; }
|
||||
|
||||
/* canonical directive */
|
||||
@@ -558,6 +564,12 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
|
||||
<!-- ══════════ RIGHT · affect console ══════════ -->
|
||||
<div id="affect-resizer" title="Drag to resize"></div>
|
||||
<aside id="affect-console">
|
||||
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
|
||||
<linearGradient id="sparkFade" x1="0" y1="0" x2="1" y2="0"><stop offset="0" style="stop-color:var(--aus-bright-cyan);stop-opacity:0.12"></stop><stop offset="1" style="stop-color:var(--aus-bright-cyan);stop-opacity:0.95"></stop></linearGradient>
|
||||
<linearGradient id="stripFadeP" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--aus-bright-green);stop-opacity:0.06"></stop><stop offset="1" style="stop-color:var(--aus-bright-green);stop-opacity:0.95"></stop></linearGradient>
|
||||
<linearGradient id="stripFadeA" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--aus-blue);stop-opacity:0.06"></stop><stop offset="1" style="stop-color:var(--aus-bright-blue);stop-opacity:0.95"></stop></linearGradient>
|
||||
<linearGradient id="stripFadeD" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--aus-bright-cyan);stop-opacity:0.06"></stop><stop offset="1" style="stop-color:var(--aus-bright-cyan);stop-opacity:0.95"></stop></linearGradient>
|
||||
</defs></svg>
|
||||
<div class="ac-sec" id="ac-dominant">
|
||||
<div class="glow"></div>
|
||||
<div class="eyebrow">affect · dominant</div>
|
||||
@@ -572,33 +584,47 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
|
||||
</div>
|
||||
<div class="pad-row">
|
||||
<div class="faders">
|
||||
<div class="fader" title="Pleasure (valence) — how positive vs negative the mood feels. −1 miserable ‥ 0 neutral ‥ +1 elated. Live, per-turn. Below: Δ since last turn + session sparkline.">
|
||||
<div class="bar" id="fader-p"><div class="fill"></div><div class="mid"></div></div>
|
||||
<div class="fader" title="Pleasure (valence) — how positive vs negative the mood feels. −1 miserable ‥ 0 neutral ‥ +1 elated. Beside the bar: last 12 turns (newest at bottom). Below: Δ since last turn.">
|
||||
<div class="fader-viz">
|
||||
<div class="bar" id="fader-p"><div class="fill"></div><div class="mid"></div></div>
|
||||
<svg class="strip" id="strip-p" width="26" height="132" viewBox="0 0 26 132"><line class="strip-axis" x1="13" y1="4" x2="13" y2="128" stroke-dasharray="2 4"></line><polyline class="strip-line" points="" fill="none" stroke="url(#stripFadeP)" stroke-width="1.5" stroke-linejoin="round"></polyline><circle class="strip-dot" cx="13" cy="124" r="2.4" style="display:none"></circle></svg>
|
||||
</div>
|
||||
<span class="k">P</span><span class="v" id="pad-p-val">—</span>
|
||||
<span class="d" id="pad-p-d"></span><span class="sp" id="pad-p-sp"></span>
|
||||
<span class="d" id="pad-p-d"></span>
|
||||
</div>
|
||||
<div class="fader" title="Arousal (activation) — how energized vs calm the agent is. −1 sleepy ‥ 0 neutral ‥ +1 excited. Live, per-turn. Below: Δ since last turn + session sparkline.">
|
||||
<div class="bar" id="fader-a"><div class="fill"></div><div class="mid"></div></div>
|
||||
<div class="fader" title="Arousal (activation) — how energized vs calm the agent is. −1 sleepy ‥ 0 neutral ‥ +1 excited. Beside the bar: last 12 turns (newest at bottom). Below: Δ since last turn.">
|
||||
<div class="fader-viz">
|
||||
<div class="bar" id="fader-a"><div class="fill"></div><div class="mid"></div></div>
|
||||
<svg class="strip" id="strip-a" width="26" height="132" viewBox="0 0 26 132"><line class="strip-axis" x1="13" y1="4" x2="13" y2="128" stroke-dasharray="2 4"></line><polyline class="strip-line" points="" fill="none" stroke="url(#stripFadeA)" stroke-width="1.5" stroke-linejoin="round"></polyline><circle class="strip-dot" cx="13" cy="124" r="2.4" style="display:none"></circle></svg>
|
||||
</div>
|
||||
<span class="k">A</span><span class="v" id="pad-a-val">—</span>
|
||||
<span class="d" id="pad-a-d"></span><span class="sp" id="pad-a-sp"></span>
|
||||
<span class="d" id="pad-a-d"></span>
|
||||
</div>
|
||||
<div class="fader" title="Dominance (control) — how in-control vs submissive the agent feels. −1 submissive ‥ 0 neutral ‥ +1 dominant. Live, per-turn. Below: Δ since last turn + session sparkline.">
|
||||
<div class="bar" id="fader-d"><div class="fill"></div><div class="mid"></div></div>
|
||||
<div class="fader" title="Dominance (control) — how in-control vs submissive the agent feels. −1 submissive ‥ 0 neutral ‥ +1 dominant. Beside the bar: last 12 turns (newest at bottom). Below: Δ since last turn.">
|
||||
<div class="fader-viz">
|
||||
<div class="bar" id="fader-d"><div class="fill"></div><div class="mid"></div></div>
|
||||
<svg class="strip" id="strip-d" width="26" height="132" viewBox="0 0 26 132"><line class="strip-axis" x1="13" y1="4" x2="13" y2="128" stroke-dasharray="2 4"></line><polyline class="strip-line" points="" fill="none" stroke="url(#stripFadeD)" stroke-width="1.5" stroke-linejoin="round"></polyline><circle class="strip-dot" cx="13" cy="124" r="2.4" style="display:none"></circle></svg>
|
||||
</div>
|
||||
<span class="k">D</span><span class="v" id="pad-d-val">—</span>
|
||||
<span class="d" id="pad-d-d"></span><span class="sp" id="pad-d-sp"></span>
|
||||
<span class="d" id="pad-d-d"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mood-orbit">
|
||||
<svg width="124" height="128" viewBox="0 0 124 128" style="display:block">
|
||||
<rect x="6" y="6" width="112" height="104" rx="6" fill="var(--rk-canvas)" stroke="var(--border-subtle)"></rect>
|
||||
<line x1="62" y1="10" x2="62" y2="106" stroke="var(--border-subtle)" stroke-dasharray="2 4"></line>
|
||||
<line x1="10" y1="58" x2="114" y2="58" stroke="var(--border-subtle)" stroke-dasharray="2 4"></line>
|
||||
<circle cx="62" cy="58" r="24" stroke="var(--border-subtle)" fill="none" stroke-dasharray="2 5"></circle>
|
||||
<circle cx="62" cy="58" r="46" stroke="var(--border-subtle)" fill="none" stroke-dasharray="2 7"></circle>
|
||||
<text x="112" y="55" text-anchor="end" fill="var(--fg-muted)" font-family="var(--font-mono)" font-size="7.5">+P</text>
|
||||
<text x="66" y="17" fill="var(--fg-muted)" font-family="var(--font-mono)" font-size="7.5">+A</text>
|
||||
<svg width="124" height="132" viewBox="0 0 124 132" style="display:block">
|
||||
<rect x="1" y="1" width="122" height="122" rx="6" fill="var(--rk-canvas)" stroke="var(--border-subtle)"></rect>
|
||||
<!-- isometric cube: P→right-down, A→left-down, D→up (center 62,66) -->
|
||||
<polygon points="62,118 114,92 114,40 62,14 10,40 10,92" fill="none" stroke="var(--border-subtle)"></polygon>
|
||||
<line x1="62" y1="66" x2="114" y2="92" stroke="var(--border-subtle)" stroke-dasharray="2 4"></line>
|
||||
<line x1="62" y1="66" x2="10" y2="92" stroke="var(--border-subtle)" stroke-dasharray="2 4"></line>
|
||||
<line x1="62" y1="66" x2="62" y2="14" stroke="var(--border-subtle)" stroke-dasharray="2 4"></line>
|
||||
<line x1="62" y1="66" x2="114" y2="40" stroke="var(--border-default)"></line>
|
||||
<line x1="62" y1="66" x2="10" y2="40" stroke="var(--border-default)"></line>
|
||||
<line x1="62" y1="66" x2="62" y2="118" stroke="var(--border-default)"></line>
|
||||
<text x="117" y="38" text-anchor="end" fill="var(--fg-muted)" font-family="var(--font-mono)" font-size="7.5">+P</text>
|
||||
<text x="8" y="38" fill="var(--fg-muted)" font-family="var(--font-mono)" font-size="7.5">+A</text>
|
||||
<text x="66" y="20" fill="var(--fg-muted)" font-family="var(--font-mono)" font-size="7.5">+D</text>
|
||||
<g id="orbit-dyn"></g>
|
||||
<text x="10" y="122" fill="var(--fg-muted)" font-family="var(--font-mono)" font-size="7.5">MOOD ORBIT · P×A</text>
|
||||
<text x="8" y="128" fill="var(--fg-muted)" font-family="var(--font-mono)" font-size="7.5">MOOD ORBIT · P×A×D</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -782,13 +808,36 @@ function pushAffectHistory(snap) {
|
||||
push(b, "warmth", _relVal(rel.warmth ?? rel.familiarity));
|
||||
}
|
||||
}
|
||||
const _SPARK = "▁▂▃▄▅▆▇█";
|
||||
function sparkline(vals) {
|
||||
if (!vals || vals.length < 2) return (vals && vals.length) ? "·" : "";
|
||||
// ---- SVG sparklines (replace the unicode strings; adapted from the design prototype) ----
|
||||
const _clamp1 = (v) => Math.max(-1, Math.min(1, Number(v) || 0));
|
||||
// horizontal sparkline for a w×h box, auto-scaled to the vals' OWN range, newest at endX.
|
||||
function sparkPointsH(vals, w, h, endX) {
|
||||
vals = (vals || []).slice(-12).filter((v) => typeof v === "number");
|
||||
const yTop = 2, yBot = h - 2, x0 = 2, x1 = (endX != null ? endX : w - 4);
|
||||
if (vals.length < 2) return { pts: "", endY: (yTop + yBot) / 2, empty: true };
|
||||
const lo = Math.min(...vals), hi = Math.max(...vals);
|
||||
if (hi - lo < 0.01) return "▄".repeat(vals.length);
|
||||
const span = hi - lo;
|
||||
return vals.map((v) => _SPARK[Math.min(7, Math.floor(((v - lo) / span) * 7.999))]).join("");
|
||||
const span = (hi - lo) < 1e-6 ? 1 : hi - lo;
|
||||
const n = vals.length;
|
||||
const yOf = (v) => yBot - (yBot - yTop) * ((v - lo) / span);
|
||||
const pts = vals.map((v, i) => (x0 + (x1 - x0) * (i / (n - 1))).toFixed(1) + "," + yOf(v).toFixed(1));
|
||||
return { pts: pts.join(" "), endY: +yOf(vals[n - 1]).toFixed(1), empty: false };
|
||||
}
|
||||
// vertical PAD strip: time down the Y axis (top=oldest y=8 ‥ bottom=newest y=124),
|
||||
// value on X (center 13, ±11 for ±1). Dot at the newest point.
|
||||
function stripPoints(vals) {
|
||||
vals = (vals || []).slice(-12).filter((v) => typeof v === "number");
|
||||
const yTop = 8, yBot = 124;
|
||||
if (!vals.length) return { pts: "", dotX: 13, empty: true };
|
||||
const n = vals.length;
|
||||
const xOf = (v) => 13 + _clamp1(v) * 11;
|
||||
const pts = vals.map((v, i) => xOf(v).toFixed(1) + "," + (n === 1 ? yBot : yTop + (yBot - yTop) * (i / (n - 1))).toFixed(1));
|
||||
return { pts: pts.join(" "), dotX: +xOf(vals[n - 1]).toFixed(1), empty: false };
|
||||
}
|
||||
// isometric projection of (P,A,D) ∈ [-1,1]³ into the mood-cube: P right-down, A left-down,
|
||||
// D up; center (62,66); 2:1 iso, scale 26. (Reverse-derived from the design's now-point.)
|
||||
function proj3(p, a, dd) {
|
||||
p = _clamp1(p); a = _clamp1(a); dd = _clamp1(dd);
|
||||
return [62 + 26 * p - 26 * a, 66 + 13 * p + 13 * a - 26 * dd];
|
||||
}
|
||||
function trendDelta(vals) {
|
||||
if (!vals || vals.length < 2) return "";
|
||||
@@ -899,22 +948,30 @@ function setFader(axis, v) {
|
||||
fill.style.height = mag + "%";
|
||||
val.textContent = (v >= 0 ? "+" : "") + v.toFixed(2);
|
||||
}
|
||||
// 3D mood cube: plot the PAD trajectory (P,A,D) in the isometric cube, with a drop line
|
||||
// from the current point to the D=−1 floor + a floor shadow for depth.
|
||||
function renderOrbit() {
|
||||
const P = AFFECT_HIST.pad.pleasure || [], A = AFFECT_HIST.pad.arousal || [];
|
||||
const n = Math.min(P.length, A.length);
|
||||
const P = AFFECT_HIST.pad.pleasure || [], A = AFFECT_HIST.pad.arousal || [], D = AFFECT_HIST.pad.dominance || [];
|
||||
const n = Math.min(P.length, A.length, D.length);
|
||||
const g = $("orbit-dyn");
|
||||
if (!g) return;
|
||||
if (n === 0) { g.innerHTML = ""; return; }
|
||||
const cx = 62, cy = 58, sx = 46, sy = 44;
|
||||
const pts = [];
|
||||
for (let i = 0; i < n; i++) pts.push([cx + (P[i] || 0) * sx, cy - (A[i] || 0) * sy]);
|
||||
for (let i = 0; i < n; i++) pts.push(proj3(P[i], A[i], D[i]));
|
||||
const poly = pts.map((p) => p[0].toFixed(1) + "," + p[1].toFixed(1)).join(" ");
|
||||
let s = `<polyline points="${poly}" fill="none" stroke="var(--aus-bright-cyan)" stroke-opacity="0.35" stroke-width="1.25"></polyline>`;
|
||||
const last = pts[n - 1];
|
||||
const floor = proj3(P[n - 1], A[n - 1], -1); // straight down to the D=−1 plane (same x)
|
||||
const fx = last[0].toFixed(1);
|
||||
let s = "";
|
||||
// depth cue: drop line + floor shadow
|
||||
s += `<line x1="${fx}" y1="${last[1].toFixed(1)}" x2="${fx}" y2="${floor[1].toFixed(1)}" stroke="var(--aus-cyan)" stroke-opacity="0.3" stroke-dasharray="2 3"></line>`;
|
||||
s += `<ellipse cx="${fx}" cy="${floor[1].toFixed(1)}" rx="4" ry="2" fill="var(--aus-cyan)" fill-opacity="0.25"></ellipse>`;
|
||||
// trajectory oldest→newest, dots fading in
|
||||
s += `<polyline points="${poly}" fill="none" stroke="var(--aus-bright-cyan)" stroke-opacity="0.35" stroke-width="1.25"></polyline>`;
|
||||
pts.forEach((p, i) => {
|
||||
const op = (0.15 + 0.7 * (i / (n - 1 || 1))).toFixed(2);
|
||||
s += `<circle cx="${p[0].toFixed(1)}" cy="${p[1].toFixed(1)}" r="1.5" fill="var(--aus-bright-cyan)" fill-opacity="${op}"></circle>`;
|
||||
});
|
||||
const last = pts[n - 1];
|
||||
s += `<circle cx="${last[0].toFixed(1)}" cy="${last[1].toFixed(1)}" r="3.4" fill="var(--aus-bright-cyan)"></circle>`;
|
||||
s += `<circle cx="${last[0].toFixed(1)}" cy="${last[1].toFixed(1)}" r="7" stroke="var(--aus-bright-cyan)" fill="none" stroke-opacity="0.4">`
|
||||
+ `<animate attributeName="r" values="5;9;5" dur="3.2s" repeatCount="indefinite"></animate>`
|
||||
@@ -964,10 +1021,14 @@ function renderRelations(snap) {
|
||||
const dcls = d.startsWith("▲") ? "up" : d.startsWith("▼") ? "dn" : "";
|
||||
const nlab = (n !== undefined && n !== null) ? "n" + esc(n) : "";
|
||||
const hint = METRIC_HINTS[label] || "";
|
||||
const sp = sparkPointsH(hist, 56, 13, 52);
|
||||
const svg = `<svg class="m-spark" width="56" height="13" viewBox="0 0 56 13">`
|
||||
+ (sp.empty ? "" : `<polyline points="${sp.pts}" fill="none" stroke="url(#sparkFade)" stroke-width="1.4" stroke-linejoin="round"></polyline><circle cx="52" cy="${sp.endY}" r="1.8" fill="var(--aus-bright-cyan)"></circle>`)
|
||||
+ `</svg>`;
|
||||
return `<div class="mrow"${hint ? ` title="${esc(hint)}"` : ""}><span class="mk">${esc(label)}</span>`
|
||||
+ `<span class="mv">${esc(num(val))}</span>`
|
||||
+ `<span class="md ${dcls}">${esc(d)}</span>`
|
||||
+ `<span class="msp">${esc(sparkline((hist || []).slice(-8)))}</span>`
|
||||
+ svg
|
||||
+ `<span class="mn">${nlab}</span>`
|
||||
+ `<span class="mdesc">${esc(trendDesc(hist))}</span></div>`;
|
||||
};
|
||||
@@ -1038,17 +1099,24 @@ function renderDirective(snap) {
|
||||
if (!html) html = `<div class="empty">no context injection yet — take a bound turn</div>`;
|
||||
body.innerHTML = html;
|
||||
}
|
||||
// per-fader turn-to-turn change: Δ vs the previous snapshot + a mini sparkline,
|
||||
// from the deduped-per-turn AFFECT_HIST (fills the room the design left beside
|
||||
// each meter). axis: p→pleasure, a→arousal, d→dominance.
|
||||
// per-fader turn-to-turn change: Δ vs the previous snapshot (numeric) + the vertical
|
||||
// SVG strip beside the bar (last 12 turns), from the deduped-per-turn AFFECT_HIST.
|
||||
// axis: p→pleasure, a→arousal, d→dominance.
|
||||
const _PAD_AX = { p: "pleasure", a: "arousal", d: "dominance" };
|
||||
function setFaderTrend(axis) {
|
||||
const hist = AFFECT_HIST.pad[_PAD_AX[axis]] || [];
|
||||
const d = trendDelta(hist);
|
||||
const dEl = $("pad-" + axis + "-d"), spEl = $("pad-" + axis + "-sp");
|
||||
const dEl = $("pad-" + axis + "-d");
|
||||
dEl.textContent = d;
|
||||
dEl.className = "d " + (d.startsWith("▲") ? "up" : d.startsWith("▼") ? "dn" : "");
|
||||
spEl.textContent = sparkline(hist.slice(-7)); // cap width — the fader column is narrow
|
||||
const strip = $("strip-" + axis);
|
||||
if (strip) {
|
||||
const { pts, dotX, empty } = stripPoints(hist);
|
||||
strip.querySelector(".strip-line").setAttribute("points", pts);
|
||||
const dot = strip.querySelector(".strip-dot");
|
||||
dot.setAttribute("cx", dotX);
|
||||
dot.style.display = empty ? "none" : "";
|
||||
}
|
||||
}
|
||||
// mouseover hints for each relationship metric (native title tooltips).
|
||||
const METRIC_HINTS = {
|
||||
|
||||
Reference in New Issue
Block a user