img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+};
diff --git a/studio/frontend/src/components/ui/chart.tsx b/studio/frontend/src/components/ui/chart.tsx
new file mode 100644
index 0000000000..127d0c52d7
--- /dev/null
+++ b/studio/frontend/src/components/ui/chart.tsx
@@ -0,0 +1,448 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "@/lib/utils";
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record
}
+ );
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error("useChart must be used within a ");
+ }
+
+ return context;
+}
+
+function ChartContainer({
+ id,
+ className,
+ children,
+ config,
+ ...props
+}: React.ComponentProps<"div"> & {
+ config: ChartConfig;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"];
+}) {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+ const containerRef = React.useRef(null);
+ const [containerSize, setContainerSize] = React.useState<{
+ width: number;
+ height: number;
+ } | null>(null);
+
+ React.useEffect(() => {
+ const element = containerRef.current;
+ if (!element) return;
+
+ const updateSizeState = () => {
+ const { width, height } = element.getBoundingClientRect();
+ const nextSize =
+ width > 0 && height > 0
+ ? {
+ width: Math.round(width),
+ height: Math.round(height),
+ }
+ : null;
+
+ setContainerSize((currentSize) => {
+ if (!nextSize) {
+ // Keep the last valid size once mounted to avoid unmount/remount thrash.
+ return currentSize;
+ }
+ if (
+ currentSize &&
+ currentSize.width === nextSize.width &&
+ currentSize.height === nextSize.height
+ ) {
+ return currentSize;
+ }
+ return nextSize;
+ });
+ };
+
+ updateSizeState();
+
+ if (typeof ResizeObserver === "undefined") {
+ const recheckSize = () => {
+ if (document.visibilityState === "visible") {
+ updateSizeState();
+ }
+ };
+
+ window.addEventListener("resize", recheckSize);
+ window.addEventListener("orientationchange", recheckSize);
+ document.addEventListener("visibilitychange", recheckSize);
+
+ return () => {
+ window.removeEventListener("resize", recheckSize);
+ window.removeEventListener("orientationchange", recheckSize);
+ document.removeEventListener("visibilitychange", recheckSize);
+ };
+ }
+
+ const observer = new ResizeObserver(() => {
+ updateSizeState();
+ });
+ observer.observe(element);
+
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+
+
+
+ {containerSize ? (
+
+ {children}
+
+ ) : null}
+
+
+ );
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color,
+ );
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+