mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cb3d860fe2
commit
f9da9c40cd
@@ -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 (
|
||||
<div className="h-screen w-screen flex flex-col bg-gray-900 text-white">
|
||||
@@ -64,9 +67,45 @@ function App() {
|
||||
<ChatPanel />
|
||||
</div>
|
||||
|
||||
{/* Bottom-right: Node Info */}
|
||||
<div className="min-h-0 min-w-0">
|
||||
<NodeInfo />
|
||||
{/* 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) useDashboardStore.getState().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) useDashboardStore.getState().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 />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-800 rounded-lg">
|
||||
<div className="text-center px-4">
|
||||
<div className="text-2xl mb-2 text-gray-600">🧭</div>
|
||||
<p className="text-gray-400 text-sm">No tour available</p>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
Generate a tour from your knowledge graph to get a guided walkthrough
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// State 2: Tour available but not started
|
||||
if (!tourActive) {
|
||||
return (
|
||||
<div className="h-full w-full bg-gray-800 rounded-lg overflow-auto p-4">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-bold text-white mb-1">Project Tour</h2>
|
||||
<p className="text-xs text-gray-400">
|
||||
{tourSteps.length} steps · Guided walkthrough of the codebase
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={startTour}
|
||||
className="w-full mb-4 bg-indigo-600 text-white text-sm font-medium py-2 px-4 rounded hover:bg-indigo-500 transition-colors"
|
||||
>
|
||||
Start Tour
|
||||
</button>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
|
||||
Steps
|
||||
</h3>
|
||||
{tourSteps.map((step, i) => (
|
||||
<div
|
||||
key={step.order}
|
||||
className="flex items-start gap-2 text-xs bg-gray-700/50 rounded px-3 py-2"
|
||||
>
|
||||
<span className="text-gray-500 font-mono shrink-0 mt-0.5">
|
||||
{i + 1}.
|
||||
</span>
|
||||
<span className="text-gray-300">{step.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="h-full w-full bg-gray-800 rounded-lg flex flex-col overflow-hidden">
|
||||
{/* Header with progress counter and exit */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-700 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
Tour
|
||||
</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{currentTourStep + 1} / {totalSteps}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={stopTour}
|
||||
className="text-[10px] text-gray-500 hover:text-gray-300 transition-colors"
|
||||
>
|
||||
Exit Tour
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="h-1 bg-gray-700 shrink-0">
|
||||
<div
|
||||
className="h-full bg-indigo-500 transition-all duration-300"
|
||||
style={{ width: `${progressPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||
{/* Step title */}
|
||||
<h2 className="text-lg font-bold text-white mb-3">{step.title}</h2>
|
||||
|
||||
{/* Description via ReactMarkdown */}
|
||||
<div className="text-sm text-gray-300 leading-relaxed mb-4 tour-markdown">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
p: ({ children }) => (
|
||||
<p className="mb-1.5 last:mb-0">{children}</p>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-white">{children}</strong>
|
||||
),
|
||||
code: ({ className, children }) => {
|
||||
const isBlock = className?.includes("language-");
|
||||
return isBlock ? (
|
||||
<code className="block bg-gray-900 rounded px-2 py-1.5 mb-1.5 overflow-x-auto text-[11px] leading-relaxed">
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<code className="bg-gray-900 rounded px-1 py-0.5 text-[11px]">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside mb-1.5 space-y-0.5">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-inside mb-1.5 space-y-0.5">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{step.description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
{/* Language lesson */}
|
||||
{step.languageLesson && (
|
||||
<div className="bg-indigo-900/40 border border-indigo-700 rounded p-3 mb-4">
|
||||
<h4 className="text-xs font-semibold text-indigo-300 uppercase tracking-wider mb-1.5">
|
||||
Language Lesson
|
||||
</h4>
|
||||
<p className="text-sm text-indigo-200 leading-relaxed">
|
||||
{step.languageLesson}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Referenced component pills */}
|
||||
{step.nodeIds.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
|
||||
Referenced Components
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{step.nodeIds.map((nodeId) => {
|
||||
const node = graph?.nodes.find((n) => n.id === nodeId);
|
||||
return (
|
||||
<button
|
||||
key={nodeId}
|
||||
onClick={() => selectNode(nodeId)}
|
||||
className="text-[11px] bg-gray-700 text-gray-300 px-2 py-0.5 rounded-full hover:bg-gray-600 hover:text-white transition-colors cursor-pointer"
|
||||
>
|
||||
{node?.name ?? nodeId}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation: dots + prev/next */}
|
||||
<div className="px-3 py-2 border-t border-gray-700 shrink-0">
|
||||
{/* Step dots */}
|
||||
<div className="flex justify-center gap-1.5 mb-2">
|
||||
{tourSteps.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setTourStep(i)}
|
||||
className={`w-2 h-2 rounded-full transition-colors ${
|
||||
i === currentTourStep
|
||||
? "bg-indigo-500"
|
||||
: "bg-gray-600 hover:bg-gray-500"
|
||||
}`}
|
||||
aria-label={`Go to step ${i + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Prev / Next buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={prevTourStep}
|
||||
disabled={isFirst}
|
||||
className="flex-1 text-xs bg-gray-700 text-gray-300 py-1.5 rounded hover:bg-gray-600 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<button
|
||||
onClick={isLast ? stopTour : nextTourStep}
|
||||
className="flex-1 text-xs bg-indigo-600 text-white py-1.5 rounded hover:bg-indigo-500 transition-colors"
|
||||
>
|
||||
{isLast ? "Finish" : "Next"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<void>;
|
||||
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<DashboardStore>()((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<DashboardStore>()((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,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user