feat(dashboard): add persona mode system with adaptive layouts

Three persona modes change the dashboard experience:
- Overview (non-technical): 2-column layout with graph + learn/chat,
  hides CodeViewer, filters graph to concept/module/file nodes only
- Learn (junior): full 4-panel layout with tabbed Details/Tour panel
- Deep Dive (experienced): full 4-panel layout with NodeInfo, tour
  tabs only shown when tour is explicitly active

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-03-14 21:11:52 +08:00
co-authored by Claude Opus 4.6
parent ccdc4b723f
commit 5628c2ed82
4 changed files with 182 additions and 59 deletions
+108 -56
View File
@@ -8,6 +8,7 @@ import NodeInfo from "./components/NodeInfo";
import ChatPanel from "./components/ChatPanel";
import LayerLegend from "./components/LayerLegend";
import LearnPanel from "./components/LearnPanel";
import PersonaSelector from "./components/PersonaSelector";
function App() {
const graph = useDashboardStore((s) => s.graph);
@@ -15,6 +16,7 @@ function App() {
const tourActive = useDashboardStore((s) => s.tourActive);
const startTour = useDashboardStore((s) => s.startTour);
const stopTour = useDashboardStore((s) => s.stopTour);
const persona = useDashboardStore((s) => s.persona);
useEffect(() => {
fetch("/knowledge-graph.json")
@@ -31,9 +33,12 @@ function App() {
<div className="h-screen w-screen flex flex-col bg-gray-900 text-white">
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 bg-gray-950 border-b border-gray-800 shrink-0">
<h1 className="text-sm font-bold tracking-widest text-gray-200">
UNDERSTAND ANYTHING
</h1>
<div className="flex items-center gap-4">
<h1 className="text-sm font-bold tracking-widest text-gray-200">
UNDERSTAND ANYTHING
</h1>
<PersonaSelector />
</div>
<div className="flex items-center gap-4 text-xs text-gray-400">
{graph && (
<>
@@ -52,64 +57,111 @@ function App() {
{/* Search Bar */}
<SearchBar />
{/* 4-panel grid */}
<div className="flex-1 grid grid-cols-2 grid-rows-2 gap-1 p-1 min-h-0">
{/* Top-left: Graph View */}
<div className="min-h-0 min-w-0">
<GraphView />
{/* Persona-adaptive layout */}
{persona === "non-technical" ? (
/* Non-technical: 2-column layout — graph + stacked learn/chat (no CodeViewer) */
<div className="flex-1 grid grid-cols-2 gap-1 p-1 min-h-0">
<div className="min-h-0 min-w-0">
<GraphView />
</div>
<div className="min-h-0 min-w-0 flex flex-col gap-1">
<div className="flex-1 min-h-0">
<LearnPanel />
</div>
<div className="flex-1 min-h-0">
<ChatPanel />
</div>
</div>
</div>
) : (
/* Junior + Experienced: 4-panel grid */
<div className="flex-1 grid grid-cols-2 grid-rows-2 gap-1 p-1 min-h-0">
{/* Top-left: Graph View */}
<div className="min-h-0 min-w-0">
<GraphView />
</div>
{/* Top-right: Code Viewer */}
<div className="min-h-0 min-w-0">
<CodeViewer />
</div>
{/* Top-right: Code Viewer */}
<div className="min-h-0 min-w-0">
<CodeViewer />
</div>
{/* Bottom-left: Chat */}
<div className="min-h-0 min-w-0">
<ChatPanel />
</div>
{/* Bottom-left: Chat */}
<div className="min-h-0 min-w-0">
<ChatPanel />
</div>
{/* Bottom-right: Node Info / Learn Panel */}
<div className="min-h-0 min-w-0 flex flex-col">
{hasTour ? (
<>
{/* Tab bar */}
<div className="flex bg-gray-800 rounded-t-lg border-b border-gray-700 shrink-0">
<button
onClick={() => {
if (tourActive) stopTour();
}}
className={`flex-1 text-xs font-medium py-1.5 px-3 transition-colors ${
!tourActive
? "text-white border-b-2 border-indigo-500"
: "text-gray-500 hover:text-gray-300"
}`}
>
Details
</button>
<button
onClick={() => {
if (!tourActive) startTour();
}}
className={`flex-1 text-xs font-medium py-1.5 px-3 transition-colors ${
tourActive
? "text-white border-b-2 border-indigo-500"
: "text-gray-500 hover:text-gray-300"
}`}
>
Tour
</button>
</div>
{/* Active panel */}
<div className="flex-1 min-h-0">
{tourActive ? <LearnPanel /> : <NodeInfo />}
</div>
</>
) : (
<NodeInfo />
)}
{/* Bottom-right: persona-dependent */}
<div className="min-h-0 min-w-0 flex flex-col">
{persona === "junior" ? (
/* Junior: always show tabbed Details/Tour panel */
hasTour ? (
<>
<div className="flex bg-gray-800 rounded-t-lg border-b border-gray-700 shrink-0">
<button
onClick={() => {
if (tourActive) stopTour();
}}
className={`flex-1 text-xs font-medium py-1.5 px-3 transition-colors ${
!tourActive
? "text-white border-b-2 border-indigo-500"
: "text-gray-500 hover:text-gray-300"
}`}
>
Details
</button>
<button
onClick={() => {
if (!tourActive) startTour();
}}
className={`flex-1 text-xs font-medium py-1.5 px-3 transition-colors ${
tourActive
? "text-white border-b-2 border-indigo-500"
: "text-gray-500 hover:text-gray-300"
}`}
>
Tour
</button>
</div>
<div className="flex-1 min-h-0">
{tourActive ? <LearnPanel /> : <NodeInfo />}
</div>
</>
) : (
<NodeInfo />
)
) : (
/* Experienced: show tabbed panel only when tour is explicitly active */
tourActive && hasTour ? (
<>
<div className="flex bg-gray-800 rounded-t-lg border-b border-gray-700 shrink-0">
<button
onClick={() => stopTour()}
className={`flex-1 text-xs font-medium py-1.5 px-3 transition-colors ${
!tourActive
? "text-white border-b-2 border-indigo-500"
: "text-gray-500 hover:text-gray-300"
}`}
>
Details
</button>
<button
className="flex-1 text-xs font-medium py-1.5 px-3 transition-colors text-white border-b-2 border-indigo-500"
>
Tour
</button>
</div>
<div className="flex-1 min-h-0">
<LearnPanel />
</div>
</>
) : (
<NodeInfo />
)
)}
</div>
</div>
</div>
)}
</div>
);
}
@@ -27,6 +27,7 @@ export default function GraphView() {
const selectNode = useDashboardStore((s) => s.selectNode);
const showLayers = useDashboardStore((s) => s.showLayers);
const tourHighlightedNodeIds = useDashboardStore((s) => s.tourHighlightedNodeIds);
const persona = useDashboardStore((s) => s.persona);
const { initialNodes, initialEdges } = useMemo(() => {
if (!graph)
@@ -35,7 +36,24 @@ export default function GraphView() {
initialEdges: [] as Edge[],
};
const flowNodes: CustomFlowNode[] = graph.nodes.map((node) => {
// Filter nodes and edges based on persona
const filteredGraphNodes =
persona === "non-technical"
? graph.nodes.filter(
(n) =>
n.type === "concept" || n.type === "module" || n.type === "file",
)
: graph.nodes;
const filteredNodeIds = new Set(filteredGraphNodes.map((n) => n.id));
const filteredGraphEdges =
persona === "non-technical"
? graph.edges.filter(
(e) => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target),
)
: graph.edges;
const flowNodes: CustomFlowNode[] = filteredGraphNodes.map((node) => {
const matchResult = searchResults.find((r) => r.nodeId === node.id);
return {
id: node.id,
@@ -54,7 +72,7 @@ export default function GraphView() {
};
});
const flowEdges: Edge[] = graph.edges.map((edge, i) => ({
const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => ({
id: `e-${i}`,
source: edge.source,
target: edge.target,
@@ -164,7 +182,7 @@ export default function GraphView() {
];
return { initialNodes: allNodes, initialEdges: laid.edges };
}, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds]);
}, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona]);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -0,0 +1,44 @@
import { useDashboardStore } from "../store";
import type { Persona } from "../store";
const personas: { id: Persona; label: string; description: string }[] = [
{
id: "non-technical",
label: "Overview",
description: "High-level architecture view",
},
{
id: "junior",
label: "Learn",
description: "Full dashboard with guided learning",
},
{
id: "experienced",
label: "Deep Dive",
description: "Code-focused with chat",
},
];
export default function PersonaSelector() {
const persona = useDashboardStore((s) => s.persona);
const setPersona = useDashboardStore((s) => s.setPersona);
return (
<div className="flex items-center gap-1 bg-gray-800 rounded-lg p-0.5">
{personas.map((p) => (
<button
key={p.id}
onClick={() => setPersona(p.id)}
title={p.description}
className={`px-2.5 py-1 rounded text-[11px] font-medium transition-colors ${
persona === p.id
? "bg-blue-600 text-white"
: "text-gray-400 hover:text-gray-300 hover:bg-gray-700"
}`}
>
{p.label}
</button>
))}
</div>
);
}
+9
View File
@@ -7,6 +7,8 @@ import type {
TourStep,
} from "@understand-anything/core/types";
export type Persona = "non-technical" | "junior" | "experienced";
export interface ChatMessage {
role: "user" | "assistant";
content: string;
@@ -33,6 +35,8 @@ interface DashboardStore {
currentTourStep: number;
tourHighlightedNodeIds: string[];
persona: Persona;
setGraph: (graph: KnowledgeGraph) => void;
selectNode: (nodeId: string | null) => void;
setSearchQuery: (query: string) => void;
@@ -41,6 +45,7 @@ interface DashboardStore {
clearChat: () => void;
toggleLayers: () => void;
explainNode: (nodeId: string) => Promise<void>;
setPersona: (persona: Persona) => void;
startTour: () => void;
stopTour: () => void;
@@ -145,6 +150,8 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
currentTourStep: 0,
tourHighlightedNodeIds: [],
persona: "junior",
setGraph: (graph) => {
const searchEngine = new SearchEngine(graph.nodes);
const query = get().searchQuery;
@@ -225,6 +232,8 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
toggleLayers: () => set((state) => ({ showLayers: !state.showLayers })),
setPersona: (persona) => set({ persona }),
explainNode: async (nodeId) => {
const { apiKey, graph, nodeExplanationCache } = get();
if (!apiKey || !graph) return;