From 4644a2c8b4031166b7fae40f8e1c1b7c9eeae2fe Mon Sep 17 00:00:00 2001 From: WhiteRaven11 <95498438+WhiteRaven11@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:55:56 +0300 Subject: [PATCH 1/5] Unify generation notifications across Studio --- components/StandaloneShell.js | 188 ++++++++++++++---- packages/Open-AI-Design-Agent | 2 +- packages/Vibe-Workflow | 2 +- .../src/components/AiInfluencerStudio.jsx | 18 +- .../studio/src/components/AudioStudio.jsx | 4 +- .../studio/src/components/ClippingStudio.jsx | 166 +++++++++++++++- .../src/components/DesignAgentStudio.jsx | 10 +- .../studio/src/components/ImageStudio.jsx | 4 +- .../studio/src/components/LipSyncStudio.jsx | 4 +- .../studio/src/components/RecastStudio.jsx | 4 +- .../src/components/VibeMotionStudio.jsx | 7 +- .../studio/src/components/VideoStudio.jsx | 4 +- .../studio/src/components/WorkflowStudio.jsx | 18 +- packages/studio/src/components/WorkflowUI.jsx | 10 +- 14 files changed, 370 insertions(+), 71 deletions(-) diff --git a/components/StandaloneShell.js b/components/StandaloneShell.js index 731b7c1ad..8a3d361c7 100644 --- a/components/StandaloneShell.js +++ b/components/StandaloneShell.js @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useParams, useRouter } from 'next/navigation'; import dynamic from 'next/dynamic'; import { ImageStudio, VideoStudio, ClippingStudio, VibeMotionStudio, LipSyncStudio, RecastStudio, CinemaStudio, AudioStudio, MarketingStudio, WorkflowStudio, AgentStudio, AppsStudio, AiInfluencerStudio, getUserBalance } from 'studio'; @@ -238,6 +238,35 @@ const getNavigationCategory = (tabId) => ( ); const STORAGE_KEY = 'muapi_key'; +const NOTIFICATIONS_STORAGE_KEY = 'open_gen_notifications_v1'; +const MAX_VISIBLE_NOTIFICATIONS = 3; + +const loadStoredNotifications = () => { + if (typeof window === 'undefined') return []; + + try { + const stored = JSON.parse(window.sessionStorage.getItem(NOTIFICATIONS_STORAGE_KEY) || '[]'); + const now = Date.now(); + return Array.isArray(stored) + ? stored.filter((notification) => notification.expiresAt > now).slice(0, MAX_VISIBLE_NOTIFICATIONS) + : []; + } catch { + return []; + } +}; + +const persistNotifications = (notifications) => { + if (typeof window === 'undefined') return; + + try { + window.sessionStorage.setItem( + NOTIFICATIONS_STORAGE_KEY, + JSON.stringify(notifications), + ); + } catch { + // Notification persistence is optional; rendering still works without storage. + } +}; export default function StandaloneShell() { const params = useParams(); @@ -327,26 +356,64 @@ export default function StandaloneShell() { const [isDragging, setIsDragging] = useState(false); const [droppedFiles, setDroppedFiles] = useState(null); - // ── Global Generation Notifications ──────────────────────────────────────── + // Global generation notifications remain mounted while users switch studios. const [notifications, setNotifications] = useState([]); - const activeTabRef = useRef(null); - useEffect(() => { activeTabRef.current = activeTab; }, [activeTab]); + const [notificationsHydrated, setNotificationsHydrated] = useState(false); + + useEffect(() => { + setNotifications(loadStoredNotifications()); + setNotificationsHydrated(true); + }, []); const pushNotification = useCallback((notif) => { + const now = Date.now(); const id = `notif-${Date.now()}-${Math.random()}`; - const entry = { ...notif, id }; - setNotifications(prev => [entry, ...prev].slice(0, 5)); - const ttl = notif.type === 'success' ? 8000 : 6000; - setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), ttl); + const ttl = 12000; + const entry = { ...notif, id, expiresAt: now + ttl }; + setNotifications((previous) => { + const next = [ + ...previous.filter((notification) => notification.expiresAt > now), + entry, + ].slice(-MAX_VISIBLE_NOTIFICATIONS); + persistNotifications(next); + return next; + }); }, []); const dismissNotification = useCallback((id) => { - setNotifications(prev => prev.filter(n => n.id !== id)); + setNotifications((previous) => { + const next = previous.filter((notification) => notification.id !== id); + persistNotifications(next); + return next; + }); }, []); + useEffect(() => { + if (!notificationsHydrated) return; + + persistNotifications(notifications); + }, [notifications, notificationsHydrated]); + + useEffect(() => { + if (notifications.length === 0) return undefined; + + const nextExpiry = Math.min(...notifications.map((notification) => notification.expiresAt)); + const timer = window.setTimeout(() => { + const now = Date.now(); + setNotifications((previous) => previous.filter((notification) => notification.expiresAt > now)); + }, Math.max(0, nextExpiry - Date.now())); + + return () => window.clearTimeout(timer); + }, [notifications]); + const makeSuccessCallback = useCallback((tabId) => (data) => { const tab = TABS.find(t => t.id === tabId); - pushNotification({ type: 'success', tabId, label: tab?.label || tabId, data }); + pushNotification({ + type: 'success', + tabId, + label: tab?.label || tabId, + resultUrl: data?.url || null, + }); }, [pushNotification]); const makeErrorCallback = useCallback((tabId) => (message) => { @@ -368,10 +435,15 @@ export default function StandaloneShell() { return () => window.removeEventListener('popstate', handlePopState); }, []); - const handleTabChange = (tabId) => { + const handleTabChange = useCallback((tabId) => { window.history.pushState(null, '', `/studio/${tabId}`); setActiveTab(tabId); - }; + }, []); + + const handleOpenNotification = useCallback((notification) => { + handleTabChange(notification.tabId); + dismissNotification(notification.id); + }, [dismissNotification, handleTabChange]); const handleTabClick = (e, tabId) => { if (e.button === 0 && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey) { @@ -848,85 +920,119 @@ export default function StandaloneShell() {
- +
{activeTab === 'design-agent' && ( - + )}
- +
- {/* ── Global Generation Notification Stack ── */} + {/* Global generation notification stack */} {notifications.length > 0 && (
{notifications.map((notif) => (
- {/* Icon */} -
{notif.type === 'success' ? ( - + ) : ( - + )} -
+ - {/* Body */} -
-

+

+

{notif.label} - - {notif.type === 'success' ? ' · Generation complete' : ' · Generation failed'} + + {notif.type === 'success' ? ' - Generation complete' : ' - Generation failed'}

{notif.type === 'error' && notif.message && ( -

+

{notif.message}

)} + {notif.type === 'success' && ( +

+ Your result is ready. +

+ )} {notif.type === 'success' && ( )}
- {/* Dismiss */}
))} diff --git a/packages/Open-AI-Design-Agent b/packages/Open-AI-Design-Agent index e179fe1a6..d22553572 160000 --- a/packages/Open-AI-Design-Agent +++ b/packages/Open-AI-Design-Agent @@ -1 +1 @@ -Subproject commit e179fe1a6c47b26ee6afac9128155d9f259a5d14 +Subproject commit d225535728b9e9ee979f5d7e0655928bee68d2fb diff --git a/packages/Vibe-Workflow b/packages/Vibe-Workflow index 4fed75125..c074ceac4 160000 --- a/packages/Vibe-Workflow +++ b/packages/Vibe-Workflow @@ -1 +1 @@ -Subproject commit 4fed75125da0c9bb9d94ad18f0b7746ed6531a9f +Subproject commit c074ceac4373f750ba5b1e66b65fc32783e80484 diff --git a/packages/studio/src/components/AiInfluencerStudio.jsx b/packages/studio/src/components/AiInfluencerStudio.jsx index ef6c871d3..86c9e117d 100644 --- a/packages/studio/src/components/AiInfluencerStudio.jsx +++ b/packages/studio/src/components/AiInfluencerStudio.jsx @@ -332,7 +332,13 @@ function HoverPill({ label, img, onClick }) { } // ─── Main Component ───────────────────────────────────────────────────────── -export default function AiInfluencerStudio({ apiKey, onGenerate, isGenerating: externalIsGenerating }) { +export default function AiInfluencerStudio({ + apiKey, + onGenerate, + onGenerationComplete, + onGenerationError, + isGenerating: externalIsGenerating, +}) { const [activeTab, setActiveTab] = useState("face"); const [selectedOptions, setSelectedOptions] = useState(() => { @@ -408,9 +414,17 @@ export default function AiInfluencerStudio({ apiKey, onGenerate, isGenerating: e setCurrentResult(res.url); setHistory((prev) => [{ url: res.url, ts: Date.now() }, ...prev]); setSelectedHistoryIdx(0); + onGenerationComplete?.({ + url: res.url, + model: INFLUENCER_MODEL, + prompt, + type: "image", + }); } } catch (err) { - toast.error(formatErrorMessage(err, "Generation failed. Please try again.")); + const message = formatErrorMessage(err, "Generation failed. Please try again."); + if (onGenerationError) onGenerationError(message); + else toast.error(message); } finally { setIsGeneratingInternal(false); } diff --git a/packages/studio/src/components/AudioStudio.jsx b/packages/studio/src/components/AudioStudio.jsx index 13987dbe2..d29b8e124 100644 --- a/packages/studio/src/components/AudioStudio.jsx +++ b/packages/studio/src/components/AudioStudio.jsx @@ -683,8 +683,8 @@ export default function AudioStudio({ } catch (e) { console.error("[AudioStudio]", e); const errMsg = formatErrorMessage(e, "Audio generation failed"); - toast.error(errMsg); - onGenerationError?.(errMsg); + if (onGenerationError) onGenerationError(errMsg); + else toast.error(errMsg); } finally { setIsGenerating(false); } diff --git a/packages/studio/src/components/ClippingStudio.jsx b/packages/studio/src/components/ClippingStudio.jsx index b3b609178..627344c68 100644 --- a/packages/studio/src/components/ClippingStudio.jsx +++ b/packages/studio/src/components/ClippingStudio.jsx @@ -23,6 +23,25 @@ import { promptMediaButtonClassName, } from "./prompt/PromptComposer.jsx"; +const MAX_VIDEO_SIZE_MB = 100; +const MAX_VIDEO_SIZE_BYTES = MAX_VIDEO_SIZE_MB * 1024 * 1024; +const CLIPPING_TOASTER_ID = "clipping-studio"; +const VIDEO_TOO_LARGE_FOR_MODE_MESSAGE = + "The file is too large for this mode. Compress or trim the video, then upload a smaller file."; +const MAX_VISIBLE_ERROR_TOASTS = 3; +const ERROR_TOAST_DURATION_MS = 7000; +const activeErrorToastIds = []; + +const forgetErrorToast = (toastId) => { + const index = activeErrorToastIds.indexOf(toastId); + if (index !== -1) activeErrorToastIds.splice(index, 1); +}; + +const dismissErrorToast = (toastId) => { + forgetErrorToast(toastId); + toast.dismiss(toastId, CLIPPING_TOASTER_ID); +}; + // --------------------------------------------------------------------------- // Inline SVG Icons // --------------------------------------------------------------------------- @@ -63,6 +82,104 @@ const CopyIcon = () => ( ); +const ErrorToast = ({ toastInstance, message }) => ( +
+ + + + {message} + +
+); + +const showErrorToast = (message) => { + const options = { + duration: ERROR_TOAST_DURATION_MS, + position: "bottom-right", + toasterId: CLIPPING_TOASTER_ID, + }; + + while (activeErrorToastIds.length >= MAX_VISIBLE_ERROR_TOASTS) { + const oldestToastId = activeErrorToastIds.shift(); + toast.remove(oldestToastId, CLIPPING_TOASTER_ID); + } + + const toastId = toast.custom( + (toastInstance) => ( + + ), + options, + ); + + activeErrorToastIds.push(toastId); + setTimeout( + () => forgetErrorToast(toastId), + ERROR_TOAST_DURATION_MS + 1000, + ); +}; + +const showVideoSizeLimitToast = () => { + showErrorToast(`Video exceeds ${MAX_VIDEO_SIZE_MB}MB limit.`); +}; + +const isFileSizeError = (error) => { + const message = String(error?.message || error || ""); + return /(?:\b413\b|payload too large|request entity too large|file(?: size)? (?:is )?too large|file is too heavy|exceeds?.*(?:size|limit)|слишком (?:больш|тяж)|превышает.*(?:размер|лимит))/i.test(message); +}; + +const showVideoUploadError = (error) => { + if (isFileSizeError(error)) { + showErrorToast(VIDEO_TOO_LARGE_FOR_MODE_MESSAGE); + return; + } + + const message = formatErrorMessage( + error, + "Video upload failed. Please try again.", + ); + showErrorToast(message); +}; + const getAspectClass = (ar) => { switch (ar) { case "16:9": return "aspect-video"; @@ -209,8 +326,8 @@ export default function ClippingStudio({ const videoFiles = droppedFiles.filter(f => f.type.startsWith('video/')); if (videoFiles.length > 0) { const file = videoFiles[0]; - if (file.size > 100 * 1024 * 1024) { - alert("Video exceeds 100MB limit."); + if (file.size > MAX_VIDEO_SIZE_BYTES) { + showVideoSizeLimitToast(); onFilesHandled?.(); return; } @@ -225,7 +342,7 @@ export default function ClippingStudio({ }) .catch(err => { setVideoUploading(false); - alert(`Failed to upload dropped file: ${err.message}`); + showVideoUploadError(err); }); } onFilesHandled?.(); @@ -286,8 +403,9 @@ export default function ClippingStudio({ const handleVideoFileChange = async (e) => { const file = e.target.files[0]; if (!file) return; - if (file.size > 100 * 1024 * 1024) { - alert("Video exceeds 100MB limit."); + if (file.size > MAX_VIDEO_SIZE_BYTES) { + showVideoSizeLimitToast(); + if (videoFileInputRef.current) videoFileInputRef.current.value = ""; return; } setVideoUploading(true); @@ -299,7 +417,7 @@ export default function ClippingStudio({ setVideoUrl(url); } catch (err) { console.error("[ClippingStudio] Video upload failed:", err); - alert(`Video upload failed: ${err.message}`); + showVideoUploadError(err); } finally { setVideoUploading(false); setVideoProgress(0); @@ -374,8 +492,11 @@ export default function ClippingStudio({ } catch (err) { console.error("[ClippingStudio] Error generating clips:", err); const errMsg = formatErrorMessage(err, "Failed to process AI clipping."); - toast.error(errMsg); - onGenerationError?.(errMsg); + const notificationMessage = isFileSizeError(err) + ? VIDEO_TOO_LARGE_FOR_MODE_MESSAGE + : errMsg; + if (onGenerationError) onGenerationError(notificationMessage); + else showErrorToast(notificationMessage); } finally { setIsGenerating(false); } @@ -988,7 +1109,34 @@ export default function ClippingStudio({ scrollbar-color: rgba(255, 255, 255, 0.08) transparent; } `} - +
); } diff --git a/packages/studio/src/components/DesignAgentStudio.jsx b/packages/studio/src/components/DesignAgentStudio.jsx index 517f6c9c3..7487a7604 100644 --- a/packages/studio/src/components/DesignAgentStudio.jsx +++ b/packages/studio/src/components/DesignAgentStudio.jsx @@ -5,7 +5,13 @@ import { CreativeCanvas } from 'design-agent'; import { getUserBalance } from '../muapi'; -export default function DesignAgentStudio({ apiKey, isHeaderVisible, onToggleHeader }) { +export default function DesignAgentStudio({ + apiKey, + isHeaderVisible, + onToggleHeader, + onGenerationComplete, + onGenerationError, +}) { const [userData, setUserData] = useState(null); useEffect(() => { @@ -38,6 +44,8 @@ export default function DesignAgentStudio({ apiKey, isHeaderVisible, onToggleHea theme="dark" onToggleHeader={onToggleHeader} isHeaderVisible={isHeaderVisible} + onGenerationComplete={onGenerationComplete} + onGenerationError={onGenerationError} />
); diff --git a/packages/studio/src/components/ImageStudio.jsx b/packages/studio/src/components/ImageStudio.jsx index 2a4978ea2..2b233fecb 100644 --- a/packages/studio/src/components/ImageStudio.jsx +++ b/packages/studio/src/components/ImageStudio.jsx @@ -1272,8 +1272,8 @@ export default function ImageStudio({ } catch (e) { console.error("[ImageStudio] Generation failed:", e); const errMsg = formatErrorMessage(e, "Image generation failed"); - toast.error(errMsg); - onGenerationError?.(errMsg); + if (onGenerationError) onGenerationError(errMsg); + else toast.error(errMsg); } finally { setGenerating(false); } diff --git a/packages/studio/src/components/LipSyncStudio.jsx b/packages/studio/src/components/LipSyncStudio.jsx index 12335ea11..bbdebceca 100644 --- a/packages/studio/src/components/LipSyncStudio.jsx +++ b/packages/studio/src/components/LipSyncStudio.jsx @@ -691,8 +691,8 @@ export default function LipSyncStudio({ } catch (e) { console.error("[LipSyncStudio]", e); const errMsg = formatErrorMessage(e, "Lip sync generation failed"); - toast.error(errMsg); - onGenerationError?.(errMsg); + if (onGenerationError) onGenerationError(errMsg); + else toast.error(errMsg); } finally { setIsGenerating(false); } diff --git a/packages/studio/src/components/RecastStudio.jsx b/packages/studio/src/components/RecastStudio.jsx index 45930b1f1..23a7ff04c 100644 --- a/packages/studio/src/components/RecastStudio.jsx +++ b/packages/studio/src/components/RecastStudio.jsx @@ -765,8 +765,8 @@ export default function RecastStudio({ } catch (e) { console.error("[RecastStudio]", e); const errMsg = formatErrorMessage(e, "Body swap generation failed"); - toast.error(errMsg); - onGenerationError?.(errMsg); + if (onGenerationError) onGenerationError(errMsg); + else toast.error(errMsg); } finally { setIsGenerating(false); } diff --git a/packages/studio/src/components/VibeMotionStudio.jsx b/packages/studio/src/components/VibeMotionStudio.jsx index 242504b6f..4d64f204f 100644 --- a/packages/studio/src/components/VibeMotionStudio.jsx +++ b/packages/studio/src/components/VibeMotionStudio.jsx @@ -193,14 +193,15 @@ export default function VibeMotionStudio({ apiKey, onGenerationComplete, onGener if (isStaleEdit) { console.warn("[VibeMotionStudio] Remix unavailable:", raw.slice(0, 120)); const msg = "This generation can't be remixed — the animation code wasn't saved server-side. Generate a new motion graphic first, then remix that result."; - toast.error(msg); + if (onGenerationError) onGenerationError(msg); + else toast.error(msg); setEditMode(false); setEditSourceId(null); } else { console.error("[VibeMotionStudio]", err); const errMsg = formatErrorMessage(raw || err, "Vibe Motion generation failed"); - toast.error(errMsg); - onGenerationError?.(errMsg); + if (onGenerationError) onGenerationError(errMsg); + else toast.error(errMsg); } } finally { setGenerating(false); diff --git a/packages/studio/src/components/VideoStudio.jsx b/packages/studio/src/components/VideoStudio.jsx index c1b55c69a..e2a0852b3 100644 --- a/packages/studio/src/components/VideoStudio.jsx +++ b/packages/studio/src/components/VideoStudio.jsx @@ -1256,8 +1256,8 @@ export default function VideoStudio({ hadError = true; console.error("[VideoStudio]", e); const errMsg = formatErrorMessage(e, "Video generation failed"); - toast.error(errMsg); - onGenerationError?.(errMsg); + if (onGenerationError) onGenerationError(errMsg); + else toast.error(errMsg); } finally { setGenerating(false); } diff --git a/packages/studio/src/components/WorkflowStudio.jsx b/packages/studio/src/components/WorkflowStudio.jsx index 17f618932..145e65f1d 100644 --- a/packages/studio/src/components/WorkflowStudio.jsx +++ b/packages/studio/src/components/WorkflowStudio.jsx @@ -125,7 +125,13 @@ function WorkflowCard({ workflow, onClick, activeTab, onRename, onDelete }) { ); } -export default function WorkflowStudio({ apiKey, isHeaderVisible = true, onToggleHeader }) { +export default function WorkflowStudio({ + apiKey, + isHeaderVisible = true, + onToggleHeader, + onGenerationComplete, + onGenerationError, +}) { const params = useParams(); const router = useRouter(); const slug = params?.slug || []; @@ -414,9 +420,15 @@ export default function WorkflowStudio({ apiKey, isHeaderVisible = true, onToggl const data = await executeWorkflow(apiKey, selectedWorkflow.id, inputs); setResult(data); + onGenerationComplete?.({ + url: data?.url || data?.output?.url || data?.outputs?.[0]?.url || null, + type: "workflow", + }); } catch (err) { console.error("Execution failed:", err); - setError(err.message || "Execution failed"); + const message = err.message || "Execution failed"; + setError(message); + onGenerationError?.(message); } finally { setIsExecuting(false); } @@ -815,6 +827,8 @@ export default function WorkflowStudio({ apiKey, isHeaderVisible = true, onToggl // Inject ID to prevent builder from assuming this is a new unsaved flow workflow_id: selectedWorkflow?.id }} + onGenerationComplete={onGenerationComplete} + onGenerationError={onGenerationError} /> ) : (
diff --git a/packages/studio/src/components/WorkflowUI.jsx b/packages/studio/src/components/WorkflowUI.jsx index 39651cee8..8c0fcc8f1 100644 --- a/packages/studio/src/components/WorkflowUI.jsx +++ b/packages/studio/src/components/WorkflowUI.jsx @@ -6,7 +6,13 @@ import "reactflow/dist/style.css"; import "react-toastify/dist/ReactToastify.css"; -const WorkflowUI = ({ workflowId, initialNodeSchemas, initialWorkflowData }) => { +const WorkflowUI = ({ + workflowId, + initialNodeSchemas, + initialWorkflowData, + onGenerationComplete, + onGenerationError, +}) => { useEffect(() => { sessionStorage.setItem("fromWorkflowBuilder", "true"); }, []); @@ -18,6 +24,8 @@ const WorkflowUI = ({ workflowId, initialNodeSchemas, initialWorkflowData }) => initialNodeSchemas={initialNodeSchemas} initialWorkflowData={initialWorkflowData} costType="dollars" + onGenerationComplete={onGenerationComplete} + onGenerationError={onGenerationError} />
); From 1d96ddd62ebff949e87b637b69b4a14161b08c21 Mon Sep 17 00:00:00 2001 From: WhiteRaven11 <95498438+WhiteRaven11@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:21:17 +0300 Subject: [PATCH 2/5] Show active generation status across Studio --- components/StandaloneShell.js | 669 +------ packages/Open-AI-Design-Agent | 2 +- packages/Vibe-Workflow | 2 +- .../src/components/AiInfluencerStudio.jsx | 573 +----- .../studio/src/components/AudioStudio.jsx | 687 +------ .../studio/src/components/CinemaStudio.jsx | 709 +------ .../studio/src/components/ClippingStudio.jsx | 732 +------ .../src/components/DesignAgentStudio.jsx | 4 + .../studio/src/components/ImageStudio.jsx | 1308 +----------- .../studio/src/components/LipSyncStudio.jsx | 706 +------ .../studio/src/components/MarketingStudio.jsx | 591 +----- .../studio/src/components/RecastStudio.jsx | 765 +------ .../src/components/VibeMotionStudio.jsx | 401 +--- .../studio/src/components/VideoStudio.jsx | 1759 +---------------- .../studio/src/components/WorkflowStudio.jsx | 591 +----- packages/studio/src/components/WorkflowUI.jsx | 4 + 16 files changed, 67 insertions(+), 9436 deletions(-) diff --git a/components/StandaloneShell.js b/components/StandaloneShell.js index 8a3d361c7..6bbf230d4 100644 --- a/components/StandaloneShell.js +++ b/components/StandaloneShell.js @@ -359,6 +359,7 @@ export default function StandaloneShell() { // Global generation notifications remain mounted while users switch studios. const [notifications, setNotifications] = useState([]); const [notificationsHydrated, setNotificationsHydrated] = useState(false); + const [generationCounts, setGenerationCounts] = useState({}); useEffect(() => { setNotifications(loadStoredNotifications()); @@ -421,669 +422,5 @@ export default function StandaloneShell() { pushNotification({ type: 'error', tabId, label: tab?.label || tabId, message }); }, [pushNotification]); - // Popstate event listener to sync tab state with URL on back/forward navigation - useEffect(() => { - const handlePopState = () => { - const path = window.location.pathname; - const segments = path.split('/').filter(Boolean); - const tabId = segments[1] || 'image'; - if (TABS.find(t => t.id === tabId)) { - setActiveTab(tabId); - } - }; - window.addEventListener('popstate', handlePopState); - return () => window.removeEventListener('popstate', handlePopState); - }, []); - - const handleTabChange = useCallback((tabId) => { - window.history.pushState(null, '', `/studio/${tabId}`); - setActiveTab(tabId); - }, []); - - const handleOpenNotification = useCallback((notification) => { - handleTabChange(notification.tabId); - dismissNotification(notification.id); - }, [dismissNotification, handleTabChange]); - - const handleTabClick = (e, tabId) => { - if (e.button === 0 && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey) { - e.preventDefault(); - handleTabChange(tabId); - return true; - } - return false; - }; - - const handleNavigationItemClick = (event, tabId) => { - if (handleTabClick(event, tabId)) { - setIsMobileOpen(false); - } - }; - - // Auto-hide header when inside a specific workflow view or design agent - useEffect(() => { - const isEditingWorkflow = (activeTab === 'workflows' || !!idFromParams) && urlWorkflowId; - const isDesignAgent = activeTab === 'design-agent'; - - if (isEditingWorkflow || isDesignAgent) { - setIsHeaderVisible(false); - } else { - setIsHeaderVisible(true); - } - }, [activeTab, urlWorkflowId, idFromParams]); - - // Global builder CSS cleanup when switching away from Workflows or Design Agent tabs - useEffect(() => { - const fromBuilder = sessionStorage.getItem("fromWorkflowBuilder"); - const fromDesignAgent = sessionStorage.getItem("fromDesignAgent"); - - if ((fromBuilder && activeTab !== 'workflows') || (fromDesignAgent && activeTab !== 'design-agent')) { - sessionStorage.removeItem("fromWorkflowBuilder"); - sessionStorage.removeItem("fromDesignAgent"); - window.location.reload(); - } - }, [activeTab]); - - const fetchBalance = useCallback(async (key) => { - try { - const data = await getUserBalance(key); - setBalance(data.balance); - } catch (err) { - console.error('Balance fetch failed:', err); - } - }, []); - - useEffect(() => { - setHasMounted(true); - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - setApiKey(stored); - fetchBalance(stored); - // Sync cookie immediately on mount to establish identity for background requests - document.cookie = `muapi_key=${stored}; path=/; max-age=31536000; SameSite=Lax`; - } - }, [fetchBalance]); - - const handleKeySave = useCallback((key) => { - localStorage.setItem(STORAGE_KEY, key); - setApiKey(key); - fetchBalance(key); - document.cookie = `muapi_key=${key}; path=/; max-age=31536000; SameSite=Lax`; - }, [fetchBalance]); - - const handleKeyChange = useCallback(() => { - localStorage.removeItem(STORAGE_KEY); - setApiKey(null); - setBalance(null); - document.cookie = "muapi_key=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"; - }, []); - - // Inject API key into all outgoing Axios requests (prop-based approach) - // We use an interceptor to be selective and NOT send the key to external domains like S3 - useEffect(() => { - // Safety: Clear any global defaults that might have been set previously - delete axios.defaults.headers.common['x-api-key']; - - if (!apiKey) return; - - const interceptorId = axios.interceptors.request.use((config) => { - // Check if URL is local/proxied - const isRelative = config.url.startsWith('/') || !config.url.startsWith('http'); - const isInternalProxy = config.url.includes('/api/app') || config.url.includes('/api/workflow') || config.url.includes('/api/agents') || config.url.includes('/api/api') || config.url.includes('/api/v1'); - - if (isRelative || isInternalProxy) { - config.headers['x-api-key'] = apiKey; - } - - return config; - }); - - return () => { - axios.interceptors.request.eject(interceptorId); - }; - }, [apiKey]); - - // Poll for balance every 30 seconds if key is present - useEffect(() => { - if (!apiKey) return; - const interval = setInterval(() => fetchBalance(apiKey), 30000); - return () => clearInterval(interval); - }, [apiKey, fetchBalance]); - - // Drag and Drop Handlers - const handleDragOver = useCallback((e) => { - e.preventDefault(); - e.stopPropagation(); - }, []); - - const handleDragEnter = useCallback((e) => { - e.preventDefault(); - e.stopPropagation(); - if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { - setIsDragging(true); - } - }, []); - - const handleDragLeave = useCallback((e) => { - e.preventDefault(); - e.stopPropagation(); - // Only set to false if we're leaving the container itself, not moving between children - if (e.currentTarget.contains(e.relatedTarget)) return; - setIsDragging(false); - }, []); - - const handleDrop = useCallback((e) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - const files = Array.from(e.dataTransfer.files); - if (files.length > 0) { - setDroppedFiles(files); - } - }, []); - - const handleFilesHandled = useCallback(() => { - setDroppedFiles(null); - }, []); - - if (!hasMounted) return ( -
-
-
- ); - - if (!apiKey) { - return ; - } - - return ( -
- {/* Drag Overlay */} - {isDragging && ( -
-
-
- - - -
-
- Drop your media here - Images, videos, or audio files -
-
-
- )} - - {/* Vadoo promo banner */} - {showVadooBanner && ( -
- - Unrestricted AI Images & Videos → Auto-Publish as YouTube Shorts & TikToks, Earn ↗ - - -
- )} - - {/* Header */} - {isHeaderVisible && ( -
- {/* Left: Mobile menu toggle + Logo + Desktop Sidebar Toggle */} -
- {/* Mobile drawer toggle */} - - - {/* Desktop Sidebar Toggle Button (Single Toggle Button) */} -
- - {/* Custom Tooltip */} -
- {isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} -
-
- - {/* Logo & Title */} -
-
- - - -
- - OpenGenerativeAI - -
-
- - {/* Active Tab Breadcrumb Badge */} -
- - - {TABS.find(t => t.id === activeTab)?.label || 'Studio'} - -
- - {/* Right: Actions */} -
-
-
- - ${balance !== null ? `${balance}` : '---'} - -
- - -
-
- )} - - {/* Main Body Layout: Left Sidebar + Studio Content Area */} -
- {/* Mobile Backdrop Overlay */} - {isMobileOpen && ( -
setIsMobileOpen(false)} - /> - )} - - {/* Left Sidebar Navigation */} - {isHeaderVisible && ( - - )} - - {/* Studio Content */} -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- {activeTab === 'design-agent' && ( - - )} -
-
- -
-
- -
-
-
- - {/* Global generation notification stack */} - {notifications.length > 0 && ( -
- {notifications.map((notif) => ( -
- - {notif.type === 'success' ? ( - - ) : ( - - )} - - -
-

- {notif.label} - - {notif.type === 'success' ? ' - Generation complete' : ' - Generation failed'} - -

- {notif.type === 'error' && notif.message && ( -

- {notif.message} -

- )} - {notif.type === 'success' && ( -

- Your result is ready. -

- )} - {notif.type === 'success' && ( - - )} -
- - -
- ))} -
- )} - - {/* Keyframe for toast slide-in */} - - - {/* Settings Modal */} - {showSettings && ( -
-
-

Settings

-

- Manage your AI studio preferences and authentication. -

- -
-
- -
- {apiKey.slice(0, 8)}•••••••••••••••• -
-
-
- -
- - -
-
-
- )} -
- ); -} + const makeGenerationStartCallback = useCallback((tabId) => () => { + setGenerationCounts((previouswkw@((𽑥((𽑥(((𽑥((aA1=I}AAM}Q(؁9д́д́ɑȵЁɑȵݡєlt((ɕ퀽ՑaA1=I}AAM}Q( 졕ٕФ9٥ѥ%ѕ ٕаaA1=I}AAM}Q(ɥɕ텍ѥٕQaA1=I}AAM}Qչ(ɥaA1=I}AAM}Q(ѥѱM ͕5=aA1=I}AAM}Qչ(9(ɽɕѥٔѕ̵ѕȁɽչᰁɅͥѥɅѥѕеltе͕(M ͕5=āܴāѥ䵍ѕȁ൅Ѽ耝́ȸԁܵձ̝(텍ѥٕQaA1=I}AAM}Q(ɅеѼȁɽlɐ͕tԁѼѕеlɐ͕tɑȁɑȵlɐ͕t(耝ѕеݡєٕѕеݡєٕ鉜ݡєltɑȁɑȵɅɕМ((((텍ѥٕQaA1=I}AAM}Q(9􉅉ͽєдѽȁѽȁܴāɅеѼɽlɐ͕tѼl՘tɽչȵձ((9큙͡ɥ텍ѥٕQaA1=I}AAM}Qѕеlɐ͕t耝ѕеݡєɽٕѕеݡє(aA1=I}AAM}Q((젅M ͕5=(9չєaA1=I}AAM}Q(((𽑥((𽹅(ͥ(((켨MՑ ѕЀ(؁9􉙱āձɕѥٕٔəܵlt(؁9텍ѥٕQ􀝥ձܵձ耉(%MՑ--ɽɽ􁽹!!􁽹ɅѥMхɅѥMх 􁽹ɅѥɅѥ 􁽹Ʌѥ єMՍ 􁽹Ʌѥɽɽ (𽑥(؁9텍ѥٕQ٥ձܵձ耉(YMՑ--ɽɽ􁽹!!􁽹ɅѥMхɅѥMх ٥􁽹ɅѥɅѥ ٥􁽹Ʌѥ єMՍ ٥􁽹Ʌѥɽɽ ٥(𽑥(؁9텍ѥٕQ􀝍ձܵձ耉( MՑ--ɽɽ􁽹!!􁽹ɅѥMхɅѥMх 􁽹ɅѥɅѥ 􁽹Ʌѥ єMՍ 􁽹Ʌѥɽɽ (𽑥(؁9텍ѥٕQ٥ѥձܵձ耉(Y5ѥMՑ--􁽹ɅѥMхɅѥMх ٥ѥ􁽹ɅѥɅѥ ٥ѥ􁽹Ʌѥ єMՍ ٥ѥ􁽹Ʌѥɽɽ ٥ѥ(𽑥(؁9텍ѥٕQ􀝱幌ձܵձ耉(1M幍MՑ--ɽɽ􁽹!!􁽹ɅѥMхɅѥMх 幌􁽹ɅѥɅѥ 幌􁽹Ʌѥ єMՍ 幌􁽹Ʌѥɽɽ 幌(𽑥(؁9텍ѥٕQ􀝉݅ձܵձ耉(IMՑ--ɽɽ􁽹!!􁽹ɅѥMхɅѥMх ݅􁽹ɅѥɅѥ ݅􁽹Ʌѥ єMՍ ݅􁽹Ʌѥɽɽ ݅(𽑥(؁9텍ѥٕQ􀝍ձܵձ耉( MՑ--􁽹ɅѥMхɅѥMх 􁽹ɅѥɅѥ 􁽹Ʌѥ єMՍ 􁽹Ʌѥɽɽ (𽑥(؁9텍ѥٕQ􀝅Ցձܵձ耉(ՑMՑ--ɽɽ􁽹!!􁽹ɅѥMхɅѥMх Ց􁽹ɅѥɅѥ Ց􁽹Ʌѥ єMՍ Ց􁽹Ʌѥɽɽ Ց(𽑥(؁9텍ѥٕQ􀝵ɭѥձܵձ耉(5ɭѥMՑ--ɽɽ􁽹!!􁽹ɅѥMхɅѥMх ɭѥ􁽹ɅѥɅѥ ɭѥ􁽹Ʌѥ єMՍ ɭѥ􁽹Ʌѥɽɽ ɭѥ(𽑥(؁9텍ѥٕQݽɭ̜ձܵձ耉(]ɭMՑ(--(!Yͥ!Yͥ(Q!͕%!Yͥ(ɅѥMхɅѥMх ݽɭ̜(ɅѥɅѥ ݽɭ̜(Ʌѥ єMՍ ݽɭ̜(Ʌѥɽɽ ݽɭ̜((𽑥(؁9텍ѥٕQ􀝅̜ձܵձ耉(MՑ--!Yͥ!Yͥ􁽹Q!͕%!Yͥ(𽑥(؁9텍ѥٕQ􀝑ͥМձܵձ耉(텍ѥٕQ􀝑ͥМ(ͥMՑ(--(!Yͥ!Yͥ(Q!͕%!Yͥ(ɅѥMхɅѥMх ͥМ(ɅѥɅѥ ͥМ(Ʌѥ єMՍ ͥМ(Ʌѥɽɽ ͥМ(((𽑥(؁9텍ѥٕQ􀝅̜ձܵձ耉(MՑ--(𽑥(؁9텍ѥٕQ􀝅ՕȜձܵձ耉(%ՕMՑ(--(ɅѥMхɅѥMх ՕȜ(ɅѥɅѥ ՕȜ(Ʌѥ єMՍ ՕȜ(Ʌѥɽɽ ՕȜ((𽑥(𽑥(𽑥((켨Ʌѥѥ٥䁅ѥѥх(졅ѥٕɅѥ̹Ѡѥѥ̹Ѡ((ɥٔє(ɥɅѥѥ٥䁅ѥѥ̈(9􉙥ᕐѽԁɥдԁltൠm٠tܵltܵmܴt്ȁٕəܵ䵅Ѽѕȵ̵ٕ(фѕѥ􉝱ѥѥх((텍ѥٕɅѥ̹Ʌѥ((흕Ʌѥх%(ɽх̈(фɅѥх흕Ʌѥх%(9ѕȵ̵ٕѼѕ̵ѕȁ́ɽչᰁɑȁɑȵ典lt̸ԁ́ѕеltѕе饹ܵ͡l||}ɝԥt(фѕѥ􉝕Ʌѥѥ٥((9􉙱ܴ͡ɥѕ̵ѕȁѥ䵍ѕȁɽչɑȁɑȵ典ԁ典((9􉠴̸ԁ̸ܴԁєɽչձɑȴȁɑȵ典ԁɑȵе典(ɥՔ(((9􉵥ܴāе͕ԁѕе饹(흕Ʌѥ́Ʌѥ(흕ɅѥչЀĀ흕Ʌѥչ耜((𽑥(((ѥѥ̹ѥ((ѥ(ɽѥ􀝕ɽȜМ耝х̝(фѥѥѥ(фѥѥхѥх%(9ѕȵ̵ٕѼѕ̵хЁ́ɽչᰁɑȁlt̸ԁ́ѕеltѕе饹ܵ͡l||}ɝԥt(屔(ɑ 聹ѥՍ̜ɝаİԤ耝ɝԤ(ѥ耝ͱ%IЀ́Չ饕ȠذḬ̇Ĥ݅ɑ̜((((9큵дԁܴ͡ɥѕ̵ѕȁѥ䵍ѕȁɽչɑȀ(ѥՍ̜(ɑȵ典ԁ典ѕе典(耝ɑȵɕԁɕѕеɕ(((ѥՍ̜(ٜݥѠ܈܈٥ ЀЈ􉹽ɽɕ Ȉɽ]ѠȸԈɽ1ɽչɽ1ɽչɥՔ(ѠԀȀЀ0؈(ٜ(耠(ٜݥѠ܈܈٥ ЀЈ􉹽ɽɕ Ȉɽ]ѠȈɽ1ɽչɽ1ɽչɥՔ(ɍȈȈ䈀(Ѡ4Ȁ؈(Ѡ4ȀݠĈ(ٜ((((؁9􉵥ܴĈ(9􉙽е͕ԁѕе饹(ѥ(9􉙽еɵѕе饹(ѥՍ̜Ʌѥє耜Ʌѥ(((ѥ􀝕ɽȜѥͅ(9дԁȁѕеltеմЁѕеɕԈѥѱѥͅ(ѥͅ(((ѥՍ̜(9дԁѕеltЁѕе饹(eȁɕձЁ́ɕ(((ѥՍ̜(ѽ(ѽ( 젤=9ѥѥѥ(9дĸԁѕеltеѕе典Ʌͥѥٕ́ѕе典(ɥ=ѥɕձс((=(ѽ((𽑥((ѽ(ѽ( 젤͵9ѥѥѥ(9дԁ܁ܴ܁͡ɥѕ̵ѕȁѥ䵍ѕȁɽչѕе饹Ʌͥѥٕ́鉜ݡєԁٕѕе饹ѱɥāɥݡє(ɥ͵́ѥѥ((ٜݥѠЈЈ٥ ЀЈ􉹽ɽɕ Ȉɽ]ѠȈɽ1ɽչɥՔ(Ѡ4؀؀4؀ٰȀȈ(ٜ(ѽ(𽑥((𽑥(((켨-ɅȁѽЁͱ(屔(Ʌ́ͱ%IЁ(ɽɅ͙ɴɅͱѕ`쁽(ѼɅ͙ɴɅͱѕ`쀀((屔((켨Mѥ́5(͡Mѥ̀(؁9􉙥ᕐ͕дɽȵʹѕ̵ѕȁѥ䵍ѕȁє(؁9􉉜ltɑȁɑȵݡєɽչᰁܵձܵʹܴ͡ᰈ(ȁ9ѕеݡєеѕеȈMѥ(9ѕеݡєѕеlt(5ȁ$Ցɕɕ́ѡѥѥ(((؁9Ё(؁9􉉜ݡєԁɑȁɑȵݡєltɽչЈ(񱅉9􉉱ѕе́еѕеݡєȈ(ѥٔA$-(𽱅(؁9ѕеltеѕеݡє(-ͱ(𽑥(𽑥(𽑥((؁9􉙱̈(ѽ( - (9􉙱āɽչɕѕеɕٕ鉜ɕѕе́е͕Ʌͥѥ(( -(ѽ(ѽ( 젤͕MMѥ̡͔(9􉙱āɽչݡєԁѕеݡєٕ鉜ݡєѕе́е͕ɅͥѥɑȁɑȵݡєԈ(( ͔(ѽ(𽑥(𽑥(𽑥((𽑥()( \ No newline at end of file diff --git a/packages/Open-AI-Design-Agent b/packages/Open-AI-Design-Agent index d22553572..80ff449ac 160000 --- a/packages/Open-AI-Design-Agent +++ b/packages/Open-AI-Design-Agent @@ -1 +1 @@ -Subproject commit d225535728b9e9ee979f5d7e0655928bee68d2fb +Subproject commit 80ff449ac99d178032dcaa3ba935fa96d05fb716 diff --git a/packages/Vibe-Workflow b/packages/Vibe-Workflow index c074ceac4..b96ad6716 160000 --- a/packages/Vibe-Workflow +++ b/packages/Vibe-Workflow @@ -1 +1 @@ -Subproject commit c074ceac4373f750ba5b1e66b65fc32783e80484 +Subproject commit b96ad67168958fdd48559a84d99072bbd83a8d73 diff --git a/packages/studio/src/components/AiInfluencerStudio.jsx b/packages/studio/src/components/AiInfluencerStudio.jsx index 86c9e117d..e93f8fc4f 100644 --- a/packages/studio/src/components/AiInfluencerStudio.jsx +++ b/packages/studio/src/components/AiInfluencerStudio.jsx @@ -188,575 +188,4 @@ const TABS_CONFIG = { label: "Left Arm", options: [ { id: "left_arm_normal", label: "Normal", img: `${CDN}/left_arm_left_arm_normal.webp`, promptVal: "normal left arm" }, - { id: "left_arm_cute", label: "Cute Prosthetic",img: `${CDN}/left_arm_make_left_arm_stylish_pink_prosthetic_wi.webp`, promptVal: "stylish pink prosthetic left arm with cute stickers" }, - { id: "left_arm_robotic", label: "Robotic", img: `${CDN}/left_arm_left_arm_robotic.webp`, promptVal: "robotic left arm" }, - { id: "left_arm_prosthetic", label: "Prosthetic", img: `${CDN}/left_arm_left_arm_prosthetic.webp`, promptVal: "prosthetic left arm" }, - { id: "left_arm_mechanical", label: "Mechanical", img: `${CDN}/left_arm_left_arm_mechanical.webp`, promptVal: "mechanical left arm" }, - { id: "left_arm_none", label: "None", img: `${CDN}/left_arm_left_arm_none.webp`, promptVal: "no left arm" }, - ], - }, - { - id: "right_arm", - label: "Right Arm", - options: [ - { id: "right_arm_normal", label: "Normal", img: `${CDN}/right_arm_right_arm_normal.webp`, promptVal: "normal right arm" }, - { id: "right_arm_cute", label: "Cute Prosthetic",img: `${CDN}/right_arm_make_right_arm_stylish_pink_prosthetic_w.webp`, promptVal: "stylish pink prosthetic right arm with cute stickers" }, - { id: "right_arm_robotic", label: "Robotic", img: `${CDN}/right_arm_right_arm_robotic.webp`, promptVal: "robotic right arm" }, - { id: "right_arm_prosthetic", label: "Prosthetic", img: `${CDN}/right_arm_right_arm_prosthetic.webp`, promptVal: "prosthetic right arm" }, - { id: "right_arm_mechanical", label: "Mechanical", img: `${CDN}/right_arm_right_arm_mechanical.webp`, promptVal: "mechanical right arm" }, - { id: "right_arm_none", label: "None", img: `${CDN}/right_arm_right_arm_none.webp`, promptVal: "no right arm" }, - ], - }, - { - id: "left_leg", - label: "Left Leg", - options: [ - { id: "left_leg_normal", label: "Normal", img: `${CDN}/left_leg_left_leg_normal.webp`, promptVal: "normal left leg" }, - { id: "left_leg_cute", label: "Cute Prosthetic",img: `${CDN}/left_leg_make_left_leg_stylish_pink_prosthetic_wi.webp`, promptVal: "stylish pink prosthetic left leg with cute stickers" }, - { id: "left_leg_robotic", label: "Robotic", img: `${CDN}/left_leg_left_leg_robotic.webp`, promptVal: "robotic left leg" }, - { id: "left_leg_prosthetic", label: "Prosthetic", img: `${CDN}/left_leg_left_leg_prosthetic.webp`, promptVal: "prosthetic left leg" }, - { id: "left_leg_mechanical", label: "Mechanical", img: `${CDN}/left_leg_left_leg_mechanical.webp`, promptVal: "mechanical left leg" }, - { id: "left_leg_none", label: "None", img: `${CDN}/left_leg_left_leg_none.webp`, promptVal: "no left leg" }, - ], - }, - { - id: "right_leg", - label: "Right Leg", - options: [ - { id: "right_leg_normal", label: "Normal", img: `${CDN}/right_leg_right_leg_normal.webp`, promptVal: "normal right leg" }, - { id: "right_leg_cute", label: "Cute Prosthetic",img: `${CDN}/right_leg_make_right_leg_stylish_pink_prosthetic_w.webp`, promptVal: "stylish pink prosthetic right leg with cute stickers" }, - { id: "right_leg_robotic", label: "Robotic", img: `${CDN}/right_leg_right_leg_robotic.webp`, promptVal: "robotic right leg" }, - { id: "right_leg_prosthetic", label: "Prosthetic", img: `${CDN}/right_leg_right_leg_prosthetic.webp`, promptVal: "prosthetic right leg" }, - { id: "right_leg_mechanical", label: "Mechanical", img: `${CDN}/right_leg_right_leg_mechanical.webp`, promptVal: "mechanical right leg" }, - { id: "right_leg_none", label: "None", img: `${CDN}/right_leg_right_leg_none.webp`, promptVal: "no right leg" }, - ], - }, - ], - }, - style: { - label: "Style", - subcategories: [ - { - id: "hair", - label: "Hair / Head Growth", - options: [ - { id: "hair_bald", label: "Bald", img: `${CDN}/hair_hair_bald.webp`, promptVal: "bald head" }, - { id: "hair_short", label: "Short Hair", img: `${CDN}/hair_hair_short.webp`, promptVal: "short hair" }, - { id: "hair_long", label: "Long Hair", img: `${CDN}/hair_hair_long.webp`, promptVal: "long flowing hair" }, - { id: "hair_afro", label: "Afro", img: `${CDN}/hair_hair_afro.webp`, promptVal: "afro hairstyle" }, - { id: "hair_punk", label: "Punk", img: `${CDN}/hair_hair_punk.webp`, promptVal: "punk mohawk hairstyle" }, - { id: "hair_fur", label: "Fur / Mane", img: `${CDN}/hair_hair_fur.webp`, promptVal: "fur mane on head" }, - { id: "hair_tentacles", label: "Tentacles", img: `${CDN}/hair_hair_tentacles.webp`, promptVal: "tentacles as hair" }, - { id: "hair_spines", label: "Spines", img: `${CDN}/hair_hair_spines.webp`, promptVal: "spines as hair" }, - ], - }, - { - id: "accessories", - label: "Accessories & Markings", - options: [ - { id: "accessory_tattoos", label: "Tattoos", img: `${CDN}/accessories_accessory_tattoos.webp`, promptVal: "covered in tattoos" }, - { id: "accessory_piercing", label: "Piercings", img: `${CDN}/accessories_accessory_piercing.webp`, promptVal: "multiple piercings" }, - { id: "accessory_scarification", label: "Scarification", img: `${CDN}/accessories_accessory_scarification.webp`, promptVal: "ritual scarification marks" }, - { id: "accessory_symbols", label: "Symbols / Markings", img: `${CDN}/accessories_accessory_symbols.webp`, promptVal: "symbolic tribal markings" }, - { id: "accessory_cyber", label: "Cyber Markings", img: `${CDN}/accessories_accessory_cyber.webp`, promptVal: "cyberpunk circuit markings" }, - ], - }, - { - id: "rendering_style", - label: "Rendering Style", - options: [ - { id: "style_hyper_realistic", label: "Hyper-Realistic", img: `${CDN}/character_type_human.webp`, promptVal: "hyper-realistic 8k photograph" }, - { id: "style_anime", label: "Anime", img: `${CDN}/character_type_elf.webp`, promptVal: "anime art style" }, - { id: "style_cartoon", label: "Cartoon", img: `${CDN}/character_type_mantis.webp`, promptVal: "cartoon illustration style" }, - { id: "style_2d", label: "2D Illustration", img: `${CDN}/character_type_alien.webp`, promptVal: "2D flat illustration style" }, - ], - }, - ], - }, -}; - -// ─── SVG Icon Components ──────────────────────────────────────────────────── -const ShuffleIcon = () => ( - - - - -); -const BoltIcon = () => ( - - - -); -const CheckIcon = () => ( - - - -); -const DownloadIcon = () => ( - - - -); - -// ─── Hover Pill — shows label, reveals image on hover ─────────────────────── -function HoverPill({ label, img, onClick }) { - const [hovered, setHovered] = useState(false); - return ( -
setHovered(true)} - onMouseLeave={() => setHovered(false)} - > - {/* Image tooltip */} - {hovered && img && ( -
-
- {label} -
-
- )} - {/* Pill */} - -
- ); -} - -// ─── Main Component ───────────────────────────────────────────────────────── -export default function AiInfluencerStudio({ - apiKey, - onGenerate, - onGenerationComplete, - onGenerationError, - isGenerating: externalIsGenerating, -}) { - const [activeTab, setActiveTab] = useState("face"); - - const [selectedOptions, setSelectedOptions] = useState(() => { - const init = {}; - Object.values(TABS_CONFIG).forEach((tab) => - tab.subcategories.forEach((sub) => { - if (sub.options?.length > 0) init[sub.id] = sub.options[0].id; - }) - ); - return init; - }); - - const [aspectRatio, setAspectRatio] = useState("3:4"); - const [customPrompt, setCustomPrompt] = useState(""); - const [isGeneratingInternal, setIsGeneratingInternal] = useState(false); - const [currentResult, setCurrentResult] = useState(null); // latest generated image - const [history, setHistory] = useState([]); // all generated images - const [selectedHistoryIdx, setSelectedHistoryIdx] = useState(null); - const [errorMsg, setErrorMsg] = useState(""); - - const isGenerating = externalIsGenerating || isGeneratingInternal; - - // ── Build prompt from selections ────────────────────────────────────────── - const buildPrompt = useCallback(() => { - const parts = []; - Object.values(TABS_CONFIG).forEach((tab) => - tab.subcategories.forEach((sub) => { - const opt = sub.options.find((o) => o.id === selectedOptions[sub.id]); - if (opt?.promptVal) parts.push(opt.promptVal); - }) - ); - let prompt = "Ultra-realistic professional portrait photograph of an AI influencer character, 8k resolution, cinematic lighting, sharp detail"; - if (parts.length) prompt += ", " + parts.join(", "); - if (customPrompt.trim()) prompt += ", " + customPrompt.trim(); - return prompt; - }, [selectedOptions, customPrompt]); - - // ── Option selection ─────────────────────────────────────────────────────── - const handleOptionSelect = (subcatId, optId) => - setSelectedOptions((p) => ({ ...p, [subcatId]: optId })); - - // ── Shuffle all options randomly ─────────────────────────────────────────── - const handleShuffle = () => { - const next = {}; - Object.values(TABS_CONFIG).forEach((tab) => - tab.subcategories.forEach((sub) => { - if (sub.options?.length > 0) - next[sub.id] = sub.options[Math.floor(Math.random() * sub.options.length)].id; - }) - ); - setSelectedOptions(next); - }; - - // ── Generate ────────────────────────────────────────────────────────────── - const handleGenerate = async () => { - if (isGenerating) return; - setIsGeneratingInternal(true); - setErrorMsg(""); - - const prompt = buildPrompt(); - try { - let res; - if (onGenerate) { - res = await onGenerate({ prompt, aspectRatio, selections: selectedOptions }); - } else { - res = await generateImage(apiKey, { - model: INFLUENCER_MODEL, - prompt, - aspect_ratio: aspectRatio, - }); - } - if (res?.url) { - setCurrentResult(res.url); - setHistory((prev) => [{ url: res.url, ts: Date.now() }, ...prev]); - setSelectedHistoryIdx(0); - onGenerationComplete?.({ - url: res.url, - model: INFLUENCER_MODEL, - prompt, - type: "image", - }); - } - } catch (err) { - const message = formatErrorMessage(err, "Generation failed. Please try again."); - if (onGenerationError) onGenerationError(message); - else toast.error(message); - } finally { - setIsGeneratingInternal(false); - } - }; - - // ── Download helper ─────────────────────────────────────────────────────── - const downloadImg = async (url) => { - try { - const res = await fetch(url); - const blob = await res.blob(); - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = `ai-influencer-${Date.now()}.webp`; - a.click(); - URL.revokeObjectURL(a.href); - } catch { - window.open(url, "_blank"); - } - }; - - // Preview image = selected history or current result - const previewUrl = - selectedHistoryIdx !== null && history[selectedHistoryIdx] - ? history[selectedHistoryIdx].url - : currentResult; - - const arMap = { "3:4": "3/4", "1:1": "1/1", "9:16": "9/16", "16:9": "16/9" }; - - // ── Collect all selected options as flat list for the pill tags bar ───────── - const selectedTags = []; - Object.keys(TABS_CONFIG).forEach((tabKey) => { - TABS_CONFIG[tabKey].subcategories.forEach((sub) => { - const selId = selectedOptions[sub.id]; - const opt = sub.options.find((o) => o.id === selId); - if (opt) selectedTags.push({ subcatId: sub.id, label: opt.label, img: opt.img }); - }); - }); - - const [showAllTags, setShowAllTags] = useState(false); - const TAGS_VISIBLE = 7; // how many pills to show before "show more" - - return ( -
- - {/* ════════════════════════════════════════════════════════════ - LEFT — Builder / Options Panel - ════════════════════════════════════════════════════════════ */} -
- - {/* Builder header */} -
- Builder - -
- - {/* Tab pills */} -
- {Object.keys(TABS_CONFIG).map((key) => ( - - ))} -
- - {/* Subcategory options scroll area */} -
- {TABS_CONFIG[activeTab]?.subcategories?.map((subcat) => ( -
-

- {subcat.label} -

-
- {subcat.options?.map((opt) => { - const sel = selectedOptions[subcat.id] === opt.id; - return ( - - ); - })} -
-
- ))} -
-
- - {/* ════════════════════════════════════════════════════════════ - CENTER — Current Character Preview - ════════════════════════════════════════════════════════════ */} -
- - {/* Center top bar: aspect ratio + generate */} -
- {/* Aspect ratio */} -
- {["3:4", "1:1", "9:16", "16:9"].map((r) => ( - - ))} -
- -
- {/* Shuffle */} - - - {/* Generate */} - -
-
- - {/* Preview area */} -
-
- {isGenerating ? ( -
-
-

Generating your AI influencer…

-
- ) : previewUrl ? ( - <> - Generated AI Character - {/* Download overlay button */} - - - ) : ( -
- - - -

Your AI influencer lives here.

-

Design and build your AI influencer
from scratch

-
- )} -
-
- - {/* ── Selected option pills ──────────────────────────────────── */} - {selectedTags.length > 0 && ( -
-
- {(showAllTags ? selectedTags : selectedTags.slice(0, TAGS_VISIBLE)).map((tag) => ( - { - // Jump builder panel to the tab that owns this subcategory - const ownerTab = Object.keys(TABS_CONFIG).find((tk) => - TABS_CONFIG[tk].subcategories.some((s) => s.id === tag.subcatId) - ); - if (ownerTab) setActiveTab(ownerTab); - }} - /> - ))} - {selectedTags.length > TAGS_VISIBLE && ( - - )} -
-
- )} - - {/* Error */} - {errorMsg && ( -
- {errorMsg} -
- )} - - {/* Custom prompt bar at bottom */} -
- setCustomPrompt(e.target.value)} - placeholder="Add extra details… e.g. neon cyberpunk lighting, dramatic shadows" - className="w-full h-9 bg-[#161616] border border-white/[0.07] rounded-xl px-3 text-[12px] text-gray-200 placeholder-gray-600 outline-none focus:border-violet-500/40 transition-colors" - /> -
-
- - {/* ════════════════════════════════════════════════════════════ - RIGHT — Generated Characters History Gallery - ════════════════════════════════════════════════════════════ */} -
- - {/* Gallery header */} -
-

Generated

-

{history.length} characters

-
- - {/* Gallery scroll */} -
- {history.length === 0 ? ( -
- - - -

Generated characters
appear here

-
- ) : ( - history.map((item, idx) => ( -
setSelectedHistoryIdx(idx)} - onKeyDown={(e) => e.key === "Enter" && setSelectedHistoryIdx(idx)} - className={`group relative w-full aspect-[3/4] rounded-xl overflow-hidden border transition-all cursor-pointer ${ - selectedHistoryIdx === idx - ? "border-violet-500 ring-1 ring-violet-500/40" - : "border-white/[0.08] hover:border-white/20" - }`} - > - {`Character - {/* Download on hover */} -
-
{ e.stopPropagation(); downloadImg(item.url); }} - onKeyDown={(e) => { if (e.key === "Enter") { e.stopPropagation(); downloadImg(item.url); } }} - className="p-1.5 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20 text-white hover:bg-white/20 transition-all cursor-pointer" - > - -
-
- {/* Index badge */} -
- #{history.length - idx} -
-
- )) - )} -
-
- -
- ); -} + { id: "left_arm_cute", label: "Cute Prosthetic",img: `${CDN}/left_arm_make_lef^kwCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCV@(1PP եȀ=ѥ́A(VCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCV@(؁9􉙱്ܵlt͡ɥɑȵȁɑȵݡєltltٕəܵ((켨 եȁȀ(؁9􉙱ѕ̵ѕȁѥ䵉ݕЁ́ɑȵɑȵݡєlt͡ɥ(9ѕеltеѕеݡєɅѥЈ ե(ѽ( 젤͕Mѕ=ѥ̠(ЁЀ(=йمՕ̡Q M} =9%х(хՉѕɥ̹Ո(ՈѥѠmՈtՈѥlt(((ɕɸ((9ѕеltѕеɅٕѕеݡєɅͥѥ́еմ((I͕(ѽ(𽑥((켨Q̀(؁9􉙱ā́ȁɑȵɑȵݡєlt͡ɥ(=й̡Q M} =9%䤀(ѽ(( 젤͕ѥٕQ(9큙āĸԁɽչѕеltе͕Ʌͥѥ(ѥٕQ􁭕(ݡєѕе͡܈(耉ѕеɅٕѕеݡєٕ鉜ݡєlt(((Q M} =9%mt(ѽ((𽑥((켨MՉѕ䁽ѥ͍́ɽɕ(؁9􉙱āٕəܵ䵅Ѽ́Ԉ(Q M} =9%mѥٕQtՉѕɥՉФ(؁Չй(9ѕеltеѕеɅɍ͔ɅݥЁȁԈ(Չй((؁9ɥɥ̴́ĸԈ(ՉйѥФ(Ё͕͕ѕ=ѥmՉйtй(ɕɸ(ѽ(й( 젤=ѥMСՉйй(9큝ɽɕѥٔеՅɔɽչᰁٕəܵɑȁɅͥѥ(͕(ɑȵݡєɥāɥݡєܵ͡(耉ɑȵݡєltٕ鉽ɑȵݡєԈ(((񥵜(Ɍй(й(􉱅(9ܵձձеٕȈ(ɽ졔쁔хɝйɽȀձ쁔хɝйɌ􁀑 9Ʌѕ}}յݕ((켨1ٕɱ䀨(؁9􉅉ͽє͕еѽɅеѼЁɽ٥ѼɅɕЁдЁāĈ(9ѕеltе͕ѕеݡєй(𽑥(켨Mѕ(͕(؁9􉅉ͽєѽāɥдāܴЁЁɽչձݡєѕеѕ̵ѕȁѥ䵍ѕȈ( %(𽑥((ѽ(((𽑥(𽑥((𽑥(𽑥((켨VCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCV@( 9QHP ɕЁ ɅѕȁAɕ٥(VCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCV@(؁9􉙱്āܴٕəܵlt((켨 ѕȁѽ聅ЁɅѥɅє(؁9􉙱ѕ̵ѕȁѥ䵉ݕ؁́ɑȵɑȵݡєlt͡ɥ(켨ЁɅѥ(؁9􉙱ԁݡєltɑȁɑȵݡєltɽչᰁĈ(lЈĈ؈tȤ(ѽ(( 젤͕Iѥȥ(9́ĸԁɽչѕеltеɅͥѥ(Iѥ(٥дѕеݡєܵܵ͡͡٥д(耉ѕеɅٕѕеݡє((((ѽ((𽑥((؁9􉙱ѕ̵ѕȁȈ(켨Mՙ(ѽ( Mՙ(9􉙱ѕ̵ѕȁĸԁ́ȁɽչᰁݡєltɑȁɑȵݡєltѕеɅٕѕеݡєٕ鉜ݡєѕеltе͕Ʌͥѥ((Mՙ%(Mՙ(ѽ((켨Ʌє(ѽ( Ʌѕ(ͅɅѥ(9큙ѕ̵ѕȁȁԁȁɽչᰁѕеltеɅͥѥܵ͡(Ʌѥ(٥дѕеݡєͽȵеݕ(耉ɅеѼȁɽ٥дѼٕɽ٥дٕѼѕеݡєܵ͡٥дٕܵ͡٥д(((Ʌѥ((ٜ9􉅹єݥѠЈЈ٥ ЀЈ􉹽ɽɕ Ȉɽ]ѠȸԈ(Ѡ4ĀɄĴ舁ɽ=̈(Ѡ4ĀɄ䈀(ٜ(Ʌѥ((耠( %Ʌє Ʌѕ((ѽ(𽑥(𽑥((켨Aɕ٥܁ɕ(؁9􉙱āѕ̵ѕȁѥ䵍ѕȁ؁ٕəܵ((9ɕѥٔɽչᰁٕəܵltɑȁɑȵݡєltܴ͡ᰁѕ̵ѕȁѥ䵍ѕȈ(屔쁅Iѥ聅5mIѥt̼Ј!耈]Ѡ耈((Ʌѥ(؁9􉙱്ѕ̵ѕȁЁѕеѕȁȈ(؁9ܴȁȁɑȵltɑȵ٥дɑȵе٥дɽչձє(9ѕеʹѕеɅеմɅѥȁ$Օˊ(𽑥(ɕ٥Uɰ((񥵜Ɍɕ٥Uɱ􁅱Ʌѕ$ ɅѕȈ9ܵձձеٕȈ(켨ݹٕɱ䁉ѽ(ѽ( 젤ݹ%ɕ٥Uɰ(9􉅉ͽєѽ́ɥд́ѕ̵ѕȁĸԁ́ĸԁɽչɽȵʹɑȁɑȵݡєѕеݡєѕеltе͕ٕ鉜Ʌͥѥ((ݹ%(Mٔ(ѽ((耠(؁9􉙱്ѕ̵ѕȁ́ѕеѕȁȈ(ٜݥѠ٥ ЀЈ􉹽ɽɕ Ȉɽ]Ѡ9ѕеɅ(Ѡ4شɄЀЀд ЀЀЀȈɍȈ܈Ј(ٜ(9ѕеʹѕеɅеմeȁ$Օȁٕ́ɔ(9ѕе́ѕеɅͥեȁ$ՕȀɽ͍Ʌэ(𽑥((𽑥(𽑥((켨RRMѕѥ̃RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR(͕ѕQ̹Ѡ(؁9؁́͡ɥ(؁9􉙱Ʌĸԁѕ̵ѕȈ(͡Q͕̀ѕQ͕̀ѕQ̹ͱQM}Y%M% 1х(!ٕA(хՉ%(х(х( 젤()յեȁѼѡхѡЁݹ́ѡ́Չѕ(ЁݹQ=й̡Q M} =9%Ѭ(Q M} =9%mѭtՉѕɥ̹ͽ̤̹хՉ%((ݹQ͕ѥٕQݹQ((((͕ѕQ̹ѠQM}Y%M% 1(ѽ(ѽ( 젤͕MQ̠ؤإ(9􉠵ltȁɽչݡєltٕ鉜ݡєltɑȁɑȵݡєltѕеltѕеɅٕѕеɅݡѕɅɅͥѥ((͡Q̀聁͡܁ɕ(ѽ((𽑥(𽑥(((켨ɽȀ(ɽ5͜(؁9؁ЁЁ́ɽչᰁɕɑȁɑȵɕѕеɕѕеlt͡ɥ(ɽ5͝(𽑥(((켨 ѽɽЁȁЁѽ(؁9؁Ё͡ɥ((ѕЈ(مՔѽAɽ( 졔͕ ѽAɽСхɝйمՔ(Ʉхϊ剕չѥɅѥ̈͡(9ܵձ䁉ltɑȁɑȵݡєltɽչᰁ́ѕеltѕеɅȵɅѱ鉽ɑȵ٥дɅͥѥ̈((𽑥(𽑥((켨VCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCV@(I%!PPɅѕ Ʌѕ́!ѽ(VCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCVCV@(؁9􉙱്ܵlt͡ɥɑȵɑȵݡєltltٕəܵ((켨䁡Ȁ(؁9́́ɑȵɑȵݡєlt͡ɥ(9ѕеltеѕеݡєɅѥЈɅѕ(9ѕеltѕеɅдԈѽ乱ѡ􁍡Ʌѕ(𽑥((켨͍ɽ(؁9􉙱āٕəܵ䵅ѼȁȈ(ѽ乱Ѡ(؁9􉙱്ѕ̵ѕȁѥ䵍ѕȁȁѕеѕȁȈ(ٜݥѠ٥ ЀЈ􉹽ɽɕ Ȉɽ]ѠĈ9ѕеɅȈ(ɕЁ̈̈ݥѠȈȈɍԈԈĸԈ履ĀԀ؀ԀĈ(ٜ(9ѕеltѕеɅɕᕐɅѕɅѕȀȁɔ(𽑥(耠(ѽ乵ѕऀ((ѕ(ɽѽ(х%( 젤͕Mѕ!ѽ%ࡥ(-ݸ졔ѕȈ͕Mѕ!ѽ%ࡥ(9큝ɽɕѥٔܵձеl̼tɽչᰁٕəܵɑȁɅͥѥͽȵѕȀ(͕ѕ!ѽ%􁥑(ɑȵ٥дɥāɥ٥д(耉ɑȵݡєltٕ鉽ɑȵݡє(((񥵜Ɍѕɱ􁅱 ɅѕȀ􁍱9ܵձձеٕȈ(켨ݹٕȀ(؁9􉅉ͽє͕дɽٕɅͥѥ䁙ѕ̵ѥ䵍ѕȁȈ((ɽѽ(х%( 졔쁔ѽAɽѥ쁑ݹ%ѕɰ(-ݸ졔쁥ѕȈ쁔ѽAɽѥ쁑ݹ%ѕɰ(9ĸԁɽչݡєɽȵʹɑȁɑȵݡєѕеݡєٕ鉜ݡєɅͥѥͽȵѕȈ((ݹ%(𽑥(𽑥(켨%(؁9􉅉ͽєѽāдāĸԁԁɽչɽȵʹѕеltѕеɅе(ѽ乱Ѡ(𽑥(𽑥(((𽑥(𽑥(QѕȁͥѥѽɥЈхM屔%ѽ=ѥ쁑Ʌѥ屔쁉ɽչ耜ň耜ɑ耜ͽɝ԰԰԰ԤM耜ɑI耜M耜ɝؤ]Ѡ耜ݽɑ ɕ耝ɕݽɐݡѕM耝ɔɅ耜(𽑥() \ No newline at end of file diff --git a/packages/studio/src/components/AudioStudio.jsx b/packages/studio/src/components/AudioStudio.jsx index d29b8e124..5ff337029 100644 --- a/packages/studio/src/components/AudioStudio.jsx +++ b/packages/studio/src/components/AudioStudio.jsx @@ -431,689 +431,4 @@ function PremiumAudioPlayer({ url, title }) { title="Mute/Unmute" type="button" > - {isMuted ? : } - - -
- - {/* Main Play/Pause Button */} - - - {/* Download Button */} - -
- - - ); -} - -// --------------------------------------------------------------------------- -// Main Audio Studio Component -// --------------------------------------------------------------------------- -export default function AudioStudio({ - apiKey, - onGenerationComplete, - onGenerationError, - historyItems, - droppedFiles, - onFilesHandled, -}) { - const LEGACY_PERSIST_KEY = "hg_audio_studio_persistent"; - const PERSIST_KEY = scopedPersistKey(LEGACY_PERSIST_KEY, apiKey); - useEffect(() => { - migrateLegacyPersistKey(LEGACY_PERSIST_KEY, PERSIST_KEY); - }, [PERSIST_KEY]); - - // ── Mode & model state ────────────────────────────────────────────────── - const [selectedModelId, setSelectedModelId] = useState(audioModels[0]?.id ?? ""); - const [params, setParams] = useState({}); - const [openDropdown, setOpenDropdown] = useState(false); - const [openParamDropdown, setOpenParamDropdown] = useState(null); - const modelBtnRef = useRef(null); - const sidebarRef = useRef(null); - - // Close dropdowns on outside click - useEffect(() => { - const handler = (e) => { - if (sidebarRef.current && !sidebarRef.current.contains(e.target)) { - setOpenDropdown(false); - setOpenParamDropdown(null); - } - }; - window.addEventListener("click", handler); - return () => window.removeEventListener("click", handler); - }, []); - - // ── Generation state ────────────────────────────────────────────────── - const [isGenerating, setIsGenerating] = useState(false); - const [generateError, setGenerateError] = useState(null); - const [activeResultUrl, setActiveResultUrl] = useState(null); - const [activeResultTitle, setActiveResultTitle] = useState(""); - const [view, setView] = useState("input"); // 'input' | 'result' - - // ── History state ──────────────────────────────────────────────────── - const [internalHistory, setInternalHistory] = useState([]); - const history = historyItems ?? internalHistory; - const [activeHistoryIdx, setActiveHistoryIdx] = useState(0); - - const selectedModel = getAudioModelById(selectedModelId); - - // ── Initialize params when model changes ────────────────────────────── - useEffect(() => { - if (!selectedModel) return; - const initial = {}; - Object.entries(selectedModel.inputs || {}).forEach(([key, schema]) => { - // Don't overwrite parameters like vocal upload, list etc. if they are already in state - if (params[key] !== undefined) { - initial[key] = params[key]; - } else { - initial[key] = schema.default !== undefined ? schema.default : ""; - } - }); - setParams(initial); - }, [selectedModelId]); // Only reset when model ID changes - - // ── Persistence: Load ──────────────────────────────────────────────────── - useEffect(() => { - try { - const stored = localStorage.getItem(PERSIST_KEY); - if (stored) { - const data = JSON.parse(stored); - if (data.selectedModelId) setSelectedModelId(data.selectedModelId); - if (data.params) setParams(data.params); - if (data.internalHistory) setInternalHistory(data.internalHistory); - if (data.activeResultUrl) setActiveResultUrl(data.activeResultUrl); - if (data.activeResultTitle) setActiveResultTitle(data.activeResultTitle); - if (data.view) setView(data.view); - } - } catch (err) { - console.warn("Failed to load AudioStudio persistence:", err); - } - }, []); - - // ── Persistence: Save ──────────────────────────────────────────────────── - useEffect(() => { - const timer = setTimeout(() => { - try { - const state = { - selectedModelId, - params, - internalHistory, - activeResultUrl, - activeResultTitle, - view, - }; - localStorage.setItem(PERSIST_KEY, JSON.stringify(state)); - } catch (err) { - console.warn("Failed to save AudioStudio persistence:", err); - } - }, 500); - return () => clearTimeout(timer); - }, [selectedModelId, params, internalHistory, activeResultUrl, activeResultTitle, view]); - - // ── Handle Dropped Files ──────────────────────────────────────────────── - useEffect(() => { - if (droppedFiles && droppedFiles.length > 0) { - const audioFiles = droppedFiles.filter(f => f.type.startsWith('audio/')); - if (audioFiles.length > 0 && selectedModel) { - // Find the first audio input field in the current model - const firstAudioField = Object.entries(selectedModel.inputs || {}).find( - ([_, schema]) => schema.field === 'audio' - ); - const firstAudioListField = Object.entries(selectedModel.inputs || {}).find( - ([_, schema]) => schema.field === 'audios_list' - ); - - if (firstAudioField) { - const [key] = firstAudioField; - // Trigger file upload helper - uploadFile(apiKey, audioFiles[0], () => {}) - .then(url => { - setParams(prev => ({ ...prev, [key]: url })); - }) - .catch(err => alert(`Failed to upload dropped file: ${err.message}`)); - } else if (firstAudioListField) { - const [key] = firstAudioListField; - uploadFile(apiKey, audioFiles[0], () => {}) - .then(url => { - setParams(prev => { - const currentList = Array.isArray(prev[key]) ? [...prev[key]] : []; - if (currentList.length < 2) currentList.push(url); - return { ...prev, [key]: currentList }; - }); - }) - .catch(err => alert(`Failed to upload dropped file: ${err.message}`)); - } - } - onFilesHandled?.(); - } - }, [droppedFiles, onFilesHandled, selectedModel, apiKey]); - - // ── History helpers ───────────────────────────────────────────────────── - const addToInternalHistory = useCallback((entry) => { - setInternalHistory((prev) => [entry, ...prev].slice(0, 30)); - }, []); - - const handleSelectHistory = (entry, index) => { - setActiveResultUrl(entry.url); - setActiveResultTitle(entry.title || entry.prompt || "Generated Track"); - setActiveHistoryIdx(index); - setView("result"); - }; - - const handleGenerate = async () => { - if (!selectedModel) return; - - // Check required fields - if (selectedModel.required) { - for (const field of selectedModel.required) { - if (!params[field] || (Array.isArray(params[field]) && params[field].length === 0)) { - alert(`Please complete the required field: ${selectedModel.inputs?.[field]?.title || field}`); - return; - } - } - } - - setIsGenerating(true); - setGenerateError(null); - - try { - const audioParams = { - ...params, - _modelId: selectedModelId, - }; - - // Call generateAudio - const res = await generateAudio(apiKey, audioParams); - - if (!res?.url) { - throw new Error("No audio URL returned by the API."); - } - - const title = params.title || params.prompt || `Generated ${selectedModel.name}`; - const entry = { - id: res.id || Date.now().toString(), - url: res.url, - title, - prompt: params.prompt || "", - model: selectedModelId, - timestamp: new Date().toISOString(), - }; - - if (!historyItems) addToInternalHistory(entry); - - setActiveResultUrl(res.url); - setActiveResultTitle(title); - setView("result"); - setActiveHistoryIdx(0); - - if (onGenerationComplete) { - onGenerationComplete({ - url: res.url, - model: selectedModelId, - prompt: params.prompt, - type: "audio", - }); - } - } catch (e) { - console.error("[AudioStudio]", e); - const errMsg = formatErrorMessage(e, "Audio generation failed"); - if (onGenerationError) onGenerationError(errMsg); - else toast.error(errMsg); - } finally { - setIsGenerating(false); - } - }; - - const handleNew = () => { - setView("input"); - setActiveResultUrl(null); - setActiveResultTitle(""); - // Keep parameters to avoid having to reupload files if they wish to adjust details - }; - - return ( -
- - {/* ─── LEFT CONFIGURATION SIDEBAR ─── */} -
-
- - {/* Model Selector */} -
- - - - {openDropdown && ( -
- {audioModels.map((model) => ( - - ))} -
- )} -
- - {/* Model Description */} - {selectedModel?.description && ( -
- Description -

{selectedModel.description}

-
- )} - - {/* Dynamic Configuration Form */} -
- {selectedModel && Object.entries(selectedModel.inputs || {}).map(([key, schema]) => { - // Skip model switcher itself (if it's in schemas) - if (key === 'model') return null; - // Audio URL file upload (single) - if (schema.type === "string" && schema.field === "audio") { - return ( - setParams(prev => ({ ...prev, [key]: url }))} - apiKey={apiKey} - /> - ); - } - // Audio URLs list file upload (multiple) - if (schema.type === "array" && schema.field === "audios_list") { - return ( - setParams(prev => ({ ...prev, [key]: urls }))} - apiKey={apiKey} - maxItems={schema.maxItems || 2} - /> - ); - } - // Boolean Toggles - if (schema.type === "boolean") { - return ( -
-
- - {schema.title || key} - - {schema.description && ( - - {schema.description} - - )} -
- -
- ); - } - // Enum Dropdowns - if (schema.enum) { - const isOpen = openParamDropdown === key; - return ( -
- - - - {isOpen && ( -
- {schema.enum.map((opt) => ( - - ))} -
- )} - {schema.description && ( - - {schema.description} - - )} -
- ); - } - - // Number Sliders & Ranges - const isNumber = schema.type === "int" || schema.type === "integer" || schema.type === "float" || schema.type === "number"; - const hasMinMax = schema.minValue !== undefined && schema.maxValue !== undefined; - if (isNumber && hasMinMax) { - const step = schema.step || (schema.type === "float" ? 0.05 : 1); - return ( -
-
- {schema.title || key} - {params[key] !== undefined ? params[key] : schema.default} -
-
- {schema.minValue} - setParams(prev => ({ ...prev, [key]: parseFloat(e.target.value) }))} - className="flex-1 h-1.5 bg-zinc-800 rounded-full appearance-none cursor-pointer accent-primary hover:bg-zinc-700 transition-all" - /> - {schema.maxValue} -
- {schema.description && ( - - {schema.description} - - )} -
- ); - } - - // Prompt / Textarea Input - if (key === "prompt") { - return ( -
- -