Compare commits
1 commit
main
...
ui-font-si
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed64e6fc13 |
9 changed files with 253 additions and 157 deletions
|
|
@ -1,9 +1,6 @@
|
|||
// 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 { authFetch } from "@/features/auth";
|
||||
import { toastError } from "@/shared/toast";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -14,6 +11,9 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { authFetch } from "@/features/auth";
|
||||
import { toastError } from "@/shared/toast";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ShutdownDialogProps {
|
||||
open: boolean;
|
||||
|
|
@ -52,8 +52,8 @@ export function ShutdownDialog({
|
|||
onAfterShutdown?.();
|
||||
document.body.innerHTML = `
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;gap:12px">
|
||||
<p style="font-size:1.1rem;font-weight:600;margin:0">Unsloth Studio has stopped.</p>
|
||||
<p style="font-size:0.9rem;color:#888;margin:0">You can now close this tab.</p>
|
||||
<p style="font-size:calc(1.1rem * var(--ui-font-scale, 1));font-weight:600;margin:0">Unsloth Studio has stopped.</p>
|
||||
<p style="font-size:calc(0.9rem * var(--ui-font-scale, 1));color:#888;margin:0">You can now close this tab.</p>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist, type StateStorage } from "zustand/middleware";
|
||||
import {
|
||||
type StateStorage,
|
||||
createJSONStorage,
|
||||
persist,
|
||||
} from "zustand/middleware";
|
||||
import type { ResolvedTheme } from "./theme-store";
|
||||
|
||||
// Best-effort persistence: localStorage can be blocked (private browsing) and
|
||||
|
|
@ -200,7 +204,9 @@ function sanitizeImportedFonts(value: unknown): ImportedFont[] {
|
|||
const source = (entry ?? {}) as Partial<ImportedFont>;
|
||||
// Cap to the backend name length so an over-long name can't fail the PUT.
|
||||
const rawName = sanitizeFont(source.name);
|
||||
const name = rawName ? rawName.slice(0, MAX_IMPORTED_FONT_NAME_LENGTH) : null;
|
||||
const name = rawName
|
||||
? rawName.slice(0, MAX_IMPORTED_FONT_NAME_LENGTH)
|
||||
: null;
|
||||
if (!name || seen.has(name)) continue;
|
||||
const dataUrl = source.dataUrl;
|
||||
if (
|
||||
|
|
@ -397,7 +403,10 @@ function readableForeground(hex: string): string {
|
|||
* FontFaces registered for imported fonts, keyed by family name. The dataUrl is
|
||||
* tracked too so a re-import under the same name (new bytes) replaces the face.
|
||||
*/
|
||||
const registeredFontFaces = new Map<string, { face: FontFace; dataUrl: string }>();
|
||||
const registeredFontFaces = new Map<
|
||||
string,
|
||||
{ face: FontFace; dataUrl: string }
|
||||
>();
|
||||
|
||||
function syncImportedFonts(fonts: ImportedFont[]): void {
|
||||
if (typeof document === "undefined" || !("fonts" in document)) return;
|
||||
|
|
@ -510,11 +519,19 @@ export function applyCustomizationToDocument(
|
|||
setVar("--custom-chat-font", null);
|
||||
}
|
||||
|
||||
// Scale text via a factor, not the root rem, so layout dimensions stay fixed.
|
||||
if (c.uiFontSize !== null && c.uiFontSize !== UI_FONT_SIZE_RANGE.default) {
|
||||
style.fontSize = `${c.uiFontSize}px`;
|
||||
el.setAttribute("data-ui-font-size", "");
|
||||
setVar(
|
||||
"--ui-font-scale",
|
||||
String(c.uiFontSize / UI_FONT_SIZE_RANGE.default),
|
||||
);
|
||||
} else {
|
||||
style.removeProperty("font-size");
|
||||
el.removeAttribute("data-ui-font-size");
|
||||
setVar("--ui-font-scale", null);
|
||||
}
|
||||
// Drop the root font-size older builds set, so it never lingers.
|
||||
style.removeProperty("font-size");
|
||||
|
||||
if (c.codeFontSize !== null) {
|
||||
el.setAttribute("data-code-font-size", "");
|
||||
|
|
@ -554,7 +571,8 @@ export function applyCustomizationToDocument(
|
|||
* (canvas-confetti, view transitions) that CSS/MotionConfig cannot reach.
|
||||
*/
|
||||
export function prefersReducedMotion(): boolean {
|
||||
const setting = useAppearanceCustomStore.getState().customization.reduceMotion;
|
||||
const setting =
|
||||
useAppearanceCustomStore.getState().customization.reduceMotion;
|
||||
if (setting === "on") return true;
|
||||
if (setting === "off") return false;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import type { ReactElement } from "react";
|
|||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import {
|
||||
CHART_CONTAINER_CLASS,
|
||||
CHART_FONT_SIZE,
|
||||
DEFAULT_CHART_MARGIN,
|
||||
DEFAULT_Y_AXIS_WIDTH,
|
||||
formatAxisMetric,
|
||||
|
|
@ -46,13 +47,18 @@ export function EvalLossChartCard({
|
|||
return (
|
||||
<Card data-tour="studio-eval-loss" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className={`text-sm${data.length > 0 ? "" : " text-muted-foreground"}`}>
|
||||
<CardTitle
|
||||
className={`text-sm${data.length > 0 ? "" : " text-muted-foreground"}`}
|
||||
>
|
||||
{t("studio.charts.evalLoss")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.length > 0 ? (
|
||||
<ChartContainer config={evalLossConfig} className={CHART_CONTAINER_CLASS}>
|
||||
<ChartContainer
|
||||
config={evalLossConfig}
|
||||
className={CHART_CONTAINER_CLASS}
|
||||
>
|
||||
<LineChart
|
||||
data={data}
|
||||
accessibilityLayer={true}
|
||||
|
|
@ -70,7 +76,7 @@ export function EvalLossChartCard({
|
|||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
|
|
@ -81,7 +87,7 @@ export function EvalLossChartCard({
|
|||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickCount={5}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
width={DEFAULT_Y_AXIS_WIDTH}
|
||||
tickFormatter={(value) => formatAxisMetric(Number(value))}
|
||||
/>
|
||||
|
|
@ -132,7 +138,7 @@ export function EvalLossChartCard({
|
|||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
|
|
@ -140,7 +146,7 @@ export function EvalLossChartCard({
|
|||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickCount={5}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
width={DEFAULT_Y_AXIS_WIDTH}
|
||||
/>
|
||||
<Line
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ import type { ReactElement } from "react";
|
|||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import type { ScaleMode } from "./types";
|
||||
import {
|
||||
CHART_SYNC_ID,
|
||||
CHART_CONTAINER_CLASS,
|
||||
CHART_FONT_SIZE,
|
||||
CHART_SYNC_ID,
|
||||
DEFAULT_CHART_MARGIN,
|
||||
DEFAULT_Y_AXIS_WIDTH,
|
||||
formatAxisMetric,
|
||||
|
|
@ -58,7 +59,10 @@ export function GradNormChartCard({
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={gradNormConfig} className={CHART_CONTAINER_CLASS}>
|
||||
<ChartContainer
|
||||
config={gradNormConfig}
|
||||
className={CHART_CONTAINER_CLASS}
|
||||
>
|
||||
<LineChart
|
||||
data={data}
|
||||
syncId={CHART_SYNC_ID}
|
||||
|
|
@ -78,7 +82,7 @@ export function GradNormChartCard({
|
|||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
|
|
@ -89,7 +93,7 @@ export function GradNormChartCard({
|
|||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickCount={5}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
width={DEFAULT_Y_AXIS_WIDTH}
|
||||
tickFormatter={(value) => {
|
||||
const num = Number(value);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
|||
import type { ScaleMode } from "./types";
|
||||
import {
|
||||
CHART_CONTAINER_CLASS,
|
||||
CHART_FONT_SIZE,
|
||||
CHART_SYNC_ID,
|
||||
DEFAULT_CHART_MARGIN,
|
||||
DEFAULT_Y_AXIS_WIDTH,
|
||||
|
|
@ -76,7 +77,7 @@ export function LearningRateChartCard({
|
|||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
|
|
@ -87,7 +88,7 @@ export function LearningRateChartCard({
|
|||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickCount={5}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
width={DEFAULT_Y_AXIS_WIDTH}
|
||||
tickFormatter={(value) => {
|
||||
const num = Number(value);
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ import {
|
|||
} from "recharts";
|
||||
import type { ScaleMode } from "./types";
|
||||
import {
|
||||
CHART_SYNC_ID,
|
||||
CHART_CONTAINER_CLASS,
|
||||
CHART_FONT_SIZE,
|
||||
CHART_SYNC_ID,
|
||||
DEFAULT_CHART_MARGIN,
|
||||
DEFAULT_Y_AXIS_WIDTH,
|
||||
formatAxisMetric,
|
||||
|
|
@ -73,7 +74,9 @@ export function TrainingLossChartCard({
|
|||
return (
|
||||
<Card data-tour="studio-training-loss" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">{t("studio.charts.trainingLoss")}</CardTitle>
|
||||
<CardTitle className="text-sm">
|
||||
{t("studio.charts.trainingLoss")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={lossConfig} className={CHART_CONTAINER_CLASS}>
|
||||
|
|
@ -96,7 +99,7 @@ export function TrainingLossChartCard({
|
|||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
|
|
@ -107,7 +110,7 @@ export function TrainingLossChartCard({
|
|||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickCount={5}
|
||||
fontSize={10}
|
||||
fontSize={CHART_FONT_SIZE}
|
||||
width={DEFAULT_Y_AXIS_WIDTH}
|
||||
tickFormatter={(value) => {
|
||||
const num = Number(value);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export const DEFAULT_VISIBLE_POINTS = 160;
|
|||
export const CHART_CONTAINER_CLASS = "h-[220px] w-full";
|
||||
export const DEFAULT_CHART_MARGIN = { top: 4, right: 8, bottom: 0, left: 4 };
|
||||
export const DEFAULT_Y_AXIS_WIDTH = 45;
|
||||
// Recharts axis text is inline SVG, so scale it with the UI font preference.
|
||||
export const CHART_FONT_SIZE = "calc(10px * var(--ui-font-scale, 1))";
|
||||
const TRAILING_ZEROES_RE = /\.?0+$/;
|
||||
const NEGATIVE_ZERO_RE = /^-0$/;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import {
|
|||
useTrainingConfigStore,
|
||||
validateTrainingConfig,
|
||||
} from "@/features/training";
|
||||
import { useT } from "@/i18n";
|
||||
import { toast } from "@/lib/toast";
|
||||
import {
|
||||
Archive04Icon,
|
||||
ChartAverageIcon,
|
||||
|
|
@ -26,9 +28,7 @@ import {
|
|||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { useRef } from "react";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import { useT } from "@/i18n";
|
||||
|
||||
const placeholderData = [
|
||||
{ step: 0, loss: 2.5 },
|
||||
|
|
@ -65,11 +65,15 @@ export function TrainingSection() {
|
|||
try {
|
||||
const config = parseYamlConfig(reader.result as string);
|
||||
store.applyConfigPatch(config);
|
||||
toast.success(t("studio.training.configLoaded"), { description: file.name });
|
||||
toast.success(t("studio.training.configLoaded"), {
|
||||
description: file.name,
|
||||
});
|
||||
} catch (err) {
|
||||
toast.error(t("studio.training.failedToLoadConfig"), {
|
||||
description:
|
||||
err instanceof Error ? err.message : t("studio.training.invalidYamlFile"),
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t("studio.training.invalidYamlFile"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -105,7 +109,10 @@ export function TrainingSection() {
|
|||
const model = (store.selectedModel ?? "model").split("/").pop();
|
||||
const method = store.trainingMethod ?? "qlora";
|
||||
const dataset = (store.dataset ?? "dataset").split("/").pop();
|
||||
const timestamp = new Date().toISOString().replace(/[:T]/g, "-").slice(0, 19);
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:T]/g, "-")
|
||||
.slice(0, 19);
|
||||
a.download = `${model}_${method}_${dataset}_${timestamp}.yaml`;
|
||||
|
||||
a.click();
|
||||
|
|
@ -127,135 +134,153 @@ export function TrainingSection() {
|
|||
className="min-h-studio-config-column"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Loss chart */}
|
||||
<div className="relative ">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[180px] w-full relative right-8 blur"
|
||||
>
|
||||
<LineChart data={placeholderData} accessibilityLayer={true}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
{/* Loss chart */}
|
||||
<div className="relative ">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[180px] w-full relative right-8 blur"
|
||||
>
|
||||
<LineChart data={placeholderData} accessibilityLayer={true}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize="calc(10px * var(--ui-font-scale, 1))"
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize="calc(10px * var(--ui-font-scale, 1))"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="loss"
|
||||
stroke="var(--color-loss)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1">
|
||||
<HugeiconsIcon
|
||||
icon={ChartAverageIcon}
|
||||
className="size-5 text-muted-foreground/50"
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="loss"
|
||||
stroke="var(--color-loss)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1">
|
||||
<HugeiconsIcon
|
||||
icon={ChartAverageIcon}
|
||||
className="size-5 text-muted-foreground/50"
|
||||
/>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{t("studio.training.chartNoDataTitle")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
{t("studio.training.chartNoDataDescription")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{t("studio.training.chartNoDataTitle")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
{t("studio.training.chartNoDataDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Start/Stop */}
|
||||
<Button
|
||||
data-tour="studio-start"
|
||||
className="w-full cursor-pointer bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
onClick={() => void startTrainingRun()}
|
||||
disabled={isStarting || isIncompatible || store.isCheckingDataset || isLoadingModel || !configValidation.ok}
|
||||
>
|
||||
<HugeiconsIcon icon={Rocket01Icon} className="size-4" />
|
||||
{isStarting
|
||||
? t("studio.training.starting")
|
||||
: isLoadingModel
|
||||
? t("studio.training.loadingModel")
|
||||
: store.isCheckingDataset
|
||||
? t("studio.training.checkingDataset")
|
||||
: t("studio.training.startTraining")}
|
||||
</Button>
|
||||
{startError && (
|
||||
<p className="text-xs text-red-500 leading-relaxed">{startError}</p>
|
||||
)}
|
||||
{isIncompatible && (
|
||||
<p className="text-xs text-red-500 leading-relaxed">
|
||||
{!store.isAudioModel && store.isDatasetAudio === true
|
||||
? t("studio.training.audioIncompatible")
|
||||
: t("studio.training.visionIncompatible")}
|
||||
{/* Start/Stop */}
|
||||
<Button
|
||||
data-tour="studio-start"
|
||||
className="w-full cursor-pointer bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
onClick={() => void startTrainingRun()}
|
||||
disabled={
|
||||
isStarting ||
|
||||
isIncompatible ||
|
||||
store.isCheckingDataset ||
|
||||
isLoadingModel ||
|
||||
!configValidation.ok
|
||||
}
|
||||
>
|
||||
<HugeiconsIcon icon={Rocket01Icon} className="size-4" />
|
||||
{isStarting
|
||||
? t("studio.training.starting")
|
||||
: isLoadingModel
|
||||
? t("studio.training.loadingModel")
|
||||
: store.isCheckingDataset
|
||||
? t("studio.training.checkingDataset")
|
||||
: t("studio.training.startTraining")}
|
||||
</Button>
|
||||
{startError && (
|
||||
<p className="text-xs text-red-500 leading-relaxed">{startError}</p>
|
||||
)}
|
||||
{isIncompatible && (
|
||||
<p className="text-xs text-red-500 leading-relaxed">
|
||||
{!store.isAudioModel && store.isDatasetAudio === true
|
||||
? t("studio.training.audioIncompatible")
|
||||
: t("studio.training.visionIncompatible")}
|
||||
</p>
|
||||
)}
|
||||
{!configValidation.ok &&
|
||||
configValidation.message &&
|
||||
!isIncompatible && (
|
||||
<p className="text-xs text-red-500 leading-relaxed">
|
||||
{configValidation.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Upload / Save / Reset */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("studio.training.configLabel")}
|
||||
</p>
|
||||
)}
|
||||
{!configValidation.ok && configValidation.message && !isIncompatible && (
|
||||
<p className="text-xs text-red-500 leading-relaxed">{configValidation.message}</p>
|
||||
)}
|
||||
|
||||
{/* Upload / Save / Reset */}
|
||||
<p className="text-xs text-muted-foreground">{t("studio.training.configLabel")}</p>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<HugeiconsIcon icon={CloudUploadIcon} className="size-3.5" />
|
||||
{t("studio.training.upload")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("studio.training.uploadConfigTooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
data-tour="studio-save"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={handleSaveConfig}
|
||||
>
|
||||
<HugeiconsIcon icon={Archive04Icon} className="size-3.5" />
|
||||
{t("studio.training.save")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("studio.training.saveConfigTooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={handleResetConfig}
|
||||
disabled={!store.selectedModel}
|
||||
>
|
||||
<HugeiconsIcon icon={CleanIcon} className="size-3.5" />
|
||||
{t("studio.training.reset")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("studio.training.resetConfigTooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".yaml,.yml"
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<HugeiconsIcon icon={CloudUploadIcon} className="size-3.5" />
|
||||
{t("studio.training.upload")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("studio.training.uploadConfigTooltip")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Button
|
||||
data-tour="studio-save"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={handleSaveConfig}
|
||||
>
|
||||
<HugeiconsIcon icon={Archive04Icon} className="size-3.5" />
|
||||
{t("studio.training.save")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("studio.training.saveConfigTooltip")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={handleResetConfig}
|
||||
disabled={!store.selectedModel}
|
||||
>
|
||||
<HugeiconsIcon icon={CleanIcon} className="size-3.5" />
|
||||
{t("studio.training.reset")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("studio.training.resetConfigTooltip")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".yaml,.yml"
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -577,6 +577,43 @@ html[data-code-font-size] :is(pre, code, kbd, samp) {
|
|||
font-size: var(--custom-code-font-size) !important;
|
||||
}
|
||||
|
||||
/* UI font size scales text and icons only. The root rem stays fixed, so
|
||||
--spacing / --radius (every box, gap, height, and corner) are untouched. We
|
||||
retune the Tailwind text tokens that all text-* utilities read, plus the
|
||||
shared icon size, by --ui-font-scale. */
|
||||
html[data-ui-font-size] {
|
||||
--text-xs: calc(0.75rem * var(--ui-font-scale));
|
||||
--text-sm: calc(0.875rem * var(--ui-font-scale));
|
||||
--text-base: calc(1rem * var(--ui-font-scale));
|
||||
--text-lg: calc(1.125rem * var(--ui-font-scale));
|
||||
--text-xl: calc(1.25rem * var(--ui-font-scale));
|
||||
--text-2xl: calc(1.5rem * var(--ui-font-scale));
|
||||
--text-3xl: calc(1.875rem * var(--ui-font-scale));
|
||||
--text-4xl: calc(2.25rem * var(--ui-font-scale));
|
||||
--text-5xl: calc(3rem * var(--ui-font-scale));
|
||||
--text-6xl: calc(3.75rem * var(--ui-font-scale));
|
||||
--text-7xl: calc(4.5rem * var(--ui-font-scale));
|
||||
--text-8xl: calc(6rem * var(--ui-font-scale));
|
||||
--text-9xl: calc(8rem * var(--ui-font-scale));
|
||||
--icon-size: calc(18px * var(--ui-font-scale));
|
||||
}
|
||||
|
||||
/* Composer text uses fixed px, not a token, so scale it explicitly. */
|
||||
html[data-ui-font-size] .unsloth-composer-input {
|
||||
font-size: calc(15px * var(--ui-font-scale));
|
||||
}
|
||||
html[data-ui-font-size] .composer-pill-btn {
|
||||
font-size: calc(14px * var(--ui-font-scale));
|
||||
}
|
||||
|
||||
/* Scale composer glyphs without resizing their button boxes. The "+" is left
|
||||
out since it owns a rotate transform for its open state. */
|
||||
html[data-ui-font-size]
|
||||
:is(.composer-pill-btn, .aui-composer-action-wrapper) svg {
|
||||
transform: scale(var(--ui-font-scale));
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
/* Pointer cursors: hand cursor over interactive elements. */
|
||||
html.pointer-cursors
|
||||
:is(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue