Merge pull request #132 from Xingkai98/feat/dashboard-file-class-views

feat(dashboard): add file/class dual-view toggle to reduce graph clutter
This commit is contained in:
Yuxiang Lin
2026-05-10 17:44:04 +08:00
committed by GitHub
Unverified
3 changed files with 100 additions and 3 deletions
@@ -109,6 +109,10 @@ function Dashboard({ accessToken }: { accessToken: string }) {
const togglePathFinder = useDashboardStore((s) => s.togglePathFinder);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
const detailLevel = useDashboardStore((s) => s.detailLevel);
const setDetailLevel = useDashboardStore((s) => s.setDetailLevel);
const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView);
const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView);
const [loadError, setLoadError] = useState<string | null>(null);
const [graphIssues, setGraphIssues] = useState<GraphIssue[]>([]);
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
@@ -443,6 +447,52 @@ function Dashboard({ accessToken }: { accessToken: string }) {
<div className="flex-1 min-w-0 overflow-x-auto scrollbar-hide">
<div className="flex items-center gap-4 w-max">
<DiffToggle />
{/* Detail level: file view (architecture) / class view (code structure) */}
{!isKnowledgeGraph && viewMode !== "domain" && (
<>
<div className="w-px h-5 bg-border-subtle" />
<div className="flex items-center bg-elevated rounded-lg p-0.5">
<button
type="button"
onClick={() => setDetailLevel("file")}
title="Files only — architecture-level dependencies (fast)"
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
detailLevel === "file"
? "bg-accent/20 text-accent"
: "text-text-muted hover:text-text-secondary"
}`}
>
Files
</button>
<button
type="button"
onClick={() => setDetailLevel("class")}
title="Files + Classes — code structure with inheritance"
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
detailLevel === "class"
? "bg-accent/20 text-accent"
: "text-text-muted hover:text-text-secondary"
}`}
>
+Classes
</button>
</div>
{detailLevel === "class" && (
<button
type="button"
onClick={toggleShowFunctionsInClassView}
title="Toggle function nodes (may slow down rendering)"
className={`text-[10px] font-semibold uppercase tracking-wider px-2 py-1 rounded border transition-colors ${
showFunctionsInClassView
? "border-amber-500/50 bg-amber-500/10 text-amber-400"
: "border-border-medium bg-elevated text-text-muted hover:text-text-secondary"
}`}
>
fn
</button>
)}
</>
)}
<div className="flex items-center gap-1">
{(isKnowledgeGraph ? [
{ key: "knowledge" as const, label: "All", color: "var(--color-node-article)" },
@@ -366,6 +366,7 @@ function useLayerDetailTopology(): LayerDetailTopology & {
layoutStatus: "computing" | "ready";
} {
const graph = useDashboardStore((s) => s.graph);
const nodesById = useDashboardStore((s) => s.nodesById);
const activeLayerId = useDashboardStore((s) => s.activeLayerId);
const selectNode = useDashboardStore((s) => s.selectNode);
const persona = useDashboardStore((s) => s.persona);
@@ -375,6 +376,8 @@ function useLayerDetailTopology(): LayerDetailTopology & {
const focusNodeId = useDashboardStore((s) => s.focusNodeId);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const drillIntoLayer = useDashboardStore((s) => s.drillIntoLayer);
const detailLevel = useDashboardStore((s) => s.detailLevel);
const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView);
const handleNodeSelect = useCallback(
(nodeId: string) => {
@@ -404,10 +407,20 @@ function useLayerDetailTopology(): LayerDetailTopology & {
// Expand layer membership to include sub-file nodes (function/class)
// whose parent file is in this layer. Joined via "contains" edges.
// File view: file nodes only (architecture-level dependencies).
// Class view: file nodes + class nodes, functions only when toggle is on.
const expandedLayerNodeIds = new Set(layerNodeIds);
for (const edge of graph.edges) {
if (edge.type === "contains" && layerNodeIds.has(edge.source)) {
expandedLayerNodeIds.add(edge.target);
if (detailLevel !== "file") {
for (const edge of graph.edges) {
if (edge.type === "contains" && layerNodeIds.has(edge.source)) {
const child = nodesById.get(edge.target);
if (!child) continue;
if (child.type === "class") {
expandedLayerNodeIds.add(edge.target);
} else if (child.type === "function" && showFunctionsInClassView) {
expandedLayerNodeIds.add(edge.target);
}
}
}
}
@@ -626,6 +639,7 @@ function useLayerDetailTopology(): LayerDetailTopology & {
};
}, [
graph,
nodesById,
activeLayerId,
persona,
diffMode,
@@ -634,6 +648,8 @@ function useLayerDetailTopology(): LayerDetailTopology & {
focusNodeId,
nodeTypeFilters,
drillIntoLayer,
detailLevel,
showFunctionsInClassView,
handleNodeSelect,
handleContainerToggle,
]);
@@ -15,6 +15,7 @@ export type NodeType = "file" | "function" | "class" | "module" | "concept" | "c
export type Complexity = "simple" | "moderate" | "complex";
export type EdgeCategory = "structural" | "behavioral" | "data-flow" | "dependencies" | "semantic" | "infrastructure" | "domain" | "knowledge";
export type ViewMode = "structural" | "domain" | "knowledge";
export type DetailLevel = "file" | "class";
export interface FilterState {
nodeTypes: Set<NodeType>;
@@ -146,6 +147,13 @@ interface DashboardStore {
nodeTypeFilters: Record<NodeCategory, boolean>;
toggleNodeTypeFilter: (category: NodeCategory) => void;
// Detail level: "file" shows only file nodes (architecture view),
// "class" shows files + class nodes (code structure view) with optional function expansion.
detailLevel: DetailLevel;
setDetailLevel: (level: DetailLevel) => void;
showFunctionsInClassView: boolean;
toggleShowFunctionsInClassView: () => void;
setGraph: (graph: KnowledgeGraph) => void;
selectNode: (nodeId: string | null) => void;
navigateToNode: (nodeId: string) => void;
@@ -331,6 +339,29 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
pendingFocusContainer: null,
})),
detailLevel: "file",
setDetailLevel: (level) =>
set({
detailLevel: level,
// Detail level changes which nodes are visible; cached positions stale.
// Reset fn toggle so it doesn't resurrect when re-entering class view.
showFunctionsInClassView: false,
containerLayoutCache: new Map(),
containerSizeMemory: new Map(),
expandedContainers: new Set(),
pendingFocusContainer: null,
}),
showFunctionsInClassView: false,
toggleShowFunctionsInClassView: () =>
set((state) => ({
showFunctionsInClassView: !state.showFunctionsInClassView,
containerLayoutCache: new Map(),
containerSizeMemory: new Map(),
expandedContainers: new Set(),
pendingFocusContainer: null,
})),
setGraph: (graph) => {
const searchEngine = new SearchEngine(graph.nodes);
const query = get().searchQuery;