diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index b3240e6..efa03bd 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -117,6 +117,13 @@ function Dashboard({ accessToken }: { accessToken: string }) { const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph); const domainGraph = useDashboardStore((s) => s.domainGraph); const setDomainGraph = useDashboardStore((s) => s.setDomainGraph); + const layoutIssues = useDashboardStore((s) => s.layoutIssues); + // Schema issues + ELK layout issues share the WarningBanner — graph-load + // problems and dashboard rendering problems are equally surfaced. + const allIssues = useMemo( + () => [...graphIssues, ...layoutIssues], + [graphIssues, layoutIssues], + ); useEffect(() => { fetch(dataUrl("meta.json", accessToken)) @@ -506,8 +513,8 @@ function Dashboard({ accessToken }: { accessToken: string }) { {/* Validation warning banner */} - {graphIssues.length > 0 && !loadError && ( - + {allIssues.length > 0 && !loadError && ( + )} {/* Error banner */} diff --git a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx index acb4f3e..6730000 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx @@ -199,8 +199,8 @@ function DomainGraphViewInner() { .then(({ positioned, issues }) => { if (cancelled) return; if (issues.length > 0) { - // TODO: Task 16 funnels these into the WarningBanner. - console.warn("[domain ELK] layout issues:", issues); + // Funnel into store so WarningBanner surfaces them. + useDashboardStore.getState().appendLayoutIssues(issues); } setLayout({ nodes: mergeElkPositions(nodesArray, positioned), diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx index fd516e2..38f50a1 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx @@ -228,37 +228,42 @@ function useOverviewGraph() { nodes: [], edges: [], }); + const [layoutStatus, setLayoutStatus] = useState<"computing" | "ready">("ready"); useEffect(() => { if (!built) { setOverview({ nodes: [], edges: [] }); + setLayoutStatus("ready"); return; } let cancelled = false; const { clusterNodes, flowEdges, dims } = built; const baseNodes = clusterNodes as unknown as Node[]; const elkInput = nodesToElkInput(baseNodes, flowEdges, dims); + setLayoutStatus("computing"); applyElkLayout(elkInput, { strict: import.meta.env.DEV }) .then(({ positioned, issues }) => { if (cancelled) return; if (issues.length > 0) { - // TODO: Task 16 wires these into the WarningBanner. Until then, - // surface them in the console so they aren't completely silent. - console.warn("[overview ELK] layout issues:", issues); + // Funnel into store so WarningBanner surfaces them. getState() + // avoids re-creating the closure on every layoutIssues change. + useDashboardStore.getState().appendLayoutIssues(issues); } const positionedNodes = mergeElkPositions(baseNodes, positioned); setOverview({ nodes: positionedNodes, edges: flowEdges }); + setLayoutStatus("ready"); }) .catch((err) => { if (cancelled) return; console.error("[overview ELK] layout failed:", err); + setLayoutStatus("ready"); }); return () => { cancelled = true; }; }, [built]); - return overview; + return { ...overview, layoutStatus }; } // ── Layer detail level: topology (ELK Stage 1) + visual overlay ───────── @@ -295,7 +300,9 @@ const EMPTY_TOPOLOGY: LayerDetailTopology = { * selectedNodeId, searchResults, tourHighlightedNodeIds, or * expandedContainers (Stage 2 concern). */ -function useLayerDetailTopology(): LayerDetailTopology { +function useLayerDetailTopology(): LayerDetailTopology & { + layoutStatus: "computing" | "ready"; +} { const graph = useDashboardStore((s) => s.graph); const activeLayerId = useDashboardStore((s) => s.activeLayerId); const selectNode = useDashboardStore((s) => s.selectNode); @@ -575,10 +582,12 @@ function useLayerDetailTopology(): LayerDetailTopology { // surrounding atoms reflow into the correct positions. const stage1Tick = useDashboardStore((s) => s.stage1Tick); const [topology, setTopology] = useState(EMPTY_TOPOLOGY); + const [layoutStatus, setLayoutStatus] = useState<"computing" | "ready">("ready"); useEffect(() => { if (!built) { setTopology(EMPTY_TOPOLOGY); + setLayoutStatus("ready"); return; } let cancelled = false; @@ -646,12 +655,13 @@ function useLayerDetailTopology(): LayerDetailTopology { edges: stage1Edges, }; + setLayoutStatus("computing"); applyElkLayout(elkInput, { strict: import.meta.env.DEV }) .then(({ positioned, issues }) => { if (cancelled) return; if (issues.length > 0) { - // TODO: Task 16 wires these into the WarningBanner. - console.warn("[layer-detail Stage 1 ELK] layout issues:", issues); + // Funnel into store so WarningBanner surfaces them. + useDashboardStore.getState().appendLayoutIssues(issues); } const allBaseNodes: Node[] = [ ...(containerFlowNodes as unknown as Node[]), @@ -670,10 +680,12 @@ function useLayerDetailTopology(): LayerDetailTopology { nodeToContainer, intraContainer, }); + setLayoutStatus("ready"); }) .catch((err) => { if (cancelled) return; console.error("[layer-detail Stage 1 ELK] layout failed:", err); + setLayoutStatus("ready"); }); return () => { @@ -736,7 +748,8 @@ function useLayerDetailTopology(): LayerDetailTopology { strict: import.meta.env.DEV, }); if (issues.length > 0) { - console.warn(`[Stage 2 ${containerId}] issues:`, issues); + // Funnel into store so WarningBanner surfaces them. + useDashboardStore.getState().appendLayoutIssues(issues); } const childPositions = new Map(); let maxX = 0; @@ -806,7 +819,7 @@ function useLayerDetailTopology(): LayerDetailTopology { bumpStage1Tick, ]); - return topology; + return { ...topology, layoutStatus }; } /** @@ -1157,7 +1170,13 @@ function useLayerDetailGraph() { [topo.containers], ); - return { nodes, edges, nodeToContainer: topo.nodeToContainer, containerIds }; + return { + nodes, + edges, + nodeToContainer: topo.nodeToContainer, + containerIds, + layoutStatus: topo.layoutStatus, + }; } // ── Main inner component (must be inside ReactFlowProvider) ──────────── @@ -1183,6 +1202,7 @@ function GraphViewInner() { edges: initialEdges, nodeToContainer, containerIds, + layoutStatus, } = navigationLevel === "overview" ? { ...overviewGraph, nodeToContainer: undefined, containerIds: undefined } : detailGraph; @@ -1347,6 +1367,22 @@ function GraphViewInner() { + {layoutStatus === "computing" && ( +
+ Computing layout… +
+ )} ); } diff --git a/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx b/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx index 753a2c5..8fd7c74 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx @@ -6,17 +6,27 @@ interface WarningBannerProps { } function buildCopyText(issues: GraphIssue[]): string { - const lines = [ - "The following issues were found in your knowledge-graph.json.", - "These are LLM generation errors — not a system bug.", - "You can ask your agent to fix these specific issues in the knowledge-graph.json file:", - "", - ]; + const hasFatal = issues.some((i) => i.level === "fatal"); + // Fatal issues are dashboard rendering bugs (e.g. ELK layout failures), not + // LLM generation errors — route the user to file a bug report instead of + // asking their agent to "fix" the knowledge-graph.json. + const lines = hasFatal + ? [ + "Some of these issues look like dashboard rendering bugs.", + "Please file an issue at github.com/Lum1104/Understand-Anything/issues with the text below.", + "", + ] + : [ + "The following issues were found in your knowledge-graph.json.", + "These are LLM generation errors — not a system bug.", + "You can ask your agent to fix these specific issues in the knowledge-graph.json file:", + "", + ]; - // Auto-corrected first, then dropped + // Show fatal first (most actionable for bug reports), then dropped, then auto-corrected. const sorted = [...issues].sort((a, b) => { - const order: Record = { "auto-corrected": 0, dropped: 1, fatal: 2 }; - return (order[a.level] ?? 2) - (order[b.level] ?? 2); + const order: Record = { fatal: 0, dropped: 1, "auto-corrected": 2 }; + return (order[a.level] ?? 3) - (order[b.level] ?? 3); }); for (const issue of sorted) { @@ -36,18 +46,25 @@ export default function WarningBanner({ issues }: WarningBannerProps) { const [expanded, setExpanded] = useState(false); const [copied, setCopied] = useState(false); + const fatal = issues.filter((i) => i.level === "fatal"); const autoCorrected = issues.filter((i) => i.level === "auto-corrected"); const dropped = issues.filter((i) => i.level === "dropped"); + const hasFatal = fatal.length > 0; // Build summary text — only mention counts > 0 const parts: string[] = []; + if (fatal.length > 0) { + parts.push(`${fatal.length} fatal error${fatal.length !== 1 ? "s" : ""}`); + } if (autoCorrected.length > 0) { parts.push(`${autoCorrected.length} auto-correction${autoCorrected.length !== 1 ? "s" : ""}`); } if (dropped.length > 0) { parts.push(`${dropped.length} dropped item${dropped.length !== 1 ? "s" : ""}`); } - const summary = `Knowledge graph loaded with ${parts.join(" and ")}`; + const summary = hasFatal + ? `Dashboard hit ${parts.join(", ")}` + : `Knowledge graph loaded with ${parts.join(" and ")}`; const handleCopy = useCallback(async () => { const text = buildCopyText(issues); @@ -62,18 +79,36 @@ export default function WarningBanner({ issues }: WarningBannerProps) { if (issues.length === 0) return null; + // Fatal issues escalate the banner from amber (warning) to red (error). + const containerClasses = hasFatal + ? "bg-red-900/25 border-b border-red-700 text-red-200 text-sm" + : "bg-amber-900/20 border-b border-amber-700 text-amber-200 text-sm"; + const hoverClasses = hasFatal + ? "hover:bg-red-900/15" + : "hover:bg-amber-900/10"; + const iconClasses = hasFatal ? "text-red-400" : "text-amber-400"; + const hintClasses = hasFatal ? "text-red-400/60" : "text-amber-400/60"; + const dividerClasses = hasFatal ? "border-red-700/50" : "border-amber-700/50"; + const footerTextClasses = hasFatal ? "text-red-200/70" : "text-amber-200/60"; + const buttonClasses = hasFatal + ? "bg-red-800/40 text-red-200 hover:bg-red-800/60" + : "bg-amber-800/40 text-amber-200 hover:bg-amber-800/60"; + const footerCopy = hasFatal + ? "Copy these issues and file a bug report on GitHub" + : "Copy these issues and ask your agent to fix them in knowledge-graph.json"; + return ( -
+
{/* Collapsed summary row */} @@ -115,9 +150,36 @@ export default function WarningBanner({ issues }: WarningBannerProps) {
{/* Issue list */}
+ {/* Fatal issues — top of list, red, most prominent */} + {fatal.length > 0 && ( +
+

+ Fatal ({fatal.length}) +

+ {fatal.map((issue, i) => ( +
+ + + + + + {issue.message} +
+ ))} +
+ )} + {/* Auto-corrected issues */} {autoCorrected.length > 0 && ( -
+
0 ? "mt-2" : ""}>

Auto-corrected ({autoCorrected.length})

@@ -136,7 +198,7 @@ export default function WarningBanner({ issues }: WarningBannerProps) { {/* Dropped issues */} {dropped.length > 0 && ( -
0 ? "mt-2" : ""}> +
0 || autoCorrected.length > 0 ? "mt-2" : ""}>

Dropped ({dropped.length})

@@ -155,14 +217,12 @@ export default function WarningBanner({ issues }: WarningBannerProps) {
{/* Footer with copy button and actionable message */} -
-

- Copy these issues and ask your agent to fix them in knowledge-graph.json -

+
+

{footerCopy}