diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index cb18ac81b1..0f57841249 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -205,6 +205,7 @@ export function ChatPage(): ReactElement { }); const [settingsOpen, setSettingsOpen] = useState(false); const [modelSelectorOpen, setModelSelectorOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(true); const viewBeforeCompareRef = useRef(null); const inferenceParams = useChatRuntimeStore((state) => state.params); const setInferenceParams = useChatRuntimeStore((state) => state.setParams); @@ -242,6 +243,7 @@ export function ChatPage(): ReactElement { const closeModelSelector = useCallback(() => setModelSelectorOpen(false), []); const openSettings = useCallback(() => setSettingsOpen(true), []); const closeSettings = useCallback(() => setSettingsOpen(false), []); + const openSidebar = useCallback(() => setSidebarOpen(true), []); const enterCompare = useCallback(() => { if (viewBeforeCompareRef.current == null) { @@ -290,6 +292,7 @@ export function ChatPage(): ReactElement { closeModelSelector, openSettings, closeSettings, + openSidebar, enterCompare, exitCompare, }), @@ -301,6 +304,7 @@ export function ChatPage(): ReactElement { exitCompare, openModelSelector, openSettings, + openSidebar, ], ); @@ -314,6 +318,8 @@ export function ChatPage(): ReactElement { void; openSettings: () => void; closeSettings: () => void; + openSidebar: () => void; enterCompare: () => void; exitCompare: () => void; }): TourStep[] { @@ -22,13 +24,24 @@ export function buildChatTourSteps({ id: "model", target: "chat-model-selector", title: "Pick a model", - body: <>Hub models vs fine-tuned adapters live here., + body: ( + <> + This selects what’s loaded for inference. Hub = base models. Fine-tuned + = your LoRA adapters from Studio. + + ), }, { id: "model-tabs", target: "chat-model-selector-popover", title: "Two tabs", - body: <>Hub: search HF. Fine-tuned: your local LoRA adapters., + body: ( + <> + Hub: search Hugging Face models. Fine-tuned: adapters (LoRA) you’ve + trained locally. If results look off, compare base vs LoRA to see what + changed. + + ), onEnter: openModelSelector, onExit: closeModelSelector, }, @@ -36,7 +49,12 @@ export function buildChatTourSteps({ id: "settings", target: "chat-settings", title: "Settings sidebar", - body: <>Sampling + system prompt live in the right sidebar., + body: ( + <> + Sampling (temperature/top-p/top-k) + system prompt live here. If you + want more deterministic outputs, lower temperature first. + + ), onEnter: openSettings, onExit: closeSettings, }, @@ -48,13 +66,24 @@ export function buildChatTourSteps({ id: "compare-btn", target: "chat-compare", title: "Compare mode", - body: <>When a LoRA is selected, you can compare base vs fine-tuned., + body: ( + <> + When a LoRA is selected, compare base vs fine-tuned side-by-side. + This is the fastest way to sanity-check your training. + + ), + onEnter: openSidebar, }, { id: "compare-view", target: "chat-compare-view", title: "Side-by-side threads", - body: <>Same prompt, 2 threads. Compose at bottom., + body: ( + <> + Same prompt, 2 threads. If LoRA is worse than base, it’s usually + data formatting, too many epochs, or a bad checkpoint choice. + + ), onEnter: enterCompare, onExit: exitCompare, }, @@ -63,4 +92,3 @@ export function buildChatTourSteps({ return steps; } - diff --git a/studio/frontend/src/features/export/tour/steps.tsx b/studio/frontend/src/features/export/tour/steps.tsx index 0635111389..63cfe36eb8 100644 --- a/studio/frontend/src/features/export/tour/steps.tsx +++ b/studio/frontend/src/features/export/tour/steps.tsx @@ -5,19 +5,34 @@ export const exportTourSteps: TourStep[] = [ id: "checkpoint", target: "export-checkpoint", title: "Pick checkpoint", - body: <>Choose which checkpoint (or final model) to export., + body: ( + <> + Pick which checkpoint to export. If you trained multiple checkpoints, + it’s worth exporting 1-2 candidates and testing in Chat. + + ), }, { id: "method", target: "export-method", title: "Export method", - body: <>Select GGUF vs safetensors, then quant if needed., + body: ( + <> + Choose the packaging. GGUF is for llama.cpp-style runtimes (pick a + quant). Safetensors is for HF/Transformers-style usage. If you’re unsure, + start with safetensors. + + ), }, { id: "cta", target: "export-cta", title: "Export", - body: <>When ready, export to local or HF Hub., + body: ( + <> + Export to local or push to HF Hub. After export, test in Chat and compare + against base to confirm behavior is what you expect. + + ), }, ]; - diff --git a/studio/frontend/src/features/studio/sections/charts-content.tsx b/studio/frontend/src/features/studio/sections/charts-content.tsx index ab1290e5eb..a6c5035551 100644 --- a/studio/frontend/src/features/studio/sections/charts-content.tsx +++ b/studio/frontend/src/features/studio/sections/charts-content.tsx @@ -264,7 +264,7 @@ export function ChartsContent({ return (
- + Training Loss @@ -546,7 +546,7 @@ export function ChartsContent({ - + Eval Loss diff --git a/studio/frontend/src/features/studio/tour/steps/base-model.tsx b/studio/frontend/src/features/studio/tour/steps/base-model.tsx index 5dfdc71b8c..cf89aa9965 100644 --- a/studio/frontend/src/features/studio/tour/steps/base-model.tsx +++ b/studio/frontend/src/features/studio/tour/steps/base-model.tsx @@ -6,8 +6,10 @@ export const studioBaseModelStep: TourStep = { title: "Base model from Hugging Face", body: ( <> - Search Hub here. Paste org/model too. - Pick something close to your domain to save compute. + Paste org/model or search. Pick a base + model close to your task (chat/instruct vs base). Smaller models iterate + faster; scale up once prompts + data look good.{" "} + ), }; diff --git a/studio/frontend/src/features/studio/tour/steps/dataset.tsx b/studio/frontend/src/features/studio/tour/steps/dataset.tsx index 95347430ef..79cacc66be 100644 --- a/studio/frontend/src/features/studio/tour/steps/dataset.tsx +++ b/studio/frontend/src/features/studio/tour/steps/dataset.tsx @@ -6,8 +6,10 @@ export const studioDatasetStep: TourStep = { title: "Dataset", body: ( <> - Search Hub or paste user/dataset. - Preview a few rows before you burn hours of compute. + Search Hub or paste user/dataset. Preview + a few rows: formatting matters more than size. If outputs look off in + Chat later, 80% chance it’s dataset formatting/template.{" "} + ), }; diff --git a/studio/frontend/src/features/studio/tour/steps/local-model.tsx b/studio/frontend/src/features/studio/tour/steps/local-model.tsx index 9dffdc653e..bd8a389364 100644 --- a/studio/frontend/src/features/studio/tour/steps/local-model.tsx +++ b/studio/frontend/src/features/studio/tour/steps/local-model.tsx @@ -6,9 +6,10 @@ export const studioLocalModelStep: TourStep = { title: "Local model path", body: ( <> - Point to a local folder (./models/...) - or a custom HF repo. Use this when you already downloaded weights.{" "} - + Use this if you already downloaded weights locally (eg{" "} + ./models/...) to avoid re-downloading. + Folder should look like a Hugging Face model (config + tokenizer + weights).{" "} + ), }; diff --git a/studio/frontend/src/features/studio/tour/steps/method.tsx b/studio/frontend/src/features/studio/tour/steps/method.tsx index 35a3b9c8a4..418ea51e48 100644 --- a/studio/frontend/src/features/studio/tour/steps/method.tsx +++ b/studio/frontend/src/features/studio/tour/steps/method.tsx @@ -6,8 +6,10 @@ export const studioMethodStep: TourStep = { 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. + LoRA: trains small adapter weights (fast, common default). QLoRA: LoRA on + 4-bit base weights (much lower VRAM). Full: updates all weights (highest + cost, usually needs more data to be worth it).{" "} + ), }; diff --git a/studio/frontend/src/features/studio/tour/steps/nav.tsx b/studio/frontend/src/features/studio/tour/steps/nav.tsx index 740df58948..9656f26cf1 100644 --- a/studio/frontend/src/features/studio/tour/steps/nav.tsx +++ b/studio/frontend/src/features/studio/tour/steps/nav.tsx @@ -6,9 +6,9 @@ export const studioNavStep: TourStep = { 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). + Studio: pick base model, dataset, hyperparams, then start training. After + you start, you’ll see a Training view with live loss/metrics. Chat is for + testing base vs LoRA adapters. Export packages checkpoints for deployment. ), }; - diff --git a/studio/frontend/src/features/studio/tour/steps/params.tsx b/studio/frontend/src/features/studio/tour/steps/params.tsx index d810c75bbd..d39baac592 100644 --- a/studio/frontend/src/features/studio/tour/steps/params.tsx +++ b/studio/frontend/src/features/studio/tour/steps/params.tsx @@ -6,8 +6,10 @@ export const studioParamsStep: TourStep = { title: "Dial hyperparams", body: ( <> - Epochs + context length + LR. Keep it boring: small changes, one at a - time. + Start boring, then iterate. We usually recommend starting with 1-3 epochs + (higher can overfit fast). If you’re unsure, change 1 knob at a time, and + watch train vs eval loss.{" "} + ), }; diff --git a/studio/frontend/src/features/studio/tour/steps/save.tsx b/studio/frontend/src/features/studio/tour/steps/save.tsx index 15bb37bb4f..d137aecf41 100644 --- a/studio/frontend/src/features/studio/tour/steps/save.tsx +++ b/studio/frontend/src/features/studio/tour/steps/save.tsx @@ -6,9 +6,8 @@ export const studioSaveStep: TourStep = { title: "Save config", body: ( <> - Save good runs. Repeatability beats vibe. You can iterate from a known - baseline. + Save configs that worked. Re-running the same baseline makes it obvious + if a change helped (or if you just got lucky). ), }; - diff --git a/studio/frontend/src/features/studio/tour/steps/start.tsx b/studio/frontend/src/features/studio/tour/steps/start.tsx index e04d2c4b4c..768071df98 100644 --- a/studio/frontend/src/features/studio/tour/steps/start.tsx +++ b/studio/frontend/src/features/studio/tour/steps/start.tsx @@ -6,9 +6,9 @@ export const studioStartStep: TourStep = { title: "Start training", body: ( <> - One click. If it fails, the error text is the first place to look (token, - path, config). + Kick off training. If it errors immediately, check HF token / local paths + / dataset access first. Start with a small run to sanity-check loss + sample + outputs before burning hours. ), }; - diff --git a/studio/frontend/src/features/studio/tour/training/steps.tsx b/studio/frontend/src/features/studio/tour/training/steps.tsx index dc0bf3715e..2cd812ad8b 100644 --- a/studio/frontend/src/features/studio/tour/training/steps.tsx +++ b/studio/frontend/src/features/studio/tour/training/steps.tsx @@ -5,19 +5,59 @@ export const studioTrainingTourSteps: TourStep[] = [ id: "nav", target: "navbar", title: "Training view", - body: <>Live run status + metrics. You can stop anytime., + body: ( + <> + This view updates live as training runs. Watch loss, speed, and ETA, and + use Stop if you need to bail out or save. + + ), }, { id: "progress", target: "studio-training-progress", title: "Progress + ETA", - body: <>Phase, steps, loss, speed, ETA. This card updates live., + body: ( + <> + Phase shows what we’re doing (loading model/dataset, configuring, + training). ETA is rough early on; it stabilizes after a few steps. + + ), + }, + { + id: "train-loss", + target: "studio-training-loss", + title: "Training loss", + body: ( + <> + Training loss should generally trend down. Absolute values vary by + dataset + tokenizer, so use it for direction more than “a magic number”. + If loss goes very low (eg below ~0.2), that can be a sign you’re + overfitting. If loss plateaus high, you likely need better data + formatting, more data, or different hyperparams. + + ), + }, + { + id: "eval-loss", + target: "studio-eval-loss", + title: "Eval loss (validation)", + body: ( + <> + Eval loss is your sanity check. If training loss keeps dropping but eval + loss goes up, you’re likely overfitting. To track it, set an eval dataset + and `eval_steps` (setting `eval_steps=1` can be very slow). + + ), }, { id: "stop", target: "studio-training-stop", title: "Stop / save", - body: <>Stop training, optionally save adapters/checkpoints., + body: ( + <> + Stop training any time. “Stop and Save” keeps the checkpoint/adapters so + you can export or compare later. + + ), }, ]; - diff --git a/studio/frontend/src/features/tour/components/guided-tour.tsx b/studio/frontend/src/features/tour/components/guided-tour.tsx index cf033f95f3..50492e3265 100644 --- a/studio/frontend/src/features/tour/components/guided-tour.tsx +++ b/studio/frontend/src/features/tour/components/guided-tour.tsx @@ -1,35 +1,17 @@ import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { HugeiconsIcon } from "@hugeicons/react"; -import { - ArrowLeft01Icon, - ArrowRight01Icon, - Cancel01Icon, - CheckmarkCircle01Icon, -} from "@hugeicons/core-free-icons"; +import { ArrowLeft01Icon, ArrowRight01Icon, Cancel01Icon, CheckmarkCircle01Icon } 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 { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react"; import { cssEscape, toRect } from "../lib/dom"; import { fireConfettiFireworks } from "../lib/confetti-fireworks"; import { computeCardPos, padded, pickPlacement } from "../lib/layout"; import { SpotlightOverlay } from "./spotlight-overlay"; import type { Placement, Rect, TourStep } from "../types"; -type GuidedTourProps = { - open: boolean; - onOpenChange: (open: boolean) => void; - steps: TourStep[]; - onSkip: () => void; - onComplete: () => void; -}; +type GuidedTourProps = { open: boolean; onOpenChange: (open: boolean) => void; steps: TourStep[]; onSkip: () => void; onComplete: () => void }; export function GuidedTour({ open, @@ -108,24 +90,25 @@ export function GuidedTour({ if (!open || !step) return; const sel = `[data-tour="${cssEscape(step.target)}"]`; - const found = document.querySelector(sel); - if (!(found instanceof HTMLElement)) { - setTargetRect(null); - return; - } - const el = found; - - if (step.target !== "navbar") { - el.scrollIntoView({ - block: "center", - inline: "center", - behavior: "smooth", - }); - } + let el: HTMLElement | null = null; + let ro: ResizeObserver | null = null; + let retryTimer = 0; + let retries = 0; let raf = 0; let t = 0; + function findTarget(): HTMLElement | null { + const found = document.querySelector(sel); + if (!(found instanceof HTMLElement)) return null; + return found; + } + + function isUsableTarget(candidate: HTMLElement): boolean { + const r = candidate.getBoundingClientRect(); + return r.width >= 6 && r.height >= 6; + } + function rectChanged(a: Rect | null, b: Rect): boolean { if (!a) return true; return ( @@ -136,8 +119,8 @@ export function GuidedTour({ ); } - function read() { - const r = el.getBoundingClientRect(); + function read(candidate: HTMLElement) { + const r = candidate.getBoundingClientRect(); const next = toRect(r); const prev = lastRectRef.current; if (rectChanged(prev, next)) { @@ -150,22 +133,53 @@ export function GuidedTour({ if (rafRef.current != null) return; rafRef.current = window.requestAnimationFrame(() => { rafRef.current = null; - read(); + if (el) read(el); }); } - raf = window.requestAnimationFrame(read); - t = window.setTimeout(schedule, 240); + function attach(candidate: HTMLElement) { + el = candidate; - const ro = new ResizeObserver(() => schedule()); - ro.observe(el); - window.addEventListener("scroll", schedule, { capture: true, passive: true }); - window.addEventListener("resize", schedule, { passive: true }); + if (step.target !== "navbar") { + el.scrollIntoView({ + block: "center", + inline: "center", + behavior: "smooth", + }); + } + + raf = window.requestAnimationFrame(() => read(el!)); + t = window.setTimeout(schedule, 240); + + ro = new ResizeObserver(() => schedule()); + ro.observe(el); + window.addEventListener("scroll", schedule, { capture: true, passive: true }); + window.addEventListener("resize", schedule, { passive: true }); + } + + function tryAttach(): boolean { + const candidate = findTarget(); + if (!candidate) return false; + if (!isUsableTarget(candidate)) return false; + attach(candidate); + return true; + } + + if (!tryAttach()) { + setTargetRect(null); + retryTimer = window.setInterval(() => { + retries += 1; + if (tryAttach() || retries > 40) { + window.clearInterval(retryTimer); + } + }, 50); + } return () => { window.cancelAnimationFrame(raf); window.clearTimeout(t); - ro.disconnect(); + if (retryTimer) window.clearInterval(retryTimer); + ro?.disconnect(); window.removeEventListener("scroll", schedule, true); window.removeEventListener("resize", schedule); if (rafRef.current != null) { @@ -173,7 +187,7 @@ export function GuidedTour({ rafRef.current = null; } }; - }, [open, step]); + }, [open, step?.id]); useLayoutEffect(() => { if (!open || !spotlightRect || !vw || !vh) return;