mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
feat(dashboard): Phase 2 — alive shell, translucent panels, living dead-states
Make the WebGPU violet field breathe THROUGH the UI and turn the dead offline/empty/error states into intentional, alive moments: - Glass veil-ramp: panel tops drop to ~0.30 opacity so the field crests through every header, while body text composites over a ~0.82 floor — more alive AND more legible than the old flat 0.45. New .veil/.veil-strong/.text-legible legibility utilities. - edge-live membrane: a state-colored synapse rim (indigo idle, synapse live, dream violet, amber alarm) + rail-dormant desaturate when offline. Shell mood derived from connection/dream state. - LifeState component replaces raw "Error: API 500"/"Offline"/"Select a run": a calm "Dormant" breathing state, a self-healing error with the raw detail demoted to a collapsed <details> + Retry, and a "Listening for the agent" offline state wired to the firewall demo. - Legibility fixes from the a11y audit: sidebar metrics text-muted->text-dim, LifeState body->text, detail->dim + veil-strong, ambient strip + text-legible. All new animation prefers-reduced-motion guarded. Memory Cinema, Black Box machinery, Graph3D, and the backdrop core untouched. svelte-check 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a68895482d
commit
f5ce349cac
5 changed files with 415 additions and 97 deletions
|
|
@ -106,44 +106,71 @@ body {
|
|||
GLASSMORPHISM SYSTEM
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
/* ── Veil + living-edge tokens — one source of truth ──────────────── */
|
||||
:root {
|
||||
/* Veil ramp: field shows through the TOP of a panel, text reads below */
|
||||
--veil-top: rgba(12, 12, 32, 0.30); /* field-visible band */
|
||||
--veil-body: rgba(12, 12, 32, 0.82); /* opaque text floor */
|
||||
--veil-ramp: 64px; /* where the ramp goes opaque */
|
||||
|
||||
/* State-colored panel rims (the "nervous system") */
|
||||
--edge-quiet: rgba(99, 102, 241, 0.22); /* idle synapse rim */
|
||||
--edge-live: rgba(129, 140, 248, 0.55); /* connected / active */
|
||||
--edge-dream: rgba(192, 132, 252, 0.50); /* dreaming */
|
||||
--edge-alarm: rgba(245, 158, 11, 0.55); /* offline/error = AMBER, not red */
|
||||
|
||||
--glass-blur: 16px;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(22, 22, 56, 0.45);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(99, 102, 241, 0.08);
|
||||
/* was rgba(22,22,56,0.45) flat → veil ramp: field bleeds through top 64px */
|
||||
background: linear-gradient(180deg, var(--veil-top) 0%, var(--veil-body) var(--veil-ramp));
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(195%);
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(195%);
|
||||
border: 1px solid var(--edge-quiet);
|
||||
box-shadow:
|
||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.03),
|
||||
0 4px 24px rgba(0, 0, 0, 0.3);
|
||||
inset 0 1px 0 0 rgba(168, 130, 255, 0.08), /* top violet sheen */
|
||||
inset 0 0 24px -16px var(--edge-live), /* faint inner rim-glow */
|
||||
0 4px 24px rgba(0, 0, 0, 0.30);
|
||||
}
|
||||
|
||||
.glass-subtle {
|
||||
background: rgba(16, 16, 42, 0.4);
|
||||
backdrop-filter: blur(12px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(150%);
|
||||
border: 1px solid rgba(99, 102, 241, 0.06);
|
||||
background: linear-gradient(180deg, rgba(10, 10, 30, 0.28) 0%, rgba(10, 10, 30, 0.74) 56px);
|
||||
backdrop-filter: blur(12px) saturate(165%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(165%);
|
||||
border: 1px solid var(--edge-quiet);
|
||||
box-shadow:
|
||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.02),
|
||||
0 2px 12px rgba(0, 0, 0, 0.2);
|
||||
inset 0 1px 0 0 rgba(168, 130, 255, 0.05),
|
||||
0 2px 12px rgba(0, 0, 0, 0.20);
|
||||
}
|
||||
|
||||
.glass-sidebar {
|
||||
background: rgba(10, 10, 26, 0.6);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
border-right: 1px solid rgba(99, 102, 241, 0.1);
|
||||
/* nav rail: denser body for the dense nav list, but field still bleeds at top */
|
||||
background: linear-gradient(180deg, rgba(14, 12, 34, 0.58) 0%, rgba(9, 8, 24, 0.52) 120px);
|
||||
backdrop-filter: blur(22px) saturate(175%);
|
||||
-webkit-backdrop-filter: blur(22px) saturate(175%);
|
||||
border-right: 1px solid rgba(129, 140, 248, 0.14);
|
||||
box-shadow:
|
||||
inset -1px 0 0 0 rgba(255, 255, 255, 0.02),
|
||||
4px 0 24px rgba(0, 0, 0, 0.3);
|
||||
inset -1px 0 0 0 rgba(168, 130, 255, 0.05),
|
||||
inset 0 0 60px -20px var(--edge-live),
|
||||
4px 0 28px rgba(0, 0, 0, 0.30);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(10, 10, 26, 0.8);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
border: 1px solid rgba(99, 102, 241, 0.1);
|
||||
/* was 0.80 flat (worst offender) — modal/command-palette depth without going opaque */
|
||||
background: linear-gradient(180deg, var(--veil-top) 0%, rgba(10, 10, 26, 0.86) 72px);
|
||||
backdrop-filter: blur(26px) saturate(185%);
|
||||
-webkit-backdrop-filter: blur(26px) saturate(185%);
|
||||
border: 1px solid rgba(129, 140, 248, 0.16);
|
||||
box-shadow:
|
||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.03),
|
||||
0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
inset 0 1px 0 0 rgba(168, 130, 255, 0.08),
|
||||
inset 0 0 40px -18px var(--edge-live),
|
||||
0 8px 36px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
/* Mobile: drop blur cost on the densest panel over the animated field */
|
||||
@media (max-width: 640px) {
|
||||
.glass { backdrop-filter: blur(8px) saturate(180%); -webkit-backdrop-filter: blur(8px) saturate(180%); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
|
|
@ -173,63 +200,10 @@ body {
|
|||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Ambient background orbs */
|
||||
@keyframes orb-float-1 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(60px, -40px) scale(1.1); }
|
||||
50% { transform: translate(-30px, -80px) scale(0.95); }
|
||||
75% { transform: translate(-60px, -20px) scale(1.05); }
|
||||
}
|
||||
|
||||
@keyframes orb-float-2 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(-50px, 30px) scale(1.08); }
|
||||
50% { transform: translate(40px, 60px) scale(0.92); }
|
||||
75% { transform: translate(20px, -40px) scale(1.03); }
|
||||
}
|
||||
|
||||
@keyframes orb-float-3 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(30px, 50px) scale(1.05); }
|
||||
50% { transform: translate(-60px, 20px) scale(0.98); }
|
||||
75% { transform: translate(40px, -30px) scale(1.1); }
|
||||
}
|
||||
|
||||
.ambient-orb {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.ambient-orb-1 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, rgba(168, 85, 247, 0.4), transparent 70%);
|
||||
top: -10%;
|
||||
right: -5%;
|
||||
animation: orb-float-1 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.ambient-orb-2 {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.35), transparent 70%);
|
||||
bottom: -15%;
|
||||
left: -5%;
|
||||
animation: orb-float-2 25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.ambient-orb-3 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(245, 158, 11, 0.2), transparent 70%);
|
||||
top: 40%;
|
||||
left: 40%;
|
||||
animation: orb-float-3 22s ease-in-out infinite;
|
||||
}
|
||||
/* Ambient background orbs removed — the WebGPU curl-noise field (BackdropEngine)
|
||||
replaced them. Two ambient systems stacked = muddy. The light-theme
|
||||
`[data-theme='light'] .ambient-orb` rule in theme.ts is now a harmless no-op
|
||||
(no DOM node mounts the orbs). */
|
||||
|
||||
/* Active nav indicator with animated gradient border */
|
||||
.nav-active-border {
|
||||
|
|
@ -545,6 +519,95 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
/* ── Local text legibility veil — bonds a soft dark plate behind a text
|
||||
cluster so body copy stays WCAG-AA over the moving field WITHOUT making
|
||||
the whole panel opaque. Apply to the TEXT wrapper, not the panel.
|
||||
Use on any text that sits on the field or in a panel's translucent top. */
|
||||
.veil { position: relative; isolation: isolate; }
|
||||
.veil::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -6px -10px;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
background: radial-gradient(120% 100% at 50% 45%,
|
||||
rgba(8, 7, 22, 0.58), rgba(8, 7, 22, 0.32) 70%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.veil-strong::before {
|
||||
inset: -10px -16px;
|
||||
background: radial-gradient(120% 100% at 50% 45%,
|
||||
rgba(6, 5, 18, 0.76), rgba(6, 5, 18, 0.46) 65%, transparent 100%);
|
||||
}
|
||||
/* Cheap inline fallback for single headings/labels over the field */
|
||||
.text-legible { text-shadow: 0 1px 10px rgba(5, 3, 13, 0.85), 0 0 2px rgba(5, 3, 13, 0.9); }
|
||||
|
||||
/* ── Living edge: a state-colored synapse membrane on any .glass rim.
|
||||
Brightens and travels only when the app has something to say. ──────── */
|
||||
@property --edge-pos { syntax: '<percentage>'; initial-value: 0%; inherits: false; }
|
||||
|
||||
.edge-live { position: relative; isolation: isolate; }
|
||||
.edge-live::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(var(--edge-angle, 115deg),
|
||||
transparent 0%,
|
||||
var(--edge-c, var(--edge-quiet)) var(--edge-pos),
|
||||
transparent calc(var(--edge-pos) + 28%));
|
||||
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
.edge-live.is-live { --edge-c: var(--edge-live); }
|
||||
.edge-live.is-dream { --edge-c: var(--edge-dream); --edge-angle: 135deg; }
|
||||
.edge-live.is-alarm { --edge-c: var(--edge-alarm); }
|
||||
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
.edge-live::before { animation: edge-travel 5.5s linear infinite; }
|
||||
.edge-live.is-alarm::before { animation-duration: 2.6s; }
|
||||
@keyframes edge-travel { to { --edge-pos: 130%; } }
|
||||
}
|
||||
/* Reduced-motion: rim still paints a quiet colored corner at --edge-pos:0% */
|
||||
|
||||
/* Dormant mind — EMPTY state: resting but alive, slower/deeper than .breathe */
|
||||
@keyframes dormant-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.55; filter: blur(26px); }
|
||||
50% { transform: scale(1.06); opacity: 0.80; filter: blur(30px); }
|
||||
}
|
||||
.dormant-aura { animation: dormant-pulse 6.5s ease-in-out infinite; }
|
||||
|
||||
/* Self-healing sweep — ERROR state: a diagnostic scan-line, "repairing itself" */
|
||||
@keyframes heal-sweep {
|
||||
0% { transform: translateY(-120%); opacity: 0; }
|
||||
12% { opacity: 0.9; }
|
||||
88% { opacity: 0.9; }
|
||||
100% { transform: translateY(120%); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Reconnect seek — OFFLINE state: a ring that expands and resets, "listening" */
|
||||
@keyframes seek-ring {
|
||||
0% { transform: scale(0.7); opacity: 0.7; }
|
||||
70% { opacity: 0; }
|
||||
100%{ transform: scale(2.2); opacity: 0; }
|
||||
}
|
||||
.seek-host { position: relative; }
|
||||
.seek-host::before {
|
||||
content: ''; position: absolute; inset: 0; border-radius: 50%;
|
||||
background: var(--color-warning); z-index: -1;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dormant-aura { animation: none; opacity: 0.6; }
|
||||
}
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
.seek-host::before { animation: seek-ring 2.2s ease-out infinite; }
|
||||
}
|
||||
|
||||
/* ── Hover lift utility — cards rise + glow when pointed at ───────────────*/
|
||||
.lift {
|
||||
transition:
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="ambient-strip relative flex h-9 w-full items-center gap-0 overflow-hidden border-b border-synapse/15 bg-black/40 px-3 text-[11px] text-dim backdrop-blur-md"
|
||||
class="ambient-strip text-legible relative flex h-9 w-full items-center gap-0 overflow-hidden border-b border-synapse/15 px-3 text-[11px] text-dim backdrop-blur-md"
|
||||
class:ambient-flash={suppressionFlash}
|
||||
aria-label="Ambient cognitive vitals"
|
||||
>
|
||||
|
|
@ -309,6 +309,12 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
/* Tinted gradient floor so 11px text-dim stays legible over the live field
|
||||
(replaces the flat bg-black/40 that could wash dim text on bright frames). */
|
||||
.ambient-strip {
|
||||
background: linear-gradient(180deg, rgba(8, 7, 22, 0.62), rgba(8, 7, 22, 0.44));
|
||||
}
|
||||
|
||||
.strip-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
167
apps/dashboard/src/lib/components/LifeState.svelte
Normal file
167
apps/dashboard/src/lib/components/LifeState.svelte
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<script lang="ts">
|
||||
import Icon, { type IconName } from './Icon.svelte';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
|
||||
let {
|
||||
variant = 'empty', // 'empty' | 'offline' | 'error' | 'dormant'
|
||||
title,
|
||||
body,
|
||||
detail = '', // raw error string, demoted into <details>
|
||||
actionLabel = '',
|
||||
onAction,
|
||||
compact = false
|
||||
}: {
|
||||
variant?: 'empty' | 'offline' | 'error' | 'dormant';
|
||||
title: string;
|
||||
body: string;
|
||||
detail?: string;
|
||||
actionLabel?: string;
|
||||
onAction?: () => void;
|
||||
compact?: boolean;
|
||||
} = $props();
|
||||
|
||||
const glyph: Record<string, IconName> = {
|
||||
empty: 'graph',
|
||||
offline: 'activation',
|
||||
error: 'reasoning',
|
||||
dormant: 'dreams'
|
||||
};
|
||||
const tone: Record<string, string> = {
|
||||
empty: 'var(--color-synapse-glow)',
|
||||
offline: 'var(--color-warning)',
|
||||
error: 'var(--color-warning)',
|
||||
dormant: 'var(--color-dream-glow)'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="life-state glass edge-live"
|
||||
class:is-alarm={variant === 'error' || variant === 'offline'}
|
||||
class:is-live={variant === 'empty'}
|
||||
class:is-dream={variant === 'dormant'}
|
||||
class:compact
|
||||
data-variant={variant}
|
||||
style:--tone={tone[variant]}
|
||||
use:reveal
|
||||
>
|
||||
{#if variant === 'error'}<span class="heal" aria-hidden="true"></span>{/if}
|
||||
<div class="halo" class:dormant-aura={variant === 'dormant'} aria-hidden="true"></div>
|
||||
|
||||
<div class="glyph" class:breathe={variant !== 'dormant'} class:seek-host={variant === 'offline'}>
|
||||
<Icon name={glyph[variant]} size={compact ? 30 : 48} strokeWidth={1.2} />
|
||||
</div>
|
||||
|
||||
<h2 class="title text-aurora veil">{title}</h2>
|
||||
<p class="body veil">{body}</p>
|
||||
|
||||
{#if detail}
|
||||
<details class="detail veil-strong"><summary>Technical detail</summary><code>{detail}</code></details>
|
||||
{/if}
|
||||
{#if actionLabel && onAction}
|
||||
<button class="act" onclick={onAction}>{actionLabel}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.life-state {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 14px;
|
||||
padding: 48px 32px;
|
||||
border-radius: 16px;
|
||||
max-width: 32rem;
|
||||
margin: 8vh auto 0;
|
||||
}
|
||||
.life-state.compact {
|
||||
padding: 22px 18px;
|
||||
gap: 9px;
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
}
|
||||
.halo {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
top: 8px;
|
||||
border-radius: 50%;
|
||||
opacity: 0.5;
|
||||
background: radial-gradient(circle, color-mix(in oklab, var(--tone) 38%, transparent), transparent 70%);
|
||||
}
|
||||
.glyph {
|
||||
color: var(--tone);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
.compact .title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.body {
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.55;
|
||||
/* --color-text, not --color-dim: AA-legible over a bright field cluster */
|
||||
color: var(--color-text);
|
||||
max-width: 34ch;
|
||||
margin: 0;
|
||||
}
|
||||
.detail {
|
||||
font-size: 0.72rem;
|
||||
/* --color-dim, not --color-muted: muted failed AA even with the veil */
|
||||
color: var(--color-dim);
|
||||
}
|
||||
.detail summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
.detail code {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in oklab, var(--color-decay) 8%, transparent);
|
||||
color: #fca5a5;
|
||||
word-break: break-all;
|
||||
text-align: left;
|
||||
}
|
||||
.act {
|
||||
margin-top: 6px;
|
||||
padding: 9px 18px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
color: var(--color-synapse-glow);
|
||||
background: color-mix(in oklab, var(--color-synapse) 14%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--color-synapse) 32%, transparent);
|
||||
transition: background 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
.act:hover {
|
||||
background: color-mix(in oklab, var(--color-synapse) 24%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.heal {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 0;
|
||||
height: 38%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(180deg, transparent,
|
||||
color-mix(in oklab, var(--color-warning) 22%, transparent), transparent);
|
||||
}
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
.heal { animation: heal-sweep 3.4s ease-in-out infinite; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.act:hover { transform: none; }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,12 +7,15 @@
|
|||
import {
|
||||
websocket,
|
||||
isConnected,
|
||||
isReconnecting,
|
||||
eventFeed,
|
||||
memoryCount,
|
||||
avgRetention,
|
||||
suppressedCount,
|
||||
uptimeSeconds,
|
||||
formatUptime
|
||||
} from '$stores/websocket';
|
||||
import { isDreaming as isDreamingFn } from '$lib/components/awareness-helpers';
|
||||
import ForgettingIndicator from '$lib/components/ForgettingIndicator.svelte';
|
||||
import BackdropEngine from '$lib/core/BackdropEngine.svelte';
|
||||
import InsightToast from '$lib/components/InsightToast.svelte';
|
||||
|
|
@ -27,9 +30,17 @@
|
|||
let cmdQuery = $state('');
|
||||
let cmdInput = $state<HTMLInputElement>(undefined as unknown as HTMLInputElement);
|
||||
|
||||
// One reactive "mood" for the whole shell: offline → alarm, dreaming → dream,
|
||||
// otherwise → live. Drives the sidebar edge, the strip rule, and the footer.
|
||||
let nowTick = $state(Date.now());
|
||||
const shellState = $derived(
|
||||
!$isConnected ? 'alarm' : isDreamingFn($eventFeed, nowTick) ? 'dream' : 'live'
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
websocket.connect();
|
||||
const teardownTheme = initTheme();
|
||||
const moodTick = setInterval(() => (nowTick = Date.now()), 1000);
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
|
|
@ -81,6 +92,7 @@
|
|||
websocket.disconnect();
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
teardownTheme();
|
||||
clearInterval(moodTick);
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -140,7 +152,7 @@
|
|||
<BackdropEngine />
|
||||
|
||||
<div class="flex flex-col md:flex-row h-screen overflow-hidden bg-void relative z-[1]">
|
||||
<nav class="hidden md:flex w-16 lg:w-56 flex-shrink-0 glass-sidebar flex-col">
|
||||
<nav class="hidden md:flex w-16 lg:w-56 flex-shrink-0 glass-sidebar flex-col edge-live is-{shellState}" class:rail-dormant={!$isConnected}>
|
||||
<a href="{base}/graph" class="logo-link flex items-center gap-3 px-4 py-5 border-b border-synapse/10">
|
||||
<div class="logo-mark w-8 h-8 rounded-lg bg-gradient-to-br from-dream to-synapse flex items-center justify-center text-bright shadow-lg shadow-synapse/20">
|
||||
<Icon name="logo" size={18} strokeWidth={1.8} />
|
||||
|
|
@ -184,13 +196,20 @@
|
|||
|
||||
<div class="px-3 py-4 border-t border-synapse/10 space-y-2">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<div class="w-2 h-2 rounded-full {$isConnected ? 'bg-recall animate-pulse-glow' : 'bg-decay'}"></div>
|
||||
<span class="hidden lg:block text-dim">{$isConnected ? 'Connected' : 'Offline'}</span>
|
||||
{#if $isConnected}
|
||||
<span class="ping-host inline-flex h-2 w-2 rounded-full bg-recall breathe text-recall"></span>
|
||||
<span class="hidden lg:block text-recall/90 text-legible">Listening</span>
|
||||
{:else}
|
||||
<span class="seek-host inline-flex h-2 w-2 rounded-full bg-warning"></span>
|
||||
<span class="hidden lg:block text-warning/90 text-legible">
|
||||
{$isReconnecting ? 'Reconnecting…' : 'Asleep — server offline'}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="ml-auto">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden lg:block text-xs text-muted space-y-0.5">
|
||||
<div class="veil hidden lg:block text-xs text-dim space-y-0.5">
|
||||
<div>{$memoryCount} memories</div>
|
||||
<div>{($avgRetention * 100).toFixed(0)}% retention</div>
|
||||
{#if $uptimeSeconds > 0}
|
||||
|
|
@ -206,7 +225,7 @@
|
|||
</nav>
|
||||
|
||||
<main class="flex-1 flex flex-col min-h-0 pb-16 md:pb-0">
|
||||
<AmbientAwarenessStrip />
|
||||
<div class="strip-rail is-{shellState}"><AmbientAwarenessStrip /></div>
|
||||
<VerdictBar />
|
||||
<div class="flex-1 min-h-0 overflow-y-auto">
|
||||
{@render children()}
|
||||
|
|
@ -311,4 +330,37 @@
|
|||
.nav-active-border .nav-icon :global(svg) {
|
||||
filter: drop-shadow(0 0 6px rgba(129, 140, 248, 0.55));
|
||||
}
|
||||
|
||||
/* Offline → the whole spine cools and desaturates: a sleeping mind. */
|
||||
.rail-dormant {
|
||||
filter: saturate(0.7) brightness(0.93);
|
||||
transition: filter 0.6s ease;
|
||||
}
|
||||
|
||||
/* The metric strip's bottom rule reacts to shell mood (reuses gradient-shift). */
|
||||
.strip-rail {
|
||||
position: relative;
|
||||
}
|
||||
.strip-rail::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--edge-c, var(--edge-live)) 50%, transparent);
|
||||
background-size: 220% 100%;
|
||||
}
|
||||
.strip-rail.is-dream::after {
|
||||
--edge-c: var(--edge-dream);
|
||||
}
|
||||
.strip-rail.is-alarm::after {
|
||||
background: var(--color-warning);
|
||||
}
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
.strip-rail.is-live::after,
|
||||
.strip-rail.is-dream::after {
|
||||
animation: gradient-shift 4s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import AnimatedNumber from '$components/AnimatedNumber.svelte';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import ReceiptCard from '$components/ReceiptCard.svelte';
|
||||
import LifeState from '$components/LifeState.svelte';
|
||||
import {
|
||||
api,
|
||||
type TraceRunSummary,
|
||||
|
|
@ -296,10 +297,12 @@
|
|||
<aside class="runs glass" use:reveal>
|
||||
<h2 class="panel-title">Runs</h2>
|
||||
{#if runs.length === 0}
|
||||
<p class="empty">
|
||||
No agent runs recorded yet. Make an MCP tool call — every call is
|
||||
recorded here.
|
||||
</p>
|
||||
<LifeState
|
||||
variant="dormant"
|
||||
compact
|
||||
title="Dormant"
|
||||
body="No runs yet. The recorder is breathing, waiting for the first tool call."
|
||||
/>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each runs as run (run.runId)}
|
||||
|
|
@ -332,9 +335,30 @@
|
|||
{#if loading}
|
||||
<div class="glass center-msg">Loading trace…</div>
|
||||
{:else if error}
|
||||
<div class="glass center-msg err">{error}</div>
|
||||
<LifeState
|
||||
variant="error"
|
||||
title="Reading the flight recorder…"
|
||||
body="The trace stream hiccuped while pulling this run. Vestige is re-establishing the connection — your runs are safe on disk."
|
||||
detail={error}
|
||||
actionLabel="Retry"
|
||||
onAction={() => selectedRunId && selectRun(selectedRunId)}
|
||||
/>
|
||||
{:else if !detail}
|
||||
<div class="glass center-msg">Select a run to replay.</div>
|
||||
{#if !$isConnected}
|
||||
<LifeState
|
||||
variant="offline"
|
||||
title="Listening for the agent"
|
||||
body="No live socket yet. The moment an MCP tool call fires, this recorder lights up and replays it — runId, retrievals, receipts, all of it."
|
||||
actionLabel="Try the firewall demo"
|
||||
onAction={() => loadFirewallDemo()}
|
||||
/>
|
||||
{:else}
|
||||
<LifeState
|
||||
variant="empty"
|
||||
title="Pick a run to replay"
|
||||
body="Every MCP tool call is recorded on the left. Select a run to watch the agent think, frame by frame."
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Scrubber -->
|
||||
<div class="scrubber glass" use:reveal>
|
||||
|
|
@ -428,7 +452,7 @@
|
|||
Memory pulse <span class="text-dim">— touched this run</span>
|
||||
</h3>
|
||||
{#if pulsedIds.length === 0}
|
||||
<p class="empty">No memories touched yet.</p>
|
||||
<p class="empty"><span class="empty-dot">·</span> No memories touched yet — they light up as the run replays.</p>
|
||||
{:else}
|
||||
<div class="pulse-grid">
|
||||
{#each pulsedIds as id (id)}
|
||||
|
|
@ -699,6 +723,15 @@
|
|||
color: var(--color-text-dim, #8b8ba7);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.empty-dot {
|
||||
color: var(--color-synapse-glow);
|
||||
}
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
.empty-dot {
|
||||
animation: breathe 3.2s ease-in-out infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.runs {
|
||||
padding: 16px;
|
||||
|
|
@ -778,9 +811,6 @@
|
|||
text-align: center;
|
||||
color: var(--color-text-dim, #8b8ba7);
|
||||
}
|
||||
.center-msg.err {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
/* Scrubber */
|
||||
.scrubber {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue