Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a1b49e3
feat(web): per-node geometry class in the unified engine
aaronsb May 14, 2026
ede92ec
feat(web): port Document Explorer to the unified engine (ADR-702 phas…
aaronsb May 14, 2026
e93ad5e
fix(web): preserve document-concept clustering and bake focus dim int…
aaronsb May 14, 2026
5990cd4
fix(web): Document Explorer visual fidelity — circles, label color, l…
aaronsb May 14, 2026
a846c5e
refactor(web): unify scale resolution for nodes and labels
aaronsb May 14, 2026
2cbd3da
feat(web): Document Explorer state survives navigation within a session
aaronsb May 14, 2026
8f4f067
fix(web): Document Explorer interaction parity — click, info card, se…
aaronsb May 14, 2026
97df7e3
fix(web): Document Explorer interaction model — hover dim + right-cli…
aaronsb May 14, 2026
41e29ff
fix(web): harmonize hover/focus dim across engine consumers
aaronsb May 15, 2026
d58094b
fix(web): soften hover dim to ~10% fade across both explorers
aaronsb May 15, 2026
a771d21
refactor(web): dim model as a tier-keyed variant map
aaronsb May 15, 2026
9fc9fbe
refactor(web): one dim value per tier — nodes, edges, labels recede t…
aaronsb May 16, 2026
5d5f71e
fix(web): Document Explorer hover follows visible edges only
aaronsb May 16, 2026
9415c40
fix(web): Document Explorer neighborhood reads structural fields, not…
aaronsb May 16, 2026
7844b63
fix(web): hover dim to 0.6 — clear but soft step-back
aaronsb May 16, 2026
01c01d4
fix(web): dim by fading toward scene background, not scaling toward b…
aaronsb May 16, 2026
cc338ac
chore(web): drop dead DOUBLE_CLICK_MS constant
aaronsb May 16, 2026
cfb7433
docs(adr): amend ADR-702 — phase 4 executed as full adoption
aaronsb May 16, 2026
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
status: Proposed
date: 2026-04-20
updated: 2026-04-20
updated: 2026-05-15
deciders:
- aaronsb
- claude
Expand Down Expand Up @@ -387,6 +387,15 @@ for widgets, the `CaretMarker` pattern, the distance-culled `<Html>` label
approach — while keeping its custom scene composition. This phase is
optional and opportunistic; no commitment to complete migration.

> **Amendment (2026-05-15): executed as full adoption.** Phase 4 was
> implemented more aggressively than scoped above — Document Explorer
> now consumes the full engine `<Scene>` path, and its custom d3 scene
> composition was retired, rather than cherry-picking traits à la
> carte. The decision's *intent* (preserve Document Explorer's distinct
> experience) is honored at the UX layer, not the rendering layer. See
> "Amendment: Phase 4 executed as full adoption" at the end of this
> document for rationale.

---

## Alternatives Considered
Expand Down Expand Up @@ -586,3 +595,54 @@ edges. Phase 1 implementation will need to cover all four from day one
because kg's existing 3D explorer already exhibits all four properties —
losing them in V2 would block cutover.

---

## Amendment: Phase 4 executed as full adoption (2026-05-15)

