mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
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:
co-authored by
Claude Opus 4.6
parent
ccdc4b723f
commit
5628c2ed82
+108
-56
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user