From ea36112a05f27b80e9c2adaa1ade383d432f1738 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Sun, 15 Feb 2026 19:20:37 +0100 Subject: [PATCH] feat: add guided tour component and integrate with Studio UI elements --- .../studio/sections/dataset-section.tsx | 20 +- .../studio/sections/model-section.tsx | 26 +- .../studio/sections/params-section.tsx | 22 +- .../studio/sections/training-section.tsx | 30 +- .../src/features/studio/studio-page.tsx | 65 ++- .../src/features/tour/guided-tour.tsx | 425 ++++++++++++++++++ studio/frontend/src/features/tour/index.ts | 3 + 7 files changed, 547 insertions(+), 44 deletions(-) create mode 100644 studio/frontend/src/features/tour/guided-tour.tsx create mode 100644 studio/frontend/src/features/tour/index.ts diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx index 0d325fbefd..ba839f2e1a 100644 --- a/studio/frontend/src/features/studio/sections/dataset-section.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx @@ -103,14 +103,15 @@ export function DatasetSection() { ); return ( - } - title="Dataset" - description="Select or upload training data" - accent="indigo" - className="lg:col-span-4 min-h-[450px]" - > -
+
+ } + title="Dataset" + description="Select or upload training data" + accent="indigo" + className="min-h-[450px]" + > +
Load from Hub @@ -316,12 +317,13 @@ export function DatasetSection() {
+
- +
); } diff --git a/studio/frontend/src/features/studio/sections/model-section.tsx b/studio/frontend/src/features/studio/sections/model-section.tsx index fcaa319e1d..b21d433fb0 100644 --- a/studio/frontend/src/features/studio/sections/model-section.tsx +++ b/studio/frontend/src/features/studio/sections/model-section.tsx @@ -129,16 +129,17 @@ export function ModelSection() { ); return ( - } - title="Model" - description="Select base model and training method" - accent="emerald" - featured={true} - badge="2x Faster Training" - className="col-span-12 shadow-border ring-1 ring-border" - > -
+
+ } + title="Model" + description="Select base model and training method" + accent="emerald" + featured={true} + badge="2x Faster Training" + className="shadow-border ring-1 ring-border" + > +
Local Model @@ -355,7 +356,8 @@ export function ModelSection() { />
-
-
+
+ +
); } diff --git a/studio/frontend/src/features/studio/sections/params-section.tsx b/studio/frontend/src/features/studio/sections/params-section.tsx index 8c753cde7e..415aca3fac 100644 --- a/studio/frontend/src/features/studio/sections/params-section.tsx +++ b/studio/frontend/src/features/studio/sections/params-section.tsx @@ -114,14 +114,15 @@ export function ParamsSection(): ReactElement { const [hyperOpen, setHyperOpen] = useState(false); return ( - } - title="Parameters" - description="Configure training hyperparameters" - accent="orange" - className="lg:col-span-4 min-h-[450px]" - > -
+
+ } + title="Parameters" + description="Configure training hyperparameters" + accent="orange" + className="min-h-[450px]" + > +
{/* Epochs */}
@@ -724,7 +725,8 @@ export function ParamsSection(): ReactElement { -
- +
+ +
); } diff --git a/studio/frontend/src/features/studio/sections/training-section.tsx b/studio/frontend/src/features/studio/sections/training-section.tsx index fa5c4e85f5..a5f4cba24e 100644 --- a/studio/frontend/src/features/studio/sections/training-section.tsx +++ b/studio/frontend/src/features/studio/sections/training-section.tsx @@ -40,14 +40,15 @@ export function TrainingSection() { const [logOpen, setLogOpen] = useState(false); return ( - } - title="Training" - description="Monitor and control training" - accent="blue" - className="lg:col-span-4 min-h-[450px]" - > -
+
+ } + title="Training" + description="Monitor and control training" + accent="blue" + className="min-h-[450px]" + > +
{/* Loss chart */}
void startTrainingRun()} disabled={isStarting} @@ -107,7 +109,12 @@ export function TrainingSection() { {/* Save / Clear */}
- @@ -194,7 +201,8 @@ export function TrainingSection() { )} -
- +
+ +
); } diff --git a/studio/frontend/src/features/studio/studio-page.tsx b/studio/frontend/src/features/studio/studio-page.tsx index 5811f161db..61654f4954 100644 --- a/studio/frontend/src/features/studio/studio-page.tsx +++ b/studio/frontend/src/features/studio/studio-page.tsx @@ -5,15 +5,18 @@ import { useTrainingRuntimeLifecycle, useTrainingRuntimeStore, } from "@/features/training"; +import { GuidedTour, type TourStep } from "@/features/tour"; import { ArrowLeft01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import type { ReactElement } from "react"; +import { type ReactElement, useEffect, useMemo, useState } from "react"; import { DatasetSection } from "./sections/dataset-section"; import { ModelSection } from "./sections/model-section"; import { ParamsSection } from "./sections/params-section"; import { TrainingSection } from "./sections/training-section"; import { TrainingView } from "./training-view"; +const STUDIO_TOUR_KEY = "tour:studio:v1"; + export function StudioPage(): ReactElement { useTrainingRuntimeLifecycle(); const showTrainingView = useTrainingRuntimeStore(shouldShowTrainingView); @@ -24,10 +27,68 @@ export function StudioPage(): ReactElement { const { dismissTrainingRun } = useTrainingActions(); const canGoBack = runtimePhase === "stopped" || runtimePhase === "error"; + const tourEnabled = hasHydratedRuntime && !isHydratingRuntime && !showTrainingView; + const [tourOpen, setTourOpen] = useState(false); + + const tourSteps = useMemo( + () => [ + { + id: "header", + target: "studio-header", + title: "This page is a pipeline", + body: "Pick a base model, attach data, tune params, then launch training. We’ll hit the 5 things that matter.", + }, + { + id: "model", + target: "studio-model", + title: "Choose model + method", + body: "Base model sets your ceiling. Method sets speed vs quality. Start here, then everything else follows.", + }, + { + id: "dataset", + target: "studio-dataset", + title: "Bring a dataset", + body: "Search Hub or paste `user/dataset`. Preview a few rows before you commit hours of compute.", + }, + { + id: "params", + target: "studio-params", + title: "Dial hyperparams", + body: "Epochs + context length + LR. Keep it boring: small changes, one at a time.", + }, + { + id: "start", + target: "studio-start", + title: "Start training", + body: "One click. If it fails, the error text is the first place to look (token, path, config).", + }, + { + id: "save", + target: "studio-save", + title: "Save config", + body: "Save good runs. Repeatability beats vibe. You can iterate from a known baseline.", + }, + ], + [], + ); + + useEffect(() => { + if (!tourEnabled) return; + if (localStorage.getItem(STUDIO_TOUR_KEY)) return; + setTourOpen(true); + }, [tourEnabled]); return (
+ localStorage.setItem(STUDIO_TOUR_KEY, "skipped")} + onComplete={() => localStorage.setItem(STUDIO_TOUR_KEY, "done")} + /> + {canGoBack && ( +
+ +
+ + +
+ + {isLast ? ( + + ) : ( + + )} +
+
+
+ +
+
+ Tip: `Esc` skips. Tour blocks clicks so you can read. +
+ + + + )} + + + + ); +} diff --git a/studio/frontend/src/features/tour/index.ts b/studio/frontend/src/features/tour/index.ts new file mode 100644 index 0000000000..750edb3f4b --- /dev/null +++ b/studio/frontend/src/features/tour/index.ts @@ -0,0 +1,3 @@ +export { GuidedTour } from "./guided-tour"; +export type { TourStep } from "./guided-tour"; +