feat: finalize studio persistence and multiple reference image upload UI

This commit is contained in:
Jaya Prasad Kavuru 2026-04-10 18:40:41 +05:30
parent b924f0caf8
commit 91ce11df6f
9 changed files with 3765 additions and 2741 deletions

View file

@ -2,21 +2,32 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
body { body {
background: #050505; background: #050505;
color: white; color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased;
} }
:root { :root {
--color-primary: #d9ff00; --color-primary: #d9ff00;
--bg-app: #050505; --bg-app: #030303;
--bg-panel: #0a0a0a; --bg-panel: #0a0a0a;
--bg-card: #111111; --bg-card: #111111;
--border-color: rgba(255,255,255,0.08); --border-color: rgba(255,255,255,0.05);
--border-radius-xl: 1rem; --border-radius-xl: 0.75rem;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
}
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
} }
.custom-scrollbar::-webkit-scrollbar { width: 4px; } .custom-scrollbar::-webkit-scrollbar { width: 4px; }
@ -24,7 +35,7 @@ body {
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; } .custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
@keyframes fade-in-up { @keyframes fade-in-up {
from { opacity: 0; transform: translateY(16px); } from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.animate-fade-in-up { animation: fade-in-up 0.4s ease forwards; } .animate-fade-in-up { animation: fade-in-up 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; }

View file

@ -14,50 +14,50 @@ export default function ApiKeyModal({ onSave }) {
}; };
return ( return (
<div className="min-h-screen bg-[#050505] flex items-center justify-center px-4"> <div className="min-h-screen bg-[#030303] flex items-center justify-center px-4 font-inter">
<div className="w-full max-w-md bg-[#0a0a0a] border border-white/10 rounded-3xl p-8"> <div className="w-full max-w-sm bg-[#0a0a0a]/40 backdrop-blur-xl border border-white/10 rounded-xl p-10 shadow-2xl">
<div className="flex flex-col items-center text-center mb-8"> <div className="flex flex-col items-center text-center mb-10">
<div className="w-16 h-16 bg-[#d9ff00]/10 rounded-2xl flex items-center justify-center border border-[#d9ff00]/20 mb-6"> <div className="w-14 h-14 bg-[#d9ff00]/5 rounded-2xl flex items-center justify-center border border-[#d9ff00]/10 mb-6 group hover:border-[#d9ff00]/30 transition-colors">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#d9ff00" strokeWidth="1.5"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#d9ff00" strokeWidth="1.5" className="group-hover:scale-110 transition-transform">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L12 17.25l-4.5-4.5L15.5 7.5z"/> <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L12 17.25l-4.5-4.5L15.5 7.5z" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</div> </div>
<h1 className="text-2xl font-black text-white uppercase tracking-wider mb-2"> <h1 className="text-xl font-bold text-white tracking-tight mb-2">
Open Higgsfield AI Open Higgsfield AI
</h1> </h1>
<p className="text-white/40 text-sm"> <p className="text-white/40 text-[13px] leading-relaxed px-4">
Enter your <a href="https://muapi.ai" target="_blank" rel="noreferrer" className="text-[#d9ff00] hover:underline">Muapi.ai</a> API key to start generating Enter your <a href="https://muapi.ai" target="_blank" rel="noreferrer" className="text-[#d9ff00] hover:text-[#e5ff33] transition-colors">Muapi.ai</a> API key to start creating
</p> </p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-6">
<div> <div className="space-y-2">
<label className="block text-xs font-bold text-white/40 uppercase tracking-widest mb-2"> <label className="block text-xs font-bold text-white/30 ml-1">
Muapi API Key API Access Key
</label> </label>
<input <input
type="password" type="password"
value={key} value={key}
onChange={(e) => { setKey(e.target.value); setError(''); }} onChange={(e) => { setKey(e.target.value); setError(''); }}
placeholder="Enter your API key..." placeholder="Paste your key here..."
className="w-full bg-black/40 border border-white/5 rounded-xl px-4 py-3 text-white placeholder:text-white/20 focus:outline-none focus:border-[#d9ff00]/40 transition-colors" className="w-full bg-white/5 border border-white/[0.03] rounded-md px-5 py-3 text-sm text-white placeholder:text-white/10 focus:outline-none focus:ring-1 focus:ring-[#d9ff00]/30 focus:bg-white/[0.07] transition-all"
suppressHydrationWarning suppressHydrationWarning
/> />
{error && <p className="mt-1 text-red-400 text-xs">{error}</p>} {error && <p className="mt-2 text-red-500/80 text-[11px] font-medium ml-1">{error}</p>}
</div> </div>
<button <button
type="submit" type="submit"
className="w-full bg-[#d9ff00] text-black font-black py-3 rounded-xl hover:opacity-90 transition-opacity" className="w-full bg-[#d9ff00] text-black font-medium py-2.5 rounded-md hover:bg-[#e5ff33] hover:scale-[1.02] active:scale-[0.98] transition-all shadow-lg shadow-[#d9ff00]/5"
suppressHydrationWarning suppressHydrationWarning
> >
Launch Studio Get Started
</button> </button>
<p className="text-center text-xs text-white/30"> <p className="text-center text-[12px] text-white/20 pt-2">
Don&apos;t have a key?{' '} Need a key?{' '}
<a href="https://muapi.ai" target="_blank" rel="noreferrer" className="text-[#d9ff00] hover:underline"> <a href="https://muapi.ai" target="_blank" rel="noreferrer" className="text-white/40 hover:text-[#d9ff00] transition-colors font-medium">
Get one free at Muapi.ai Get one free
</a> </a>
</p> </p>
</form> </form>

View file

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { ImageStudio, VideoStudio, LipSyncStudio, CinemaStudio } from 'studio'; import { ImageStudio, VideoStudio, LipSyncStudio, CinemaStudio, getUserBalance } from 'studio';
import ApiKeyModal from './ApiKeyModal'; import ApiKeyModal from './ApiKeyModal';
const TABS = [ const TABS = [
@ -16,25 +16,47 @@ const STORAGE_KEY = 'muapi_key';
export default function StandaloneShell() { export default function StandaloneShell() {
const [apiKey, setApiKey] = useState(null); const [apiKey, setApiKey] = useState(null);
const [activeTab, setActiveTab] = useState('image'); const [activeTab, setActiveTab] = useState('image');
const [balance, setBalance] = useState(null);
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [hasMounted, setHasMounted] = useState(false); const [hasMounted, setHasMounted] = useState(false);
const fetchBalance = useCallback(async (key) => {
try {
const data = await getUserBalance(key);
setBalance(data.balance);
} catch (err) {
console.error('Balance fetch failed:', err);
}
}, []);
useEffect(() => { useEffect(() => {
setHasMounted(true); setHasMounted(true);
const stored = localStorage.getItem(STORAGE_KEY); const stored = localStorage.getItem(STORAGE_KEY);
if (stored) setApiKey(stored); if (stored) {
}, []); setApiKey(stored);
fetchBalance(stored);
}
}, [fetchBalance]);
const handleKeySave = useCallback((key) => { const handleKeySave = useCallback((key) => {
localStorage.setItem(STORAGE_KEY, key); localStorage.setItem(STORAGE_KEY, key);
setApiKey(key); setApiKey(key);
}, []); fetchBalance(key);
}, [fetchBalance]);
const handleKeyChange = useCallback(() => { const handleKeyChange = useCallback(() => {
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
setApiKey(null); setApiKey(null);
setBalance(null);
}, []); }, []);
// 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]);
if (!hasMounted) return ( if (!hasMounted) return (
<div className="min-h-screen bg-[#050505] flex items-center justify-center"> <div className="min-h-screen bg-[#050505] flex items-center justify-center">
<div className="animate-spin text-[#d9ff00] text-3xl"></div> <div className="animate-spin text-[#d9ff00] text-3xl"></div>
@ -46,39 +68,55 @@ export default function StandaloneShell() {
} }
return ( return (
<div className="h-screen bg-[#050505] flex flex-col overflow-hidden"> <div className="h-screen bg-[#030303] flex flex-col overflow-hidden text-white">
{/* Header */} {/* Header */}
<header className="flex-shrink-0 flex items-center justify-between px-4 pt-4 pb-0 border-b border-white/5"> <header className="flex-shrink-0 h-14 border-b border-white/[0.03] flex items-center justify-between px-6 bg-black/20 backdrop-blur-md z-40">
<div className="flex items-center gap-3"> {/* Left: Logo */}
<span className="text-white font-black text-lg tracking-wider uppercase"> <div className="flex items-center gap-2">
Open Higgsfield AI <div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
</span> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="black" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
</div>
<span className="text-sm font-bold tracking-tight hidden sm:block">OpenHiggsfield</span>
</div> </div>
{/* Tabs */} {/* Center: Navigation */}
<nav className="flex items-center gap-1"> <nav className="absolute left-1/2 -translate-x-1/2 flex items-center gap-6">
{TABS.map((tab) => ( {TABS.map((tab) => (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${ className={`relative py-4 text-[13px] font-medium transition-all whitespace-nowrap px-1 ${
activeTab === tab.id activeTab === tab.id
? 'bg-[#d9ff00] text-black' ? 'text-[#d9ff00]'
: 'text-white/50 hover:text-white' : 'text-white/50 hover:text-white'
}`} }`}
> >
{tab.label} {tab.label}
{activeTab === tab.id && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-[#d9ff00] rounded-full" />
)}
</button> </button>
))} ))}
</nav> </nav>
{/* Settings */} {/* Right: Actions */}
<button <div className="flex items-center gap-4">
onClick={() => setShowSettings(true)} <div className="flex items-center gap-3 bg-white/5 px-3 py-1.5 rounded-full border border-white/5 transition-colors">
className="text-white/40 hover:text-white text-sm transition-colors" <div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
> <div className="flex flex-col">
Settings <span className="text-xs font-bold text-white/90">
</button> ${balance !== null ? `${balance}` : '---'}
</span>
</div>
</div>
<div
onClick={() => setShowSettings(true)}
className="w-8 h-8 rounded-full bg-gradient-to-tr from-[#d9ff00] to-yellow-200 border border-white/20 cursor-pointer hover:scale-105 transition-transform"
/>
</div>
</header> </header>
{/* Studio Content */} {/* Studio Content */}
@ -91,22 +129,34 @@ export default function StandaloneShell() {
{/* Settings Modal */} {/* Settings Modal */}
{showSettings && ( {showSettings && (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in-up">
<div className="bg-[#111] border border-white/10 rounded-2xl p-8 w-full max-w-md"> <div className="bg-[#0a0a0a] border border-white/10 rounded-xl p-8 w-full max-w-sm shadow-2xl">
<h2 className="text-white font-bold text-xl mb-6">Settings</h2> <h2 className="text-white font-bold text-lg mb-2">Settings</h2>
<p className="text-white/50 text-sm mb-4"> <p className="text-white/40 text-[13px] mb-8">
Current API key: <span className="text-white/80 font-mono">{apiKey.slice(0, 8)}</span> Manage your AI studio preferences and authentication.
</p> </p>
<div className="space-y-4 mb-8">
<div className="bg-white/5 border border-white/[0.03] rounded-md p-4">
<label className="block text-xs font-bold text-white/30 mb-2">
Active API Key
</label>
<div className="text-[13px] font-mono text-white/80">
{apiKey.slice(0, 8)}
</div>
</div>
</div>
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={handleKeyChange} onClick={handleKeyChange}
className="flex-1 py-2 rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 text-sm transition-colors" className="flex-1 h-10 rounded-md bg-red-500/10 text-red-400 hover:bg-red-500/20 text-xs font-semibold transition-all"
> >
Change API Key Change Key
</button> </button>
<button <button
onClick={() => setShowSettings(false)} onClick={() => setShowSettings(false)}
className="flex-1 py-2 rounded-lg bg-white/5 text-white hover:bg-white/10 text-sm transition-colors" 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 Close
</button> </button>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,3 +4,4 @@ export { default as ImageStudio } from './components/ImageStudio';
export { default as VideoStudio } from './components/VideoStudio'; export { default as VideoStudio } from './components/VideoStudio';
export { default as LipSyncStudio } from './components/LipSyncStudio'; export { default as LipSyncStudio } from './components/LipSyncStudio';
export { default as CinemaStudio } from './components/CinemaStudio'; export { default as CinemaStudio } from './components/CinemaStudio';
export * from './muapi';

View file

@ -170,3 +170,16 @@ export function uploadFile(apiKey, file, onProgress) {
}); });
} }
export async function getUserBalance(apiKey) {
const response = await fetch(`${BASE_URL}/api/v1/account/balance`, {
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey
}
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`Failed to fetch balance: ${response.status} - ${errText.slice(0, 100)}`);
}
return await response.json();
}