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:
parent
362e679b66
commit
448aaa13cb
14 changed files with 193 additions and 82 deletions
|
|
@ -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={
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 it’s dataset formatting/template.{" "}
|
||||
<ReadMore href="https://docs.unsloth.ai/basics/fine-tuning-llms-guide" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 you’re unsure, change 1 knob at a time, and
|
||||
watch train vs eval loss.{" "}
|
||||
<ReadMore href="https://docs.unsloth.ai/basics/lora-hyperparameters-guide" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue