tour light card
This commit is contained in:
parent
ea36112a05
commit
0db321ab85
6 changed files with 127 additions and 32 deletions
|
|
@ -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
|
||||
|
|
|
|||
61
studio/frontend/src/components/ui/shine-border.tsx
Normal file
61
studio/frontend/src/components/ui/shine-border.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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. We’ll 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>
|
||||
|
|
|
|||
|
|
@ -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 ?? "Let’s 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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue