vestige/apps/dashboard/src/routes/(app)/settings/+page.svelte

260 lines
9.9 KiB
Svelte
Raw Normal View History

<script lang="ts">
import { api } from '$stores/api';
import { isConnected, memoryCount, avgRetention } from '$stores/websocket';
// Operation states
let consolidating = $state(false);
let dreaming = $state(false);
let consolidationResult = $state<Record<string, unknown> | null>(null);
let dreamResult = $state<Record<string, unknown> | null>(null);
// Stats
let stats = $state<Record<string, unknown> | null>(null);
let retentionDist = $state<Record<string, unknown> | null>(null);
let loadingStats = $state(true);
// Health
let health = $state<Record<string, unknown> | null>(null);
$effect(() => {
loadAllData();
});
async function loadAllData() {
loadingStats = true;
try {
const [s, h, r] = await Promise.all([
api.stats().catch(() => null),
api.health().catch(() => null),
api.retentionDistribution().catch(() => null),
]);
stats = s as Record<string, unknown> | null;
health = h as Record<string, unknown> | null;
retentionDist = r as Record<string, unknown> | null;
} finally {
loadingStats = false;
}
}
async function runConsolidation() {
consolidating = true;
consolidationResult = null;
try {
consolidationResult = await api.consolidate() as unknown as Record<string, unknown>;
await loadAllData();
} catch { /* ignore */ }
finally { consolidating = false; }
}
async function runDream() {
dreaming = true;
dreamResult = null;
try {
dreamResult = await api.dream() as unknown as Record<string, unknown>;
await loadAllData();
} catch { /* ignore */ }
finally { dreaming = false; }
}
</script>
<div class="p-6 max-w-4xl mx-auto space-y-8">
<div class="flex items-center justify-between">
<h1 class="text-xl text-bright font-semibold">Settings & System</h1>
<button onclick={loadAllData} class="text-xs text-dim hover:text-text transition">Refresh</button>
</div>
<!-- System Health Overview -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg text-center">
<div class="text-2xl text-bright font-bold">{$memoryCount}</div>
<div class="text-xs text-dim mt-1">Memories</div>
</div>
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg text-center">
<div class="text-2xl font-bold" style="color: {$avgRetention > 0.7 ? '#10b981' : $avgRetention > 0.4 ? '#f59e0b' : '#ef4444'}">{($avgRetention * 100).toFixed(1)}%</div>
<div class="text-xs text-dim mt-1">Avg Retention</div>
</div>
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg text-center">
<div class="text-2xl text-bright font-bold flex items-center justify-center gap-2">
<div class="w-2.5 h-2.5 rounded-full {$isConnected ? 'bg-recall animate-pulse-glow' : 'bg-decay'}"></div>
<span class="text-sm">{$isConnected ? 'Online' : 'Offline'}</span>
</div>
<div class="text-xs text-dim mt-1">WebSocket</div>
</div>
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg text-center">
<div class="text-2xl text-synapse-glow font-bold">v2.0</div>
<div class="text-xs text-dim mt-1">Vestige</div>
</div>
</div>
<!-- Cognitive Operations -->
<section class="space-y-4">
<h2 class="text-sm text-bright font-semibold flex items-center gap-2">
<span class="text-dream"></span> Cognitive Operations
</h2>
<!-- Consolidation -->
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg space-y-3">
<div class="flex items-center justify-between">
<div>
<div class="text-sm text-text font-medium">FSRS-6 Consolidation</div>
<div class="text-xs text-dim">Apply spaced-repetition decay, regenerate embeddings, run maintenance</div>
</div>
<button onclick={runConsolidation} disabled={consolidating}
class="px-4 py-2 bg-warning/20 border border-warning/40 text-warning text-sm rounded-lg hover:bg-warning/30 transition disabled:opacity-50 flex items-center gap-2">
{#if consolidating}
<span class="w-3 h-3 border border-warning/50 border-t-warning rounded-full animate-spin"></span>
Running...
{:else}
Consolidate
{/if}
</button>
</div>
{#if consolidationResult}
<div class="bg-deep/50 p-3 rounded-lg border border-subtle/10">
<div class="grid grid-cols-3 gap-3 text-center">
{#if consolidationResult.processed !== undefined}
<div>
<div class="text-lg text-text font-semibold">{consolidationResult.processed}</div>
<div class="text-[10px] text-muted">Processed</div>
</div>
{/if}
{#if consolidationResult.decayed !== undefined}
<div>
<div class="text-lg text-decay font-semibold">{consolidationResult.decayed}</div>
<div class="text-[10px] text-muted">Decayed</div>
</div>
{/if}
{#if consolidationResult.embedded !== undefined}
<div>
<div class="text-lg text-synapse-glow font-semibold">{consolidationResult.embedded}</div>
<div class="text-[10px] text-muted">Embedded</div>
</div>
{/if}
</div>
</div>
{/if}
</div>
<!-- Dream -->
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg space-y-3">
<div class="flex items-center justify-between">
<div>
<div class="text-sm text-text font-medium">Memory Dream Cycle</div>
<div class="text-xs text-dim">Replay memories, discover hidden connections, synthesize insights</div>
</div>
<button onclick={runDream} disabled={dreaming}
class="px-4 py-2 bg-dream/20 border border-dream/40 text-dream-glow text-sm rounded-lg hover:bg-dream/30 transition disabled:opacity-50 flex items-center gap-2
{dreaming ? 'glow-dream animate-pulse-glow' : ''}">
{#if dreaming}
<span class="w-3 h-3 border border-dream/50 border-t-dream rounded-full animate-spin"></span>
Dreaming...
{:else}
Dream
{/if}
</button>
</div>
{#if dreamResult}
<div class="bg-deep/50 p-3 rounded-lg border border-subtle/10 space-y-2">
{#if dreamResult.insights && Array.isArray(dreamResult.insights)}
<div class="text-xs text-bright font-medium">Insights Discovered:</div>
{#each dreamResult.insights as insight}
<div class="text-xs text-dim bg-dream/5 border border-dream/10 rounded p-2">
{typeof insight === 'string' ? insight : JSON.stringify(insight)}
</div>
{/each}
{/if}
{#if dreamResult.connections_found !== undefined}
<div class="text-xs text-dim">Connections found: <span class="text-dream-glow">{dreamResult.connections_found}</span></div>
{/if}
{#if dreamResult.memories_replayed !== undefined}
<div class="text-xs text-dim">Memories replayed: <span class="text-text">{dreamResult.memories_replayed}</span></div>
{/if}
</div>
{/if}
</div>
</section>
<!-- Retention Distribution -->
{#if retentionDist}
<section class="space-y-4">
<h2 class="text-sm text-bright font-semibold flex items-center gap-2">
<span class="text-recall"></span> Retention Distribution
</h2>
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg">
{#if retentionDist.buckets && Array.isArray(retentionDist.buckets)}
<div class="flex items-end gap-1 h-32">
{#each retentionDist.buckets as bucket, i}
{@const maxCount = Math.max(...(retentionDist.buckets as {count: number}[]).map((b: {count: number}) => b.count), 1)}
{@const height = ((bucket as {count: number}).count / maxCount) * 100}
{@const color = i < 2 ? '#ef4444' : i < 4 ? '#f59e0b' : i < 7 ? '#6366f1' : '#10b981'}
<div class="flex-1 flex flex-col items-center gap-1">
<div class="text-[9px] text-muted">{(bucket as {count: number}).count}</div>
<div
class="w-full rounded-t transition-all duration-500"
style="height: {Math.max(height, 2)}%; background: {color}; opacity: 0.7"
></div>
<div class="text-[9px] text-muted">{i * 10}%</div>
</div>
{/each}
</div>
{/if}
</div>
</section>
{/if}
<!-- Keyboard Shortcuts -->
<section class="space-y-4">
<h2 class="text-sm text-bright font-semibold flex items-center gap-2">
<span class="text-synapse"></span> Keyboard Shortcuts
</h2>
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg">
<div class="grid grid-cols-2 gap-2 text-xs">
{#each [
{ key: '⌘ K', desc: 'Command palette' },
{ key: '/', desc: 'Focus search' },
{ key: 'G', desc: 'Go to Graph' },
{ key: 'M', desc: 'Go to Memories' },
{ key: 'T', desc: 'Go to Timeline' },
{ key: 'F', desc: 'Go to Feed' },
{ key: 'E', desc: 'Go to Explore' },
{ key: 'S', desc: 'Go to Stats' },
] as shortcut}
<div class="flex items-center gap-2 py-1">
<kbd class="px-1.5 py-0.5 bg-deep rounded text-[10px] font-mono text-muted min-w-[2rem] text-center">{shortcut.key}</kbd>
<span class="text-dim">{shortcut.desc}</span>
</div>
{/each}
</div>
</div>
</section>
<!-- About -->
<section class="space-y-4">
<h2 class="text-sm text-bright font-semibold flex items-center gap-2">
<span class="text-memory"></span> About
</h2>
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg space-y-3">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-dream to-synapse flex items-center justify-center text-bright text-xl font-bold shadow-lg shadow-synapse/20">
V
</div>
<div>
<div class="text-sm text-bright font-semibold">Vestige v2.0 "Cognitive Leap"</div>
<div class="text-xs text-dim">Your AI's long-term memory system</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-xs text-dim pt-2 border-t border-subtle/10">
<div>29 cognitive modules</div>
<div>FSRS-6 spaced repetition</div>
<div>Nomic Embed v1.5 (256d)</div>
<div>Jina Reranker v1 Turbo</div>
<div>USearch HNSW (20x FAISS)</div>
<div>Local-first, zero cloud</div>
</div>
<div class="text-[10px] text-muted pt-1">
Built with Rust + Axum + SvelteKit 2 + Svelte 5 + Three.js + Tailwind CSS 4
</div>
</div>
</section>
</div>