From 51800ff6052b65811936048be2b7edeaac484844 Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Wed, 20 May 2026 11:27:46 -0700 Subject: [PATCH] feat: Learning Hub Guided Tours --- src/components/Learn/FeaturedTours.tsx | 139 +++++++++++------- src/components/Learn/LearnComingSoon.tsx | 43 ------ src/components/Learn/ToursLibrary.tsx | 117 +++++++++++++++ src/components/Learn/tours.json | 90 ++++++++++++ src/components/Learn/tours.ts | 40 +++++ src/routes/Dashboard/Learn/LearnToursView.tsx | 10 +- src/routes/Dashboard/Learn/Tour.tsx | 8 + src/routes/router.ts | 12 ++ 8 files changed, 354 insertions(+), 105 deletions(-) delete mode 100644 src/components/Learn/LearnComingSoon.tsx create mode 100644 src/components/Learn/ToursLibrary.tsx create mode 100644 src/components/Learn/tours.json create mode 100644 src/components/Learn/tours.ts create mode 100644 src/routes/Dashboard/Learn/Tour.tsx diff --git a/src/components/Learn/FeaturedTours.tsx b/src/components/Learn/FeaturedTours.tsx index 92d4e47be..d6257188d 100644 --- a/src/components/Learn/FeaturedTours.tsx +++ b/src/components/Learn/FeaturedTours.tsx @@ -5,39 +5,41 @@ import { Button } from "@/components/ui/button"; import { Icon } from "@/components/ui/icon"; import { BlockStack, InlineStack } from "@/components/ui/layout"; import { Heading, Paragraph, Text } from "@/components/ui/typography"; +import { APP_ROUTES } from "@/routes/router"; import { tracking } from "@/utils/tracking"; +import { tours as tourCards } from "./tours"; + interface FeaturedTour { id: string; title: string; duration: string; tag?: "new" | "popular"; + available: boolean; } -const STUB_TOURS: FeaturedTour[] = [ - { - id: "first-pipeline", - title: "Build your first pipeline", - duration: "4 min", - tag: "popular", - }, - { id: "using-secrets", title: "Using secrets safely", duration: "2 min" }, - { - id: "multinode-tasks", - title: "Run multinode tasks", - duration: "3 min", - tag: "new", - }, - { - id: "custom-components", - title: "Create a custom component", - duration: "5 min", - }, +const FEATURED_TOUR_IDS: Array> = [ + { id: "navigating-the-editor", tag: "new" }, + { id: "first-pipeline", tag: "popular" }, + { id: "using-secrets" }, + { id: "multinode-tasks" }, ]; +function buildFeaturedTours(): FeaturedTour[] { + return FEATURED_TOUR_IDS.flatMap(({ id, tag }) => { + const card = tourCards.find((c) => c.id === id); + if (!card) return []; + return [ + { id, title: card.title, duration: card.duration, tag, available: false }, + ]; + }); +} + export function FeaturedTours() { + const featured = buildFeaturedTours(); + return ( -
+
@@ -60,44 +62,71 @@ export function FeaturedTours() { -
    - {STUB_TOURS.map((tour) => ( -
  • - -
  • + + + + + ))} -
+
); } + +function FeaturedTourLabel({ tour }: { tour: FeaturedTour }) { + return ( + + + + {tour.title} + + {tour.tag && ( + + {tour.tag} + + )} + {!tour.available && ( + + Coming soon + + )} + + + {tour.duration} + + + ); +} diff --git a/src/components/Learn/LearnComingSoon.tsx b/src/components/Learn/LearnComingSoon.tsx deleted file mode 100644 index e8181ec91..000000000 --- a/src/components/Learn/LearnComingSoon.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Link } from "@tanstack/react-router"; - -import { Icon, type IconName } from "@/components/ui/icon"; -import { BlockStack } from "@/components/ui/layout"; -import { Heading, Paragraph } from "@/components/ui/typography"; - -interface LearnComingSoonProps { - title: string; - description: string; - icon: IconName; -} - -export function LearnComingSoon({ - title, - description, - icon, -}: LearnComingSoonProps) { - return ( - -
-
- - {title} - - {description} - - - - - Coming soon. - - - ← Back to Learning Hub - - -
- ); -} diff --git a/src/components/Learn/ToursLibrary.tsx b/src/components/Learn/ToursLibrary.tsx new file mode 100644 index 000000000..62c1349b8 --- /dev/null +++ b/src/components/Learn/ToursLibrary.tsx @@ -0,0 +1,117 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Icon } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Heading, Paragraph, Text } from "@/components/ui/typography"; +import { tracking } from "@/utils/tracking"; + +import { + type Tour, + TOUR_DIFFICULTY_BLURBS, + TOUR_DIFFICULTY_COLORS, + TOUR_DIFFICULTY_ICONS, + TOUR_DIFFICULTY_ORDER, + type TourDifficulty, + tours, +} from "./tours"; + +function TourCard({ tour }: { tour: Tour }) { + return ( + + + {tour.title} + + {tour.description} + + + + + + + {tour.area} + + + {tour.duration} + + + + + + + ); +} + +function DifficultySection({ + difficulty, + tours: difficultyTours, +}: { + difficulty: TourDifficulty; + tours: Tour[]; +}) { + if (difficultyTours.length === 0) { + return null; + } + + return ( + + + + + + {TOUR_DIFFICULTY_BLURBS[difficulty]} + + +
+ {difficultyTours.map((tour) => ( + + ))} +
+
+ ); +} + +export function ToursLibrary() { + const buckets = new Map(); + for (const tour of tours) { + const list = buckets.get(tour.difficulty) ?? []; + list.push(tour); + buckets.set(tour.difficulty, list); + } + + return ( + + {TOUR_DIFFICULTY_ORDER.map((difficulty) => ( + + ))} + + ); +} diff --git a/src/components/Learn/tours.json b/src/components/Learn/tours.json new file mode 100644 index 000000000..8131b5185 --- /dev/null +++ b/src/components/Learn/tours.json @@ -0,0 +1,90 @@ +[ + { + "id": "first-pipeline", + "title": "Build your first pipeline", + "description": "Walk through dragging components from the library, wiring tasks together, and submitting your very first run.", + "difficulty": "Beginner", + "duration": "4 min", + "area": "Editor" + }, + { + "id": "navigating-the-editor", + "title": "Find your way around the editor", + "description": "Get oriented with the canvas, dockable panels, properties view, canvas tools, and the menu bar so you know where every feature lives.", + "difficulty": "Beginner", + "duration": "5 min", + "area": "Editor" + }, + { + "id": "component-library-basics", + "title": "Browse the component library", + "description": "Learn how to search, filter, favorite, and inspect components before dropping them on the canvas.", + "difficulty": "Beginner", + "duration": "3 min", + "area": "Library" + }, + { + "id": "submitting-and-monitoring", + "title": "Submit a run and watch it execute", + "description": "Submit a pipeline, locate it in All Runs, and step through task logs as the run progresses.", + "difficulty": "Beginner", + "duration": "4 min", + "area": "Runs" + }, + { + "id": "using-secrets", + "title": "Use secrets without leaking credentials", + "description": "Create a secret, reference it from a task via the lightning-bolt menu, and confirm it never lands in the pipeline YAML.", + "difficulty": "Intermediate", + "duration": "3 min", + "area": "Security" + }, + { + "id": "create-component", + "title": "Create a custom component in the browser", + "description": "Author a Python or YAML component using the in-app editor, no local development environment required.", + "difficulty": "Intermediate", + "duration": "5 min", + "area": "Library" + }, + { + "id": "subgraphs", + "title": "Group tasks into reusable subgraphs", + "description": "Select related tasks, bundle them into a subgraph, and use the Pipeline Tree window to navigate nested levels.", + "difficulty": "Intermediate", + "duration": "4 min", + "area": "Editor" + }, + { + "id": "templatized-run-names", + "title": "Templatize run names for experiment tracking", + "description": "Enable the beta feature and craft a name template so every submitted run gets a unique, scannable label.", + "difficulty": "Intermediate", + "duration": "3 min", + "area": "Runs" + }, + { + "id": "caching", + "title": "Save hours with task-level caching", + "description": "Understand how cache keys are derived, when caches hit, and how to disable caching on a single task when you must.", + "difficulty": "Advanced", + "duration": "5 min", + "area": "Performance" + }, + { + "id": "github-libraries", + "title": "Register a GitHub repo as a component source", + "description": "Point Tangle at a shared GitHub repository to pull in an entire library of team components at once.", + "difficulty": "Advanced", + "duration": "4 min", + "area": "Library" + }, + { + "id": "multinode-tasks", + "title": "Configure a multi-node task", + "description": "Set the multi-node annotation, reference per-node system data, and submit a distributed run across multiple workers.", + "difficulty": "Advanced", + "duration": "6 min", + "area": "Compute" + } +] diff --git a/src/components/Learn/tours.ts b/src/components/Learn/tours.ts new file mode 100644 index 000000000..73fe84faf --- /dev/null +++ b/src/components/Learn/tours.ts @@ -0,0 +1,40 @@ +import type { IconName } from "@/components/ui/icon"; + +import toursData from "./tours.json"; + +export type TourDifficulty = "Beginner" | "Intermediate" | "Advanced"; + +export interface Tour { + id: string; + title: string; + description: string; + difficulty: TourDifficulty; + duration: string; + area: string; +} + +export const tours = toursData as Tour[]; + +export const TOUR_DIFFICULTY_ORDER: TourDifficulty[] = [ + "Beginner", + "Intermediate", + "Advanced", +]; + +export const TOUR_DIFFICULTY_ICONS: Record = { + Beginner: "Sprout", + Intermediate: "Mountain", + Advanced: "Trophy", +}; + +export const TOUR_DIFFICULTY_COLORS: Record = { + Beginner: "text-emerald-500", + Intermediate: "text-amber-500", + Advanced: "text-violet-500", +}; + +export const TOUR_DIFFICULTY_BLURBS: Record = { + Beginner: "Start here. No prior knowledge required.", + Intermediate: "Layer in workflows that build on the basics.", + Advanced: "Complex features and edge cases worth knowing.", +}; diff --git a/src/routes/Dashboard/Learn/LearnToursView.tsx b/src/routes/Dashboard/Learn/LearnToursView.tsx index 70d6f25bd..9cfdc79f1 100644 --- a/src/routes/Dashboard/Learn/LearnToursView.tsx +++ b/src/routes/Dashboard/Learn/LearnToursView.tsx @@ -1,5 +1,5 @@ -import { LearnComingSoon } from "@/components/Learn/LearnComingSoon"; import { LearnPageHeader } from "@/components/Learn/LearnPageHeader"; +import { ToursLibrary } from "@/components/Learn/ToursLibrary"; import { BlockStack } from "@/components/ui/layout"; export function LearnToursView() { @@ -7,15 +7,11 @@ export function LearnToursView() { - + ); } diff --git a/src/routes/Dashboard/Learn/Tour.tsx b/src/routes/Dashboard/Learn/Tour.tsx new file mode 100644 index 000000000..14461bb02 --- /dev/null +++ b/src/routes/Dashboard/Learn/Tour.tsx @@ -0,0 +1,8 @@ +export function TourPage() { + return ( +
+

Tour

+

Tour component placeholder

+
+ ); +} diff --git a/src/routes/router.ts b/src/routes/router.ts index 5692898b6..17006b8b5 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -27,6 +27,7 @@ import { LearnExamplesView } from "./Dashboard/Learn/LearnExamplesView"; import { LearnHomeView } from "./Dashboard/Learn/LearnHomeView"; import { LearnTipsView } from "./Dashboard/Learn/LearnTipsView"; import { LearnToursView } from "./Dashboard/Learn/LearnToursView"; +import { TourPage } from "./Dashboard/Learn/Tour"; import Editor from "./Editor"; import { ImportPage } from "./Import"; import NotFoundPage from "./NotFoundPage"; @@ -54,6 +55,8 @@ const EDITOR_V2_BASE_PATH = "/editor-v2"; const RUNS_V2_BASE_PATH = "/runs-v2"; const SETTINGS_PATH = "/settings"; const IMPORT_PATH = "/app/editor/import-pipeline"; +const TOUR_BASE_PATH = "/tour"; + export const APP_ROUTES = { HOME: "/", DASHBOARD: "/", @@ -66,6 +69,8 @@ export const APP_ROUTES = { LEARN_EXAMPLES: `${LEARN_BASE_PATH}/examples`, LEARN_TIPS: `${LEARN_BASE_PATH}/tips`, LEARN_TOURS: `${LEARN_BASE_PATH}/tours`, + TOUR: TOUR_BASE_PATH, + TOUR_DETAIL: `${TOUR_BASE_PATH}/$tourId`, IMPORT: IMPORT_PATH, PIPELINE_EDITOR: `${EDITOR_PATH}/$name`, RUN_DETAIL: `${RUNS_BASE_PATH}/$id`, @@ -170,6 +175,12 @@ const learnToursRoute = createRoute({ component: LearnToursView, }); +const tourRoute = createRoute({ + getParentRoute: () => mainLayout, + path: APP_ROUTES.TOUR_DETAIL, + component: TourPage, +}); + const quickStartRoute = createRoute({ getParentRoute: () => mainLayout, path: "/quick-start", @@ -351,6 +362,7 @@ const appRouteTree = mainLayout.addChildren([ runV2WithSubgraphRoute, pipelineFoldersRoute, artifactPreviewRoute, + tourRoute, ]); const rootRouteTree = rootRoute.addChildren([