From d6772fdd94fc9f51f1324e85fbb91a6eb8166ed6 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 10 Nov 2026 21:49:49 -0500 Subject: [PATCH] frontend: uuidv4 for local ids instead of Math.random (Sonar S2245) (#2820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clears the 38 open typescript:S2245 issues (18 files). Each `Math.random().toString(36)…` made a local id: an upload row, a reference title, a batch or subscriber id, an idempotency token, an image cache-buster. They now use `uuidv4()` from `uuid`, which is already a dependency and imported the same way elsewhere (`import { v4 as uuidv4 } from "uuid"`). It reads `crypto.getRandomValues`, so it also works where `crypto.randomUUID` doesn't (outside a secure context). The `crypto.randomUUID ? … : Math.random()…` fallbacks therefore collapse to `uuidv4()`, and `subscriberId ? subscriberId : …` becomes `subscriberId || uuidv4()`. Ids are now 36 characters instead of 5–13. Nothing parses or compares their length. Typecheck is left to CI. ## Summary by CodeRabbit * **Improvements** * Creative tools now handle identifiers more consistently for generated content, uploads, reference media, and generation batches. * Completed generation results are displayed more reliably when there isn’t a matching pending result. * These improvements span image, video, audio, 3D, drawing, and visual-effects workflows, helping keep related media and results organized across the app. Also: `ImageTo3DStore` and `ImageTo3DWorldStore` shared the same 30-line `completeGeneration` (find the subscriber's result, else the first pending one, and mark it completed, or add a completed one). It's now one `completeGenerationResult` in `pages/Stores/`, which removes the duplication the new-code gate counted against this PR. --- .../experiences/ImageTo3DExperience.tsx | 5 +- .../generation-feed/desktopMediaActions.ts | 5 +- .../src/pages/PageDraw/BaseImageSelector.tsx | 5 +- .../src/pages/PageImage/TextToImageStore.ts | 7 +-- .../PageImageTo3DObject/ImageTo3DStore.ts | 54 ++----------------- .../PageImageTo3DWorld/ImageTo3DWorldStore.ts | 54 ++----------------- .../src/pages/PageVideo/ImageToVideoStore.ts | 9 ++-- .../VideoFrameExtractor.tsx | 2 +- .../pages/Stores/completeGenerationResult.ts | 50 +++++++++++++++++ .../pagedraw/src/lib/HistoryStack.tsx | 3 +- .../components/pagedraw/src/lib/PageDraw.tsx | 3 +- .../promptbox/src/lib/ImagePromptRow.tsx | 33 +++++------- .../promptbox/src/lib/PromptBoxAudio.tsx | 3 +- .../promptbox/src/lib/PromptBoxImage.tsx | 9 ++-- .../promptbox/src/lib/PromptBoxVideo.tsx | 9 ++-- .../src/lib/common/AudioReferenceRow.tsx | 9 ++-- .../components/vfx/src/lib/PromptBoxVFX.tsx | 11 ++-- frontend/libs/components/vfx/src/lib/api.ts | 6 +-- frontend/libs/components/vfx/src/lib/store.ts | 6 +-- 19 files changed, 112 insertions(+), 171 deletions(-) create mode 100644 frontend/apps/artcraft/app/src/pages/Stores/completeGenerationResult.ts diff --git a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx index d7f13b3b62..44a6ee170e 100644 --- a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx +++ b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx @@ -70,10 +70,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..c6036397c3 100644 --- a/frontend/apps/artcraft/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts +++ b/frontend/apps/artcraft/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts @@ -3,6 +3,7 @@ import { listen } from "@tauri-apps/api/event"; import { MediaFilesApi, MediaUploadApi } from "@storyteller/api"; import { v4 as uuidv4 } from "uuid"; import { captureModelThumbnail } from "./captureModelThumbnail"; +import { completeGenerationAction } from "../Stores/completeGenerationResult"; export interface PendingExternalImage { url: string; @@ -72,11 +73,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, @@ -90,52 +87,7 @@ export const useImageTo3DStore = create((set, get) => ({ set((s) => ({ results: [result, ...s.results] })); return id; }, - completeGeneration: ( - modelUrl: string, - mediaToken: string, - maybeSubscriberId?: string, - ) => { - console.log("[ImageTo3DStore] completeGeneration", { - modelUrl, - mediaToken, - maybeSubscriberId, - }); - const pending = maybeSubscriberId - ? get().results.find((r) => r.subscriberId === maybeSubscriberId) - : get().results.find((r) => r.status === "pending"); - - set((s) => { - const results = [...s.results]; - const targetIdx = pending - ? results.findIndex((r) => r.subscriberId === pending.subscriberId) - : -1; - - if (targetIdx === -1) { - const generatedId = - crypto.randomUUID?.() ?? Math.random().toString(36).slice(2); - const result: ImageTo3DResult = { - id: generatedId, - subscriberId: generatedId, - mode: "image", - timestamp: Date.now(), - note: "Generated Model", - status: "completed", - modelUrl, - mediaToken, - }; - return { results: [result, ...results] }; - } - - results[targetIdx] = { - ...results[targetIdx], - status: "completed", - modelUrl, - mediaToken, - }; - - return { results }; - }); - }, + completeGeneration: completeGenerationAction(set, "ImageTo3DStore", "Generated Model"), uploadCoverImage: async (mediaToken: string, thumbnailDataUrl: string) => { try { // Check if already uploaded diff --git a/frontend/apps/artcraft/app/src/pages/PageImageTo3DWorld/ImageTo3DWorldStore.ts b/frontend/apps/artcraft/app/src/pages/PageImageTo3DWorld/ImageTo3DWorldStore.ts index 9dce0c3180..0a390525e0 100644 --- a/frontend/apps/artcraft/app/src/pages/PageImageTo3DWorld/ImageTo3DWorldStore.ts +++ b/frontend/apps/artcraft/app/src/pages/PageImageTo3DWorld/ImageTo3DWorldStore.ts @@ -2,6 +2,7 @@ import { create } from "zustand"; import { listen } from "@tauri-apps/api/event"; import { MediaFilesApi, MediaUploadApi } from "@storyteller/api"; import { v4 as uuidv4 } from "uuid"; +import { completeGenerationAction } from "../Stores/completeGenerationResult"; export type ImageTo3DWorldResult = { id: string; @@ -54,11 +55,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, @@ -71,52 +68,7 @@ export const useImageTo3DWorldStore = create( set((s) => ({ results: [result, ...s.results] })); return id; }, - completeGeneration: ( - modelUrl: string, - mediaToken: string, - maybeSubscriberId?: string, - ) => { - console.log("[ImageTo3DWorldStore] completeGeneration", { - modelUrl, - mediaToken, - maybeSubscriberId, - }); - const pending = maybeSubscriberId - ? get().results.find((r) => r.subscriberId === maybeSubscriberId) - : get().results.find((r) => r.status === "pending"); - - set((s) => { - const results = [...s.results]; - const targetIdx = pending - ? results.findIndex((r) => r.subscriberId === pending.subscriberId) - : -1; - - if (targetIdx === -1) { - const generatedId = - crypto.randomUUID?.() ?? Math.random().toString(36).slice(2); - const result: ImageTo3DWorldResult = { - id: generatedId, - subscriberId: generatedId, - mode: "image", - timestamp: Date.now(), - note: "Generated World", - status: "completed", - modelUrl, - mediaToken, - }; - return { results: [result, ...results] }; - } - - results[targetIdx] = { - ...results[targetIdx], - status: "completed", - modelUrl, - mediaToken, - }; - - return { results }; - }); - }, + completeGeneration: completeGenerationAction(set, "ImageTo3DWorldStore", "Generated World"), uploadCoverFromPreview: async (mediaToken: string) => { try { const result = get().results.find((r) => r.mediaToken === mediaToken); 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 e89b08c93c..0bfe87bfa9 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/apps/artcraft/app/src/pages/Stores/completeGenerationResult.ts b/frontend/apps/artcraft/app/src/pages/Stores/completeGenerationResult.ts new file mode 100644 index 0000000000..e46dd227ae --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/Stores/completeGenerationResult.ts @@ -0,0 +1,50 @@ +import { v4 as uuidv4 } from "uuid"; + +interface GenerationResult { + id: string; + mode: "image" | "text"; + timestamp: number; + note?: string; + status: "pending" | "completed"; + subscriberId: string; + modelUrl?: string; + mediaToken?: string; +} + +/** + * The results after a 3D job finishes: the one with that subscriber id (else the first still + * pending) becomes completed with the model; with none to update, a completed one is added first. + */ +export function completeGenerationResult( + results: readonly T[], + subscriberId: string | undefined, + completed: { modelUrl: string; mediaToken: string; note: string }, +): T[] { + const { modelUrl, mediaToken, note } = completed; + const index = subscriberId + ? results.findIndex((r) => r.subscriberId === subscriberId) + : results.findIndex((r) => r.status === "pending"); + if (index === -1) { + const id = uuidv4(); + // The other fields of T are optional, as the 3D result types declare them. + const added = { id, subscriberId: id, mode: "image", timestamp: Date.now(), note, status: "completed", modelUrl, mediaToken } as T; + return [added, ...results]; + } + const next = [...results]; + next[index] = { ...next[index], status: "completed", modelUrl, mediaToken }; + return next; +} + +/** A store's `completeGeneration`: logs the finished job under `store` and applies completeGenerationResult. */ +export function completeGenerationAction( + set: (update: (state: { results: T[] }) => { results: T[] }) => void, + store: string, + note: string, +) { + return (modelUrl: string, mediaToken: string, maybeSubscriberId?: string) => { + console.log(`[${store}] completeGeneration`, { modelUrl, mediaToken, maybeSubscriberId }); + set((s) => ({ + results: completeGenerationResult(s.results, maybeSubscriberId, { modelUrl, mediaToken, note }), + })); + }; +} diff --git a/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx b/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx index b7efddfc6d..b1399d47ea 100644 --- a/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx +++ b/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx @@ -9,6 +9,7 @@ import { showActionReminder, } from "@storyteller/ui-action-reminder-modal"; import { clickOnEnterOrSpace } from "@storyteller/common"; +import { v4 as uuidv4 } from "uuid"; export type { ImageBundle }; @@ -54,7 +55,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 6178ef79ea..873959e901 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; @@ -451,19 +452,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, @@ -483,7 +482,7 @@ export const ImagePromptRow = ({ }); } else { const refVideo: RefVideo = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: URL.createObjectURL(file), file, mediaToken: "", @@ -565,19 +564,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, @@ -595,7 +592,7 @@ export const ImagePromptRow = ({ }); } else { const refAudio: RefAudio = { - id: Math.random().toString(36).substring(7), + id: uuidv4(), url: URL.createObjectURL(file), file, mediaToken: "", @@ -632,7 +629,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 { @@ -684,9 +681,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) { @@ -770,7 +765,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, @@ -797,7 +792,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, @@ -821,7 +816,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",