mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(dashboard): layout-issue banner + Computing layout overlay
This commit is contained in:
@@ -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 }) {
|
||||
<SearchBar />
|
||||
|
||||
{/* Validation warning banner */}
|
||||
{graphIssues.length > 0 && !loadError && (
|
||||
<WarningBanner issues={graphIssues} />
|
||||
{allIssues.length > 0 && !loadError && (
|
||||
<WarningBanner issues={allIssues} />
|
||||
)}
|
||||
|
||||
{/* Error banner */}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<LayerDetailTopology>(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<string, { x: number; y: number }>();
|
||||
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() {
|
||||
<TourFitView />
|
||||
<SelectedNodeFitView />
|
||||
</ReactFlow>
|
||||
{layoutStatus === "computing" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "rgba(10,10,10,0.5)",
|
||||
pointerEvents: "none",
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#d4a574", fontSize: 14 }}>Computing layout…</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, number> = { "auto-corrected": 0, dropped: 1, fatal: 2 };
|
||||
return (order[a.level] ?? 2) - (order[b.level] ?? 2);
|
||||
const order: Record<string, number> = { 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 (
|
||||
<div className="bg-amber-900/20 border-b border-amber-700 text-amber-200 text-sm">
|
||||
<div className={containerClasses}>
|
||||
{/* Collapsed summary row */}
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
className="w-full flex items-center gap-2 px-5 py-3 text-left hover:bg-amber-900/10 transition-colors"
|
||||
className={`w-full flex items-center gap-2 px-5 py-3 text-left transition-colors ${hoverClasses}`}
|
||||
>
|
||||
{/* Chevron icon */}
|
||||
<svg
|
||||
className={`w-4 h-4 shrink-0 text-amber-400 transition-transform duration-200 ${
|
||||
className={`w-4 h-4 shrink-0 ${iconClasses} transition-transform duration-200 ${
|
||||
expanded ? "rotate-90" : ""
|
||||
}`}
|
||||
fill="none"
|
||||
@@ -90,7 +125,7 @@ export default function WarningBanner({ issues }: WarningBannerProps) {
|
||||
|
||||
{/* Warning icon */}
|
||||
<svg
|
||||
className="w-4 h-4 shrink-0 text-amber-400"
|
||||
className={`w-4 h-4 shrink-0 ${iconClasses}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -105,7 +140,7 @@ export default function WarningBanner({ issues }: WarningBannerProps) {
|
||||
|
||||
<span className="flex-1">{summary}</span>
|
||||
|
||||
<span className="text-amber-400/60 text-xs shrink-0">
|
||||
<span className={`text-xs shrink-0 ${hintClasses}`}>
|
||||
{expanded ? "click to collapse" : "click to expand"}
|
||||
</span>
|
||||
</button>
|
||||
@@ -115,9 +150,36 @@ export default function WarningBanner({ issues }: WarningBannerProps) {
|
||||
<div className="px-5 pb-4">
|
||||
{/* Issue list */}
|
||||
<div className="space-y-1 mb-3">
|
||||
{/* Fatal issues — top of list, red, most prominent */}
|
||||
{fatal.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-red-400 mb-1">
|
||||
Fatal ({fatal.length})
|
||||
</h4>
|
||||
{fatal.map((issue, i) => (
|
||||
<div
|
||||
key={`ft-${i}`}
|
||||
className="flex items-start gap-2 py-0.5 pl-2 text-red-200"
|
||||
>
|
||||
<span className="text-red-400 shrink-0 mt-0.5">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="text-xs">{issue.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Auto-corrected issues */}
|
||||
{autoCorrected.length > 0 && (
|
||||
<div>
|
||||
<div className={fatal.length > 0 ? "mt-2" : ""}>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-amber-400 mb-1">
|
||||
Auto-corrected ({autoCorrected.length})
|
||||
</h4>
|
||||
@@ -136,7 +198,7 @@ export default function WarningBanner({ issues }: WarningBannerProps) {
|
||||
|
||||
{/* Dropped issues */}
|
||||
{dropped.length > 0 && (
|
||||
<div className={autoCorrected.length > 0 ? "mt-2" : ""}>
|
||||
<div className={fatal.length > 0 || autoCorrected.length > 0 ? "mt-2" : ""}>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-orange-400 mb-1">
|
||||
Dropped ({dropped.length})
|
||||
</h4>
|
||||
@@ -155,14 +217,12 @@ export default function WarningBanner({ issues }: WarningBannerProps) {
|
||||
</div>
|
||||
|
||||
{/* Footer with copy button and actionable message */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-amber-700/50">
|
||||
<p className="text-xs text-amber-200/60">
|
||||
Copy these issues and ask your agent to fix them in knowledge-graph.json
|
||||
</p>
|
||||
<div className={`flex items-center justify-between pt-2 border-t ${dividerClasses}`}>
|
||||
<p className={`text-xs ${footerTextClasses}`}>{footerCopy}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1.5 px-3 py-1 rounded text-xs font-medium bg-amber-800/40 text-amber-200 hover:bg-amber-800/60 transition-colors shrink-0 ml-4"
|
||||
className={`flex items-center gap-1.5 px-3 py-1 rounded text-xs font-medium transition-colors shrink-0 ml-4 ${buttonClasses}`}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { SearchEngine } from "@understand-anything/core/search";
|
||||
import type { SearchResult } from "@understand-anything/core/search";
|
||||
import type { GraphIssue } from "@understand-anything/core/schema";
|
||||
import type {
|
||||
KnowledgeGraph,
|
||||
TourStep,
|
||||
@@ -173,6 +174,12 @@ interface DashboardStore {
|
||||
|
||||
stage1Tick: number;
|
||||
bumpStage1Tick: () => void;
|
||||
|
||||
// Layout-time issues (e.g. ELK input repair). Funneled into the
|
||||
// WarningBanner alongside graph-validation issues.
|
||||
layoutIssues: GraphIssue[];
|
||||
appendLayoutIssues: (issues: GraphIssue[]) => void;
|
||||
clearLayoutIssues: () => void;
|
||||
}
|
||||
|
||||
function getSortedTour(graph: KnowledgeGraph): TourStep[] {
|
||||
@@ -261,6 +268,7 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
expandedContainers: new Set(),
|
||||
containerSizeMemory: new Map(),
|
||||
stage1Tick: 0,
|
||||
layoutIssues: [],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -584,4 +592,19 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
|
||||
stage1Tick: 0,
|
||||
bumpStage1Tick: () => set((s) => ({ stage1Tick: s.stage1Tick + 1 })),
|
||||
|
||||
layoutIssues: [],
|
||||
appendLayoutIssues: (issues) =>
|
||||
set((state) => {
|
||||
if (issues.length === 0) return {};
|
||||
// Dedupe by level+message so a re-running effect doesn't repeatedly
|
||||
// pile up identical issues.
|
||||
const seen = new Set(
|
||||
state.layoutIssues.map((i) => `${i.level}|${i.message}`),
|
||||
);
|
||||
const fresh = issues.filter((i) => !seen.has(`${i.level}|${i.message}`));
|
||||
if (fresh.length === 0) return {};
|
||||
return { layoutIssues: [...state.layoutIssues, ...fresh] };
|
||||
}),
|
||||
clearLayoutIssues: () => set({ layoutIssues: [] }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user