From a04cdfd577d659934b6fbaf454ac1c63833f4cd1 Mon Sep 17 00:00:00 2001
From: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com>
Date: Fri, 14 Aug 2026 14:58:11 +0500
Subject: [PATCH 1/6] fix(explorer): show a retryable error when the graph
fails to load
The dependency-pre-bundle overlay had no failure path: on a fetch
error it kept rendering the last progress frame forever with no
retry. Route isError/error out of the load query, surface a real
error card with the underlying message, and let retry re-fetch
without a full page reload.
---
.../GraphWorkspace/GraphLoadingOverlay.tsx | 95 +++++++++++++++++++
.../GraphWorkspace/GraphWorkspace.tsx | 22 ++++-
2 files changed, 115 insertions(+), 2 deletions(-)
diff --git a/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx b/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx
index 77a94130..a1ca4ea8 100644
--- a/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx
+++ b/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState, type CSSProperties } from "react";
+import { AlertTriangle, RefreshCw } from "lucide-react";
import { GRAPH_THEME, withAlpha } from "./graphTheme";
import { GRAPH_LOAD_STAGE_SEQUENCE, createGraphLoadProgress, getGraphLoadStageLabel } from "./graphLoading";
@@ -121,6 +122,58 @@ const LOADING_OVERLAY_CSS = `
0% { transform: translateX(-120%); }
100% { transform: translateX(360%); }
}
+ .graph-stage-loader-card[data-error="true"] {
+ pointer-events: auto;
+ border-color: rgba(255, 123, 114, 0.32);
+ background:
+ radial-gradient(circle at top left, rgba(255, 123, 114, 0.12), transparent 32%),
+ linear-gradient(145deg, rgba(7, 17, 31, 0.96), rgba(24, 14, 18, 0.86));
+ }
+ .graph-stage-loader-error-mark {
+ width: 38px;
+ height: 38px;
+ flex: 0 0 auto;
+ border-radius: 12px;
+ display: grid;
+ place-items: center;
+ color: #ff9e97;
+ background: rgba(255, 123, 114, 0.12);
+ border: 1px solid rgba(255, 123, 114, 0.28);
+ }
+ .graph-stage-loader-error-detail {
+ padding: 10px 12px;
+ border-radius: 10px;
+ background: rgba(0, 0, 0, 0.32);
+ border: 1px solid rgba(255, 123, 114, 0.18);
+ color: #ffb4ae;
+ font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
+ font-size: 12px;
+ line-height: 1.55;
+ word-break: break-word;
+ }
+ .graph-stage-loader-retry {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 9px 16px;
+ border-radius: 8px;
+ font-size: 13px;
+ font-weight: 700;
+ cursor: pointer;
+ border: 1px solid rgba(127, 208, 255, 0.4);
+ background: linear-gradient(135deg, rgba(74, 163, 255, 0.28), rgba(56, 210, 160, 0.16));
+ color: #e8f6ff;
+ transition: 160ms ease;
+ }
+ .graph-stage-loader-retry:hover {
+ border-color: rgba(127, 208, 255, 0.62);
+ background: linear-gradient(135deg, rgba(74, 163, 255, 0.4), rgba(56, 210, 160, 0.24));
+ transform: translateY(-1px);
+ }
+ .graph-stage-loader-retry:focus-visible {
+ outline: 2px solid #7fd0ff;
+ outline-offset: 2px;
+ }
`;
function formatLayoutSource(source: GraphLoadProgress["layoutSource"]) {
@@ -170,10 +223,14 @@ export function GraphLoadingOverlay({
progress,
visible,
showGraphBehind,
+ error = null,
+ onRetry,
}: {
progress: GraphLoadProgress | null;
visible: boolean;
showGraphBehind: boolean;
+ error?: string | null;
+ onRetry?: () => void;
}) {
const [renderVisible, setRenderVisible] = useState(visible);
const [exiting, setExiting] = useState(false);
@@ -226,6 +283,44 @@ export function GraphLoadingOverlay({
return null;
}
+ if (error) {
+ return (
+
+
+
+
+
+
+
+ Could not load the graph
+
+
+ The Explorer API did not return graph data. Check that the backend is running and reachable, then try again.
+
+
+
+
+
{error}
+
+ {onRetry ? (
+
+
+
+ ) : null}
+
+
+ );
+ }
+
const activeProgress = progress ?? displayProgress;
const isLiveStage = activeProgress.phase === "stabilizing_layout" || activeProgress.showGraphBehind || showGraphBehind;
const overlayBackground = isLiveStage
diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
index df2cd6ec..14a94416 100644
--- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
+++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
@@ -1225,12 +1225,28 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap
}));
}, []);
- const { data: summary, isLoading, isFetching } = useLoadGraph({
+ const {
+ data: summary,
+ isLoading,
+ isFetching,
+ isError: isGraphLoadError,
+ error: graphLoadError,
+ refetch: refetchGraph,
+ } = useLoadGraph({
enabled: true,
onGraphReady: applyGraphReadySummary,
onProgress: handleLoadProgress,
});
+ const graphLoadErrorMessage = isGraphLoadError
+ ? (graphLoadError instanceof Error ? graphLoadError.message : "Unknown error while loading the graph.")
+ : null;
+
+ const handleRetryGraphLoad = useCallback(() => {
+ setLoadingProgress(null);
+ void refetchGraph();
+ }, [refetchGraph]);
+
useEffect(() => {
if (isLayoutRunning) {
return;
@@ -1901,7 +1917,7 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap
viewMode,
]);
- const showLoadingOverlay = !graphReady && (isLoading || isFetching || Boolean(loadingProgress));
+ const showLoadingOverlay = !graphReady && (isLoading || isFetching || Boolean(loadingProgress) || isGraphLoadError);
const showSettlingStatus = graphReady && loadingProgress?.phase === "stabilizing_layout";
const hasGraphContent = Boolean(summary?.nodeCount);
const activePath = pathResult?.path ?? EMPTY_PATH;
@@ -2946,6 +2962,8 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap
progress={loadingProgress}
visible={showLoadingOverlay}
showGraphBehind={hasGraphContent || Boolean(loadingProgress?.showGraphBehind)}
+ error={graphLoadErrorMessage}
+ onRetry={handleRetryGraphLoad}
/>
From 513ffdf8fe8447079240f6cb38013eeee8a03dba Mon Sep 17 00:00:00 2001
From: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com>
Date: Fri, 14 Aug 2026 14:58:19 +0500
Subject: [PATCH 2/6] fix(explorer): reflect real backend connectivity on the
landing page
The status dot and 'System Online' text were static, so a dead
backend still looked healthy. Track checking/online/offline explicitly
and drive both off the same state so they can't disagree.
---
explorer/src/App.tsx | 50 ++++++++++++++++++++++++++++++++------------
1 file changed, 37 insertions(+), 13 deletions(-)
diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx
index f3f97520..8f94d477 100644
--- a/explorer/src/App.tsx
+++ b/explorer/src/App.tsx
@@ -67,6 +67,14 @@ type GraphStatsPayload = {
edges?: number;
};
+type ConnectionStatus = 'checking' | 'online' | 'offline';
+
+const CONNECTION_STATUS_LABEL: Record = {
+ checking: 'Connecting…',
+ online: 'System Online',
+ offline: 'Backend Unreachable',
+};
+
const queryClient = new QueryClient();
const PREVIEW_DOTS = Array.from({ length: 42 }, (_, i) => ({
@@ -719,19 +727,34 @@ const shellStyles = `
align-items: center;
gap: 10px;
margin-bottom: 24px;
+ --status-color: #4cc38a;
+ --status-shadow-a: 0 0 0 3px rgba(76, 195, 138, 0.22), 0 0 12px rgba(76, 195, 138, 0.5);
+ --status-shadow-b: 0 0 0 5px rgba(76, 195, 138, 0.1), 0 0 20px rgba(76, 195, 138, 0.35);
+ }
+
+ .landing-status-bar[data-status='checking'] {
+ --status-color: #f2b66d;
+ --status-shadow-a: 0 0 0 3px rgba(242, 182, 109, 0.22), 0 0 12px rgba(242, 182, 109, 0.5);
+ --status-shadow-b: 0 0 0 5px rgba(242, 182, 109, 0.1), 0 0 20px rgba(242, 182, 109, 0.35);
+ }
+
+ .landing-status-bar[data-status='offline'] {
+ --status-color: #ff7b72;
+ --status-shadow-a: 0 0 0 3px rgba(255, 123, 114, 0.22), 0 0 12px rgba(255, 123, 114, 0.5);
+ --status-shadow-b: 0 0 0 5px rgba(255, 123, 114, 0.1), 0 0 20px rgba(255, 123, 114, 0.35);
}
.landing-status-dot {
width: 8px;
height: 8px;
border-radius: 999px;
- background: #4cc38a;
- box-shadow: 0 0 0 3px rgba(76, 195, 138, 0.22), 0 0 12px rgba(76, 195, 138, 0.5);
+ background: var(--status-color);
+ box-shadow: var(--status-shadow-a);
animation: landing-pulse 2.4s ease-in-out infinite;
}
.landing-status-text {
- color: #4cc38a;
+ color: var(--status-color);
font: 700 11px/1 "JetBrains Mono", monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
@@ -1323,8 +1346,8 @@ const shellStyles = `
}
@keyframes landing-pulse {
- 0%, 100% { box-shadow: 0 0 0 3px rgba(76, 195, 138, 0.22), 0 0 12px rgba(76, 195, 138, 0.5); }
- 50% { box-shadow: 0 0 0 5px rgba(76, 195, 138, 0.1), 0 0 20px rgba(76, 195, 138, 0.35); }
+ 0%, 100% { box-shadow: var(--status-shadow-a); }
+ 50% { box-shadow: var(--status-shadow-b); }
}
.workspace-loading {
@@ -1494,10 +1517,10 @@ function WelcomeScreen({
onOpenDecisions: () => void;
onOpenManage: () => void;
}) {
- const [stats, setStats] = useState<{ nodes: number | null; edges: number | null; ready: boolean }>({
+ const [stats, setStats] = useState<{ nodes: number | null; edges: number | null; status: ConnectionStatus }>({
nodes: null,
edges: null,
- ready: false,
+ status: 'checking',
});
useEffect(() => {
@@ -1507,31 +1530,32 @@ function WelcomeScreen({
.then((response) => (response.ok ? response.json() as Promise : null))
.then((payload) => {
if (!payload) {
- setStats((current) => ({ ...current, ready: false }));
+ setStats((current) => ({ ...current, status: 'offline' }));
return;
}
setStats({
nodes: getNumberStat(payload, ['node_count', 'nodeCount', 'nodes']),
edges: getNumberStat(payload, ['edge_count', 'edgeCount', 'edges']),
- ready: true,
+ status: 'online',
});
})
.catch((error: unknown) => {
if (error instanceof DOMException && error.name === 'AbortError') {
return;
}
- setStats((current) => ({ ...current, ready: false }));
+ setStats((current) => ({ ...current, status: 'offline' }));
});
return () => controller.abort();
}, []);
+ const isOnline = stats.status === 'online';
const metrics: LandingMetric[] = [
{ label: 'Knowledge nodes', value: formatMetric(stats.nodes, 'Live'), tone: 'cyan' },
{ label: 'Relationships mapped', value: formatMetric(stats.edges, 'Ready'), tone: 'mint' },
{ label: 'Graph modes', value: '3', tone: 'amber' },
- { label: stats.ready ? 'Dataset online' : 'Ready to explore', value: stats.ready ? 'Active' : 'Standby', tone: 'rose' },
+ { label: isOnline ? 'Dataset online' : 'Ready to explore', value: isOnline ? 'Active' : 'Standby', tone: 'rose' },
];
const secondaryLaunchers: LandingAction[] = [
@@ -1574,9 +1598,9 @@ function WelcomeScreen({
{/* ── Hero ── */}
-
+
-
System Online
+
{CONNECTION_STATUS_LABEL[stats.status]}
Semantica v2 · Semantic Intelligence
From 8df3abf6a0d6004bdbff31e232f5261f60d0ca61 Mon Sep 17 00:00:00 2001
From: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com>
Date: Fri, 14 Aug 2026 14:58:59 +0500
Subject: [PATCH 3/6] feat(explorer): let search results be dismissed, round
relevance scores
The results strip had no close affordance and stayed pinned until the
next search. Add a header row with a dismiss button, and round scores
to whole numbers instead of showing three decimals of a raw relevance
value nobody can act on.
---
.../GraphWorkspace/GraphWorkspace.tsx | 48 ++++++++++++++-----
1 file changed, 35 insertions(+), 13 deletions(-)
diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
index 14a94416..54f3f8c4 100644
--- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
+++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
@@ -12,6 +12,7 @@ import {
RefreshCw,
Search,
Users,
+ X,
ZoomIn,
ZoomOut,
} from "lucide-react";
@@ -1539,6 +1540,11 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap
}
}, [searchQuery]);
+ const handleClearSearchResults = useCallback(() => {
+ setSearchResults([]);
+ setSearchError("");
+ }, []);
+
const handleRunPredictions = useCallback(async () => {
if (!inspectableNodeId) return;
setIsRunningPredictions(true);
@@ -2855,20 +2861,36 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap
{searchError ?
{searchError}
: null}
{searchResults.length ? (
-
- {searchResults.map((result) => (
-