Initial commit

Co-authored-by: scott-oai <142930063+scott-oai@users.noreply.github.com>
Co-authored-by: Zhuohan Li <zhuohan@openai.com>
This commit is contained in:
Dominik Kundel 2025-08-05 08:25:17 -07:00 committed by Scott Lessans
commit 253cdca537
70 changed files with 15013 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,196 @@
@import "tailwindcss";
@import "tw-animate-css";
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: normal;
font-weight: 400;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-Regular.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: italic;
font-weight: 400;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-RegularItalic.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: normal;
font-weight: 500;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-Medium.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: italic;
font-weight: 500;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-MediumItalic.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: normal;
font-weight: 600;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-Semibold.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: italic;
font-weight: 600;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-SemiboldItalic.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: normal;
font-weight: 700;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-Bold.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: italic;
font-weight: 700;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-BoldItalic.woff2")
format("woff2");
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--default-font-family: "OpenAI Sans";
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--default-font-family: "OpenAI Sans";
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Harmony Demo",
description: "Harmony Demo",
icons: {
icon: "/openai_logo.svg",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`antialiased`}>{children}</body>
</html>
);
}

View file

@ -0,0 +1,14 @@
"use client";
import HarmonyDemo from "@/components/HarmonyDemo";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export default function Home() {
return (
<QueryClientProvider client={queryClient}>
<HarmonyDemo />
</QueryClientProvider>
);
}

View file

