tour steps split

This commit is contained in:
Shine1i 2026-02-15 19:57:07 +01:00
commit 2b6229f049
13 changed files with 170 additions and 64 deletions

View file

@ -5,10 +5,10 @@ import {
useTrainingRuntimeLifecycle,
useTrainingRuntimeStore,
} from "@/features/training";
import { GuidedTour, type TourStep } from "@/features/tour";
import { GuidedTour, studioTourSteps } from "@/features/tour";
import { ArrowLeft01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type ReactElement, useEffect, useMemo, useState } from "react";
import { type ReactElement, useEffect, useState } from "react";
import { DatasetSection } from "./sections/dataset-section";
import { ModelSection } from "./sections/model-section";
import { ParamsSection } from "./sections/params-section";
@ -30,60 +30,6 @@ export function StudioPage(): ReactElement {
const tourEnabled = hasHydratedRuntime && !isHydratingRuntime && !showTrainingView;
const [tourOpen, setTourOpen] = useState(false);
const tourSteps = useMemo<TourStep[]>(
() => [
{
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: "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: "Dataset",
body: "Search Hub or paste `user/dataset`. Preview a few rows before you burn 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;
@ -96,7 +42,7 @@ export function StudioPage(): ReactElement {
<GuidedTour
open={tourOpen}
onOpenChange={setTourOpen}
steps={tourSteps}
steps={studioTourSteps}
onSkip={() => localStorage.setItem(STUDIO_TOUR_KEY, "skipped")}
onComplete={() => localStorage.setItem(STUDIO_TOUR_KEY, "done")}
/>

View file

@ -9,23 +9,31 @@ import {
} from "@hugeicons/core-free-icons";
import { Dialog as DialogPrimitive } from "radix-ui";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
type ReactNode,
useEffect,
useId,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
export type TourStep = {
id: string;
target: string; // data-tour="<target>"
title: string;
body: string;
body: ReactNode;
};
type Rect = { x: number; y: number; w: number; h: number };
type Placement = "right" | "left" | "top" | "bottom";
function clamp(n: number, min: number, max: number) {
function clamp(n: number, min: number, max: number): number {
return Math.min(max, Math.max(min, n));
}
function cssEscape(value: string) {
function cssEscape(value: string): string {
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
return CSS.escape(value);
}
@ -70,7 +78,7 @@ function computeCardPos(
vw: number,
vh: number,
gap: number,
) {
): { left: number; top: number } {
let left = 12;
let top = 12;
@ -182,7 +190,7 @@ export function GuidedTour({
const spotlightRect = useMemo(() => {
if (!targetRect || !vw || !vh) return null;
const pad = step?.target === "navbar" ? 8 : 14;
const pad = step?.target === "navbar" ? 6 : 14;
return padded(targetRect, pad, vw, vh);
}, [step?.target, targetRect, vw, vh]);
@ -246,12 +254,14 @@ export function GuidedTour({
const ro = new ResizeObserver(() => schedule());
ro.observe(el);
window.addEventListener("scroll", schedule, { capture: true, passive: true });
window.addEventListener("resize", schedule, { passive: true });
return () => {
window.cancelAnimationFrame(raf);
window.clearTimeout(t);
ro.disconnect();
window.removeEventListener("scroll", schedule, true);
window.removeEventListener("resize", schedule);
if (rafRef.current != null) {
window.cancelAnimationFrame(rafRef.current);
rafRef.current = null;

View file

@ -1,3 +1,3 @@
export { GuidedTour } from "./guided-tour";
export type { TourStep } from "./guided-tour";
export { studioTourSteps } from "./steps/studio";

View file

@ -0,0 +1,11 @@
export function ReadMore({ href = "#" }: { href?: string }) {
return (
<a
href={href}
className="text-emerald-600 underline underline-offset-2 hover:text-emerald-700"
>
Read more
</a>
);
}

View file

@ -0,0 +1,15 @@
import type { TourStep } from "@/features/tour";
import { ReadMore } from "../read-more";
export const studioBaseModelStep: TourStep = {
id: "base-model",
target: "studio-base-model",
title: "Base model from Hugging Face",
body: (
<>
Search Hub here. Paste <span className="font-mono">org/model</span> too.
Pick something close to your domain to save compute. <ReadMore />
</>
),
};

View file

@ -0,0 +1,15 @@
import type { TourStep } from "@/features/tour";
import { ReadMore } from "../read-more";
export const studioDatasetStep: TourStep = {
id: "dataset",
target: "studio-dataset",
title: "Dataset",
body: (
<>
Search Hub or paste <span className="font-mono">user/dataset</span>.
Preview a few rows before you burn hours of compute. <ReadMore />
</>
),
};

View file

@ -0,0 +1,21 @@
import type { TourStep } from "@/features/tour";
import { studioBaseModelStep } from "./base-model";
import { studioDatasetStep } from "./dataset";
import { studioLocalModelStep } from "./local-model";
import { studioMethodStep } from "./method";
import { studioNavStep } from "./nav";
import { studioParamsStep } from "./params";
import { studioSaveStep } from "./save";
import { studioStartStep } from "./start";
export const studioTourSteps: TourStep[] = [
studioNavStep,
studioLocalModelStep,
studioBaseModelStep,
studioMethodStep,
studioDatasetStep,
studioParamsStep,
studioStartStep,
studioSaveStep,
];

View file

@ -0,0 +1,16 @@
import type { TourStep } from "@/features/tour";
import { ReadMore } from "../read-more";
export const studioLocalModelStep: TourStep = {
id: "local-model",
target: "studio-local-model",
title: "Local model path",
body: (
<>
Point to a local folder (<span className="font-mono">./models/...</span>)
or a custom HF repo. Use this when you already downloaded weights.{" "}
<ReadMore />
</>
),
};

View file

@ -0,0 +1,15 @@
import type { TourStep } from "@/features/tour";
import { ReadMore } from "../read-more";
export const studioMethodStep: TourStep = {
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. <ReadMore />
</>
),
};

View file

@ -0,0 +1,14 @@
import type { TourStep } from "@/features/tour";
export const studioNavStep: TourStep = {
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).
</>
),
};

View file

@ -0,0 +1,15 @@
import type { TourStep } from "@/features/tour";
import { ReadMore } from "../read-more";
export const studioParamsStep: TourStep = {
id: "params",
target: "studio-params",
title: "Dial hyperparams",
body: (
<>
Epochs + context length + LR. Keep it boring: small changes, one at a
time. <ReadMore />
</>
),
};

View file

@ -0,0 +1,14 @@
import type { TourStep } from "@/features/tour";
export const studioSaveStep: TourStep = {
id: "save",
target: "studio-save",
title: "Save config",
body: (
<>
Save good runs. Repeatability beats vibe. You can iterate from a known
baseline.
</>
),
};

View file

@ -0,0 +1,14 @@
import type { TourStep } from "@/features/tour";
export const studioStartStep: TourStep = {
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).
</>
),
};