feat: improve guided tour descriptions and add sidebar state management

- Updated step descriptions across Studio, Chat, and Export tours for better clarity.
- Added `openSidebar` state management function and integrated it into the tour logic.
- Improved target detection in guided tours with retry logic for better handling of unavailable elements.
This commit is contained in:
Shine1i 2026-02-16 00:55:13 +01:00
commit 448aaa13cb
14 changed files with 193 additions and 82 deletions

View file

@ -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<ChatView | null>(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 {
<GuidedTour {...tour.tourProps} />
<SidebarProvider
defaultOpen={true}
open={sidebarOpen}
onOpenChange={setSidebarOpen}
className="!min-h-0 h-full max-w-7xl mx-auto px-4"
style={
{

View file

@ -6,6 +6,7 @@ export function buildChatTourSteps({
closeModelSelector,
openSettings,
closeSettings,
openSidebar,
enterCompare,
exitCompare,
}: {
@ -14,6 +15,7 @@ export function buildChatTourSteps({
closeModelSelector: () => 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 whats 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) youve
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, its usually
data formatting, too many epochs, or a bad checkpoint choice.
</>
),
onEnter: enterCompare,
onExit: exitCompare,
},
@ -63,4 +92,3 @@ export function buildChatTourSteps({
return steps;
}

View file

@ -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,
its 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 youre 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.
</>
),
},
];

View file

@ -264,7 +264,7 @@ export function ChartsContent({
return (
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<Card size="sm">
<Card data-tour="studio-training-loss" size="sm">
<CardHeader>
<CardTitle className="text-sm pl-2">Training Loss</CardTitle>
<CardAction>
@ -546,7 +546,7 @@ export function ChartsContent({
</CardContent>
</Card>
<Card size="sm">
<Card data-tour="studio-eval-loss" size="sm">
<CardHeader>
<CardTitle className="text-sm text-muted-foreground pl-2">
Eval Loss

View file

@ -6,8 +6,10 @@ export const studioBaseModelStep: TourStep = {
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 />
Paste <span className="font-mono">org/model</span> 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.{" "}
<ReadMore href="https://docs.unsloth.ai/basics/fine-tuning-llms-guide" />
</>
),
};

View file

@ -6,8 +6,10 @@ export const studioDatasetStep: TourStep = {
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 />
Search Hub or paste <span className="font-mono">user/dataset</span>. Preview
a few rows: formatting matters more than size. If outputs look off in
Chat later, 80% chance its dataset formatting/template.{" "}
<ReadMore href="https://docs.unsloth.ai/basics/fine-tuning-llms-guide" />
</>
),
};

View file

@ -6,9 +6,10 @@ export const studioLocalModelStep: TourStep = {
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 />
Use this if you already downloaded weights locally (eg{" "}
<span className="font-mono">./models/...</span>) to avoid re-downloading.
Folder should look like a Hugging Face model (config + tokenizer + weights).{" "}
<ReadMore href="https://docs.unsloth.ai/basics/fine-tuning-llms-guide" />
</>
),
};

View file

@ -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. <ReadMore />
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).{" "}
<ReadMore href="https://docs.unsloth.ai/basics/lora-hyperparameters-guide" />
</>
),
};

View file

@ -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, youll see a Training view with live loss/metrics. Chat is for
testing base vs LoRA adapters. Export packages checkpoints for deployment.
</>
),
};

View file

@ -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. <ReadMore />
Start boring, then iterate. We usually recommend starting with 1-3 epochs
(higher can overfit fast). If youre unsure, change 1 knob at a time, and
watch train vs eval loss.{" "}
<ReadMore href="https://docs.unsloth.ai/basics/lora-hyperparameters-guide" />
</>
),
};

View file

@ -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).
</>
),
};

View file

@ -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.
</>
),
};

View file

@ -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 were 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 youre
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, youre 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.
</>
),
},
];

View file

@ -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;