@ -0,0 +1,487 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import {
load_harmony_encoding,
type JsHarmonyEncoding,
initSync as initHarmony,
JsStreamableParser,
} from "@openai/harmony";
import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
import { ChevronsUpDown, PlusIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
const SPECIAL_TOKENS_VALUES = {
"<|startoftext|>": 199998,
"<|endoftext|>": 199999,
"<|return|>": 200002,
"<|constrain|>": 200003,
"<|channel|>": 200005,
"<|start|>": 200006,
"<|end|>": 200007,
"<|message|>": 200008,
"<|call|>": 200012,
};
// Map from token id to special token string
const TOKEN_ID_TO_SPECIAL_TOKEN: Record<number, string> = Object.entries(
SPECIAL_TOKENS_VALUES
).reduce((acc, [key, value]) => {
acc[value] = key;
return acc;
}, {} as Record<number, string>);
const TOKEN_COLORS = [
"bg-red-300/30 data-[active=true]:bg-red-300",
"bg-teal-300/30 data-[active=true]:bg-teal-300",
"bg-amber-300/30 data-[active=true]:bg-amber-300",
"bg-indigo-300/30 data-[active=true]:bg-indigo-300",
"bg-pink-300/30 data-[active=true]:bg-pink-300",
"bg-green-300/30 data-[active=true]:bg-green-300",
"bg-orange-300/30 data-[active=true]:bg-orange-300",
"bg-purple-300/30 data-[active=true]:bg-purple-300",
"bg-yellow-300/30 data-[active=true]:bg-yellow-300",
"bg-cyan-300/30 data-[active=true]:bg-cyan-300",
"bg-lime-300/30 data-[active=true]:bg-lime-300",
"bg-violet-300/30 data-[active=true]:bg-violet-300",
"bg-emerald-300/30 data-[active=true]:bg-emerald-300",
"bg-rose-300/30 data-[active=true]:bg-rose-300",
"bg-sky-300/30 data-[active=true]:bg-sky-300",
"bg-fuchsia-300/30 data-[active=true]:bg-fuchsia-300",
"bg-blue-300/30 data-[active=true]:bg-blue-300",
];
const isSpecialToken = (token: string | number) => {
if (typeof token === "number") {
return Object.values(SPECIAL_TOKENS_VALUES).includes(token);
}
return Object.keys(SPECIAL_TOKENS_VALUES).includes(token);
};
const getSpecialTokenDescription = (token: number | string) => {
const tokenName =
typeof token === "number" ? TOKEN_ID_TO_SPECIAL_TOKEN[token] : token;
switch (tokenName) {
case "<|return|>":
return "Stop token. Indicates the model is done sampling its response.";
case "<|constrain|>":
return "Part of the message header to indicate the content type of the message. Primarily used for function calls to constrain the response to JSON.";
case "<|channel|>":
return "Part of the message header to indicate the channel the model's response is intended for.";
case "<|start|>":
return "Indicates the start of a new message. Tokens following this token are part of the header.";
case "<|end|>":
return "Indicates the end of a message. The next token is the start of the next message. This should not end sampling.";
case "<|message|>":
return "Completes the message header. Tokens following this token are part of the message content.";
case "<|call|>":
return "Stop token. Indicates the model is done sampling its response and is ready to perform the function call.";
default:
return `Unknown special token: ${tokenName}`;
}
};
function WrappedSpecialToken({
token,
children,
}: {
token: string | number;
children: React.ReactNode;
}) {
if (isSpecialToken(token)) {
return (
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent>{getSpecialTokenDescription(token)}</TooltipContent>
</Tooltip>
);
}
return children;
}
function HighlightedTokens({
tokens,
highlightedIndex,
onHover,
wrapWhitespace,
highlightTokens,
editable,
onEdit,
}: {
tokens: (number | string)[];
highlightedIndex?: number;
onHover?: (index?: number) => void;
wrapWhitespace?: boolean;
highlightTokens?: boolean;
editable?: boolean;
onEdit?: (text: string | number[]) => void;
}) {
const areTokenIds = tokens.every((token) => typeof token === "number");
const [isEditing, setIsEditing] = useState(false);
const [text, setText] = useState(
areTokenIds ? JSON.stringify(tokens) : tokens.join("")
);
const editorRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (editorRef.current) {
editorRef.current.focus();
editorRef.current.select();
}
}, [editorRef.current, isEditing]);
useEffect(() => {
if (editable) {
setText(areTokenIds ? JSON.stringify(tokens) : tokens.join(""));
}
}, [tokens]);
if (isEditing) {
return (
<div className="aspect-video rounded-xl p-4 font-mono bg-gray-50 border border-gray-200">
<textarea
className="w-full h-full font-mono text-sm"
value={text}
onChange={(e) => setText(e.target.value)}
onBlur={() => {
setIsEditing(false);
onEdit?.(text);
}}
ref={editorRef}
/>
</div>
);
}
return (
<div
className="aspect-video rounded-xl p-4 font-mono bg-gray-50 border border-gray-200"
onClick={() => editable && setIsEditing(true)}
>
<pre
className={cn(
"text-sm max-h-40 overflow-y-auto min-h-full",
wrapWhitespace ? "whitespace-pre-wrap break-all" : "whitespace-pre"
)}
>
{areTokenIds && "["}
{tokens.map((token, index) => (
<WrappedSpecialToken token={token} key={index}>
<code
className={cn(
// highlightTokens ? "rounded-[3px] px-1 ml-1 mr-1" : "",
areTokenIds ? "ml-1 mr-1" : "",
// highlightedIndex === index ? "bg-blue-500 text-white" : "",
isSpecialToken(token) && !areTokenIds
? "font-semibold"
: isSpecialToken(token)
? "underline decoration-dotted"
: "",
highlightTokens ? TOKEN_COLORS[index % TOKEN_COLORS.length] : ""
)}
onMouseEnter={() => onHover?.(index)}
onMouseLeave={() => onHover?.(undefined)}
data-active={highlightedIndex === index}
>
{token}
</code>
{areTokenIds && index < tokens.length - 1 && ","}
</WrappedSpecialToken>
))}
{areTokenIds && "]"}
</pre>
</div>
);
}
type Message = {
role: "assistant" | "user" | "system" | "developer" | "tool";
channel?: string;
recipient?: string;
content_type?: string;
content: {
type: "text";
text: string;
}[];
};
function MessageItem({ message }: { message: Message }) {
return (
<div className="text-sm border rounded-md p-1">
<div className="border border-dashed border-gray-300 rounded-md">
<Collapsible defaultOpen>
<div className="flex items-center justify-between">
<span className="bg-gray-300 text-xs rounded-tl-sm rounded-br-sm px-2 py-1 -ml-[1px]">
header
</span>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="icon" className="size-4 px-4">
<ChevronsUpDown />
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<div className="p-2 flex flex-col gap-1 mt-1">
<dl className="text-xs flex gap-1">
<dt>role:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.role}
</dd>
</dl>
{message.channel && (
<dl className="text-xs flex gap-1">
<dt>content:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.channel}
</dd>
</dl>
)}
{message.recipient && (
<dl className="text-xs flex gap-1">
<dt>recipient:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.recipient}
</dd>
</dl>
)}
{message.content_type && (
<dl className="text-xs flex gap-1">
<dt>content_type:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.content_type}
</dd>
</dl>
)}
</div>
</CollapsibleContent>
</Collapsible>
</div>
<div className="text-sm flex flex-col gap-4 mt-3 mb-1">
{message.content.map((content: any, idx: number) => (
<pre
className="bg-gray-100/50 p-4 rounded-md text-xs overflow-x-scroll"
key={idx}
>
{content.text}
</pre>
))}
</div>
</div>
);
}
function useHarmony() {
const [harmony, setHarmony] = useState<JsHarmonyEncoding | null>(null);
useEffect(() => {
const fetchHarmony = async () => {
const response = await fetch("/openai_harmony_bg.wasm");
const wasm = await response.arrayBuffer();
initHarmony({ module: wasm });
console.log("wasm", wasm);
const encoding = await load_harmony_encoding(
"HarmonyGptOss",
typeof window !== "undefined" ? `${window.location.origin}/` : "/"
);
setHarmony(encoding);
};
fetchHarmony();
}, []);
return harmony;
}
function useTokens(text: string | undefined) {
const [highlightedIndex, setHighlightedIndex] = useState<number | undefined>(
undefined
);
const encoding = useHarmony();
const [tokenData, setTokenData] = useState<{
tokens: number[];
tokenTexts: string[];
messages: Message[];
}>({ tokens: [], tokenTexts: [], messages: [] });
useEffect(() => {
if (!text || !encoding) return;
const tokens = Array.from(encoding.encode(text, encoding.specialTokens()));
// @ts-ignore
const tokenTexts = tokens.map((t) => encoding.decodeUtf8([t]));
const parser = new JsStreamableParser(encoding, "user");
for (const token of tokens) {
parser.process(token);
}
const messages = JSON.parse(parser.messages).map((m: any) => ({
...m,
content:
typeof m.content === "string"
? [{ type: "text", text: m.content }]
: m.content,
}));
console.log("messages", typeof messages, messages);
setTokenData({
tokens: Array.from(tokens),
tokenTexts: Array.from(tokenTexts),
messages: messages,
});
}, [text, encoding]);
return {
...tokenData,
highlightedIndex,
setHighlightedIndex,
};
}
function App() {
const [text, setText] = useState(
`<|start|>user<|message|>What is the weather in SF?<|end|><|start|>assistant<|channel|>analysis<|message|>User asks: “What is the weather in SF?” We need to use lookup_weather tool.<|end|><|start|>assistant to=functions.lookup_weather<|channel|>commentary <|constrain|>json<|message|>{"location": "San Francisco"}<|end|><|start|>assistant`
);
const {
tokens,
tokenTexts,
highlightedIndex,
setHighlightedIndex,
messages,
} = useTokens(text);
const [wrapWhitespace, setWrapWhitespace] = useState(true);
const [highlightTokens, setHighlightTokens] = useState(true);
return (
<div className="p-8 pt-6 max-w-screen-xl mx-auto">
<h1 className="text-3xl font-bold mb-4 tracking-tight">
OpenAI harmony response format
</h1>
<p className="text-sm text-gray-500 mb-4">
The{" "}
<a
href="https://github.com/openai/open-models"
className="text-blue-500 focus:underline hover:underline"
target="_blank"
>
gpt-oss
</a>{" "}
models were trained on the harmony response format for defining
conversation structures, generating reasoning output and structuring
function calls. If you are not using gpt-oss directly but through an API
or a provider like{" "}
<a
href="https://ollama.ai"
className="text-blue-500"
target="_blank"
rel="noopener noreferrer"
>
Ollama
</a>
, you will not have to be concerned about this as your inference
solution will handle the formatting.
</p>
<p className="text-sm text-gray-500 mb-8">
If you are trying to count input tokens for text to an OpenAI model,
visit the{" "}
<a
href="https://platform.openai.com/tokenizer"
className="text-blue-500 focus:underline hover:underline"
target="_blank"
>
OpenAI Tokenizer
</a>{" "}
instead.
</p>
<div className="grid md:grid-cols-2 gap-4 mt-8">
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-2">Messages</h3>
<div className="flex flex-col gap-4">
{messages.map((message: Message, idx: number) => (
<MessageItem key={idx} message={message} />
))}
{/* <Button
variant="outline"
className="border border-dashed border-gray-300 rounded-md text-center py-8 text-sm text-gray-500 cursor-pointer"
>
<PlusIcon /> Add new message
</Button> */}
</div>
</div>
<div className="flex flex-col gap-4">
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-2">
Decoded tokens{" "}
<span className="text-xs text-gray-500 font-light">
{/* (click to edit) */}
</span>
</h3>
<HighlightedTokens
tokens={tokenTexts}
highlightedIndex={highlightedIndex}
onHover={setHighlightedIndex}
wrapWhitespace={wrapWhitespace}
highlightTokens={highlightTokens}
editable={true}
onEdit={(text) => typeof text === "string" && setText(text)}
/>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-2">
Token IDs
</h3>
<HighlightedTokens
tokens={tokens}
highlightedIndex={highlightedIndex}
onHover={setHighlightedIndex}
wrapWhitespace={wrapWhitespace}
highlightTokens={highlightTokens}
/>
</div>
{/* <div className="flex flex-col md:flex-row gap-2">
<div className="flex items-center space-x-2">
<Switch
id="wrap-whitespace"
checked={wrapWhitespace}
onCheckedChange={() => setWrapWhitespace((val) => !val)}
/>
<Label htmlFor="wrap-whitespace">Wrap Whitespace</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="highlight-tokens"
checked={highlightTokens}
onCheckedChange={() => setHighlightTokens((val) => !val)}
/>
<Label htmlFor="highlight-tokens">Highlight Tokens</Label>
</div>
</div> */}
</div>
</div>
</div>
);
}
const HarmonyDemo = dynamic(() => Promise.resolve(App), {
ssr: false,
});
export default HarmonyDemo;

View file

@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,33 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View file

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View file

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View file

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }