tour light card

This commit is contained in:
Shine1i 2026-02-15 19:31:49 +01:00
commit 0db321ab85
6 changed files with 127 additions and 32 deletions

View file

@ -29,7 +29,7 @@ export function Navbar() {
const [logoHovered, setLogoHovered] = useState(false);
return (
<header className="top-0 z-40 h-16 w-full">
<header data-tour="navbar" className="top-0 z-40 h-16 w-full">
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-6">
{/* Left: logo */}
<div

View file

@ -0,0 +1,61 @@
import * as React from "react"
import { cn } from "@/lib/utils"
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* 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 (
<div
style={
{
"--border-width": `${borderWidth}px`,
"--duration": `${duration}s`,
backgroundImage: `radial-gradient(transparent,transparent, ${
Array.isArray(shineColor) ? shineColor.join(",") : shineColor
},transparent,transparent)`,
backgroundSize: "300% 300%",
mask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMaskComposite: "xor",
maskComposite: "exclude",
padding: "var(--border-width)",
...style,
} as React.CSSProperties
}
className={cn(
"motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]",
className
)}
{...props}
/>
)
}

View file

@ -140,9 +140,9 @@ export function ModelSection() {
className="shadow-border ring-1 ring-border"
>
<div className="grid gap-4 lg:grid-cols-4">
<div className="flex flex-col gap-2">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Local Model
<div data-tour="studio-local-model" className="flex flex-col gap-2">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Local Model
<Tooltip>
<TooltipTrigger asChild={true}>
<button
@ -168,7 +168,7 @@ export function ModelSection() {
</InputGroup>
</div>
<div className="flex flex-col gap-2">
<div data-tour="studio-base-model" className="flex flex-col gap-2">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Base Model
<Tooltip>
@ -272,7 +272,7 @@ export function ModelSection() {
</div>
</div>
<div className="flex flex-col gap-2">
<div data-tour="studio-method" className="flex flex-col gap-2">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Method
<Tooltip>

View file

@ -33,22 +33,34 @@ export function StudioPage(): ReactElement {
const tourSteps = useMemo<TourStep[]>(
() => [
{
id: "header",
target: "studio-header",
title: "This page is a pipeline",
body: "Pick a base model, attach data, tune params, then launch training. Well 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 */}
<div data-tour="studio-header" className="mb-8 flex flex-col gap-0.5">
<div className="mb-8 flex flex-col gap-0.5">
<h1 className="text-2xl font-semibold tracking-tight">
Fine-tuning Studio
</h1>

View file

@ -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",
}}
>
<ShineBorder
borderWidth={1}
duration={12}
shineColor={[
"rgba(16,185,129,0.65)",
"rgba(34,211,238,0.65)",
"rgba(16,185,129,0.35)",
]}
/>
<div
className={cn(
"absolute z-10 size-3 rotate-45 rounded-[3px] bg-[#0b0f16]/95 ring-1 ring-white/10",
"absolute z-10 size-3 rotate-45 rounded-[3px] bg-white/95 ring-1 ring-black/10",
placement === "right" &&
"-left-1 top-1/2 -translate-y-1/2",
placement === "left" &&
@ -335,15 +345,15 @@ export function GuidedTour({
aria-hidden={true}
/>
<div className="absolute inset-x-0 top-0 h-20 bg-gradient-to-b from-emerald-400/18 via-cyan-300/6 to-transparent" />
<div className="absolute -left-14 -top-16 size-44 rounded-full bg-emerald-400/10 blur-2xl" />
<div className="absolute -right-14 -bottom-16 size-44 rounded-full bg-cyan-300/10 blur-2xl" />
<div className="absolute -left-14 -top-16 size-44 rounded-full bg-emerald-400/20 blur-2xl" />
<div className="absolute -right-14 -bottom-16 size-44 rounded-full bg-cyan-300/18 blur-2xl" />
<div className="relative p-5">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="inline-flex items-center gap-2 rounded-full bg-white/6 px-2.5 py-1 text-[10px] font-mono text-white/70 ring-1 ring-white/10">
<div className="inline-flex items-center gap-2 rounded-full bg-black/[0.04] px-2.5 py-1 text-[10px] font-mono text-foreground/60 ring-1 ring-black/10">
{idx + 1}/{total}
<span className="size-1 rounded-full bg-emerald-300/70" />
<span className="size-1 rounded-full bg-emerald-500/70" />
guided tour
</div>
<DialogPrimitive.Title
@ -352,7 +362,7 @@ export function GuidedTour({
>
{step?.title ?? "Quick tour"}
</DialogPrimitive.Title>
<DialogPrimitive.Description className="mt-1.5 text-sm leading-relaxed text-white/72">
<DialogPrimitive.Description className="mt-1.5 text-sm leading-relaxed text-foreground/70">
{step?.body ?? "Lets get you oriented."}
</DialogPrimitive.Description>
</div>
@ -360,7 +370,7 @@ export function GuidedTour({
<Button
variant="ghost"
size="icon-sm"
className="text-white/70 hover:text-white hover:bg-white/8"
className="text-foreground/60 hover:text-foreground hover:bg-black/[0.05]"
onClick={() => requestClose("skip")}
aria-label="Skip tour"
>
@ -371,7 +381,7 @@ export function GuidedTour({
<div className="mt-5 flex items-center justify-between gap-3">
<Button
variant="ghost"
className="text-white/70 hover:text-white hover:bg-white/8"
className="text-foreground/60 hover:text-foreground hover:bg-black/[0.05]"
onClick={() => requestClose("skip")}
>
Skip
@ -380,7 +390,7 @@ export function GuidedTour({
<div className="flex items-center gap-2">
<Button
variant="outline"
className="border-white/12 bg-white/6 text-white hover:bg-white/10 hover:text-white"
className="border-black/10 bg-white/70 text-foreground hover:bg-white hover:text-foreground"
disabled={idx === 0}
onClick={() => setIdx((i) => Math.max(0, i - 1))}
>
@ -390,7 +400,7 @@ export function GuidedTour({
{isLast ? (
<Button
variant="dark"
className="bg-gradient-to-r from-emerald-300 to-cyan-200 text-black hover:from-emerald-200 hover:to-cyan-100"
className="bg-gradient-to-r from-emerald-500 to-cyan-400 text-white hover:from-emerald-600 hover:to-cyan-500"
onClick={() => requestClose("complete")}
>
<HugeiconsIcon icon={CheckmarkCircle01Icon} className="size-4" />
@ -399,7 +409,7 @@ export function GuidedTour({
) : (
<Button
variant="dark"
className="bg-gradient-to-r from-emerald-300 to-cyan-200 text-black hover:from-emerald-200 hover:to-cyan-100"
className="bg-gradient-to-r from-emerald-500 to-cyan-400 text-white hover:from-emerald-600 hover:to-cyan-500"
onClick={() => setIdx((i) => Math.min(total - 1, i + 1))}
>
Next
@ -410,8 +420,8 @@ export function GuidedTour({
</div>
</div>
<div className="h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="px-5 py-3 text-[11px] text-white/55">
<div className="h-px bg-gradient-to-r from-transparent via-black/10 to-transparent" />
<div className="px-5 py-3 text-[11px] text-foreground/55">
Tip: `Esc` skips. Tour blocks clicks so you can read.
</div>
</motion.div>

View file

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