Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions dashboard/src/ui/StateChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,6 @@ export const STATE_LAMP: Record<DomainStateKind, string> = {
unsupported_schema: 'bg-state-unsupported-schema',
};

/** The lamp utility for a state, for registers that print the state's word
* beside a swatch without the chip's full chrome. One table, so a register
* and the chips beneath it cannot disagree about a hue. */
export function stateLampClass(kind: DomainStateKind): string {
return STATE_LAMP[kind] ?? 'bg-state-unsupported-schema';
}

export function StateChip({
kind,
detail,
Expand Down
19 changes: 18 additions & 1 deletion dashboard/src/ui/archetypes/ExplorerSplit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export function DataRow({
selected,
onSelect,
onInspect,
rowId,
children,
className,
height,
Expand All @@ -205,7 +206,22 @@ export function DataRow({
}: {
selected?: boolean;
onSelect?: () => void;
/**
* Hover inspects; click selects. A workspace whose inspector previews the
* row under the pointer (or under keyboard focus) wires it here, and the row
* fires it for both pointer movement over it and focus arriving so the
* keyboard path reaches exactly what the pointer does.
*
* Movement, not entry: a list that scrolls under a parked pointer — which is
* what the roving arrow keys do — synthesises enter events for whichever row
* slides beneath it, and those would steal inspection from the row the
* keyboard just focused. The browser fires no move for a scroll.
*/
onInspect?: () => void;
/** The row's stable identity, stamped as `data-row-id` so a list can find
* the row a selection made elsewhere (a scene, the address) refers to and
* bring it into view. */
rowId?: string;
children: ReactNode;
className?: string;
/**
Expand All @@ -224,8 +240,9 @@ export function DataRow({
<button
type="button"
onClick={onSelect}
onPointerEnter={onInspect}
onPointerMove={onInspect}
onFocus={onInspect}
data-row-id={rowId}
aria-pressed={selected ?? false}
style={{ height: height != null ? `${height}px` : 'var(--row-height-data)' }}
className={cn(
Expand Down
50 changes: 33 additions & 17 deletions dashboard/src/workspaces/knowledge/FactConstellation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export function FactConstellation({
onInspect,
onSelect,
graphRead,
className,
}: {
model: ConstellationModel;
/** The fact the reader is inspecting (hover or focus, sticky until Escape). */
Expand All @@ -86,7 +85,6 @@ export function FactConstellation({
onSelect: (factId: string) => void;
/** The daemon's own reading of the graph sub-read, printed beside coverage. */
graphRead: MemoryReadStatusV1 | undefined;
className?: string;
}) {
const haloId = useId();
// A satellite under the pointer dims the field to its wiring but has no fact
Expand All @@ -98,7 +96,12 @@ export function FactConstellation({
inspectedFactId == null ? null : (model.nodeIdByFact.get(inspectedFactId) ?? null);
const selectedNodeId =
selectedFactId == null ? null : (model.nodeIdByFact.get(selectedFactId) ?? null);
const focusNodeId = hoveredNodeId ?? inspectedNodeId;
// Inspection dims what it is not wired to; selection does not. A selected
// fact is marked by its ring and stays legible in a field drawn at full
// strength, so a reader who has chosen one is not left looking at a dimmed
// store until they press Escape.
const focusNodeId =
hoveredNodeId ?? (inspectedNodeId !== selectedNodeId ? inspectedNodeId : null);
const focusSet = useMemo(() => {
if (focusNodeId == null) return null;
const set = new Set<string>([focusNodeId]);
Expand All @@ -113,7 +116,7 @@ export function FactConstellation({

return (
<figure
className={cn('td-optic td-grain td-scanlines relative flex min-h-0 flex-col', className)}
className="td-optic td-grain td-scanlines relative flex flex-col"
data-testid="fact-constellation"
>
<Corners tone="signal" />
Expand All @@ -131,7 +134,7 @@ export function FactConstellation({
</span>
</figcaption>
{empty ? (
<div className="relative z-10 flex flex-1 items-center justify-center p-6">
<div className="relative z-10 flex min-h-[220px] items-center justify-center p-6">
<p className="max-w-md text-center text-xs leading-relaxed text-text-muted">
The memory graph returned no roots to draw
{model.coverage.completeness === 'complete'
Expand All @@ -140,12 +143,17 @@ export function FactConstellation({
</p>
</div>
) : (
// The key sits beside the field from `lg`, in the width the 5:3 drawing
// leaves free at the sides of a wider aperture, and beneath it below.
// The field is one of two readings of the same rows, and the ledger
// under it must stay in view at 1440x900 rather than below the fold.
<div className="relative z-10 flex flex-col lg:flex-row lg:items-stretch">
<svg
role="img"
aria-label={description}
viewBox={`0 0 ${CONSTELLATION_WORLD.width} ${CONSTELLATION_WORLD.height}`}
preserveAspectRatio="xMidYMid meet"
className="relative z-10 block min-h-[240px] w-full flex-1 select-none"
className="block h-[clamp(180px,30vh,360px)] w-full select-none lg:min-w-0 lg:flex-1"
data-testid="fact-constellation-svg"
onPointerLeave={() => setHoveredNodeId(null)}
>
Expand All @@ -171,6 +179,10 @@ export function FactConstellation({
inspected={node.id === focusNodeId}
selected={node.id === selectedNodeId}
onEnter={() => {
// Movement, not entry, for the same reason the ledger rows
// inspect on move: a field scrolling under a parked pointer
// must not steal inspection from the keyboard.
if (hoveredNodeId === node.id) return;
setHoveredNodeId(node.id);
if (node.factId != null) onInspect(node.factId);
}}
Expand Down Expand Up @@ -201,11 +213,12 @@ export function FactConstellation({
))}
</g>
</svg>
<div className="flex flex-wrap items-start justify-between gap-x-6 gap-y-1.5 px-3 pb-2 pt-1 lg:w-44 lg:shrink-0 lg:flex-col lg:justify-start lg:gap-y-3 lg:pt-2">
<TrustLegend model={model} />
<RelationLegend model={model} />
</div>
</div>
)}
<div className="relative z-10 flex flex-wrap items-end justify-between gap-x-4 gap-y-2 px-3 pb-2 pt-1">
<TrustLegend model={model} />
<RelationLegend model={model} />
</div>
<CoverageFooter model={model} graphRead={graphRead} />
</figure>
);
Expand Down Expand Up @@ -353,7 +366,7 @@ function Body({
data-fact-id={node.factId ?? undefined}
data-inspected={inspected || undefined}
data-selected={selected || undefined}
onPointerEnter={onEnter}
onPointerMove={onEnter}
onPointerLeave={onLeave}
onClick={onClick}
className={cn(
Expand Down Expand Up @@ -454,12 +467,15 @@ function Glyph({
}
}

/** The legends run as ruled rows rather than stacked lists: the field is one
* of two readings of the same rows and the ledger beneath it has to stay in
* view, so the key spends width, of which there is plenty, not height. */
function TrustLegend({ model }: { model: ConstellationModel }) {
return (
<dl aria-label="Trust bands" className="flex flex-col gap-0.5">
<dl aria-label="Trust bands" className="flex flex-wrap items-center gap-x-3 gap-y-1">
<dt className="td-legend">trust</dt>
{model.bands.map((band) => (
<dd key={band.id} className="flex items-center gap-2 text-3xs text-text-muted">
<dd key={band.id} className="flex items-center gap-1.5 text-3xs text-text-muted">
<span
aria-hidden
className={cn(
Expand Down Expand Up @@ -493,12 +509,12 @@ function RelationLegend({ model }: { model: ConstellationModel }) {
);
}
return (
<dl aria-label="Relations and bodies" className="flex flex-col gap-0.5">
<dl aria-label="Relations and bodies" className="flex flex-wrap items-center gap-x-3 gap-y-1">
<dt className="td-legend">relations · bodies</dt>
{model.relationKinds.map((kind) => {
const style = relationStyle(kind);
return (
<dd key={kind} className="flex items-center gap-2 text-3xs text-text-muted">
<dd key={kind} className="flex items-center gap-1.5 text-3xs text-text-muted">
<svg aria-hidden width="22" height="6" viewBox="0 0 22 6" className="shrink-0">
<line
x1="1"
Expand All @@ -519,7 +535,7 @@ function RelationLegend({ model }: { model: ConstellationModel }) {
{satelliteKinds.map((kind) => {
const glyph = nodeGlyph(kind);
return (
<dd key={kind} className="flex items-center gap-2 text-3xs text-text-muted">
<dd key={kind} className="flex items-center gap-1.5 text-3xs text-text-muted">
<svg aria-hidden width="22" height="10" viewBox="0 0 22 10" className="shrink-0">
<Glyph
shape={glyph.shape}
Expand Down Expand Up @@ -569,7 +585,7 @@ function CoverageFooter({
</span>,
<span key="relations">
{coverage.drawnRelations.toLocaleString()} of {coverage.relationCount.toLocaleString()}{' '}
relations drawn under a limit of {coverage.relationLimit.toLocaleString()}
relations drawn, limit {coverage.relationLimit.toLocaleString()}
</span>,
];
if (coverage.unavailableFactCandidates > 0) {
Expand Down
15 changes: 11 additions & 4 deletions dashboard/src/workspaces/knowledge/FactInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ function Provenance({
typeof metadata === 'object' &&
Object.keys(metadata as Record<string, unknown>).length > 0;
return (
<dl className="grid grid-cols-[minmax(6rem,8rem)_1fr] gap-x-3 gap-y-1 text-2xs">
<dl className="grid grid-cols-[minmax(7rem,9.5rem)_1fr] gap-x-3 gap-y-1 text-2xs">
<Term label="source label">
{subject.source_label ?? <span className="text-text-muted">none recorded</span>}
</Term>
Expand Down Expand Up @@ -407,9 +407,14 @@ function Provenance({
<Term label="updated">{stamp(subject.updated_at)}</Term>
<Term label="last recalled">{formatMicrosUtc(subject.last_recalled_at, { nullAs: 'never recalled' })}</Term>
<Term label="projected as of">{stamp(subject.projected_as_of)}</Term>
<Term label="retrievals · accesses">
<Term label="retrievals">
<span className="td-value" data-cell="numeric">
{subject.retrieval_count?.toLocaleString() ?? '—'} · {subject.access_count?.toLocaleString() ?? '—'}
{subject.retrieval_count?.toLocaleString() ?? '—'}
</span>
</Term>
<Term label="accesses">
<span className="td-value" data-cell="numeric">
{subject.access_count?.toLocaleString() ?? '—'}
</span>
</Term>
<Term label="metadata">
Expand Down Expand Up @@ -439,7 +444,9 @@ function stamp(micros: number | null | undefined): ReactNode {
function Term({ label, children }: { label: string; children: ReactNode }) {
return (
<>
<dt className="td-legend pt-px">{label}</dt>
{/* Legends are nowrap by default; a two-word term in a 24rem inspector
* has to be allowed to break rather than run under its value. */}
<dt className="td-legend whitespace-normal pt-px leading-snug">{label}</dt>
<dd className="min-w-0 break-words text-text-secondary">{children}</dd>
</>
);
Expand Down
Loading
Loading