Phase 4 was scoped above as *optional, opportunistic, à-la-carte*: the
Document Explorer would borrow individual engine traits while keeping
its own d3 scene composition, with "no commitment to complete
migration." It was instead implemented as **full engine adoption**
(PR #368): the Document Explorer's standalone d3 force implementation
was retired and the component now renders through the same engine
`<Scene>` path as the Force Graph plugin.

### Why the scope changed

Once Phases 1–3 landed, the cost/benefit inverted relative to what was
assumed when this ADR was written:

- **The engine boundary held cleanly.** Every trait the Document
Explorer needed (per-node geometry classes, render-collapsed
clustering edges, label color/offset overrides, the shared dim
model) was expressible as additive, opt-in engine props with
Force-Graph-preserving defaults. The "keep custom composition"
hedge existed to avoid contorting the engine; that risk did not
materialize.
- **Unification compounds.** With both explorers on one `<Scene>`,
engine work lands in both at once. Concrete instance from this
cycle: harmonizing hover/focus dimming (and the later
lerp-toward-background fix) was a single shared `dimModel.ts` change
consumed identically by both — impossible without shared rendering.
- **2D/3D projection for free.** Full adoption gave the Document
Explorer the projection toggle with no bespoke work, which the
à-la-carte path would not have.

### What the original intent still buys

ADR-085's concern — the Document Explorer has "a specific radial
layout and its own interaction model" — is preserved, but at the
**UX layer rather than the rendering layer**: distinct node glyphs
(document vs. concept), its own color scheme, document viewer, sidebar,
and a structural (not edge-derived) hover/focus neighborhood model.
The engine renders; the Document Explorer still decides what its
experience is. "Keep it distinct" was honored; "keep it on a separate
rendering stack" was not, and that is the deliberate change recorded
here.

### Status of the à-la-carte hedge

Withdrawn. Phase 4 is no longer "optional and opportunistic" — it is
done, as full adoption. No engine-divergent Document Explorer stack
remains to migrate later.

53 changes: 26 additions & 27 deletions web/src/components/documents/DocumentExplorerWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,45 +26,42 @@ import { SavedQueriesPanel } from '../shared/SavedQueriesPanel';
import { useQueryReplay, type ReplayableDefinition } from '../../hooks/useQueryReplay';
import { usePassageSearch } from '../../hooks/usePassageSearch';
import { useGraphStore } from '../../store/graphStore';
import { useDocumentExplorerStore, type SidebarDocument } from '../../store/documentExplorerStore';
import { mapWorkingGraphToRawGraph } from '../../utils/cypherResultMapper';
import { cypherToStatement } from '../../utils/programBuilder';

/** Sidebar document entry (from findDocumentsByConcepts). */
interface SidebarDocument {
document_id: string;
filename: string;
ontology: string;
content_type: string;
concept_ids: string[]; // concepts overlapping with query
totalConceptCount: number; // ALL concepts for this doc (after hydration)
}

export const DocumentExplorerWorkspace: React.FC = () => {
const { replayQuery } = useQueryReplay();
const [activeRailTab, setActiveRailTab] = useState('savedQueries');

// Pipeline state
// Pipeline state — kept local; these reset on remount intentionally
// (a stale "Loading..." indicator on a re-entry would be wrong).
const [isLoading, setIsLoading] = useState(false);
const [loadingMessage, setLoadingMessage] = useState('');
const [error, setError] = useState<string | null>(null);

// Document list (populated from query)
const [sidebarDocs, setSidebarDocs] = useState<SidebarDocument[]>([]);

// Graph data
const [explorerData, setExplorerData] = useState<DocumentExplorerData | null>(null);

// Focus state — which document is focused in the graph
const [focusedDocId, setFocusedDocId] = useState<string | null>(null);

// Document viewer
// Session state — lives in a Zustand store so it survives navigating
// away and back. Mirrors how Force Graph keeps `rawGraphData` in
// `graphStore`. See `documentExplorerStore` for why we don't persist
// these to localStorage.
const sidebarDocs = useDocumentExplorerStore((s) => s.sidebarDocs);
const explorerData = useDocumentExplorerStore((s) => s.explorerData);
const focusedDocId = useDocumentExplorerStore((s) => s.focusedDocId);
const setSidebarDocs = useDocumentExplorerStore((s) => s.setSidebarDocs);
const setExplorerData = useDocumentExplorerStore((s) => s.setExplorerData);
const setFocusedDocId = useDocumentExplorerStore((s) => s.setFocusedDocId);
const resetSession = useDocumentExplorerStore((s) => s.reset);

// Document viewer is per-mount UI — modal-style state that doesn't
// need to survive navigation.
const [viewingDocument, setViewingDocument] = useState<{
document_id: string;
filename: string;
content_type: string;
} | null>(null);

// Settings
// Settings — local, matching Force Graph's pattern where settings
// reset on remount.
const [settings, setSettings] = useState<DocumentExplorerSettings>(DEFAULT_SETTINGS);

// Multi-query passage search (extracted hook)
Expand All @@ -89,9 +86,7 @@ export const DocumentExplorerWorkspace: React.FC = () => {
const handleLoadExplorationQuery = useCallback(async (query: ReplayableDefinition) => {
setIsLoading(true);
setError(null);
setFocusedDocId(null);
setExplorerData(null);
setSidebarDocs([]);
resetSession();
resetQueries();

try {
Expand Down Expand Up @@ -271,8 +266,12 @@ export const DocumentExplorerWorkspace: React.FC = () => {
// ---------------------------------------------------------------------------

const handleFocusDocument = useCallback((docId: string) => {
setFocusedDocId(prev => prev === docId ? null : docId);
}, []);
// Read current focus from the store directly so this callback can stay
// dependency-free — the Zustand setter doesn't accept an updater fn
// the way `useState` does.
const current = useDocumentExplorerStore.getState().focusedDocId;
setFocusedDocId(current === docId ? null : docId);
}, [setFocusedDocId]);

const handleViewDocument = useCallback((doc: SidebarDocument) => {
setViewingDocument({
Expand Down
Loading
Loading