diff --git a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx index 3f0e4f3b7a..811ed96034 100644 --- a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx +++ b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx @@ -69,10 +69,7 @@ const formatTime = (timestamp: number) => { return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }; -const generateId = () => - typeof crypto !== "undefined" && crypto.randomUUID - ? crypto.randomUUID() - : Math.random().toString(36).slice(2, 10); +const generateId = () => uuidv4(); const WORLD_MODEL_PAGE = ModelPage.ImageTo3DWorld; const OBJECT_MODEL_PAGE = ModelPage.ImageTo3DObject; diff --git a/frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts b/frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts index 165580074f..48a549b869 100644 --- a/frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts +++ b/frontend/apps/artcraft/app/src/components/generation-feed/desktopMediaActions.ts @@ -39,6 +39,7 @@ import { type GalleryItem, } from "@storyteller/ui-generation-list"; import { useTabStore } from "~/pages/Stores/TabState"; +import { v4 as uuidv4 } from "uuid"; // Media item actions shared by the TopBar gallery modal / lightbox wiring and // the create pages' generation feed (rows + cards). All of them operate on @@ -130,7 +131,7 @@ export async function downloadMediaFilesToFolder( export async function applyMakeVideoFromImage(url: string, mediaToken?: string) { try { const referenceImage: RefImage = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url, file: new File([], "library-image"), mediaToken: mediaToken || "", @@ -178,7 +179,7 @@ export function applyRecreateFromPromptData(data: { for (const ci of contextImages) { const base = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: ci.media_links.cdn_url, mediaToken: ci.media_token, }; diff --git a/frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx b/frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx index e4ecd13384..17c2a81c92 100644 --- a/frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageDraw/BaseImageSelector.tsx @@ -11,6 +11,7 @@ import { CostCalculatorButton } from "@storyteller/ui-pricing-modal"; import { ModelPage } from "@storyteller/ui-model-selector"; import { UploadEntryCard } from "~/components/media/UploadEntryCard"; import { BlankCanvasModal, type BaseSelectorImage } from "@storyteller/ui-pagedraw"; +import { v4 as uuidv4 } from "uuid"; const MAX_GALLERY_SELECTIONS = 1; @@ -64,7 +65,7 @@ export const BaseImageSelector = ({ setIsBlankCanvasModalOpen(false); const blankImage: BaseSelectorImage = { url: "", - mediaToken: `blank_canvas_${width}x${height}_${Math.random().toString(36).substring(2, 8)}`, + mediaToken: `blank_canvas_${width}x${height}_${uuidv4()}`, isBlankCanvas: true, blankCanvasWidth: width, blankCanvasHeight: height, @@ -79,7 +80,7 @@ export const BaseImageSelector = ({ const reader = new FileReader(); reader.onloadend = () => { uploadImage({ - title: `reference-image-${Math.random().toString(36).substring(2, 15)}`, + title: `reference-image-${uuidv4()}`, assetFile: file, progressCallback: (newState) => { if (newState.status === UploaderStates.success && newState.data) { diff --git a/frontend/apps/artcraft/app/src/pages/PageImage/TextToImageStore.ts b/frontend/apps/artcraft/app/src/pages/PageImage/TextToImageStore.ts index c900486f53..44cde6f55f 100644 --- a/frontend/apps/artcraft/app/src/pages/PageImage/TextToImageStore.ts +++ b/frontend/apps/artcraft/app/src/pages/PageImage/TextToImageStore.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import type { GeneratedImage } from "@storyteller/tauri-events"; import type { ModelCreator } from "@storyteller/model-list"; +import { v4 as uuidv4 } from "uuid"; export type TextToImageBatch = { id: string; @@ -43,11 +44,7 @@ export const useTextToImageStore = create((set, get) => ({ subscriberId?: string, modelCreator?: ModelCreator, ) => { - const id = subscriberId - ? subscriberId - : crypto.randomUUID - ? crypto.randomUUID() - : Math.random().toString(36).slice(2); + const id = subscriberId || uuidv4(); const batch: TextToImageBatch = { id, prompt, diff --git a/frontend/apps/artcraft/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts b/frontend/apps/artcraft/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts index ca1dfaf42e..23e75a36b0 100644 --- a/frontend/apps/artcraft/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts +++ b/frontend/apps/artcraft/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts @@ -72,11 +72,7 @@ export const useImageTo3DStore = create((set, get) => ({ meshOnly: boolean, subscriberId?: string, ) => { - const id = subscriberId - ? subscriberId - : crypto.randomUUID - ? crypto.randomUUID() - : Math.random().toString(36).slice(2); + const id = subscriberId || uuidv4(); const result: ImageTo3DResult = { id, mode, @@ -111,8 +107,7 @@ export const useImageTo3DStore = create((set, get) => ({ : -1; if (targetIdx === -1) { - const generatedId = - crypto.randomUUID?.() ?? Math.random().toString(36).slice(2); + const generatedId = uuidv4(); const result: ImageTo3DResult = { id: generatedId, subscriberId: generatedId, diff --git a/frontend/apps/artcraft/app/src/pages/PageImageTo3DWorld/ImageTo3DWorldStore.ts b/frontend/apps/artcraft/app/src/pages/PageImageTo3DWorld/ImageTo3DWorldStore.ts index 9dce0c3180..c32f23261e 100644 --- a/frontend/apps/artcraft/app/src/pages/PageImageTo3DWorld/ImageTo3DWorldStore.ts +++ b/frontend/apps/artcraft/app/src/pages/PageImageTo3DWorld/ImageTo3DWorldStore.ts @@ -54,11 +54,7 @@ export const useImageTo3DWorldStore = create( previewUrl: string | undefined, subscriberId?: string, ) => { - const id = subscriberId - ? subscriberId - : crypto.randomUUID - ? crypto.randomUUID() - : Math.random().toString(36).slice(2); + const id = subscriberId || uuidv4(); const result: ImageTo3DWorldResult = { id, mode, @@ -92,8 +88,7 @@ export const useImageTo3DWorldStore = create( : -1; if (targetIdx === -1) { - const generatedId = - crypto.randomUUID?.() ?? Math.random().toString(36).slice(2); + const generatedId = uuidv4(); const result: ImageTo3DWorldResult = { id: generatedId, subscriberId: generatedId, diff --git a/frontend/apps/artcraft/app/src/pages/PageVideo/ImageToVideoStore.ts b/frontend/apps/artcraft/app/src/pages/PageVideo/ImageToVideoStore.ts index 84a64f637d..f870b260cd 100644 --- a/frontend/apps/artcraft/app/src/pages/PageVideo/ImageToVideoStore.ts +++ b/frontend/apps/artcraft/app/src/pages/PageVideo/ImageToVideoStore.ts @@ -1,4 +1,5 @@ import { create } from "zustand"; +import { v4 as uuidv4 } from "uuid"; export type VideoItem = { media_token: string; @@ -33,11 +34,7 @@ type ImageToVideoState = { export const useImageToVideoStore = create((set, get) => ({ batches: [], startBatch: (prompt: string, modelLabel: string, subscriberId?: string) => { - const id = subscriberId - ? subscriberId - : crypto.randomUUID - ? crypto.randomUUID() - : Math.random().toString(36).slice(2); + const id = subscriberId || uuidv4(); const batch: ImageToVideoBatch = { id, prompt, @@ -61,7 +58,7 @@ export const useImageToVideoStore = create((set, get) => ({ ? s.batches.findIndex((b) => b.id === pending.id) : -1; if (idx === -1) { - const id = Math.random().toString(36).slice(2); + const id = uuidv4(); const batch: ImageToVideoBatch = { id, prompt, diff --git a/frontend/apps/artcraft/app/src/pages/PageVideoFrameExtractor/VideoFrameExtractor.tsx b/frontend/apps/artcraft/app/src/pages/PageVideoFrameExtractor/VideoFrameExtractor.tsx index 709ad65c88..a1665b8b4a 100644 --- a/frontend/apps/artcraft/app/src/pages/PageVideoFrameExtractor/VideoFrameExtractor.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageVideoFrameExtractor/VideoFrameExtractor.tsx @@ -420,7 +420,7 @@ export const VideoFrameExtractor = () => { const file = new File([blob], fileName, { type: "image/png" }); const referenceImage: RefImage = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: frameUrl, file: file, mediaToken: uploadResponse.data, diff --git a/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx b/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx index ed423d71f3..119e864db2 100644 --- a/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx +++ b/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx @@ -8,6 +8,7 @@ import { isActionReminderOpen, showActionReminder, } from "@storyteller/ui-action-reminder-modal"; +import { v4 as uuidv4 } from "uuid"; export type { ImageBundle }; @@ -53,7 +54,7 @@ export const HistoryStack = ({ // This is used to force image reloads in different sessions // and prevent fetching CORS-tainted images from cache - const sessionRandBuster = useRef(Math.random()); + const sessionRandBuster = useRef(uuidv4()); const scrollRef = useRef(null); const prevPendingCountRef = useRef(0); diff --git a/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx b/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx index 5052fdefd9..55d69c7c3c 100644 --- a/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx +++ b/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx @@ -50,6 +50,7 @@ import { } from "./components/Model3DOverlay"; import { CameraIcon, Maximize2Icon, RefreshCwIcon } from "lucide-react"; import type { PageDrawAdapter } from "./adapter"; +import { v4 as uuidv4 } from "uuid"; const PAGE_ID: ModelPage = ModelPage.Canvas2D; @@ -857,7 +858,7 @@ const PageDraw = ({ const subscriberId: string = crypto?.randomUUID?.() ?? - `inpaint-${Date.now()}-${Math.random().toString(36).slice(2)}`; + `inpaint-${Date.now()}-${uuidv4()}`; adapter.onEnqueueMeta?.({ prompt, diff --git a/frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx b/frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx index 25f9760918..afddb4c32e 100644 --- a/frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx +++ b/frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx @@ -44,6 +44,7 @@ import { import { CSS } from "@dnd-kit/utilities"; import type { UploadMediaFn } from "@storyteller/api"; +import { v4 as uuidv4 } from "uuid"; export type { UploadMediaFn as UploadImageFn } from "@storyteller/api"; type UploadImageFn = UploadMediaFn; @@ -448,19 +449,17 @@ export const ImagePromptRow = ({ break; } - const uploadId = Math.random().toString(36).substring(7); + const uploadId = uuidv4(); setUploadingVideo({ id: uploadId, file }); if (uploadVideo) { await uploadVideo({ - title: `reference-video-${Math.random() - .toString(36) - .substring(2, 15)}`, + title: `reference-video-${uuidv4()}`, assetFile: file, progressCallback: (newState) => { if (newState.status === UploaderStates.success && newState.data) { const refVideo: RefVideo = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: URL.createObjectURL(file), file, mediaToken: newState.data, @@ -480,7 +479,7 @@ export const ImagePromptRow = ({ }); } else { const refVideo: RefVideo = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: URL.createObjectURL(file), file, mediaToken: "", @@ -562,19 +561,17 @@ export const ImagePromptRow = ({ break; } - const uploadId = Math.random().toString(36).substring(7); + const uploadId = uuidv4(); setUploadingAudio({ id: uploadId, file }); if (uploadAudio) { await uploadAudio({ - title: `reference-audio-${Math.random() - .toString(36) - .substring(2, 15)}`, + title: `reference-audio-${uuidv4()}`, assetFile: file, progressCallback: (newState) => { if (newState.status === UploaderStates.success && newState.data) { const refAudio: RefAudio = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: URL.createObjectURL(file), file, mediaToken: newState.data, @@ -592,7 +589,7 @@ export const ImagePromptRow = ({ }); } else { const refAudio: RefAudio = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: URL.createObjectURL(file), file, mediaToken: "", @@ -629,7 +626,7 @@ export const ImagePromptRow = ({ : files.slice(0, availableSlots); filesToProcess.forEach(async (file) => { - const uploadId = Math.random().toString(36).substring(7); + const uploadId = uuidv4(); if (uploadTarget === "end") { setUploadingEnd({ id: uploadId, file, previewUrl: "" }); } else { @@ -681,9 +678,7 @@ export const ImagePromptRow = ({ if (uploadImage) { await uploadImage({ - title: `reference-image-${Math.random() - .toString(36) - .substring(2, 15)}`, + title: `reference-image-${uuidv4()}`, assetFile: file, progressCallback: (newState) => { if (newState.status === UploaderStates.success && newState.data) { @@ -767,7 +762,7 @@ export const ImagePromptRow = ({ } currentTotal += duration; newVideos.push({ - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: item.fullImage, file: new File([], "library-video"), mediaToken: item.id, @@ -794,7 +789,7 @@ export const ImagePromptRow = ({ const item = selectedItems[0]; if (item && item.fullImage) { setEndFrameImage?.({ - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: item.fullImage, file: new File([], "library-image"), mediaToken: item.id, @@ -818,7 +813,7 @@ export const ImagePromptRow = ({ selectedItems.slice(0, availableSlots).forEach((item) => { if (!item.fullImage) return; newRefs.push({ - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: item.fullImage, file: new File([], "library-image"), mediaToken: item.id, diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx index 150e5617ca..8473ce4ae6 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx @@ -47,6 +47,7 @@ import { import { AudioTuningPopover } from "./common/AudioTuningPopover"; import { SoundsSettingsPopover } from "./common/SoundsSettingsPopover"; import { StylePromptRow } from "./common/StylePromptRow"; +import { v4 as uuidv4 } from "uuid"; const DEFAULT_AUDIO_MODEL_ID = "suno_music"; @@ -273,7 +274,7 @@ export const PromptBoxAudio = ({ } total += duration; added.push({ - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: item.fullImage, file: new File([], "library-audio"), mediaToken: item.id, diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx index 7040aa8448..40827a1d6f 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx @@ -36,6 +36,7 @@ import { type DroppedFiles, } from "./deck/usePromptBoxDrop"; import { DeckAddAction, DeckItem } from "./deck/deckTypes"; +import { v4 as uuidv4 } from "uuid"; interface PromptBoxImageProps { useJobContext: () => JobContextType; @@ -92,7 +93,7 @@ export const PromptBoxImage = ({ useEffect(() => { if (imageMediaId && url) { const referenceImage: RefImage = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: url, file: new File([], "library-image"), mediaToken: imageMediaId, @@ -276,7 +277,7 @@ export const PromptBoxImage = ({ useEffect(() => { if (imageMediaId && url) { const referenceImage: RefImage = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: url, file: new File([], "library-image"), mediaToken: imageMediaId, @@ -376,9 +377,7 @@ export const PromptBoxImage = ({ gtagEvent("enqueue_image"); - const subscriberId = crypto.randomUUID - ? crypto.randomUUID() - : Math.random().toString(36).slice(2); + const subscriberId = uuidv4(); setTimeout(() => { console.debug("Turn off blocking of prompt box..."); diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx index 1bf1f72388..ae3a190344 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx @@ -59,6 +59,7 @@ import { } from "./PromptFullscreenModal"; import { PromptFullscreenButton } from "./PromptFullscreenButton"; import { PromptClearAllButton } from "./PromptClearAllButton"; +import { v4 as uuidv4 } from "uuid"; declare global { interface Window { @@ -128,7 +129,7 @@ export const PromptBoxVideo = ({ useEffect(() => { if (imageMediaId && url) { const referenceImage: RefImage = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: url, file: new File([], "library-image"), mediaToken: imageMediaId, @@ -333,7 +334,7 @@ export const PromptBoxVideo = ({ useEffect(() => { if (imageMediaId && url) { const referenceImage: RefImage = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: url, file: new File([], "library-image"), mediaToken: imageMediaId, @@ -1217,9 +1218,7 @@ export const PromptBoxVideo = ({ const enqueuePromises: Promise[] = []; for (let i = 0; i < count; i++) { - const subscriberId = crypto.randomUUID - ? crypto.randomUUID() - : Math.random().toString(36).slice(2); + const subscriberId = uuidv4(); subscriberIds.push(subscriberId); enqueuePromises.push(GenerateVideo(buildRequest(subscriberId))); } diff --git a/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx b/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx index d533c3ba11..e8e737fae3 100644 --- a/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx +++ b/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx @@ -16,6 +16,7 @@ import { AUDIO_FILE_TYPE_ERROR, isAudioFile, } from "./audioFiles"; +import { v4 as uuidv4 } from "uuid"; /** Lets the prompt box push dropped/pasted files through the same upload * path the row's own file inputs use. */ @@ -104,12 +105,12 @@ export const AudioReferenceRow = forwardRef< if (uploadAudio) { setIsUploadingAudio(true); await uploadAudio({ - title: `reference-audio-${Math.random().toString(36).substring(2, 15)}`, + title: `reference-audio-${uuidv4()}`, assetFile: file, progressCallback: (newState) => { if (newState.status === UploaderStates.success && newState.data) { const refAudio: RefAudio = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: URL.createObjectURL(file), file, mediaToken: newState.data, @@ -145,12 +146,12 @@ export const AudioReferenceRow = forwardRef< setIsUploadingImage(true); await uploadImage({ - title: `reference-image-${Math.random().toString(36).substring(2, 15)}`, + title: `reference-image-${uuidv4()}`, assetFile: file, progressCallback: (newState) => { if (newState.status === UploaderStates.success && newState.data) { const refImage: RefImage = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: URL.createObjectURL(file), file, mediaToken: newState.data, diff --git a/frontend/libs/components/vfx/src/lib/PromptBoxVFX.tsx b/frontend/libs/components/vfx/src/lib/PromptBoxVFX.tsx index 6bb4225714..a006ac30da 100644 --- a/frontend/libs/components/vfx/src/lib/PromptBoxVFX.tsx +++ b/frontend/libs/components/vfx/src/lib/PromptBoxVFX.tsx @@ -17,6 +17,7 @@ import { type VFXModelId, type VFXResolution, } from "./types"; +import { v4 as uuidv4 } from "uuid"; export type VFXUploadFn = (args: { title: string; @@ -95,12 +96,12 @@ export const PromptBoxVFX = ({ setSourceUploading(true); await uploadVideo({ - title: `vfx-source-${Math.random().toString(36).slice(2, 10)}`, + title: `vfx-source-${uuidv4()}`, assetFile: file, progressCallback: (state) => { if (state.status === UploaderStates.success && state.data) { setSource({ - id: Math.random().toString(36).slice(2), + id: uuidv4(), url: previewUrl, mediaToken: state.data, }); @@ -136,12 +137,12 @@ export const PromptBoxVFX = ({ setUploading(true); await uploadImage({ - title: `vfx-${kind}-${Math.random().toString(36).slice(2, 10)}`, + title: `vfx-${kind}-${uuidv4()}`, assetFile: file, progressCallback: (state) => { if (state.status === UploaderStates.success && state.data) { setRef({ - id: Math.random().toString(36).slice(2), + id: uuidv4(), url: previewUrl, mediaToken: state.data, }); @@ -183,7 +184,7 @@ export const PromptBoxVFX = ({ return; } const ref = { - id: Math.random().toString(36).slice(2), + id: uuidv4(), url: item.fullImage || item.thumbnail || "", mediaToken: item.id, }; diff --git a/frontend/libs/components/vfx/src/lib/api.ts b/frontend/libs/components/vfx/src/lib/api.ts index 6464f25101..7bc42fd840 100644 --- a/frontend/libs/components/vfx/src/lib/api.ts +++ b/frontend/libs/components/vfx/src/lib/api.ts @@ -1,4 +1,5 @@ import { StorytellerApiHostStore } from "@storyteller/api"; +import { v4 as uuidv4 } from "uuid"; export type SubmitVFXJobRequest = { source_video_media_token: string; @@ -64,8 +65,5 @@ export async function submitVFXJob( } export function newIdempotencyToken(): string { - if (typeof crypto !== "undefined" && crypto.randomUUID) { - return crypto.randomUUID(); - } - return Math.random().toString(36).slice(2) + Date.now().toString(36); + return uuidv4(); } diff --git a/frontend/libs/components/vfx/src/lib/store.ts b/frontend/libs/components/vfx/src/lib/store.ts index 2a503cbe23..eb44d97cf3 100644 --- a/frontend/libs/components/vfx/src/lib/store.ts +++ b/frontend/libs/components/vfx/src/lib/store.ts @@ -9,6 +9,7 @@ import { type VFXResult, type VFXSubTab, } from "./types"; +import { v4 as uuidv4 } from "uuid"; type VFXState = { subTab: VFXSubTab; @@ -90,10 +91,7 @@ export const useVFXStore = create()( startResult: () => { const { prompt, resolution, source, mask, reference } = get(); - const id = - typeof crypto !== "undefined" && crypto.randomUUID - ? crypto.randomUUID() - : Math.random().toString(36).slice(2); + const id = uuidv4(); const result: VFXResult = { id, status: "pending",