diff --git a/.gitignore b/.gitignore index 1453f625c2..3bb5a62a82 100755 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ models/ # IDE / Editors .vscode/ .idea/ +.claude/ *.swp *.swo diff --git a/studio/frontend/src/config/training.ts b/studio/frontend/src/config/training.ts index fd2b13e87c..362766e04b 100644 --- a/studio/frontend/src/config/training.ts +++ b/studio/frontend/src/config/training.ts @@ -98,8 +98,21 @@ export const MODELS: ModelOption[] = [ hfRepo: "Qwen/Qwen-VL-Chat", }, // TTS models - { id: "bark", name: "Bark", type: "tts", params: "1B", hfRepo: "suno/bark", recommended: true }, - { id: "xtts-v2", name: "XTTS v2", type: "tts", params: "500M", hfRepo: "coqui/XTTS-v2" }, + { + id: "bark", + name: "Bark", + type: "tts", + params: "1B", + hfRepo: "suno/bark", + recommended: true, + }, + { + id: "xtts-v2", + name: "XTTS v2", + type: "tts", + params: "500M", + hfRepo: "coqui/XTTS-v2", + }, // Embedding models { id: "bge-large", @@ -111,24 +124,6 @@ export const MODELS: ModelOption[] = [ { id: "e5-large", name: "E5 Large", type: "embeddings", params: "335M" }, { id: "gte-large", name: "GTE Large", type: "embeddings", params: "335M" }, // Text models - { - id: "llama-3.1-8b", - name: "Llama 3.1 8B", - type: "text", - params: "8B", - vram: "~6GB", - context: "128K", - hfRepo: "unsloth/Llama-3.1-8B", - recommended: true, - }, - { - id: "llama-3.1-70b", - name: "Llama 3.1 70B", - type: "text", - params: "70B", - vram: "~40GB", - context: "128K", - }, { id: "mistral-7b", name: "Mistral 7B", @@ -154,24 +149,6 @@ export const MODELS: ModelOption[] = [ vram: "~3GB", context: "128K", }, - { - id: "gemma-2-9b", - name: "Gemma 2 9B", - type: "text", - params: "9B", - vram: "~7GB", - context: "8K", - }, - { - id: "gemma-3-27b", - name: "Gemma 3 27B", - type: "text", - params: "27B", - vram: "~18GB", - context: "128K", - hfRepo: "unsloth/gemma-3-27b", - recommended: true, - }, ]; export const DATASETS: DatasetOption[] = [ @@ -259,7 +236,9 @@ export const DEFAULT_HYPERPARAMS = { }; export function findModelById(id: string | null): ModelOption | undefined { - if (!id) return undefined; + if (!id) { + return undefined; + } return MODELS.find((m) => m.id === id || m.hfRepo === id); } diff --git a/studio/frontend/src/features/export/components/export-dialog.tsx b/studio/frontend/src/features/export/components/export-dialog.tsx index dcbd785fd0..4f66048270 100644 --- a/studio/frontend/src/features/export/components/export-dialog.tsx +++ b/studio/frontend/src/features/export/components/export-dialog.tsx @@ -68,7 +68,9 @@ export function ExportDialog({ Export Model - Choose where to save your exported model. + + Choose where to save your exported model. +
@@ -94,18 +96,32 @@ export function ExportDialog({
- - onHfUsernameChange(e.target.value)} /> + + onHfUsernameChange(e.target.value)} + />
- - onModelNameChange(e.target.value)} /> + + onModelNameChange(e.target.value)} + />
- + Get token - +
- onHfTokenChange(e.target.value)} /> + onHfTokenChange(e.target.value)} + /> -

Leave empty if already logged in via CLI.

+

+ Leave empty if already logged in via CLI. +

