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() {
{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' && (
{ handleTabChange(notif.tabId); dismissNotification(notif.id); }}
- className="mt-1.5 text-[11px] font-bold text-[#22d3ee] hover:underline"
+ type="button"
+ onClick={() => handleOpenNotification(notif)}
+ className="mt-1.5 text-[11px] font-bold text-cyan-300 transition-colors hover:text-cyan-100"
+ aria-label={`Open ${notif.label} result`}
>
- Open →
+ Open
)}
- {/* Dismiss */}
dismissNotification(notif.id)}
- className="flex-shrink-0 text-white/30 hover:text-white/70 transition-colors text-lg leading-none mt-0.5"
- aria-label="Dismiss"
+ className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-white/5 hover:text-zinc-200 focus:outline-none focus:ring-1 focus:ring-white/20"
+ aria-label="Dismiss notification"
>
- ×
+
+
+
))}
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}
+
dismissErrorToast(toastInstance.id)}
+ className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-white/5 hover:text-zinc-200 focus:outline-none focus:ring-1 focus:ring-red-400/60"
+ aria-label="Dismiss notification"
+ >
+
+
+
+
+
+);
+
+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 (
-
- {/* Drag Overlay */}
- {isDragging && (
-
-
-
-
- Drop your media here
- Images, videos, or audio files
-
-
-
- )}
-
- {/* Vadoo promo banner */}
- {showVadooBanner && (
-
- )}
-
- {/* Header */}
- {isHeaderVisible && (
-
- {/* Left: Mobile menu toggle + Logo + Desktop Sidebar Toggle */}
-
- {/* Mobile drawer toggle */}
-
setIsMobileOpen(!isMobileOpen)}
- className="md:hidden p-2 rounded-lg bg-white/5 hover:bg-white/10 text-white/70 hover:text-white transition-colors"
- aria-label="Toggle Navigation Menu"
- >
-
-
-
-
-
-
-
- {/* 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}` : '---'}
-
-
-
-
setShowSettings(true)}
- className="flex items-center gap-2 px-3 py-1.5 rounded-md border border-white/10 bg-white/5 text-[13px] font-bold text-white/80 hover:text-white hover:bg-white/10 hover:border-white/20 transition-colors"
- aria-label="Settings"
- >
-
-
-
-
- Settings
-
-
-
- )}
-
- {/* Main Body Layout: Left Sidebar + Studio Content Area */}
-
- {/* Mobile Backdrop Overlay */}
- {isMobileOpen && (
-
setIsMobileOpen(false)}
- />
- )}
-
- {/* Left Sidebar Navigation */}
- {isHeaderVisible && (
-
-
-
- {NAVIGATION_CATEGORIES.map((category) => {
- const isCategoryActive = activeCategory?.id === category.id;
- const isCollapsed = isSidebarCollapsed && !isMobileOpen;
- const isCategoryOpen = !isCollapsed && expandedCategoryId === category.id;
- const categoryPanelId = `navigation-category-${category.id}`;
-
- return (
-
-
handleCategoryToggle(category.id)}
- aria-label={category.label}
- aria-expanded={isCategoryOpen}
- aria-controls={isCollapsed ? undefined : categoryPanelId}
- title={isCollapsed ? category.label : undefined}
- className={`
- group relative flex items-center rounded-xl transition-all duration-150 font-semibold
- ${isCollapsed ? 'h-11 w-11 justify-center mx-auto' : 'px-3 py-2.5 w-full gap-3 text-left'}
- ${isCategoryActive
- ? 'bg-gradient-to-r from-[#22d3ee]/15 to-purple-500/10 text-[#22d3ee] border border-[#22d3ee]/20 shadow-[0_0_15px_rgba(34,211,238,0.08)]'
- : isCategoryOpen
- ? 'bg-white/[0.06] text-white border border-white/[0.08]'
- : 'text-white/60 hover:text-white hover:bg-white/[0.04] border border-transparent'
- }
- `}
- >
- {isCategoryActive && (
-
- )}
-
-
- {category.icon}
-
-
- {!isCollapsed && (
- <>
-
- {category.label}
-
-
-
-
- >
- )}
-
-
- {!isCollapsed && isCategoryOpen && (
-
- )}
-
- );
- })}
-
-
- {EXPLORE_APPS_TAB && (
-
- )}
-
-
- )}
-
- {/* 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' && (
-
handleOpenNotification(notif)}
- className="mt-1.5 text-[11px] font-bold text-cyan-300 transition-colors hover:text-cyan-100"
- aria-label={`Open ${notif.label} result`}
- >
- Open
-
- )}
-
-
-
dismissNotification(notif.id)}
- className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-white/5 hover:text-zinc-200 focus:outline-none focus:ring-1 focus:ring-white/20"
- aria-label="Dismiss notification"
- >
-
-
-
-
-
- ))}
-
- )}
-
- {/* Keyframe for toast slide-in */}
-
-
- {/* Settings Modal */}
- {showSettings && (
-
-
-
Settings
-
- Manage your AI studio preferences and authentication.
-
-
-
-
-
- Active API Key
-
-
- {apiKey.slice(0, 8)}••••••••••••••••
-
-
-
-
-
-
- Change Key
-
- setShowSettings(false)}
- className="flex-1 h-10 rounded-md bg-white/5 text-white/80 hover:bg-white/10 text-xs font-semibold transition-all border border-white/5"
- >
- Close
-
-
-
-
- )}
-
- );
-}
+ 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Ѽltɽչȵձ((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ɽչɥՔ(Ѡ44ٰȀȈ(ٜ(ѽ((((((켨-ɅȁѽЁͱ(屔(Ʌ́ͱ%IЁ(ɽɅ͙ɴɅͱѕ`쁽(ѼɅ͙ɴɅͱѕ`쀀((屔((켨Mѥ́5(͡Mѥ̀(9ᕐ͕дɽȵʹѕ̵ѕȁѥ䵍ѕȁє(9ltɑȁɑȵݡєɽչᰁܵձܵʹܴ͡ᰈ(ȁ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 && (
-
-
-
-
-
- )}
- {/* Pill */}
-
- {label}
-
-
- );
-}
-
-// ─── 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
- setSelectedOptions((() => {
- 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;
- })())}
- className="text-[11px] text-gray-500 hover:text-white transition-colors font-medium"
- >
- Reset
-
-
-
- {/* Tab pills */}
-
- {Object.keys(TABS_CONFIG).map((key) => (
- setActiveTab(key)}
- className={`flex-1 py-1.5 rounded-lg text-[12px] font-semibold transition-all ${
- activeTab === key
- ? "bg-white text-black shadow"
- : "text-gray-500 hover:text-white hover:bg-white/[0.06]"
- }`}
- >
- {TABS_CONFIG[key].label}
-
- ))}
-
-
- {/* Subcategory options scroll area */}
-
- {TABS_CONFIG[activeTab]?.subcategories?.map((subcat) => (
-
-
- {subcat.label}
-
-
- {subcat.options?.map((opt) => {
- const sel = selectedOptions[subcat.id] === opt.id;
- return (
-
handleOptionSelect(subcat.id, opt.id)}
- className={`group relative aspect-square rounded-xl overflow-hidden border transition-all ${
- sel
- ? "border-white/80 ring-1 ring-white/30 shadow-lg"
- : "border-white/[0.08] hover:border-white/25"
- }`}
- >
- { e.target.onerror = null; e.target.src = `${CDN}/character_type_human.webp`; }}
- />
- {/* Label overlay */}
-
- {opt.label}
-
- {/* Selected check badge */}
- {sel && (
-
-
-
- )}
-
- );
- })}
-
-
- ))}
-
-
-
- {/* ════════════════════════════════════════════════════════════
- CENTER — Current Character Preview
- ════════════════════════════════════════════════════════════ */}
-
-
- {/* Center top bar: aspect ratio + generate */}
-
- {/* Aspect ratio */}
-
- {["3:4", "1:1", "9:16", "16:9"].map((r) => (
- setAspectRatio(r)}
- className={`px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all ${
- aspectRatio === r
- ? "bg-violet-600 text-white shadow-md shadow-violet-600/40"
- : "text-gray-500 hover:text-white"
- }`}
- >
- {r}
-
- ))}
-
-
-
- {/* Shuffle */}
-
-
- Shuffle
-
-
- {/* Generate */}
-
- {isGenerating ? (
- <>
-
-
-
-
- Generating…
- >
- ) : (
- <> Generate Character>
- )}
-
-
-
-
- {/* Preview area */}
-
-
- {isGenerating ? (
-
-
-
Generating your AI influencer…
-
- ) : previewUrl ? (
- <>
-
- {/* Download overlay button */}
-
downloadImg(previewUrl)}
- className="absolute bottom-3 right-3 flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-black/60 backdrop-blur-sm border border-white/10 text-white text-[11px] font-semibold hover:bg-black/80 transition-all"
- >
-
- Save
-
- >
- ) : (
-
-
-
-
-
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 && (
- setShowAllTags((v) => !v)}
- className="h-[22px] px-2 rounded-md bg-white/[0.04] hover:bg-white/[0.09] border border-white/[0.08] text-[11px] text-gray-500 hover:text-gray-300 whitespace-nowrap transition-all"
- >
- {showAllTags ? "hide" : `show more`}
-
- )}
-
-
- )}
-
- {/* 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"
- }`}
- >
-
- {/* 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ѕе́ѕеɅͥեȁ$ՕȀɽ͍Ʌэ((((((켨RR Mѕѥ̃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̠ؤإ(9ltȁɽչݡє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 */}
-