From f9da9c40cd65c1ce34949b1b6e11f85ad7af452b Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 14 Mar 2026 20:23:03 +0800 Subject: [PATCH] feat(dashboard): add LearnPanel component and tour store state Add tour state management (tourActive, currentTourStep, tourHighlightedNodeIds) to the Zustand store with start/stop/navigate actions. Create LearnPanel component with three states: no tour, tour preview, and active tour with step navigation, markdown descriptions, language lesson boxes, and referenced component pills. Replace bottom-right panel in App.tsx with tabbed Details/Tour view when a tour is available. Co-Authored-By: Claude Opus 4.6 --- packages/dashboard/src/App.tsx | 45 +++- .../dashboard/src/components/LearnPanel.tsx | 226 ++++++++++++++++++ packages/dashboard/src/store.ts | 70 ++++++ 3 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 packages/dashboard/src/components/LearnPanel.tsx diff --git a/packages/dashboard/src/App.tsx b/packages/dashboard/src/App.tsx index 13c4142..c633bb1 100644 --- a/packages/dashboard/src/App.tsx +++ b/packages/dashboard/src/App.tsx @@ -7,10 +7,12 @@ import SearchBar from "./components/SearchBar"; import NodeInfo from "./components/NodeInfo"; import ChatPanel from "./components/ChatPanel"; import LayerLegend from "./components/LayerLegend"; +import LearnPanel from "./components/LearnPanel"; function App() { const graph = useDashboardStore((s) => s.graph); const setGraph = useDashboardStore((s) => s.setGraph); + const tourActive = useDashboardStore((s) => s.tourActive); useEffect(() => { fetch("/knowledge-graph.json") @@ -21,6 +23,7 @@ function App() { const nodeCount = graph?.nodes.length ?? 0; const edgeCount = graph?.edges.length ?? 0; + const hasTour = (graph?.tour.length ?? 0) > 0; return (
@@ -64,9 +67,45 @@ function App() {
- {/* Bottom-right: Node Info */} -
- + {/* Bottom-right: Node Info / Learn Panel */} +
+ {hasTour ? ( + <> + {/* Tab bar */} +
+ + +
+ {/* Active panel */} +
+ {tourActive ? : } +
+ + ) : ( + + )}
diff --git a/packages/dashboard/src/components/LearnPanel.tsx b/packages/dashboard/src/components/LearnPanel.tsx new file mode 100644 index 0000000..c1641bb --- /dev/null +++ b/packages/dashboard/src/components/LearnPanel.tsx @@ -0,0 +1,226 @@ +import ReactMarkdown from "react-markdown"; +import { useDashboardStore } from "../store"; + +export default function LearnPanel() { + const graph = useDashboardStore((s) => s.graph); + const tourActive = useDashboardStore((s) => s.tourActive); + const currentTourStep = useDashboardStore((s) => s.currentTourStep); + const startTour = useDashboardStore((s) => s.startTour); + const stopTour = useDashboardStore((s) => s.stopTour); + const setTourStep = useDashboardStore((s) => s.setTourStep); + const nextTourStep = useDashboardStore((s) => s.nextTourStep); + const prevTourStep = useDashboardStore((s) => s.prevTourStep); + const selectNode = useDashboardStore((s) => s.selectNode); + + const tourSteps = graph?.tour + ? [...graph.tour].sort((a, b) => a.order - b.order) + : []; + const hasTour = tourSteps.length > 0; + + // State 1: No tour available + if (!hasTour) { + return ( +
+
+
🧭
+

No tour available

+

+ Generate a tour from your knowledge graph to get a guided walkthrough +

+
+
+ ); + } + + // State 2: Tour available but not started + if (!tourActive) { + return ( +
+
+

Project Tour

+

+ {tourSteps.length} steps · Guided walkthrough of the codebase +

+
+ + + +
+

+ Steps +

+ {tourSteps.map((step, i) => ( +
+ + {i + 1}. + + {step.title} +
+ ))} +
+
+ ); + } + + // State 3: Tour active + const step = tourSteps[currentTourStep]; + if (!step) return null; + + const totalSteps = tourSteps.length; + const progressPct = ((currentTourStep + 1) / totalSteps) * 100; + const isFirst = currentTourStep === 0; + const isLast = currentTourStep === totalSteps - 1; + + return ( +
+ {/* Header with progress counter and exit */} +
+
+

+ Tour +

+ + {currentTourStep + 1} / {totalSteps} + +
+ +
+ + {/* Progress bar */} +
+
+
+ + {/* Scrollable content */} +
+ {/* Step title */} +

{step.title}

+ + {/* Description via ReactMarkdown */} +
+ ( +

{children}

+ ), + strong: ({ children }) => ( + {children} + ), + code: ({ className, children }) => { + const isBlock = className?.includes("language-"); + return isBlock ? ( + + {children} + + ) : ( + + {children} + + ); + }, + ul: ({ children }) => ( +
    + {children} +
+ ), + ol: ({ children }) => ( +
    + {children} +
+ ), + }} + > + {step.description} +
+
+ + {/* Language lesson */} + {step.languageLesson && ( +
+

+ Language Lesson +

+

+ {step.languageLesson} +

+
+ )} + + {/* Referenced component pills */} + {step.nodeIds.length > 0 && ( +
+

+ Referenced Components +

+
+ {step.nodeIds.map((nodeId) => { + const node = graph?.nodes.find((n) => n.id === nodeId); + return ( + + ); + })} +
+
+ )} +
+ + {/* Navigation: dots + prev/next */} +
+ {/* Step dots */} +
+ {tourSteps.map((_, i) => ( +
+ + {/* Prev / Next buttons */} +
+ + +
+
+
+ ); +} diff --git a/packages/dashboard/src/store.ts b/packages/dashboard/src/store.ts index e65f67d..c208f8f 100644 --- a/packages/dashboard/src/store.ts +++ b/packages/dashboard/src/store.ts @@ -22,6 +22,10 @@ interface DashboardStore { showLayers: boolean; + tourActive: boolean; + currentTourStep: number; + tourHighlightedNodeIds: string[]; + setGraph: (graph: KnowledgeGraph) => void; selectNode: (nodeId: string | null) => void; setSearchQuery: (query: string) => void; @@ -29,6 +33,12 @@ interface DashboardStore { sendChatMessage: (message: string) => Promise; clearChat: () => void; toggleLayers: () => void; + + startTour: () => void; + stopTour: () => void; + setTourStep: (step: number) => void; + nextTourStep: () => void; + prevTourStep: () => void; } function buildSystemPrompt( @@ -115,6 +125,10 @@ export const useDashboardStore = create()((set, get) => ({ showLayers: false, + tourActive: false, + currentTourStep: 0, + tourHighlightedNodeIds: [], + setGraph: (graph) => { const searchEngine = new SearchEngine(graph.nodes); const query = get().searchQuery; @@ -194,4 +208,60 @@ export const useDashboardStore = create()((set, get) => ({ clearChat: () => set({ chatMessages: [] }), toggleLayers: () => set((state) => ({ showLayers: !state.showLayers })), + + startTour: () => { + const { graph } = get(); + if (!graph || graph.tour.length === 0) return; + const sorted = [...graph.tour].sort((a, b) => a.order - b.order); + set({ + tourActive: true, + currentTourStep: 0, + tourHighlightedNodeIds: sorted[0].nodeIds, + selectedNodeId: null, + }); + }, + + stopTour: () => + set({ + tourActive: false, + currentTourStep: 0, + tourHighlightedNodeIds: [], + }), + + setTourStep: (step) => { + const { graph } = get(); + if (!graph || graph.tour.length === 0) return; + const sorted = [...graph.tour].sort((a, b) => a.order - b.order); + if (step < 0 || step >= sorted.length) return; + set({ + currentTourStep: step, + tourHighlightedNodeIds: sorted[step].nodeIds, + }); + }, + + nextTourStep: () => { + const { graph, currentTourStep } = get(); + if (!graph || graph.tour.length === 0) return; + const sorted = [...graph.tour].sort((a, b) => a.order - b.order); + if (currentTourStep < sorted.length - 1) { + const next = currentTourStep + 1; + set({ + currentTourStep: next, + tourHighlightedNodeIds: sorted[next].nodeIds, + }); + } + }, + + prevTourStep: () => { + const { graph, currentTourStep } = get(); + if (!graph || graph.tour.length === 0) return; + if (currentTourStep > 0) { + const sorted = [...graph.tour].sort((a, b) => a.order - b.order); + const prev = currentTourStep - 1; + set({ + currentTourStep: prev, + tourHighlightedNodeIds: sorted[prev].nodeIds, + }); + } + }, }));