refactor: create utilities for dataset manual mapping and improve UI logic consistency in dataset preview dialog
This commit is contained in:
parent
1420b7c28a
commit
be745d1637
7 changed files with 177 additions and 162 deletions
|
|
@ -1,9 +1,10 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import type { CheckFormatResponse } from "@/features/training/types/datasets";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AlertCircleIcon, CheckmarkCircle02Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { CheckFormatResponse } from "@/features/training/types/datasets";
|
||||
|
||||
export function HeaderPick({
|
||||
label,
|
||||
|
|
@ -40,30 +41,30 @@ export function DatasetMappingCard({
|
|||
input: string | null;
|
||||
output: string | null;
|
||||
}) {
|
||||
const tone = mappingOk ? "ok" : "warn";
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
tone === "ok"
|
||||
? "rounded-xl corner-squircle ring-1 ring-emerald-200/70 bg-emerald-50/70 px-5 py-4 mb-4 text-emerald-950 dark:ring-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-50"
|
||||
: "rounded-xl corner-squircle ring-1 ring-amber-200/70 bg-amber-50/70 px-5 py-4 mb-4 text-amber-950 dark:ring-amber-900/50 dark:bg-amber-950/30 dark:text-amber-50"
|
||||
}
|
||||
className={cn(
|
||||
"rounded-xl corner-squircle ring-1 px-5 py-4 mb-4",
|
||||
mappingOk
|
||||
? "ring-emerald-200/70 bg-emerald-50/70 text-emerald-950 dark:ring-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-50"
|
||||
: "ring-amber-200/70 bg-amber-50/70 text-amber-950 dark:ring-amber-900/50 dark:bg-amber-950/30 dark:text-amber-50",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={
|
||||
tone === "ok"
|
||||
? "rounded-xl corner-squircle bg-emerald-500/15 p-2 shrink-0"
|
||||
: "rounded-xl corner-squircle bg-amber-500/15 p-2 shrink-0"
|
||||
}
|
||||
className={cn(
|
||||
"rounded-xl corner-squircle p-2 shrink-0",
|
||||
mappingOk ? "bg-emerald-500/15" : "bg-amber-500/15",
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={mappingOk ? CheckmarkCircle02Icon : AlertCircleIcon}
|
||||
className={
|
||||
tone === "ok"
|
||||
? "size-4 text-emerald-700 dark:text-emerald-300"
|
||||
: "size-4 text-amber-700 dark:text-amber-300"
|
||||
}
|
||||
className={cn(
|
||||
"size-4",
|
||||
mappingOk
|
||||
? "text-emerald-700 dark:text-emerald-300"
|
||||
: "text-amber-700 dark:text-amber-300",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
|
|
@ -71,11 +72,12 @@ export function DatasetMappingCard({
|
|||
{mappingOk ? "Mapping ready" : "Map dataset columns"}
|
||||
</p>
|
||||
<p
|
||||
className={
|
||||
tone === "ok"
|
||||
? "text-xs text-emerald-800/80 dark:text-emerald-200/80 mt-0.5"
|
||||
: "text-xs text-amber-800/80 dark:text-amber-200/80 mt-0.5"
|
||||
}
|
||||
className={cn(
|
||||
"text-xs mt-0.5",
|
||||
mappingOk
|
||||
? "text-emerald-800/80 dark:text-emerald-200/80"
|
||||
: "text-amber-800/80 dark:text-amber-200/80",
|
||||
)}
|
||||
>
|
||||
{mappingOk
|
||||
? "Looks good. We'll convert this dataset automatically."
|
||||
|
|
@ -163,17 +165,17 @@ export function deriveDefaultMapping(
|
|||
data: CheckFormatResponse,
|
||||
isVlm: boolean,
|
||||
): { input: string | null; output: string | null } {
|
||||
if (isVlm) {
|
||||
const input =
|
||||
data.detected_image_column ?? pickRole(data.suggested_mapping, "image");
|
||||
const output =
|
||||
data.detected_text_column ?? pickRole(data.suggested_mapping, "text");
|
||||
if (input && output && input === output) return { input, output: null };
|
||||
return { input: input ?? null, output: output ?? null };
|
||||
const input = isVlm
|
||||
? data.detected_image_column ?? pickRole(data.suggested_mapping, "image")
|
||||
: pickRole(data.suggested_mapping, "user");
|
||||
const output = isVlm
|
||||
? data.detected_text_column ?? pickRole(data.suggested_mapping, "text")
|
||||
: pickRole(data.suggested_mapping, "assistant");
|
||||
|
||||
if (input && output && input === output) {
|
||||
return { input, output: null };
|
||||
}
|
||||
const input = pickRole(data.suggested_mapping, "user");
|
||||
const output = pickRole(data.suggested_mapping, "assistant");
|
||||
if (input && output && input === output) return { input, output: null };
|
||||
|
||||
return { input: input ?? null, output: output ?? null };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -8,12 +9,12 @@ import {
|
|||
import { DataTable } from "@/components/ui/data-table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Database02Icon, AlertCircleIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { useTrainingActions, useTrainingConfigStore } from "@/features/training";
|
||||
import { checkDatasetFormat } from "@/features/training/api/datasets-api";
|
||||
import type { CheckFormatResponse } from "@/features/training/types/datasets";
|
||||
import { Database02Icon, AlertCircleIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { collectPreviewImages, formatCell } from "./dataset-preview-dialog-utils";
|
||||
import {
|
||||
DatasetMappingCard,
|
||||
|
|
@ -22,10 +23,6 @@ import {
|
|||
deriveDefaultMapping,
|
||||
} from "./dataset-preview-dialog-mapping";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type DatasetPreviewDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
|
|
@ -38,10 +35,6 @@ type DatasetPreviewDialogProps = {
|
|||
isVlm?: boolean;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function DatasetPreviewDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
|
|
@ -57,8 +50,12 @@ export function DatasetPreviewDialog({
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const manualMapping = useTrainingConfigStore((s) => s.datasetManualMapping);
|
||||
const setManualMapping = useTrainingConfigStore((s) => s.setDatasetManualMapping);
|
||||
const { manualMapping, setManualMapping } = useTrainingConfigStore(
|
||||
useShallow((s) => ({
|
||||
manualMapping: s.datasetManualMapping,
|
||||
setManualMapping: s.setDatasetManualMapping,
|
||||
})),
|
||||
);
|
||||
const { isStarting, startError, startTrainingRun } = useTrainingActions();
|
||||
|
||||
const mappingEnabled = !!data?.requires_manual_mapping;
|
||||
|
|
@ -79,12 +76,14 @@ export function DatasetPreviewDialog({
|
|||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (initialData) {
|
||||
setData(initialData);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
|
@ -115,21 +114,13 @@ export function DatasetPreviewDialog({
|
|||
}, [open, datasetName, hfToken, datasetSubset, datasetSplit, isVlm, initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !datasetName || !data?.requires_manual_mapping) return;
|
||||
if (!open || !datasetName) return;
|
||||
if (!data?.requires_manual_mapping) return;
|
||||
if (manualMapping.input || manualMapping.output) return;
|
||||
const derived = deriveDefaultMapping(data, isVlm);
|
||||
if (!derived.input && !derived.output) return;
|
||||
setManualMapping(derived);
|
||||
}, [
|
||||
open,
|
||||
datasetName,
|
||||
data?.requires_manual_mapping,
|
||||
isVlm,
|
||||
manualMapping.input,
|
||||
manualMapping.output,
|
||||
setManualMapping,
|
||||
data,
|
||||
]);
|
||||
}, [open, datasetName, data, isVlm, manualMapping.input, manualMapping.output, setManualMapping]);
|
||||
|
||||
const rows = data?.preview_samples ?? [];
|
||||
const columns = data?.columns ?? [];
|
||||
|
|
@ -159,38 +150,30 @@ export function DatasetPreviewDialog({
|
|||
</span>
|
||||
{mappingEnabled && (
|
||||
<div className="flex items-center gap-3">
|
||||
{(manualMapping.output == null || manualMapping.output !== colName) &&
|
||||
(manualMapping.input == null || manualMapping.input === colName) && (
|
||||
<HeaderPick
|
||||
label={leftLabel}
|
||||
checked={manualMapping.input === colName}
|
||||
onCheckedChange={(checked) => {
|
||||
setManualMapping({
|
||||
input: checked ? colName : null,
|
||||
output:
|
||||
checked && manualMapping.output === colName
|
||||
? null
|
||||
: manualMapping.output,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(manualMapping.input == null || manualMapping.input !== colName) &&
|
||||
(manualMapping.output == null || manualMapping.output === colName) && (
|
||||
<HeaderPick
|
||||
label={rightLabel}
|
||||
checked={manualMapping.output === colName}
|
||||
onCheckedChange={(checked) => {
|
||||
setManualMapping({
|
||||
input:
|
||||
checked && manualMapping.input === colName
|
||||
? null
|
||||
: manualMapping.input,
|
||||
output: checked ? colName : null,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{canShowInputPicker(colName, manualMapping) && (
|
||||
<HeaderPick
|
||||
label={leftLabel}
|
||||
checked={manualMapping.input === colName}
|
||||
onCheckedChange={(checked) => {
|
||||
setManualMapping({
|
||||
input: checked ? colName : null,
|
||||
output: manualMapping.output,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{canShowOutputPicker(colName, manualMapping) && (
|
||||
<HeaderPick
|
||||
label={rightLabel}
|
||||
checked={manualMapping.output === colName}
|
||||
onCheckedChange={(checked) => {
|
||||
setManualMapping({
|
||||
input: manualMapping.input,
|
||||
output: checked ? colName : null,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -235,8 +218,7 @@ export function DatasetPreviewDialog({
|
|||
</span>
|
||||
);
|
||||
}
|
||||
const full =
|
||||
typeof value === "string" ? value : JSON.stringify(value);
|
||||
const full = typeof value === "string" ? value : JSON.stringify(value);
|
||||
return (
|
||||
<p
|
||||
className="text-[13px] leading-relaxed line-clamp-6"
|
||||
|
|
@ -325,10 +307,10 @@ export function DatasetPreviewDialog({
|
|||
: "--"
|
||||
}
|
||||
/>
|
||||
<MetaRow
|
||||
label="Columns"
|
||||
value={
|
||||
<span className="flex items-center gap-1.5 flex-wrap">
|
||||
<MetaRow
|
||||
label="Columns"
|
||||
value={
|
||||
<span className="flex items-center gap-1.5 flex-wrap">
|
||||
{columns.map((col) => (
|
||||
<Badge
|
||||
key={col}
|
||||
|
|
@ -417,4 +399,18 @@ function MetaRow({
|
|||
);
|
||||
}
|
||||
|
||||
// mapping UI extracted to ./dataset-preview-dialog-mapping.tsx
|
||||
function canShowInputPicker(
|
||||
colName: string,
|
||||
mapping: { input: string | null; output: string | null },
|
||||
): boolean {
|
||||
if (mapping.output === colName) return false;
|
||||
return mapping.input == null || mapping.input === colName;
|
||||
}
|
||||
|
||||
function canShowOutputPicker(
|
||||
colName: string,
|
||||
mapping: { input: string | null; output: string | null },
|
||||
): boolean {
|
||||
if (mapping.input === colName) return false;
|
||||
return mapping.output == null || mapping.output === colName;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,20 +190,21 @@ export function DatasetSection() {
|
|||
ref={scrollRef}
|
||||
className="max-h-64 overflow-y-auto overscroll-contain [scrollbar-width:thin]"
|
||||
>
|
||||
<ComboboxList className="p-1 !max-h-none !overflow-visible">
|
||||
{(id: string) => {
|
||||
const r = hfResults.find((ds) => ds.id === id);
|
||||
const detail = r?.totalExamples
|
||||
? `${formatCompact(r.totalExamples)} rows`
|
||||
: r?.sizeCategory
|
||||
? r.sizeCategory
|
||||
: r?.downloads != null
|
||||
? `↓${formatCompact(r.downloads)}`
|
||||
: null;
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={id}
|
||||
value={id}
|
||||
<ComboboxList className="p-1 !max-h-none !overflow-visible">
|
||||
{(id: string) => {
|
||||
const r = hfResults.find((ds) => ds.id === id);
|
||||
let detail: string | null = null;
|
||||
if (r?.totalExamples) {
|
||||
detail = `${formatCompact(r.totalExamples)} rows`;
|
||||
} else if (r?.sizeCategory) {
|
||||
detail = r.sizeCategory;
|
||||
} else if (r?.downloads != null) {
|
||||
detail = `↓${formatCompact(r.downloads)}`;
|
||||
}
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={id}
|
||||
value={id}
|
||||
className="justify-between"
|
||||
>
|
||||
<Tooltip>
|
||||
|
|
|
|||
|
|
@ -14,15 +14,7 @@ export function buildTrainingStartPayload(
|
|||
const adapterMethod = config.trainingMethod !== "full";
|
||||
const isQlorMethod = config.trainingMethod === "qlora";
|
||||
const hfDataset = config.datasetSource === "huggingface" ? config.dataset : null;
|
||||
const manual = config.datasetManualMapping;
|
||||
const isVlm = config.modelType === "vision";
|
||||
const customFormatMapping =
|
||||
manual.input && manual.output
|
||||
? {
|
||||
[manual.input]: isVlm ? "image" : "user",
|
||||
[manual.output]: isVlm ? "text" : "assistant",
|
||||
}
|
||||
: undefined;
|
||||
const customFormatMapping = buildCustomFormatMapping(config);
|
||||
|
||||
return {
|
||||
model_name: config.selectedModel ?? "",
|
||||
|
|
@ -73,3 +65,16 @@ export function buildTrainingStartPayload(
|
|||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCustomFormatMapping(
|
||||
config: TrainingConfigState,
|
||||
): Record<string, string> | undefined {
|
||||
const { input, output } = config.datasetManualMapping;
|
||||
if (!input || !output) return undefined;
|
||||
|
||||
if (config.modelType === "vision") {
|
||||
return { [input]: "image", [output]: "text" };
|
||||
}
|
||||
|
||||
return { [input]: "user", [output]: "assistant" };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useCallback } from "react";
|
||||
import { useTrainingConfigStore } from "../stores/training-config-store";
|
||||
import { useTrainingRuntimeStore } from "../stores/training-runtime-store";
|
||||
import { useDatasetPreviewDialogStore } from "../stores/dataset-preview-dialog-store";
|
||||
import { startTraining, stopTraining, resetTraining } from "../api/train-api";
|
||||
import { buildTrainingStartPayload } from "../api/mappers";
|
||||
import { checkDatasetFormat } from "../api/datasets-api";
|
||||
import { buildTrainingStartPayload } from "../api/mappers";
|
||||
import { startTraining, stopTraining, resetTraining } from "../api/train-api";
|
||||
import { syncTrainingRuntimeFromBackend } from "../lib/sync-runtime";
|
||||
import { validateTrainingConfig } from "../lib/validation";
|
||||
import { useDatasetPreviewDialogStore } from "../stores/dataset-preview-dialog-store";
|
||||
import { useTrainingConfigStore } from "../stores/training-config-store";
|
||||
import { useTrainingRuntimeStore } from "../stores/training-runtime-store";
|
||||
import type { TrainingConfigState } from "../types/config";
|
||||
|
||||
export function useTrainingActions() {
|
||||
const isStarting = useTrainingRuntimeStore((state) => state.isStarting);
|
||||
|
|
@ -27,8 +28,7 @@ export function useTrainingActions() {
|
|||
runtimeStore.setStarting(true);
|
||||
|
||||
try {
|
||||
const datasetName =
|
||||
config.datasetSource === "huggingface" ? config.dataset : config.uploadedFile;
|
||||
const datasetName = getDatasetName(config);
|
||||
const isVlm = config.modelType === "vision";
|
||||
|
||||
if (datasetName) {
|
||||
|
|
@ -40,29 +40,24 @@ export function useTrainingActions() {
|
|||
isVlm,
|
||||
});
|
||||
|
||||
if (check.requires_manual_mapping) {
|
||||
const existing = useTrainingConfigStore.getState().datasetManualMapping;
|
||||
const hasMapping = !!existing.input && !!existing.output;
|
||||
if (check.requires_manual_mapping && !hasManualMapping(config)) {
|
||||
const hintInput = isVlm
|
||||
? check.detected_image_column
|
||||
: pickRoleColumn(check.suggested_mapping, "user");
|
||||
const hintOutput = isVlm
|
||||
? check.detected_text_column
|
||||
: pickRoleColumn(check.suggested_mapping, "assistant");
|
||||
|
||||
if (!hasMapping) {
|
||||
const hintInput = isVlm
|
||||
? check.detected_image_column
|
||||
: pickRoleColumn(check.suggested_mapping, "user");
|
||||
const hintOutput = isVlm
|
||||
? check.detected_text_column
|
||||
: pickRoleColumn(check.suggested_mapping, "assistant");
|
||||
|
||||
if (hintInput || hintOutput) {
|
||||
useTrainingConfigStore.getState().setDatasetManualMapping({
|
||||
input: hintInput ?? null,
|
||||
output: hintOutput ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
runtimeStore.setStarting(false);
|
||||
dialogStore.openMapping(check);
|
||||
return false;
|
||||
if (hintInput || hintOutput) {
|
||||
useTrainingConfigStore.getState().setDatasetManualMapping({
|
||||
input: hintInput ?? null,
|
||||
output: hintOutput ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
runtimeStore.setStarting(false);
|
||||
dialogStore.openMapping(check);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +116,18 @@ export function useTrainingActions() {
|
|||
};
|
||||
}
|
||||
|
||||
function getDatasetName(config: TrainingConfigState): string | null {
|
||||
return config.datasetSource === "huggingface"
|
||||
? config.dataset
|
||||
: config.uploadedFile;
|
||||
}
|
||||
|
||||
function hasManualMapping(config: TrainingConfigState): boolean {
|
||||
return (
|
||||
!!config.datasetManualMapping.input && !!config.datasetManualMapping.output
|
||||
);
|
||||
}
|
||||
|
||||
function pickRoleColumn(
|
||||
mapping: Record<string, string> | null | undefined,
|
||||
role: string,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import type { TrainingConfigState, TrainingConfigStore } from "../types/config";
|
|||
const MIN_STEP: StepNumber = 1;
|
||||
const MAX_STEP: StepNumber = STEPS.length as StepNumber;
|
||||
|
||||
function emptyManualMapping(): TrainingConfigState["datasetManualMapping"] {
|
||||
return { input: null, output: null };
|
||||
}
|
||||
|
||||
const initialState: TrainingConfigState = {
|
||||
currentStep: MIN_STEP,
|
||||
modelType: null,
|
||||
|
|
@ -18,7 +22,7 @@ const initialState: TrainingConfigState = {
|
|||
dataset: null,
|
||||
datasetSubset: null,
|
||||
datasetSplit: null,
|
||||
datasetManualMapping: { input: null, output: null },
|
||||
datasetManualMapping: emptyManualMapping(),
|
||||
uploadedFile: null,
|
||||
...DEFAULT_HYPERPARAMS,
|
||||
};
|
||||
|
|
@ -63,16 +67,16 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
dataset,
|
||||
datasetSubset: null,
|
||||
datasetSplit: null,
|
||||
datasetManualMapping: { input: null, output: null },
|
||||
datasetManualMapping: emptyManualMapping(),
|
||||
}),
|
||||
setDatasetSubset: (datasetSubset) =>
|
||||
set({
|
||||
datasetSubset,
|
||||
datasetSplit: null,
|
||||
datasetManualMapping: { input: null, output: null },
|
||||
datasetManualMapping: emptyManualMapping(),
|
||||
}),
|
||||
setDatasetSplit: (datasetSplit) =>
|
||||
set({ datasetSplit, datasetManualMapping: { input: null, output: null } }),
|
||||
set({ datasetSplit, datasetManualMapping: emptyManualMapping() }),
|
||||
setDatasetManualMapping: (datasetManualMapping) =>
|
||||
set({ datasetManualMapping }),
|
||||
setUploadedFile: (uploadedFile) => set({ uploadedFile }),
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const globalCrypto = globalThis.crypto as Crypto | undefined;
|
||||
const hasUuid =
|
||||
globalCrypto && typeof (globalCrypto as Crypto).randomUUID === "function";
|
||||
import "./index.css";
|
||||
import { App } from "./app/app";
|
||||
|
||||
if (globalCrypto && !hasUuid) {
|
||||
const globalCrypto = globalThis.crypto as Crypto | undefined;
|
||||
|
||||
if (globalCrypto && typeof globalCrypto.randomUUID !== "function") {
|
||||
// Some envs ship `crypto` but no `randomUUID()` (or a non-function stub).
|
||||
// Provide a best-effort v4 UUID using `getRandomValues` when available.
|
||||
const getRandomByte = () => {
|
||||
if (typeof globalCrypto.getRandomValues === "function") {
|
||||
return globalCrypto.getRandomValues(new Uint8Array(1))[0];
|
||||
const cryptoRef = globalCrypto;
|
||||
|
||||
function getRandomByte(): number {
|
||||
if (typeof cryptoRef.getRandomValues === "function") {
|
||||
return cryptoRef.getRandomValues(new Uint8Array(1))[0];
|
||||
}
|
||||
return Math.floor(Math.random() * 256);
|
||||
};
|
||||
}
|
||||
|
||||
(globalCrypto as Crypto).randomUUID = (() =>
|
||||
cryptoRef.randomUUID = (() =>
|
||||
"10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
|
||||
(+c ^ (getRandomByte() & (15 >> (+c / 4)))).toString(16),
|
||||
)) as Crypto["randomUUID"];
|
||||
}
|
||||
|
||||
import "./index.css";
|
||||
import { App } from "./app/app";
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) {
|
||||
throw new Error("Root element not found");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue