UNPKG

use-vibes

Version:

Transform any DOM element into an AI-powered micro-app

241 lines 9.61 kB
// Module-level state for tracking and preventing duplicate calls export const MODULE_STATE = { pendingImageGenCalls: new Map(), pendingPrompts: new Set(), processingRequests: new Set(), requestTimestamps: new Map(), requestCounter: 0, createdDocuments: new Map(), // Track document IDs created for each generation request pendingDocumentCreations: new Map(), // Track document creation promises }; // Helper to safely remove a key from all tracking collections export const cleanupRequestKey = (key) => { MODULE_STATE.pendingImageGenCalls.delete(key); MODULE_STATE.processingRequests.delete(key); MODULE_STATE.pendingPrompts.delete(key); // We also clean up the document creation promise to prevent memory leaks MODULE_STATE.pendingDocumentCreations.delete(key); // Keep the createdDocuments entry as it's the actual deduplication map // But do clean up the timestamp to prevent memory leaks MODULE_STATE.requestTimestamps.delete(key); }; // Periodically clean up stale requests (every minute) setInterval(() => { const now = Date.now(); for (const [key, timestamp] of MODULE_STATE.requestTimestamps.entries()) { if (now - timestamp > 5 * 60 * 1000) { // For stale requests that are over 5 minutes old, // also remove them from createdDocuments tracking MODULE_STATE.createdDocuments.delete(key); // Clean up all other state cleanupRequestKey(key); } } }, 60000); // Check every minute /** * Synchronous hash function to create a key from the prompt string and options * @param prompt The prompt string to hash * @param options Optional image generation options * @returns A hash string for the input */ export function hashInput(prompt, options) { // Create a string that includes both prompt and relevant options const inputString = JSON.stringify({ prompt, // Only include relevant options properties to avoid unnecessary regeneration options: options ? { size: options.size, quality: options.quality, model: options.model, style: options.style, } : undefined, }); // Use a fast non-crypto hash for immediate results (FNV-1a algorithm) let hash = 2166136261; // FNV offset basis for (let i = 0; i < inputString.length; i++) { hash ^= inputString.charCodeAt(i); // Multiply by the FNV prime (32-bit) hash = Math.imul(hash, 16777619); } // Convert to hex string and take first 12 chars const hashHex = (hash >>> 0).toString(16).padStart(8, '0'); const requestId = hashHex.slice(0, 12); // Add a timestamp to make the ID unique even for identical requests return `${requestId}-${Date.now().toString(36)}`; } // Convert base64 to File object export function base64ToFile(base64Data, filename) { const byteString = atob(base64Data); const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } const blob = new Blob([ab], { type: 'image/png' }); return new File([blob], filename, { type: 'image/png' }); } /** * Generate a version ID for the file namespace * @param versionNumber - The numeric version (1-based) * @returns A formatted version string like "v1", "v2", etc. */ export function generateVersionId(versionNumber) { return `v${versionNumber}`; } /** * Get all version information from a document, or create a default if none exists * @param document - The image document * @returns Array of version info objects */ export function getVersionsFromDocument(document) { // Check if document has proper version structure if (document?.versions && document.versions.length > 0) { return { versions: document.versions, currentVersion: document.currentVersion ?? document.versions.length, }; } // Legacy document with just an 'image' file - convert to version format if (document?._files?.image) { return { versions: [{ id: 'v1', created: document.created || Date.now() }], currentVersion: 1, }; } // No versions found return { versions: [], currentVersion: 0 }; } /** * Generate a prompt key for the prompts namespace * @param promptNumber - The numeric prompt (1-based) * @returns A formatted prompt string like "p1", "p2", etc. */ export function generatePromptKey(promptNumber) { return `p${promptNumber}`; } /** * Get all prompt information from a document, or create a default if none exists * @param document - The image document * @returns Object with prompts record and currentPromptKey */ export function getPromptsFromDocument(document) { // Check if document has proper prompts structure if (document?.prompts && document?.currentPromptKey) { return { prompts: document.prompts, currentPromptKey: document.currentPromptKey, }; } // Legacy document with just a 'prompt' string - convert to prompts format if (document?.prompt) { return { prompts: { p1: { text: document.prompt, created: document.created || Date.now() }, }, currentPromptKey: 'p1', }; } // No prompts found return { prompts: {}, currentPromptKey: '' }; } /** * Add a new version to an image document * @param document - The existing image document * @param newImageFile - The new image file to add as a version * @param newPrompt - Optional new prompt to use for this version * @returns Updated document with the new version added */ export function addNewVersion(document, newImageFile, newPrompt) { // Get existing versions or initialize const { versions } = getVersionsFromDocument(document); const versionCount = versions.length + 1; const newVersionId = generateVersionId(versionCount); // Get existing prompts or initialize const { prompts, currentPromptKey } = getPromptsFromDocument(document); // Handle prompt versioning const updatedPrompts = { ...prompts }; let updatedCurrentPromptKey = currentPromptKey; // If a new prompt is provided and it's different from the current one, create a new prompt version if (newPrompt && (!currentPromptKey || newPrompt !== prompts[currentPromptKey]?.text)) { const promptCount = Object.keys(updatedPrompts).length + 1; updatedCurrentPromptKey = generatePromptKey(promptCount); updatedPrompts[updatedCurrentPromptKey] = { text: newPrompt, created: Date.now(), }; } else if (!updatedCurrentPromptKey && document.prompt) { // Legacy migration - create p1 from document.prompt updatedCurrentPromptKey = 'p1'; updatedPrompts['p1'] = { text: document.prompt, created: document.created || Date.now(), }; } // Copy existing files and add the new version const updatedFiles = { ...(document._files || {}) }; updatedFiles[newVersionId] = newImageFile; // Handle legacy documents by migrating 'image' to 'v1' if needed if (versionCount === 1 && document._files?.image) { updatedFiles['v1'] = document._files.image; delete updatedFiles.image; } return { ...document, currentVersion: versionCount - 1, // Make it 0-based versions: [ ...versions, { id: newVersionId, created: Date.now(), promptKey: updatedCurrentPromptKey, }, ], prompts: updatedPrompts, currentPromptKey: updatedCurrentPromptKey, _files: updatedFiles, }; } /** * Extract only the options properties that matter for image generation * to avoid unnecessary re-renders or regenerations */ export function getRelevantOptions(options) { if (!options) return {}; const relevantOptions = {}; if (options.size) relevantOptions.size = options.size; if (options.quality) relevantOptions.quality = options.quality; if (options.model) relevantOptions.model = options.model; if (options.style) relevantOptions.style = options.style; return relevantOptions; } /** * Generate a safe filename based on prompt text and timestamp * @param promptText - The prompt text to include in the filename * @returns A formatted filename like "prompt-text-20250518-2117.png" */ export function generateSafeFilename(promptText) { // Limit prompt length to avoid extremely long filenames const maxPromptChars = 40; // Clean the prompt by removing special characters and converting to lowercase const cleanedPrompt = promptText .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') // Keep only alphanumeric, spaces, and hyphens .replace(/\s+/g, '-') // Replace spaces with hyphens .substring(0, maxPromptChars) // Limit length .replace(/-+$/g, ''); // Remove trailing hyphens // Generate date part in format YYYYMMDD-HHMM const now = new Date(); const datePart = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`; const timePart = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}`; // Construct filename return `${cleanedPrompt}-${datePart}-${timePart}.png`; } //# sourceMappingURL=utils.js.map