feat: finalize studio persistence and multiple reference image upload UI
This commit is contained in:
parent
b924f0caf8
commit
91ce11df6f
9 changed files with 3765 additions and 2741 deletions
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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'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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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