'use client'; import { useState, useEffect, useCallback } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { ImageStudio, VideoStudio, LipSyncStudio, CinemaStudio, MarketingStudio, WorkflowStudio, AgentStudio, AppsStudio, getUserBalance } from 'studio'; import axios from 'axios'; import ApiKeyModal from './ApiKeyModal'; const TABS = [ { id: 'image', label: 'Image Studio' }, { id: 'video', label: 'Video Studio' }, { id: 'lipsync', label: 'Lip Sync' }, { id: 'cinema', label: 'Cinema Studio' }, { id: 'marketing', label: 'Marketing Studio' }, { id: 'workflows', label: 'Workflows' }, { id: 'agents', label: 'Agents' }, { id: 'apps', label: 'Explore Apps' }, ]; const STORAGE_KEY = 'muapi_key'; export default function StandaloneShell() { const params = useParams(); const router = useRouter(); const slug = params?.slug || []; const idFromParams = params?.id; const tabFromParams = params?.tab; // Helper to extract workflow details precisely from either route structure const getWorkflowInfo = useCallback(() => { if (idFromParams) { return { id: idFromParams, tab: tabFromParams || null }; } const wfIndex = slug.findIndex(s => s === 'workflows' || s === 'workflow'); if (wfIndex === -1) return { id: null, tab: null }; return { id: slug[wfIndex + 1] || null, tab: slug[wfIndex + 2] || null }; }, [slug, idFromParams, tabFromParams]); const { id: urlWorkflowId } = getWorkflowInfo(); // Initialize activeTab from URL slug/params or default to 'image' const getInitialTab = () => { if (idFromParams || slug.includes('workflow')) return 'workflows'; if (slug.includes('agents')) return 'agents'; if (slug.includes('apps')) return 'apps'; const firstSegment = slug[0]; if (firstSegment && TABS.find(t => t.id === firstSegment)) return firstSegment; return 'image'; }; const [apiKey, setApiKey] = useState(null); const [activeTab, setActiveTab] = useState(getInitialTab()); const [balance, setBalance] = useState(null); const [showSettings, setShowSettings] = useState(false); const [isHeaderVisible, setIsHeaderVisible] = useState(true); const [hasMounted, setHasMounted] = useState(false); // Drag and Drop State const [isDragging, setIsDragging] = useState(false); const [droppedFiles, setDroppedFiles] = useState(null); // Sync tab with URL if user navigates manually or via browser back/forward useEffect(() => { const info = getWorkflowInfo(); if (info.id) { setActiveTab('workflows'); } else if (slug.includes('agents')) { setActiveTab('agents'); } else if (slug.includes('apps')) { setActiveTab('apps'); } else { const firstSegment = slug[0]; if (firstSegment && TABS.find(t => t.id === firstSegment)) { setActiveTab(firstSegment); } } }, [slug, getWorkflowInfo]); const handleTabChange = (tabId) => { setActiveTab(tabId); router.push(`/studio/${tabId}`); }; // Auto-hide header when inside a specific workflow view useEffect(() => { const isEditingWorkflow = (activeTab === 'workflows' || !!idFromParams) && urlWorkflowId; if (isEditingWorkflow) { setIsHeaderVisible(false); } else { setIsHeaderVisible(true); } }, [activeTab, urlWorkflowId, idFromParams]); // Global builder CSS cleanup when switching away from Workflows tab useEffect(() => { const fromBuilder = sessionStorage.getItem("fromWorkflowBuilder"); if (fromBuilder && activeTab !== 'workflows') { sessionStorage.removeItem("fromWorkflowBuilder"); 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 (
Manage your AI studio preferences and authentication.