Studio: Always show API usage examples and docs links (#5270)
* chore: always show API usage examples and docs links * chore: add colon to API setup docs label * settings: move help links, add API shortcut, and update API copy
This commit is contained in:
parent
dbea77e347
commit
820b5c2e20
5 changed files with 120 additions and 114 deletions
|
|
@ -31,16 +31,14 @@ import {
|
|||
import { useAnimatedThemeToggle } from "@/components/ui/animated-theme-toggler";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Book03Icon,
|
||||
ChefHatIcon,
|
||||
ColumnInsertIcon,
|
||||
CursorInfo02Icon,
|
||||
Delete02Icon,
|
||||
Download03Icon,
|
||||
GemIcon,
|
||||
MessageSearch01Icon,
|
||||
Key01Icon,
|
||||
Search01Icon,
|
||||
NewReleasesIcon,
|
||||
PowerIcon,
|
||||
PencilEdit02Icon,
|
||||
LayoutAlignLeftIcon,
|
||||
|
|
@ -547,6 +545,15 @@ export function AppSidebar() {
|
|||
<span>Settings</span>
|
||||
<DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => useSettingsDialogStore.getState().openDialog("api-keys")}
|
||||
>
|
||||
<HugeiconsIcon icon={Key01Icon} strokeWidth={1.75} className="size-[18px]" />
|
||||
<span>API Keys</span>
|
||||
<span className="ml-auto rounded-[6px] border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] leading-none font-semibold text-emerald-700 dark:text-emerald-300">
|
||||
New
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
ref={anchorRef as React.Ref<HTMLDivElement>}
|
||||
onSelect={(e) => { e.preventDefault(); toggleTheme(); }}
|
||||
|
|
@ -571,47 +578,6 @@ export function AppSidebar() {
|
|||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="mx-2.5! my-2.5! h-0! border-t border-border/70 bg-transparent!" />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href="https://unsloth.ai/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<HugeiconsIcon icon={Book03Icon} strokeWidth={1.75} className="size-[18px]" />
|
||||
<span>Learn More</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href="https://unsloth.ai/docs/new/changelog"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={NewReleasesIcon}
|
||||
strokeWidth={1.75}
|
||||
className="size-[18px]"
|
||||
/>
|
||||
<span>What's New</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href="https://github.com/unslothai/unsloth/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={MessageSearch01Icon}
|
||||
strokeWidth={1.75}
|
||||
className="size-[18px]"
|
||||
/>
|
||||
<span>Feedback</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="mx-2.5! my-2.5! h-0! border-t border-border/70 bg-transparent!" />
|
||||
<DropdownMenuItem onSelect={() => setShutdownOpen(true)}>
|
||||
<HugeiconsIcon icon={PowerIcon} strokeWidth={1.75} className="size-[18px]" />
|
||||
<span>Shutdown</span>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { copyToClipboard } from "@/lib/copy-to-clipboard";
|
||||
import {
|
||||
ArrowDown01Icon,
|
||||
ArrowUpRight01Icon,
|
||||
Copy01Icon,
|
||||
Tick02Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type Lang = "curl" | "python" | "tools";
|
||||
|
|
@ -20,6 +19,29 @@ const TABS: { id: Lang; label: string }[] = [
|
|||
{ id: "tools", label: "Tools" },
|
||||
];
|
||||
|
||||
const DOC_LINKS = [
|
||||
{
|
||||
label: "Claude Code",
|
||||
href: "https://unsloth.ai/docs/basics/claude-code",
|
||||
},
|
||||
{
|
||||
label: "Codex",
|
||||
href: "https://unsloth.ai/docs/basics/codex",
|
||||
},
|
||||
{
|
||||
label: "OpenClaw",
|
||||
href: "https://unsloth.ai/docs/integrations/openclaw",
|
||||
},
|
||||
{
|
||||
label: "OpenCode",
|
||||
href: "https://unsloth.ai/docs/integrations/opencode",
|
||||
},
|
||||
{
|
||||
label: "Hermes Agent",
|
||||
href: "https://unsloth.ai/docs/integrations/hermes-agent",
|
||||
},
|
||||
];
|
||||
|
||||
function buildSnippets(base: string) {
|
||||
return {
|
||||
curl: `curl ${base}/v1/chat/completions \\
|
||||
|
|
@ -56,7 +78,6 @@ for chunk in response:
|
|||
}
|
||||
|
||||
export function UsageExamples() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [lang, setLang] = useState<Lang>("curl");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const snippets = useMemo(
|
||||
|
|
@ -76,71 +97,62 @@ export function UsageExamples() {
|
|||
|
||||
return (
|
||||
<section className="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="flex w-fit items-center gap-1.5 rounded text-xs font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowDown01Icon}
|
||||
className={cn("size-3.5 transition-transform", open && "rotate-180")}
|
||||
/>
|
||||
{open ? "Hide usage examples" : "Show usage examples"}
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.18, ease: [0.165, 0.84, 0.44, 1] }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-3 overflow-hidden rounded-lg border border-border bg-muted/20">
|
||||
<div className="flex items-center justify-between border-b border-border px-2 py-1.5">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{TABS.map((t) => {
|
||||
const active = lang === t.id;
|
||||
return (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
onClick={() => setLang(t.id)}
|
||||
aria-pressed={active}
|
||||
className={cn(
|
||||
"rounded px-2 py-1 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
active
|
||||
? "bg-background text-foreground shadow-border"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<h2 className="mb-2 text-sm font-semibold text-foreground">Usage examples</h2>
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-muted/20">
|
||||
<div className="flex items-center justify-between border-b border-border px-2 py-1.5">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{TABS.map((t) => {
|
||||
const active = lang === t.id;
|
||||
return (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-[11px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-label="Copy snippet"
|
||||
onClick={() => setLang(t.id)}
|
||||
aria-pressed={active}
|
||||
className={cn(
|
||||
"rounded px-2 py-1 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
active
|
||||
? "bg-background text-foreground shadow-border"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={copied ? Tick02Icon : Copy01Icon}
|
||||
className={cn("size-3.5", copied && "text-emerald-600")}
|
||||
/>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
{t.label}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto p-3 font-mono text-[11px] leading-relaxed text-foreground">
|
||||
{snippets[lang]}
|
||||
</pre>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-[11px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-label="Copy snippet"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={copied ? Tick02Icon : Copy01Icon}
|
||||
className={cn("size-3.5", copied && "text-emerald-600")}
|
||||
/>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto p-3 font-mono text-[11px] leading-relaxed text-foreground">
|
||||
{snippets[lang]}
|
||||
</pre>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-border px-3 py-2 text-[11px] text-muted-foreground">
|
||||
<span>Setup docs:</span>
|
||||
{DOC_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-0.5 rounded font-medium text-foreground underline decoration-border underline-offset-2 transition-colors hover:decoration-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{link.label}
|
||||
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ interface TabDef {
|
|||
id: SettingsTab;
|
||||
label: string;
|
||||
icon: typeof Settings02Icon;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
const TABS: TabDef[] = [
|
||||
|
|
@ -38,8 +39,8 @@ const TABS: TabDef[] = [
|
|||
{ id: "profile", label: "Profile", icon: UserIcon },
|
||||
{ id: "appearance", label: "Appearance", icon: PaintBrush02Icon },
|
||||
{ id: "chat", label: "Chat", icon: Message01Icon },
|
||||
{ id: "api-keys", label: "API Keys", icon: Key01Icon },
|
||||
{ id: "about", label: "About", icon: SparklesIcon },
|
||||
{ id: "api-keys", label: "API Keys", icon: Key01Icon, badge: "New" },
|
||||
{ id: "about", label: "Help", icon: SparklesIcon },
|
||||
];
|
||||
|
||||
function renderTab(tab: SettingsTab) {
|
||||
|
|
@ -121,7 +122,12 @@ export function SettingsDialog() {
|
|||
strokeWidth={1.5}
|
||||
className="relative z-10 size-[18px]"
|
||||
/>
|
||||
<span className="relative z-10">{tab.label}</span>
|
||||
<span className="relative z-10 min-w-0 truncate">{tab.label}</span>
|
||||
{tab.badge ? (
|
||||
<span className="relative z-10 ml-auto rounded-[6px] border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] leading-none font-semibold text-emerald-700 dark:text-emerald-300">
|
||||
{tab.badge}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Book03Icon,
|
||||
Cancel01Icon,
|
||||
MessageNotification01Icon,
|
||||
NewReleasesIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
|
@ -48,9 +49,9 @@ export function AboutTab() {
|
|||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<header className="flex flex-col gap-1">
|
||||
<h1 className="text-lg font-semibold font-heading">About</h1>
|
||||
<h1 className="text-lg font-semibold font-heading">Help</h1>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Unsloth Studio build info and support.
|
||||
Documentation, release notes, feedback, and Studio build info.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
|
@ -79,6 +80,18 @@ export function AboutTab() {
|
|||
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
|
||||
</a>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Release notes">
|
||||
<a
|
||||
href="https://unsloth.ai/docs/new/changelog"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon icon={NewReleasesIcon} className="size-3.5" />
|
||||
What's new
|
||||
<HugeiconsIcon icon={ArrowUpRight01Icon} className="size-3" />
|
||||
</a>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Feedback">
|
||||
<a
|
||||
href="https://github.com/unslothai/unsloth/issues"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,16 @@ export function ApiKeysTab() {
|
|||
<header className="flex flex-col gap-1">
|
||||
<h1 className="text-lg font-semibold font-heading">API Keys</h1>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Access Unsloth Studio programmatically via the OpenAI-compatible API.
|
||||
Access Unsloth programmatically via the OpenAI-compatible API.{" "}
|
||||
<a
|
||||
href="https://unsloth.ai/docs/basics/api"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-medium text-foreground underline decoration-border underline-offset-2 transition-colors hover:decoration-foreground"
|
||||
>
|
||||
Read the API docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue