Sixteen mutating routes now refuse a caller who cannot present the token the server started with; queries stay open, because a question is not a write. The environment variable is ELY_EYE_AUTH_TOKEN, following the repo's env_prefix convention — the shorter name I reached for first returned 403 where the smoke expected 401, and the server printed the reason. Words from a stranger are quarantined on the way in: an HTTP upload is third_party by definition, and a sentence carrying an instruction-shaped phrase is quarantined whoever wrote it. A quarantined atom stays findable and stays out of the compiled context until a second, live, unquarantined source agrees with its rarest words. Corroboration is computed at read time because it is a property of the corpus now, not of the moment a memory was written; it is term-level, and the docstring says so. forget now erases the source row and the compiled contexts beside the atoms, so a forgotten memory leaves nothing that can still speak. The semantic cache key carries a store revision, which fixes a real staleness: a context compiled before new evidence arrived kept its old quarantine decision forever. bench.audit --product still pointed at evals.py, which the p1-6 split moved into backend/src/ely_eye/proofs/. It failed fast, but the anti-self-justification rule had quietly stopped auditing the product; it now walks the package, and a test pins that the package exists and holds no re-roll. Gates: pytest 306 passed; ruff clean; mypy cold cache 24 errors in 9 files, the recorded baseline, net zero; pyright 0 errors on the 23 backend files and the 2 bench files; go build/vet/test and gofmt clean; npm run build 221.39 kB with eslint exit 0; bench/filesize_check clean; bench.audit --product clean over 31 harness files; scripts/mcp_write_smoke.sh PASS with the gate live across two servers and the MCP child.
147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
import { Activity, Archive, Database, Gauge, RefreshCw, Server } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import { getAdapters, getStatus, readToken, writeToken } from './api';
|
|
import { AdapterPanel } from './components/AdapterPanel';
|
|
import { CartridgePanel } from './components/CartridgePanel';
|
|
import { CompilerPanel } from './components/CompilerPanel';
|
|
import { IngestPanel } from './components/IngestPanel';
|
|
import { MemoryPanel } from './components/MemoryPanel';
|
|
import { ProofPanel } from './components/ProofPanel';
|
|
import { SensesPanel } from './components/SensesPanel';
|
|
import { Kpi, Panel, Quiet } from './components/ui';
|
|
import { errorMessage, formatBytes, formatNumber, formatRuntimeDetail, formatVram } from './lib/format';
|
|
import type { AdapterStatus, CacheLayerStatus, SystemStatus } from './types';
|
|
|
|
export function App() {
|
|
const [status, setStatus] = useState<SystemStatus | null>(null);
|
|
const [adapters, setAdapters] = useState<AdapterStatus[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [token, setToken] = useState(readToken);
|
|
|
|
async function refresh() {
|
|
const [nextStatus, nextAdapters] = await Promise.all([getStatus(), getAdapters()]);
|
|
setStatus(nextStatus);
|
|
setAdapters(nextAdapters);
|
|
}
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function loadInitial() {
|
|
try {
|
|
const [nextStatus, nextAdapters] = await Promise.all([getStatus(), getAdapters()]);
|
|
if (!cancelled) {
|
|
setStatus(nextStatus);
|
|
setAdapters(nextAdapters);
|
|
}
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
setError(errorMessage(err));
|
|
}
|
|
}
|
|
}
|
|
void loadInitial();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
async function refreshQuietly() {
|
|
try {
|
|
await refresh();
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
}
|
|
}
|
|
|
|
const layers: CacheLayerStatus[] = status?.memory.layers ?? [];
|
|
const boundAdapters = adapters.filter((adapter) => adapter.cartridge_bound).length;
|
|
|
|
return (
|
|
<main className="app-shell">
|
|
<header className="topbar">
|
|
<div>
|
|
<p className="eyebrow">Ely-Eye v4.0</p>
|
|
<h1>Local VLM Context OS</h1>
|
|
</div>
|
|
<div className="topbar-actions">
|
|
<input
|
|
className="token-input"
|
|
type="password"
|
|
value={token}
|
|
placeholder="写入令牌"
|
|
aria-label="写入令牌"
|
|
title="服务器设置了 ELY_EYE_AUTH_TOKEN 时,写入需要它"
|
|
onChange={(event) => {
|
|
setToken(event.target.value);
|
|
writeToken(event.target.value);
|
|
}}
|
|
/>
|
|
<button
|
|
className="icon-button"
|
|
title="Refresh status"
|
|
onClick={() => refreshQuietly()}
|
|
>
|
|
<RefreshCw size={18} />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{error && <div className="error-line">{error}</div>}
|
|
|
|
<section className="kpi-grid" aria-label="system status">
|
|
<Kpi
|
|
icon={<Server size={18} />}
|
|
label="Runtime"
|
|
value={status?.runtime.available ? 'ready' : 'offline'}
|
|
detail={formatRuntimeDetail(status)}
|
|
/>
|
|
<Kpi
|
|
icon={<Gauge size={18} />}
|
|
label="GPU"
|
|
value={status?.runtime.gpu_name ?? 'unknown'}
|
|
detail={formatVram(status)}
|
|
/>
|
|
<Kpi
|
|
icon={<Database size={18} />}
|
|
label="Library"
|
|
value={formatNumber(status?.memory.library_token_equivalent ?? 0)}
|
|
detail={`${formatNumber(status?.memory.active_token_equivalent ?? 0)} token eq open`}
|
|
/>
|
|
<Kpi
|
|
icon={<Archive size={18} />}
|
|
label="Cartridges"
|
|
value={String(status?.memory.cartridges ?? 0)}
|
|
detail={`${boundAdapters} adapters bound`}
|
|
/>
|
|
</section>
|
|
|
|
<section className="workspace-grid">
|
|
<MemoryPanel onChanged={refreshQuietly} />
|
|
<CartridgePanel onChanged={refreshQuietly} />
|
|
<CompilerPanel onChanged={refreshQuietly} />
|
|
<IngestPanel onChanged={refreshQuietly} />
|
|
<SensesPanel senses={status?.senses ?? null} />
|
|
<Panel title="Cache Trace" icon={<Activity size={17} />}>
|
|
<div className="trace-list">
|
|
{layers.map((layer) => (
|
|
<div key={layer.layer} className="trace-row">
|
|
<span>{layer.layer}</span>
|
|
<meter
|
|
min={0}
|
|
max={Math.max(1, layer.hit_count + layer.miss_count)}
|
|
value={layer.hit_count}
|
|
/>
|
|
<small>{formatBytes(layer.bytes_estimate)}</small>
|
|
</div>
|
|
))}
|
|
{layers.length === 0 && <Quiet>Waiting for retrieval</Quiet>}
|
|
</div>
|
|
</Panel>
|
|
<AdapterPanel adapters={adapters} />
|
|
<ProofPanel />
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|