From ea36112a05f27b80e9c2adaa1ade383d432f1738 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Sun, 15 Feb 2026 19:20:37 +0100 Subject: [PATCH 1/9] 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"; + From 0db321ab857de5c11ad70d1f62039dba3df33971 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Sun, 15 Feb 2026 19:31:49 +0100 Subject: [PATCH 2/9] tour light card --- studio/frontend/src/components/navbar.tsx | 2 +- .../src/components/ui/shine-border.tsx | 61 +++++++++++++++++++ .../studio/sections/model-section.tsx | 10 +-- .../src/features/studio/studio-page.tsx | 34 +++++++---- .../src/features/tour/guided-tour.tsx | 40 +++++++----- studio/frontend/src/index.css | 12 ++++ 6 files changed, 127 insertions(+), 32 deletions(-) create mode 100644 studio/frontend/src/components/ui/shine-border.tsx diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index aee6105cf3..9b9cdf557e 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -29,7 +29,7 @@ export function Navbar() { const [logoHovered, setLogoHovered] = useState(false); return ( -
+
{/* Left: logo */}
{ + /** + * Width of the border in pixels + * @default 1 + */ + borderWidth?: number + /** + * Duration of the animation in seconds + * @default 14 + */ + duration?: number + /** + * Color of the border, can be a single color or an array of colors + * @default "#000000" + */ + shineColor?: string | string[] +} + +/** + * Shine Border + * + * An animated background border effect component with configurable properties. + */ +export function ShineBorder({ + borderWidth = 1, + duration = 14, + shineColor = "#000000", + className, + style, + ...props +}: ShineBorderProps) { + return ( +
+ ) +} diff --git a/studio/frontend/src/features/studio/sections/model-section.tsx b/studio/frontend/src/features/studio/sections/model-section.tsx index b21d433fb0..a2d58ad066 100644 --- a/studio/frontend/src/features/studio/sections/model-section.tsx +++ b/studio/frontend/src/features/studio/sections/model-section.tsx @@ -140,9 +140,9 @@ export function ModelSection() { className="shadow-border ring-1 ring-border" >
-
- - Local Model +
+ + Local Model
-
+
Base Model @@ -272,7 +272,7 @@ export function ModelSection() {
-
+
Method diff --git a/studio/frontend/src/features/studio/studio-page.tsx b/studio/frontend/src/features/studio/studio-page.tsx index 61654f4954..1a6d0ace85 100644 --- a/studio/frontend/src/features/studio/studio-page.tsx +++ b/studio/frontend/src/features/studio/studio-page.tsx @@ -33,22 +33,34 @@ export function StudioPage(): ReactElement { 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: "nav", + target: "navbar", + title: "Quick orientation", + body: "Studio is where you fine-tune. Export ships results. Chat is for poking at models. This tour is Studio-only (for now).", }, { - 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: "local-model", + target: "studio-local-model", + title: "Local model path", + body: "Point to a local folder (`./models/...`) or a custom HF repo. Use this when you already downloaded weights.", + }, + { + id: "base-model", + target: "studio-base-model", + title: "Base model from Hugging Face", + body: "Search Hub here. Paste `org/model` too. Pick something close to your target domain to save compute.", + }, + { + id: "method", + target: "studio-method", + title: "Method: QLoRA vs LoRA vs Full", + body: "QLoRA: lowest VRAM (4-bit). LoRA: fast + solid (16-bit adapters). Full: slowest, highest cost, updates all weights.", }, { 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.", + title: "Dataset", + body: "Search Hub or paste `user/dataset`. Preview a few rows before you burn hours of compute.", }, { id: "params", @@ -102,7 +114,7 @@ export function StudioPage(): ReactElement { )} {/* Header */} -
+

Fine-tuning Studio

diff --git a/studio/frontend/src/features/tour/guided-tour.tsx b/studio/frontend/src/features/tour/guided-tour.tsx index 6fabe80d26..d94185e5f6 100644 --- a/studio/frontend/src/features/tour/guided-tour.tsx +++ b/studio/frontend/src/features/tour/guided-tour.tsx @@ -1,4 +1,5 @@ import { Button } from "@/components/ui/button"; +import { ShineBorder } from "@/components/ui/shine-border"; import { cn } from "@/lib/utils"; import { HugeiconsIcon } from "@hugeicons/react"; import { @@ -313,16 +314,25 @@ export function GuidedTour({ transition={{ duration: 0.22, ease: [0.165, 0.84, 0.44, 1] }} className={cn( "relative overflow-hidden rounded-[28px] corner-squircle", - "bg-[#0b0f16]/95 text-white ring-1 ring-white/10", - "shadow-[0_30px_120px_rgba(0,0,0,0.65)]", + "bg-white/95 text-foreground ring-1 ring-black/10", + "shadow-[0_30px_120px_rgba(0,0,0,0.35)]", )} style={{ fontFamily: "'Figtree Variable', ui-sans-serif, sans-serif", }} > +
-
-
+
+
-
+
{idx + 1}/{total} - + guided tour
{step?.title ?? "Quick tour"} - + {step?.body ?? "Let’s get you oriented."}
@@ -360,7 +370,7 @@ export function GuidedTour({
-
-
+
+
Tip: `Esc` skips. Tour blocks clicks so you can read.
diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index df14df684a..f8b445a1b2 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -242,6 +242,18 @@ background-position: calc(100% + var(--shiny-width)) 0; } } + --animate-shine: shine var(--duration) infinite linear; + @keyframes shine { + 0% { + background-position: 0% 0%; + } + 50% { + background-position: 100% 100%; + } + 100% { + background-position: 0% 0%; + } + } } @layer base { From ddffa2bbdc7ab6c0b6457c0f2a7198f29806f074 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Sun, 15 Feb 2026 19:41:15 +0100 Subject: [PATCH 3/9] rm shine + perf --- studio/frontend/src/components/navbar.tsx | 7 ++- .../src/components/ui/shine-border.tsx | 61 ------------------- .../src/features/tour/guided-tour.tsx | 53 ++++++++++------ studio/frontend/src/index.css | 12 ---- 4 files changed, 39 insertions(+), 94 deletions(-) delete mode 100644 studio/frontend/src/components/ui/shine-border.tsx diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index 9b9cdf557e..6c02dba493 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -29,7 +29,7 @@ export function Navbar() { const [logoHovered, setLogoHovered] = useState(false); return ( -
+
{/* Left: logo */}
{/* Center: pill nav */} -