fix(explorer): resolve 3 code-review bugs in GraphWorkspace, DecisionWorkspace, and index.css

- GraphWorkspace: set isRunningPredictions=true before link-prediction fetch
  and false in finally block; pass isRunningPredictions prop to
  LazyGraphInspectorPanel so the inspector button disables and shows a
  spinner during the request (was declared but never wired — broke
  noUnusedLocals TypeScript build)

- DecisionWorkspace: add AbortController to the /api/decisions useEffect
  so the fetch is cancelled on unmount; add per-call AbortController to
  handleSelectDecision for /api/decisions/:id/chain; add res.ok guards
  before .json() on both fetches; encodeURIComponent on decision_id to
  prevent path-injection edge cases

- index.css: add missing @keyframes skeleton-pulse rule (0%/100% opacity
  0.45, 50% opacity 0.85) — KGOverviewTab skeletonBarStyle referenced
  this animation but it was never defined, leaving skeleton bars static

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
KaifAhmad1
2026-04-13 14:16:59 +05:30
co-authored by Claude Sonnet 4.6
parent 3ea1283626
commit 670027fd22
3 changed files with 24 additions and 5 deletions
+6
View File
@@ -63,3 +63,9 @@ code, pre, .mono {
.animate-spin {
animation: spin 1s linear infinite;
}
/* Skeleton pulse animation for loading placeholders */
@keyframes skeleton-pulse {
0%, 100% { opacity: 0.45; }
50% { opacity: 0.85; }
}
@@ -203,15 +203,21 @@ export function DecisionWorkspace() {
const [filterQuery, setFilterQuery] = useState("");
useEffect(() => {
const controller = new AbortController();
setListLoading(true);
fetch("/api/decisions")
.then((res) => res.json())
fetch("/api/decisions", { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`Failed to load decisions: ${res.status}`);
return res.json();
})
.then((data) => {
setDecisions(data);
if (data.length > 0) void handleSelectDecision(data[0]);
})
.catch(console.error)
.catch((err) => { if (err.name !== "AbortError") console.error(err); })
.finally(() => setListLoading(false));
return () => controller.abort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const filteredDecisions = useMemo(() => {
@@ -229,15 +235,18 @@ export function DecisionWorkspace() {
setSelectedDecision(d);
setLoading(true);
setChain([]);
const controller = new AbortController();
try {
const res = await fetch(`/api/decisions/${d.decision_id}/chain`);
const res = await fetch(`/api/decisions/${encodeURIComponent(d.decision_id)}/chain`, { signal: controller.signal });
if (!res.ok) throw new Error(`Failed to load chain: ${res.status}`);
const data = await res.json();
setChain(data.chain || []);
} catch (e) {
console.error(e);
if ((e as DOMException).name !== "AbortError") console.error(e);
} finally {
setLoading(false);
}
return () => controller.abort();
};
return (
@@ -816,6 +816,7 @@ export function GraphWorkspace() {
const handleRunPredictions = useCallback(async () => {
if (!selectedNodeId) return;
setIsRunningPredictions(true);
try {
const response = await fetch("/api/enrich/links", {
method: "POST",
@@ -835,6 +836,8 @@ export function GraphWorkspace() {
} catch (predictionError) {
console.error("[GraphWorkspace] prediction failed", predictionError);
setPredictions([]);
} finally {
setIsRunningPredictions(false);
}
}, [predictionType, selectedNodeId]);
@@ -1609,6 +1612,7 @@ export function GraphWorkspace() {
predictionType={predictionType}
onPredictionTypeChange={setPredictionType}
onRunPredictions={() => void handleRunPredictions()}
isRunningPredictions={isRunningPredictions}
pathTargetId={pathTargetId}
onPathTargetChange={setPathTargetId}
onTracePath={() => void handleTracePath()}