mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-30 04:40:16 +00:00
## Folder & Project
- Renamed `semantica-explorer/` → `explorer/` (cleaner path)
- Browser tab title: `Semantica Knowledge Explorer`
- Brand pill: `SEM` → `SKE` (tooltip: Semantica Knowledge Explorer)
- Nav rail label: `Explore` → `Knowledge Explorer`
- package.json name: `semantica-knowledge-explorer`
- Downgraded Vite 8 → Vite 5 for Node v20.17.0 compatibility
## App Shell
- Dynamic per-workspace kicker labels replacing static "Workspace" pill:
Graph Studio · Vocabulary Browser · Reasoning Engine · SPARQL Query ·
Decision Intelligence · Knowledge Audit · Graph Governance
## Enrich Workspace — 2 new tabs
### Entity Resolution tab
- Similarity threshold slider (0.50–0.99)
- Run Dedup Scan → POST /api/enrich/dedup
- Flagged pairs list with colour-coded score bars (red/amber/green)
- Expandable inline diff: primary vs duplicate side-by-side
- One-click Merge → POST /api/enrich/merge with logEvent dispatch
- Dismiss per pair; Clear all button
- Merge history sidebar pulled live from Registry store
### Registry tab (Document Registry)
- Live chronological audit log of all KG mutations in-session
- Colour-coded op-type badges: IMPORT · MERGE · ADD NODE · ADD EDGE ·
INFER · DELETE · EXPORT · VOCAB
- Filter pills to narrow by operation type
- Expandable JSON detail rows per entry
- Clear log button
- Entirely client-side via registryStore (no backend needed)
## Manage Workspace — 2 new tabs
### KG Overview tab
- Stats chips: total nodes, edges, graph density
- Node type breakdown bar chart (up to 8 types, colour-coded)
- Edge type breakdown bar chart from /api/graph/stats
- Top-10 most connected nodes ranked by degree
- Skeleton loading states + Refresh button
### Ontology Summary tab
- Read-only SKOS scheme tree (scheme → top concepts → narrower)
- Concept detail panel: labels, notation, description, narrower nav
- "Open Full Browser" button deep-links to Vocabulary Browser tab
## Decision Workspace polish
- CausalFlowDiagram: vertical node cards connected by relationship pills
- Outcome badges: colour-coded (green=approved, red=rejected, amber=deferred)
- Live filter input across decision ID, category, and outcome
- Animated skeleton loading while list fetches
## Graph Inspector polish
- PathFlowViz: clickable node chips connected by edge-type labels;
clicking a chip focuses that node in the canvas
- Link Prediction button shows spinner while computing
- Empty states for path trace and candidate links sections
## Registry dispatch — WebSocket
- ADD_NODE events → logEvent("add-node", …) in GraphWorkspace WS handler
- ADD_EDGE events → logEvent("add-edge", …) in GraphWorkspace WS handler
- Import, Export, Merge already dispatched logEvent on API response
## Graph visibility overhaul
### Edge colours (were nearly transparent, now clearly visible)
- edgeBackbone: rgba(…, 0.04) → rgba(…, 0.38)
- edgeStructure: rgba(…, 0.009) → rgba(…, 0.28)
- edgeInspection: rgba(…, 0.026) → rgba(…, 0.48)
- Muted edges: 0.009–0.02 → 0.12–0.26
- Focus edges: 0.16 → 0.42
### Edge sizes
- default minSize: 0.18 → 0.9 (always at least 1 pixel wide)
- path minSize: 1.8 → 2.4
- inactive/muted: hide:true → hide:false (dimmed not hidden)
### Node sizes
- default sizeMultiplier: 0.72 → 0.92
- default minSize: 0.68 → 3.5 (visible at all zoom levels)
- overview nodeScale: 0.66 → 0.88
- nodeTintMix (colour): 0.03 → 0.14
- nodeCoreMix (brightness): 0.52 → 0.72
### Label budget
- overview: 10 → 28 labels
- structure: 36 → 60 labels
- inspection: 80 → 120 labels
### Sigma settings
- renderEdgeLabels: false → true (relationship type on every edge)
- edgeLabelSize: — → 10
- labelRenderedSizeThreshold: 4 → 2
- labelDensity: 0.86 → 1.1
- hideLabelsOnMove: true → false (labels stay visible while panning)
- hideEdgesOnMove: true → false (edges stay visible while panning)
- minCameraRatio: — → 0.04 (prevents zooming inside a node)
- maxCameraRatio: — → 8 (graph stays visible when zoomed out)
### Zoom controls
- Added Zoom In (+) and Zoom Out (−) buttons to graph toolbar
- Smooth animated zoom via camera.animatedZoom / animatedUnzoom (200ms)
- Mouse scroll wheel clamped between minCameraRatio and maxCameraRatio
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
|
|
|
import { GRAPH_THEME, withAlpha } from "./graphTheme";
|
|
import { GRAPH_LOAD_STAGE_SEQUENCE, createGraphLoadProgress, getGraphLoadStageLabel } from "./graphLoading";
|
|
import type { GraphLoadProgress } from "./types";
|
|
|
|
const LOADING_OVERLAY_CSS = `
|
|
.graph-stage-loader {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
pointer-events: none;
|
|
opacity: 1;
|
|
transition: opacity 220ms ease, transform 220ms ease;
|
|
}
|
|
.graph-stage-loader[data-exiting="true"] {
|
|
opacity: 0;
|
|
transform: scale(0.985);
|
|
}
|
|
.graph-stage-loader-card {
|
|
width: min(540px, calc(100% - 48px));
|
|
border-radius: 24px;
|
|
padding: 20px 20px 18px;
|
|
border: 1px solid rgba(127, 208, 255, 0.18);
|
|
background:
|
|
radial-gradient(circle at top right, rgba(242, 182, 109, 0.12), transparent 28%),
|
|
radial-gradient(circle at top left, rgba(127, 208, 255, 0.14), transparent 30%),
|
|
linear-gradient(145deg, rgba(7, 17, 31, 0.94), rgba(12, 25, 43, 0.82));
|
|
box-shadow: 0 26px 90px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255,255,255,0.05);
|
|
backdrop-filter: blur(18px) saturate(1.08);
|
|
-webkit-backdrop-filter: blur(18px) saturate(1.08);
|
|
}
|
|
.graph-stage-loader-card[data-live="true"] {
|
|
width: min(500px, calc(100% - 56px));
|
|
background:
|
|
radial-gradient(circle at top right, rgba(242, 182, 109, 0.08), transparent 26%),
|
|
radial-gradient(circle at top left, rgba(127, 208, 255, 0.12), transparent 28%),
|
|
linear-gradient(145deg, rgba(7, 17, 31, 0.84), rgba(11, 24, 40, 0.72));
|
|
box-shadow: 0 18px 54px rgba(0, 0, 0, 0.26), inset 0 1px 0 rgba(255,255,255,0.04);
|
|
}
|
|
.graph-stage-loader-beacon {
|
|
position: relative;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 999px;
|
|
background: linear-gradient(135deg, rgba(127, 208, 255, 0.98), rgba(242, 182, 109, 0.94));
|
|
box-shadow: 0 0 18px rgba(127, 208, 255, 0.4);
|
|
}
|
|
.graph-stage-loader-beacon::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: -7px;
|
|
border-radius: inherit;
|
|
border: 1px solid rgba(127, 208, 255, 0.18);
|
|
animation: graph-loader-beacon 1.9s ease-out infinite;
|
|
}
|
|
.graph-stage-loader-track {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
gap: 8px;
|
|
}
|
|
.graph-stage-loader-step {
|
|
border-radius: 999px;
|
|
padding: 7px 0;
|
|
text-align: center;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
border: 1px solid rgba(127, 208, 255, 0.08);
|
|
color: rgba(143, 168, 198, 0.72);
|
|
background: rgba(255, 255, 255, 0.02);
|
|
}
|
|
.graph-stage-loader-step[data-state="done"] {
|
|
color: rgba(214, 232, 250, 0.92);
|
|
border-color: rgba(127, 208, 255, 0.18);
|
|
background: rgba(89, 155, 220, 0.14);
|
|
}
|
|
.graph-stage-loader-step[data-state="active"] {
|
|
color: #eff7ff;
|
|
border-color: rgba(242, 182, 109, 0.24);
|
|
background: linear-gradient(135deg, rgba(49, 108, 172, 0.28), rgba(242, 182, 109, 0.16));
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
}
|
|
.graph-stage-loader-bar {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 12px;
|
|
overflow: hidden;
|
|
border-radius: 999px;
|
|
border: 1px solid rgba(127, 208, 255, 0.1);
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}
|
|
.graph-stage-loader-bar-fill {
|
|
display: block;
|
|
height: 100%;
|
|
border-radius: inherit;
|
|
background: linear-gradient(90deg, rgba(74, 163, 255, 0.9), rgba(127, 208, 255, 0.96), rgba(242, 182, 109, 0.92));
|
|
box-shadow: 0 0 30px rgba(74, 163, 255, 0.28);
|
|
transition: width 220ms ease;
|
|
}
|
|
.graph-stage-loader-bar-indeterminate::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 1px;
|
|
bottom: 1px;
|
|
width: 34%;
|
|
border-radius: 999px;
|
|
background: linear-gradient(90deg, rgba(74, 163, 255, 0), rgba(127, 208, 255, 0.94), rgba(242, 182, 109, 0.82), rgba(74, 163, 255, 0));
|
|
box-shadow: 0 0 26px rgba(127, 208, 255, 0.18);
|
|
animation: graph-loader-sweep 1.5s cubic-bezier(0.22, 1, 0.36, 1) infinite;
|
|
}
|
|
@keyframes graph-loader-beacon {
|
|
0% { transform: scale(0.72); opacity: 0.6; }
|
|
100% { transform: scale(1.44); opacity: 0; }
|
|
}
|
|
@keyframes graph-loader-sweep {
|
|
0% { transform: translateX(-120%); }
|
|
100% { transform: translateX(360%); }
|
|
}
|
|
`;
|
|
|
|
function formatLayoutSource(source: GraphLoadProgress["layoutSource"]) {
|
|
switch (source) {
|
|
case "provided":
|
|
return "Persisted layout";
|
|
case "carried":
|
|
return "Preserved layout";
|
|
case "runtime":
|
|
return "Runtime layout";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function formatLayoutState(state: GraphLoadProgress["layoutState"]) {
|
|
switch (state) {
|
|
case "bootstrapping":
|
|
return "Bootstrapping";
|
|
case "running":
|
|
return "Settling";
|
|
case "interactive":
|
|
return "Interactive";
|
|
case "stabilized":
|
|
return "Stable";
|
|
case "failed":
|
|
return "Fallback";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const loadingMetricStyle = {
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
gap: 6,
|
|
padding: "7px 10px",
|
|
borderRadius: 999,
|
|
border: "1px solid rgba(127, 208, 255, 0.12)",
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
color: "#b8cade",
|
|
fontSize: 11,
|
|
fontWeight: 600,
|
|
} satisfies CSSProperties;
|
|
|
|
export function GraphLoadingOverlay({
|
|
progress,
|
|
visible,
|
|
showGraphBehind,
|
|
}: {
|
|
progress: GraphLoadProgress | null;
|
|
visible: boolean;
|
|
showGraphBehind: boolean;
|
|
}) {
|
|
const [renderVisible, setRenderVisible] = useState(visible);
|
|
const [exiting, setExiting] = useState(false);
|
|
const [displayProgress, setDisplayProgress] = useState<GraphLoadProgress>(
|
|
progress ?? createGraphLoadProgress({
|
|
phase: "bootstrapping",
|
|
message: "Preparing graph session",
|
|
progressKind: "indeterminate",
|
|
}),
|
|
);
|
|
const exitTimerRef = useRef<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (progress) {
|
|
setDisplayProgress(progress);
|
|
}
|
|
}, [progress]);
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
if (exitTimerRef.current !== null) {
|
|
window.clearTimeout(exitTimerRef.current);
|
|
exitTimerRef.current = null;
|
|
}
|
|
setRenderVisible(true);
|
|
setExiting(false);
|
|
return;
|
|
}
|
|
|
|
if (!renderVisible) {
|
|
return;
|
|
}
|
|
|
|
setExiting(true);
|
|
exitTimerRef.current = window.setTimeout(() => {
|
|
setRenderVisible(false);
|
|
setExiting(false);
|
|
exitTimerRef.current = null;
|
|
}, 220);
|
|
|
|
return () => {
|
|
if (exitTimerRef.current !== null) {
|
|
window.clearTimeout(exitTimerRef.current);
|
|
exitTimerRef.current = null;
|
|
}
|
|
};
|
|
}, [renderVisible, visible]);
|
|
|
|
if (!renderVisible) {
|
|
return null;
|
|
}
|
|
|
|
const activeProgress = progress ?? displayProgress;
|
|
const isLiveStage = activeProgress.phase === "stabilizing_layout" || activeProgress.showGraphBehind || showGraphBehind;
|
|
const overlayBackground = isLiveStage
|
|
? "linear-gradient(180deg, rgba(1,4,9,0.04), rgba(1,4,9,0.18))"
|
|
: "linear-gradient(180deg, rgba(1,4,9,0.22), rgba(1,4,9,0.5))";
|
|
const determinateRatio = activeProgress.progressKind === "determinate" && activeProgress.total
|
|
? Math.max(0.05, Math.min(activeProgress.loaded ?? 0, activeProgress.total) / Math.max(activeProgress.total, 1))
|
|
: null;
|
|
const layoutSource = formatLayoutSource(activeProgress.layoutSource);
|
|
const layoutState = formatLayoutState(activeProgress.layoutState);
|
|
|
|
return (
|
|
<div
|
|
className="graph-stage-loader"
|
|
data-exiting={exiting}
|
|
style={{ background: overlayBackground }}
|
|
>
|
|
<style>{LOADING_OVERLAY_CSS}</style>
|
|
<div className="graph-stage-loader-card" data-live={isLiveStage}>
|
|
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 14, marginBottom: 14 }}>
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ color: "#ffffff", fontSize: 20, fontWeight: 700, letterSpacing: "-0.03em", marginBottom: 6 }}>
|
|
{activeProgress.title}
|
|
</div>
|
|
<div style={{ color: "#8fa8c6", fontSize: 13, lineHeight: 1.5 }}>
|
|
{activeProgress.message}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: "inline-flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
|
|
<div className="graph-stage-loader-beacon" aria-hidden="true" />
|
|
<div style={{ color: "#d7e9fb", fontSize: 11, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase" }}>
|
|
Stage {activeProgress.stageIndex ?? 1}/{activeProgress.stageCount ?? GRAPH_LOAD_STAGE_SEQUENCE.length}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="graph-stage-loader-track" style={{ marginBottom: 14 }}>
|
|
{GRAPH_LOAD_STAGE_SEQUENCE.map((phase, index) => {
|
|
const current = activeProgress.stageIndex ?? 1;
|
|
const state = index + 1 < current ? "done" : index + 1 === current ? "active" : "upcoming";
|
|
return (
|
|
<div key={phase} className="graph-stage-loader-step" data-state={state}>
|
|
{getGraphLoadStageLabel(phase)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, alignItems: "baseline", marginBottom: 8 }}>
|
|
<div style={{ color: "#dce9f6", fontSize: 12, fontWeight: 600 }}>
|
|
{activeProgress.progressKind === "determinate" && activeProgress.total
|
|
? `${(activeProgress.loaded ?? 0).toLocaleString()} / ${activeProgress.total.toLocaleString()} in current stage`
|
|
: "Working through this stage"}
|
|
</div>
|
|
<div style={{ color: "#90a8c5", fontSize: 11, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase" }}>
|
|
{activeProgress.progressKind === "determinate" && determinateRatio !== null
|
|
? `${Math.round(determinateRatio * 100)}%`
|
|
: "Live"}
|
|
</div>
|
|
</div>
|
|
|
|
<div className={`graph-stage-loader-bar ${activeProgress.progressKind === "indeterminate" ? "graph-stage-loader-bar-indeterminate" : ""}`}>
|
|
{activeProgress.progressKind === "determinate" && determinateRatio !== null ? (
|
|
<span className="graph-stage-loader-bar-fill" style={{ width: `${Math.round(determinateRatio * 100)}%` }} />
|
|
) : null}
|
|
</div>
|
|
|
|
<div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginTop: 14 }}>
|
|
<span style={loadingMetricStyle}>
|
|
{activeProgress.nodesLoaded.toLocaleString()}
|
|
{activeProgress.nodesTotal ? ` / ${activeProgress.nodesTotal.toLocaleString()}` : ""} nodes
|
|
</span>
|
|
<span style={loadingMetricStyle}>
|
|
{activeProgress.edgesLoaded.toLocaleString()}
|
|
{activeProgress.edgesTotal ? ` / ${activeProgress.edgesTotal.toLocaleString()}` : ""} relationships
|
|
</span>
|
|
{layoutSource ? (
|
|
<span style={{ ...loadingMetricStyle, color: "#a9ddff", borderColor: withAlpha(GRAPH_THEME.palette.accent.hovered, 0.22) }}>
|
|
{layoutSource}
|
|
{layoutState ? ` · ${layoutState}` : ""}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|