- - + +
@@ -153,7 +189,9 @@ export function ExportDialog({ {exportMethod === "gguf" && quantLevels.length > 0 && (
Quantizations - {quantLevels.join(", ")} + + {quantLevels.join(", ")} +
)}
@@ -163,7 +201,9 @@ export function ExportDialog({
- + diff --git a/studio/frontend/src/features/export/components/method-picker.tsx b/studio/frontend/src/features/export/components/method-picker.tsx index b0052f3069..ba0ca50543 100644 --- a/studio/frontend/src/features/export/components/method-picker.tsx +++ b/studio/frontend/src/features/export/components/method-picker.tsx @@ -5,7 +5,10 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import { CheckmarkCircle01Icon, InformationCircleIcon } from "@hugeicons/core-free-icons"; +import { + CheckmarkCircle01Icon, + InformationCircleIcon, +} from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { EXPORT_METHODS, type ExportMethod } from "../constants"; @@ -20,14 +23,24 @@ export function MethodPicker({ value, onChange }: MethodPickerProps) { Export Method - - How your model is packaged for deployment.{" "} - Read more + + Read more + @@ -49,34 +62,61 @@ export function MethodPicker({ value, onChange }: MethodPickerProps) {
{selected && ( - + )}
{m.title} - - e.stopPropagation()}> - + + e.stopPropagation()} + > + {m.tooltip}{" "} - Read more + + Read more + {m.badge && ( - + {m.badge} )}
- {m.description} + + {m.description} +
); diff --git a/studio/frontend/src/features/export/components/quant-picker.tsx b/studio/frontend/src/features/export/components/quant-picker.tsx index e16dbd5494..9b6c070453 100644 --- a/studio/frontend/src/features/export/components/quant-picker.tsx +++ b/studio/frontend/src/features/export/components/quant-picker.tsx @@ -4,7 +4,11 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import { CheckmarkCircle01Icon, InformationCircleIcon, LayersIcon } from "@hugeicons/core-free-icons"; +import { + CheckmarkCircle01Icon, + InformationCircleIcon, + LayersIcon, +} from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { QUANT_OPTIONS } from "../constants"; @@ -23,20 +27,38 @@ export function QuantPicker({ value, onChange }: QuantPickerProps) { return (
- - Quantization Levels + + + Quantization Levels + - - - Lower quantization (Q2, Q3) = smaller files but reduced quality. Q4–Q5 is a good balance.{" "} - Read more + Lower quantization (Q2, Q3) = smaller files but reduced quality. + Q4–Q5 is a good balance.{" "} + + Read more + - — select one or more + + — select one or more +
{QUANT_OPTIONS.map((q) => { @@ -53,7 +75,12 @@ export function QuantPicker({ value, onChange }: QuantPickerProps) { : "ring-border text-muted-foreground hover:text-foreground hover:ring-foreground/20", )} > - {active && } + {active && ( + + )} {q.label} {q.size} {q.recommended && !active && ( diff --git a/studio/frontend/src/features/export/constants.ts b/studio/frontend/src/features/export/constants.ts index b289372f05..154058982d 100644 --- a/studio/frontend/src/features/export/constants.ts +++ b/studio/frontend/src/features/export/constants.ts @@ -9,9 +9,27 @@ export const EXPORT_METHODS: { tooltip: string; badge?: string; }[] = [ - { value: "merged", title: "Merged Model", description: "Full 16-bit model ready for inference.", tooltip: "Merges adapter weights into the base model. Best for direct deployment with vLLM or TGI." }, - { value: "lora", title: "LoRA Only", description: "Lightweight adapter files (~100 MB). Needs base model.", tooltip: "Exports only the trained adapter. Pair with the base model at inference time to save storage." }, - { value: "gguf", title: "GGUF / Llama.cpp", description: "Quantized formats for local AI runners.", tooltip: "Converts to GGUF for llama.cpp, Ollama, and other local runners. Pick a quantization level below." }, + { + value: "merged", + title: "Merged Model", + description: "Full 16-bit model ready for inference.", + tooltip: + "Merges adapter weights into the base model. Best for direct deployment with vLLM or TGI.", + }, + { + value: "lora", + title: "LoRA Only", + description: "Lightweight adapter files (~100 MB). Needs base model.", + tooltip: + "Exports only the trained adapter. Pair with the base model at inference time to save storage.", + }, + { + value: "gguf", + title: "GGUF / Llama.cpp", + description: "Quantized formats for local AI runners.", + tooltip: + "Converts to GGUF for llama.cpp, Ollama, and other local runners. Pick a quantization level below.", + }, ]; export const QUANT_OPTIONS = [ @@ -27,17 +45,27 @@ export const QUANT_OPTIONS = [ { value: "f16", label: "F16", size: "~14.2 GB" }, ]; -export function getEstimatedSize(method: ExportMethod | null, quantLevels: string[]) { - const sizeOf = (v: string) => QUANT_OPTIONS.find((q) => q.value === v)?.size ?? "—"; +export function getEstimatedSize( + method: ExportMethod | null, + quantLevels: string[], +) { + const sizeOf = (v: string) => + QUANT_OPTIONS.find((q) => q.value === v)?.size ?? "—"; if (method === "gguf" && quantLevels.length > 0) { - if (quantLevels.length === 1) return sizeOf(quantLevels[0]); + if (quantLevels.length === 1) { + return sizeOf(quantLevels[0]); + } const total = quantLevels .map((q) => Number.parseFloat(sizeOf(q).replace(/[^0-9.]/g, ""))) .reduce((a, b) => a + b, 0); return `~${total.toFixed(1)} GB (${quantLevels.length} files)`; } - if (method === "merged") return "~14.2 GB"; - if (method === "lora") return "~100 MB"; + if (method === "merged") { + return "~14.2 GB"; + } + if (method === "lora") { + return "~100 MB"; + } return "—"; } diff --git a/studio/frontend/src/features/export/export-page.tsx b/studio/frontend/src/features/export/export-page.tsx index 09c6514b65..8c242941cb 100644 --- a/studio/frontend/src/features/export/export-page.tsx +++ b/studio/frontend/src/features/export/export-page.tsx @@ -1,3 +1,4 @@ +import { SectionCard } from "@/components/section-card"; import { Button } from "@/components/ui/button"; import { Select, @@ -7,15 +8,14 @@ import { SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; -import { SectionCard } from "@/components/section-card"; -import { findModelById } from "@/config/training"; -import { useWizardStore } from "@/stores/training"; -import { isAdapterMethod } from "@/types/training"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { findModelById } from "@/config/training"; +import { useWizardStore } from "@/stores/training"; +import { isAdapterMethod } from "@/types/training"; import { InformationCircleIcon, PackageIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { AnimatePresence, motion } from "motion/react"; @@ -33,21 +33,32 @@ import { } from "./constants"; export function ExportPage() { - const { trainingMethod, selectedModel, saveSteps, trainingMetrics, epochs, loraRank, hfToken, setHfToken } = - useWizardStore( - useShallow((s) => ({ - trainingMethod: s.trainingMethod, - selectedModel: s.selectedModel, - saveSteps: s.saveSteps, - trainingMetrics: s.trainingMetrics, - epochs: s.epochs, - loraRank: s.loraRank, - hfToken: s.hfToken, - setHfToken: s.setHfToken, - })), - ); + const { + trainingMethod, + selectedModel, + saveSteps, + trainingMetrics, + epochs, + loraRank, + hfToken, + setHfToken, + } = useWizardStore( + useShallow((s) => ({ + trainingMethod: s.trainingMethod, + selectedModel: s.selectedModel, + saveSteps: s.saveSteps, + trainingMetrics: s.trainingMetrics, + epochs: s.epochs, + loraRank: s.loraRank, + hfToken: s.hfToken, + setHfToken: s.setHfToken, + })), + ); const isAdapter = isAdapterMethod(trainingMethod); - const modelInfo = useMemo(() => findModelById(selectedModel), [selectedModel]); + const modelInfo = useMemo( + () => findModelById(selectedModel), + [selectedModel], + ); const checkpoints = useMemo(() => { if (isAdapter) { @@ -55,7 +66,11 @@ export function ExportPage() { const total = trainingMetrics?.totalSteps ?? 500; const entries: { value: string; label: string; detail: string }[] = []; for (let step = interval; step <= total; step += interval) { - const loss = (1.5 - (step / total) * 0.7 + Math.random() * 0.05).toFixed(2); + const loss = ( + 1.5 - + (step / total) * 0.7 + + Math.random() * 0.05 + ).toFixed(2); entries.push({ value: `checkpoint-${step}`, label: `checkpoint-${step}`, @@ -64,7 +79,13 @@ export function ExportPage() { } return entries.reverse(); } - return [{ value: "final-model", label: "Final Model", detail: "Full fine-tuned weights" }]; + return [ + { + value: "final-model", + label: "Final Model", + detail: "Full fine-tuned weights", + }, + ]; }, [isAdapter, saveSteps, trainingMetrics?.totalSteps]); const [checkpoint, setCheckpoint] = useState(null); @@ -79,19 +100,28 @@ export function ExportPage() { const handleMethodChange = (method: ExportMethod) => { setExportMethod(method); - if (method !== "gguf") setQuantLevels([]); + if (method !== "gguf") { + setQuantLevels([]); + } }; const estimatedSize = getEstimatedSize(exportMethod, quantLevels); - const canExport = checkpoint && exportMethod && (exportMethod !== "gguf" || quantLevels.length > 0); + const canExport = + checkpoint && + exportMethod && + (exportMethod !== "gguf" || quantLevels.length > 0); const baseModelName = modelInfo?.name ?? selectedModel ?? "—"; return (
-

Export Model

-

Export your fine-tuned model for deployment

+

+ Export Model +

+

+ Export your fine-tuned model for deployment +

{/* Top row: Checkpoint + metadata | Guide */} @@ -109,27 +139,47 @@ export function ExportPage() { [...DATASETS].sort((a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0)), + () => + [...DATASETS].sort( + (a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0), + ), [], ); const datasetMap = useMemo(() => { - const map = new Map(); + const map = new Map< + string, + { + label: string; + description?: string; + size?: string; + totalExamples?: number; + sizeCategory?: string; + downloads?: number; + } + >(); for (const d of curatedDatasets) { - map.set(d.id, { label: d.name, description: d.description, size: d.size }); + map.set(d.id, { + label: d.name, + description: d.description, + size: d.size, + }); } for (const r of hfResults) { if (!map.has(r.id)) { - map.set(r.id, { label: r.id, downloads: r.downloads, totalExamples: r.totalExamples, sizeCategory: r.sizeCategory }); + map.set(r.id, { + label: r.id, + downloads: r.downloads, + totalExamples: r.totalExamples, + sizeCategory: r.sizeCategory, + }); } } return map; @@ -89,14 +119,24 @@ export function DatasetSection() { } const q = debouncedQuery.toLowerCase(); const curatedIds = curatedDatasets - .filter((d) => d.name.toLowerCase().includes(q) || d.id.toLowerCase().includes(q)) + .filter( + (d) => + d.name.toLowerCase().includes(q) || d.id.toLowerCase().includes(q), + ) .map((d) => d.id); - const liveIds = hfResults.map((r) => r.id).filter((id) => !curatedIds.includes(id)); + const liveIds = hfResults + .map((r) => r.id) + .filter((id) => !curatedIds.includes(id)); return [...curatedIds, ...liveIds]; }, [debouncedQuery, curatedDatasets, hfResults]); const allIds = useMemo( - () => [...new Set([...curatedDatasets.map((d) => d.id), ...hfResults.map((r) => r.id)])], + () => [ + ...new Set([ + ...curatedDatasets.map((d) => d.id), + ...hfResults.map((r) => r.id), + ]), + ], [curatedDatasets, hfResults], ); @@ -129,7 +169,8 @@ export function DatasetSection() { - Search Hugging Face datasets or enter a path like 'username/dataset-name'.{" "} + Search Hugging Face datasets or enter a path like + 'username/dataset-name'.{" "} datasetMap.get(id)?.label ?? id} autoHighlight={true} > - + {isLoading ? ( -
Searching…
+
+ Searching… +
) : ( No datasets found )} -
+
{(id: string) => { const meta = datasetMap.get(id); const label = meta?.label ?? id; - const rowLabel = meta?.size ?? (meta?.totalExamples ? `${formatCompact(meta.totalExamples)} rows` : null); + const rowLabel = + meta?.size ?? + (meta?.totalExamples + ? `${formatCompact(meta.totalExamples)} rows` + : null); return ( - + - - {label} + + + {label} + - + {label} diff --git a/studio/frontend/src/features/studio/sections/model-section.tsx b/studio/frontend/src/features/studio/sections/model-section.tsx index f8edb3e148..e6f8849ff0 100644 --- a/studio/frontend/src/features/studio/sections/model-section.tsx +++ b/studio/frontend/src/features/studio/sections/model-section.tsx @@ -25,8 +25,12 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { MODEL_TYPE_TO_HF_TASK, MODELS } from "@/config/training"; -import { useDebouncedValue, useHfModelSearch, useInfiniteScroll } from "@/hooks"; +import { MODELS, MODEL_TYPE_TO_HF_TASK } from "@/config/training"; +import { + useDebouncedValue, + useHfModelSearch, + useInfiniteScroll, +} from "@/hooks"; import { formatCompact } from "@/lib/utils"; import { useWizardStore } from "@/stores/training"; import type { TrainingMethod } from "@/types/training"; @@ -76,26 +80,51 @@ export function ModelSection() { const [inputValue, setInputValue] = useState(""); const debouncedQuery = useDebouncedValue(inputValue); const task = modelType ? MODEL_TYPE_TO_HF_TASK[modelType] : undefined; - const { results: hfResults, isLoading, isLoadingMore, hasMore, fetchMore } = useHfModelSearch(debouncedQuery, { + const { + results: hfResults, + isLoading, + isLoadingMore, + hasMore, + fetchMore, + } = useHfModelSearch(debouncedQuery, { task, accessToken: hfToken || undefined, }); const curatedModels = useMemo(() => { - if (!modelType) return MODELS; + if (!modelType) { + return MODELS; + } return MODELS.filter((m) => m.type === modelType).sort( (a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0), ); }, [modelType]); const modelMap = useMemo(() => { - const map = new Map(); + const map = new Map< + string, + { + label: string; + params?: string; + totalParams?: number; + downloads?: number; + recommended?: boolean; + } + >(); for (const m of curatedModels) { - map.set(m.hfRepo ?? m.id, { label: m.name, params: m.params, recommended: m.recommended }); + map.set(m.hfRepo ?? m.id, { + label: m.name, + params: m.params, + recommended: m.recommended, + }); } for (const r of hfResults) { if (!map.has(r.id)) { - map.set(r.id, { label: r.id, downloads: r.downloads, totalParams: r.totalParams }); + map.set(r.id, { + label: r.id, + downloads: r.downloads, + totalParams: r.totalParams, + }); } } return map; @@ -107,14 +136,26 @@ export function ModelSection() { } const q = debouncedQuery.toLowerCase(); const curatedIds = curatedModels - .filter((m) => m.name.toLowerCase().includes(q) || m.id.toLowerCase().includes(q) || m.hfRepo?.toLowerCase().includes(q)) + .filter( + (m) => + m.name.toLowerCase().includes(q) || + m.id.toLowerCase().includes(q) || + m.hfRepo?.toLowerCase().includes(q), + ) .map((m) => m.hfRepo ?? m.id); - const liveIds = hfResults.map((r) => r.id).filter((id) => !curatedIds.includes(id)); + const liveIds = hfResults + .map((r) => r.id) + .filter((id) => !curatedIds.includes(id)); return [...curatedIds, ...liveIds]; }, [debouncedQuery, curatedModels, hfResults]); const allIds = useMemo( - () => [...new Set([...curatedModels.map((m) => m.hfRepo ?? m.id), ...hfResults.map((r) => r.id)])], + () => [ + ...new Set([ + ...curatedModels.map((m) => m.hfRepo ?? m.id), + ...hfResults.map((r) => r.id), + ]), + ], [curatedModels, hfResults], ); @@ -161,7 +202,10 @@ export function ModelSection() { placeholder="./models/my-model" value={ selectedModel - ? (MODELS.find((m) => m.id === selectedModel || m.hfRepo === selectedModel)?.hfRepo ?? selectedModel) + ? (MODELS.find( + (m) => + m.id === selectedModel || m.hfRepo === selectedModel, + )?.hfRepo ?? selectedModel) : "" } onChange={(e) => setSelectedModel(e.target.value || null)} @@ -222,19 +266,35 @@ export function ModelSection() { ) : ( No models found )} -
+
{(id: string) => { const meta = modelMap.get(id); const label = meta?.label ?? id; - const sizeLabel = meta?.params ?? (meta?.totalParams ? formatCompact(meta.totalParams) : null); + const sizeLabel = + meta?.params ?? + (meta?.totalParams + ? formatCompact(meta.totalParams) + : null); return ( - + - - {label} + + + {label} + - + {label} diff --git a/studio/frontend/src/hooks/use-hf-dataset-search.ts b/studio/frontend/src/hooks/use-hf-dataset-search.ts index 748dff6ba2..d10b7d11e9 100644 --- a/studio/frontend/src/hooks/use-hf-dataset-search.ts +++ b/studio/frontend/src/hooks/use-hf-dataset-search.ts @@ -23,7 +23,9 @@ interface CardDataWithInfo { function extractTotalExamples( cardData: CardDataWithInfo | undefined, ): number | undefined { - if (!cardData?.dataset_info) return undefined; + if (!cardData?.dataset_info) { + return undefined; + } const infos = Array.isArray(cardData.dataset_info) ? cardData.dataset_info : [cardData.dataset_info]; diff --git a/studio/frontend/src/hooks/use-hf-paginated-search.ts b/studio/frontend/src/hooks/use-hf-paginated-search.ts index 7beeb1d1b5..bc76b96613 100644 --- a/studio/frontend/src/hooks/use-hf-paginated-search.ts +++ b/studio/frontend/src/hooks/use-hf-paginated-search.ts @@ -25,7 +25,9 @@ async function pullBatch( const items: T[] = []; for (let i = 0; i < size; i++) { const result = await iter.next(); - if (result.done) return { items, done: true }; + if (result.done) { + return { items, done: true }; + } items.push(mapItem(result.value)); } return { items, done: false }; @@ -67,7 +69,9 @@ export function useHfPaginatedSearch( pullBatch(iter, mapItem, BATCH) .then(({ items, done }) => { - if (versionRef.current !== v) return; + if (versionRef.current !== v) { + return; + } setState({ results: items, isLoading: false, @@ -77,7 +81,9 @@ export function useHfPaginatedSearch( }); }) .catch((err) => { - if (versionRef.current !== v) return; + if (versionRef.current !== v) { + return; + } setState({ results: [], isLoading: false, @@ -91,14 +97,18 @@ export function useHfPaginatedSearch( const fetchMore = useCallback(() => { const iter = iterRef.current; const { isLoading, isLoadingMore, hasMore } = stateRef.current; - if (!iter || isLoading || isLoadingMore || !hasMore) return; + if (!iter || isLoading || isLoadingMore || !hasMore) { + return; + } const v = versionRef.current; setState((prev) => ({ ...prev, isLoadingMore: true })); pullBatch(iter, mapItem, BATCH) .then(({ items, done }) => { - if (versionRef.current !== v) return; + if (versionRef.current !== v) { + return; + } setState((prev) => ({ ...prev, results: [...prev.results, ...items], @@ -107,7 +117,9 @@ export function useHfPaginatedSearch( })); }) .catch(() => { - if (versionRef.current !== v) return; + if (versionRef.current !== v) { + return; + } setState((prev) => ({ ...prev, isLoadingMore: false, hasMore: false })); }); }, [mapItem]); diff --git a/studio/frontend/src/hooks/use-infinite-scroll.ts b/studio/frontend/src/hooks/use-infinite-scroll.ts index 488739604f..14c80f4810 100644 --- a/studio/frontend/src/hooks/use-infinite-scroll.ts +++ b/studio/frontend/src/hooks/use-infinite-scroll.ts @@ -6,10 +6,14 @@ export function useInfiniteScroll(fetchMore: () => void) { useEffect(() => { const el = sentinelRef.current; - if (!el) return; + if (!el) { + return; + } const obs = new IntersectionObserver( ([e]) => { - if (e.isIntersecting) fetchMore(); + if (e.isIntersecting) { + fetchMore(); + } }, { threshold: 0, root: scrollRef.current }, ); diff --git a/studio/frontend/src/hooks/use-mobile.ts b/studio/frontend/src/hooks/use-mobile.ts index 2b0fe1dfef..a93d583938 100644 --- a/studio/frontend/src/hooks/use-mobile.ts +++ b/studio/frontend/src/hooks/use-mobile.ts @@ -1,19 +1,21 @@ -import * as React from "react" +import * as React from "react"; -const MOBILE_BREAKPOINT = 768 +const MOBILE_BREAKPOINT = 768; export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) + const [isMobile, setIsMobile] = React.useState( + undefined, + ); React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); - return !!isMobile + return !!isMobile; }