Merge pull request #64 from jaiprasad04/feat/modernize-studio-upload
feat: finalize studio persistence and multiple reference image upload UI
This commit is contained in:
commit
21a09ebea2
9 changed files with 3765 additions and 2741 deletions
|
|
@ -2,21 +2,32 @@
|
|||
@tailwind components;
|
||||
@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; }
|
||||
|
||||
body {
|
||||
background: #050505;
|
||||
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 {
|
||||
--color-primary: #d9ff00;
|
||||
--bg-app: #050505;
|
||||
--bg-app: #030303;
|
||||
--bg-panel: #0a0a0a;
|
||||
--bg-card: #111111;
|
||||
--border-color: rgba(255,255,255,0.08);
|
||||
--border-radius-xl: 1rem;
|
||||
--border-color: rgba(255,255,255,0.05);
|
||||
--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; }
|
||||
|
|
@ -24,7 +35,7 @@ body {
|
|||
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
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; }
|
||||
|
|
|
|||
|
|
@ -14,50 +14,50 @@ export default function ApiKeyModal({ onSave }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050505] flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-md bg-[#0a0a0a] border border-white/10 rounded-3xl p-8">
|
||||
<div className="flex flex-col items-center text-center mb-8">
|
||||
<div className="w-16 h-16 bg-[#d9ff00]/10 rounded-2xl flex items-center justify-center border border-[#d9ff00]/20 mb-6">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#d9ff00" strokeWidth="1.5">
|
||||
<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"/>
|
||||
<div className="min-h-screen bg-[#030303] flex items-center justify-center px-4 font-inter">
|
||||
<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-10">
|
||||
<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="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" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</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
|
||||
</h1>
|
||||
<p className="text-white/40 text-sm">
|
||||
Enter your <a href="https://muapi.ai" target="_blank" rel="noreferrer" className="text-[#d9ff00] hover:underline">Muapi.ai</a> API key to start generating
|
||||
<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:text-[#e5ff33] transition-colors">Muapi.ai</a> API key to start creating
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-white/40 uppercase tracking-widest mb-2">
|
||||
Muapi API Key
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-bold text-white/30 ml-1">
|
||||
API Access Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={key}
|
||||
onChange={(e) => { setKey(e.target.value); setError(''); }}
|
||||
placeholder="Enter your API key..."
|
||||
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"
|
||||
placeholder="Paste your key here..."
|
||||
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
|
||||
/>
|
||||
{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>
|
||||
|
||||
<button
|
||||
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
|
||||
>
|
||||
Launch Studio
|
||||
Get Started
|
||||
</button>
|
||||
|
||||
<p className="text-center text-xs text-white/30">
|
||||
Don't have a key?{' '}
|
||||
<a href="https://muapi.ai" target="_blank" rel="noreferrer" className="text-[#d9ff00] hover:underline">
|
||||
Get one free at Muapi.ai →
|
||||
<p className="text-center text-[12px] text-white/20 pt-2">
|
||||
Need a key?{' '}
|
||||
<a href="https://muapi.ai" target="_blank" rel="noreferrer" className="text-white/40 hover:text-[#d9ff00] transition-colors font-medium">
|
||||
Get one free →
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
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';
|
||||
|
||||
const TABS = [
|
||||
|
|
@ -16,25 +16,47 @@ const STORAGE_KEY = 'muapi_key';
|
|||
export default function StandaloneShell() {
|
||||
const [apiKey, setApiKey] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState('image');
|
||||
const [balance, setBalance] = useState(null);
|
||||
const [showSettings, setShowSettings] = 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(() => {
|
||||
setHasMounted(true);
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) setApiKey(stored);
|
||||
}, []);
|
||||
if (stored) {
|
||||
setApiKey(stored);
|
||||
fetchBalance(stored);
|
||||
}
|
||||
}, [fetchBalance]);
|
||||
|
||||
const handleKeySave = useCallback((key) => {
|
||||
localStorage.setItem(STORAGE_KEY, key);
|
||||
setApiKey(key);
|
||||
}, []);
|
||||
fetchBalance(key);
|
||||
}, [fetchBalance]);
|
||||
|
||||
const handleKeyChange = useCallback(() => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
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 (
|
||||
<div className="min-h-screen bg-[#050505] flex items-center justify-center">
|
||||
<div className="animate-spin text-[#d9ff00] text-3xl">◌</div>
|
||||
|
|
@ -46,39 +68,55 @@ export default function StandaloneShell() {
|
|||
}
|
||||
|
||||
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 className="flex-shrink-0 flex items-center justify-between px-4 pt-4 pb-0 border-b border-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-white font-black text-lg tracking-wider uppercase">
|
||||
Open Higgsfield AI
|
||||
</span>
|
||||
<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">
|
||||
{/* Left: Logo */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
|
||||
<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>
|
||||
|
||||
{/* Tabs */}
|
||||
<nav className="flex items-center gap-1">
|
||||
{/* Center: Navigation */}
|
||||
<nav className="absolute left-1/2 -translate-x-1/2 flex items-center gap-6">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={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
|
||||
? 'bg-[#d9ff00] text-black'
|
||||
? 'text-[#d9ff00]'
|
||||
: 'text-white/50 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
{activeTab === tab.id && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-[#d9ff00] rounded-full" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Settings */}
|
||||
<button
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="text-white/40 hover:text-white text-sm transition-colors"
|
||||
>
|
||||
⚙ Settings
|
||||
</button>
|
||||
{/* Right: Actions */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3 bg-white/5 px-3 py-1.5 rounded-full border border-white/5 transition-colors">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold text-white/90">
|
||||
${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>
|
||||
|
||||
{/* Studio Content */}
|
||||
|
|
@ -91,22 +129,34 @@ export default function StandaloneShell() {
|
|||
|
||||
{/* Settings Modal */}
|
||||
{showSettings && (
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
|
||||
<div className="bg-[#111] border border-white/10 rounded-2xl p-8 w-full max-w-md">
|
||||
<h2 className="text-white font-bold text-xl mb-6">Settings</h2>
|
||||
<p className="text-white/50 text-sm mb-4">
|
||||
Current API key: <span className="text-white/80 font-mono">{apiKey.slice(0, 8)}••••••••</span>
|
||||
<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-[#0a0a0a] border border-white/10 rounded-xl p-8 w-full max-w-sm shadow-2xl">
|
||||
<h2 className="text-white font-bold text-lg mb-2">Settings</h2>
|
||||
<p className="text-white/40 text-[13px] mb-8">
|
||||
Manage your AI studio preferences and authentication.
|
||||
</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">
|
||||
<button
|
||||
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
|
||||
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
|
||||
</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
|
|
@ -4,3 +4,4 @@ export { default as ImageStudio } from './components/ImageStudio';
|
|||
export { default as VideoStudio } from './components/VideoStudio';
|
||||
export { default as LipSyncStudio } from './components/LipSyncStudio';
|
||||
export { default as CinemaStudio } from './components/CinemaStudio';
|
||||
export * from './muapi';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue