feat: Add image generation studio with Muapi API integration

- Add ImageStudio component with prompt input, model/AR/resolution pickers
- Integrate Muapi API client with x-api-key auth and result polling
- Add generation history sidebar with thumbnails and download
- Add AuthModal and SettingsModal for API key management
- Configure Vite proxy for CORS-free API access in development
- Add model definitions with endpoint mappings from schema data
- Add Tailwind CSS styling with dark theme and glassmorphism design
- Add Header component with settings and logout controls
This commit is contained in:
Anil Matcha 2026-02-12 22:02:20 +05:30
commit e0efb745d5
19 changed files with 7397 additions and 439 deletions

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>open-higgsfield-ai</title>
<title>Open-Higgsfield AI</title>
</head>
<body>
<div id="app"></div>

2006
models_dump.json Normal file

File diff suppressed because it is too large Load diff

2189
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,14 @@
"preview": "vite preview"
},
"devDependencies": {
"vite": "^7.3.1"
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^5.4.0"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/vite": "^4.1.18",
"puppeteer": "^24.37.2"
}
}

5
postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}

View file

@ -0,0 +1,60 @@
export function AuthModal(onSuccess) {
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm px-6';
const modal = document.createElement('div');
modal.className = 'w-full max-w-md bg-panel-bg border border-white/10 rounded-3xl p-8 shadow-3xl animate-fade-in-up';
modal.innerHTML = `
<div class="flex flex-col items-center text-center mb-8">
<div class="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center border border-primary/20 shadow-glow mb-6">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#d9ff00" stroke-width="2">
<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 3m-3-3l-2.25-2.25"/>
</svg>
</div>
<h2 class="text-2xl font-black text-white uppercase tracking-wider mb-2">Muapi API Key Required</h2>
<p class="text-secondary text-sm">Please provide your Muapi.ai API key to start creating high-aesthetic images.</p>
</div>
<div class="space-y-6">
<div class="space-y-2">
<label class="text-[10px] font-bold text-muted uppercase tracking-widest ml-1">Your API Key</label>
<input
type="password"
id="muapi-key-input"
placeholder="sk-..."
class="w-full bg-black/40 border border-white/5 rounded-2xl px-5 py-4 text-white placeholder:text-muted focus:outline-none focus:border-primary/50 transition-colors shadow-inner"
>
</div>
<div class="flex flex-col gap-3">
<button id="save-key-btn" class="w-full bg-primary text-black font-black py-4 rounded-2xl hover:shadow-glow hover:scale-[1.02] active:scale-[0.98] transition-all">
Initialize Studio
</button>
<a href="https://muapi.ai" target="_blank" class="text-center text-[11px] font-bold text-muted hover:text-white transition-colors py-2 uppercase tracking-tighter">
Get an API Key at Muapi.ai
</a>
</div>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const input = modal.querySelector('#muapi-key-input');
const btn = modal.querySelector('#save-key-btn');
btn.onclick = () => {
const key = input.value.trim();
if (key) {
localStorage.setItem('muapi_key', key);
document.body.removeChild(overlay);
if (onSuccess) onSuccess();
} else {
input.classList.add('border-red-500/50');
setTimeout(() => input.classList.remove('border-red-500/50'), 2000);
}
};
return overlay;
}

75
src/components/Header.js Normal file
View file

@ -0,0 +1,75 @@
export function Header() {
const header = document.createElement('header');
header.className = 'w-full flex flex-col z-50 sticky top-0';
// 2. Main Navigation Bar
const navBar = document.createElement('div');
navBar.className = 'w-full h-16 bg-black flex items-center justify-between px-4 md:px-6 border-b border-white/5 backdrop-blur-md bg-opacity-95';
const leftPart = document.createElement('div');
leftPart.className = 'flex items-center gap-8';
// Logo
const logoContainer = document.createElement('div');
logoContainer.className = 'cursor-pointer hover:scale-110 transition-transform';
logoContainer.innerHTML = `
<div class="w-8 h-8 bg-white rounded-lg flex items-center justify-center p-1.5 shadow-lg">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" fill="black"/>
<path d="M2 17L12 22L22 17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
`;
const menu = document.createElement('nav');
menu.className = 'hidden lg:flex items-center gap-6 text-[13px] font-bold text-secondary';
const items = ['Explore', 'Image', 'Video', 'Edit', 'Character', 'Contests', 'Vibe Motion', 'Cinema Studio', 'AI Influencer', 'Apps', 'Assist', 'Community'];
items.forEach(item => {
const link = document.createElement('a');
link.textContent = item;
link.className = `hover:text-white transition-all cursor-pointer relative group ${item === 'Image' ? 'text-white' : ''}`;
// Active Indicator or Dot
if (item === 'Image') {
const dot = document.createElement('div');
dot.className = 'absolute -bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-primary rounded-full';
link.appendChild(dot);
}
if (item === 'Contests') {
link.innerHTML += ' <span class="bg-primary/10 text-primary text-[8px] px-1.5 py-0.5 rounded-full ml-1 border border-primary/20">New</span>';
}
menu.appendChild(link);
});
leftPart.appendChild(logoContainer);
leftPart.appendChild(menu);
const rightPart = document.createElement('div');
rightPart.className = 'flex items-center gap-4';
const keyBtn = document.createElement('button');
keyBtn.className = 'p-2 text-secondary hover:text-white transition-colors';
keyBtn.title = 'Update API Key';
keyBtn.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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 3m-3-3l-2.25-2.25"/>
</svg>
`;
keyBtn.onclick = () => {
localStorage.removeItem('muapi_key');
window.location.reload();
};
rightPart.appendChild(keyBtn);
navBar.appendChild(leftPart);
navBar.appendChild(rightPart);
header.appendChild(navBar);
return header;
}

View file

@ -0,0 +1,578 @@
import { muapi } from '../lib/muapi.js';
import { t2iModels, getAspectRatiosForModel } from '../lib/models.js';
import { AuthModal } from './AuthModal.js';
export function ImageStudio() {
const container = document.createElement('div');
container.className = 'w-full h-full flex flex-col items-center justify-center bg-app-bg relative p-4 md:p-6 overflow-hidden';
// --- State ---
const defaultModel = t2iModels[0];
let selectedModel = defaultModel.id;
let selectedModelName = defaultModel.name;
let selectedAr = '1:1';
let dropdownOpen = null;
// Helper: Get valid resolutions/quality options for a model
const getResolutionsForModel = (modelId) => {
const model = t2iModels.find(m => m.id === modelId);
if (!model) return ['1K']; // Default
// Check for specific resolution enum
if (model.inputs?.resolution?.enum) {
return model.inputs.resolution.enum.map(r => r.toUpperCase());
}
// Check for megapixels enum
if (model.inputs?.megapixels?.enum) {
return model.inputs.megapixels.enum;
}
// Fallback logic based on common models
if (modelId.includes('flux')) return ['1K']; // Flux usually fixed
if (modelId.includes('midjourney')) return ['1K'];
// Default set for others if not specified
return ['1K', '2K', '4K'];
};
// ==========================================
// 1. HERO SECTION
// ==========================================
const hero = document.createElement('div');
hero.className = 'flex flex-col items-center mb-10 md:mb-20 animate-fade-in-up transition-all duration-700';
hero.innerHTML = `
<div class="mb-10 relative group">
<div class="absolute inset-0 bg-primary/20 blur-[100px] rounded-full opacity-40 group-hover:opacity-70 transition-opacity duration-1000"></div>
<div class="relative w-24 h-24 md:w-32 md:h-32 bg-teal-900/40 rounded-3xl flex items-center justify-center border border-white/5 overflow-hidden">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" class="text-primary opacity-20 absolute -right-4 -bottom-4">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
</svg>
<div class="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center border border-primary/20 shadow-glow relative z-10">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="text-primary">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
</div>
<!-- Sparkles -->
<div class="absolute top-4 right-4 text-primary animate-pulse"></div>
</div>
</div>
<h1 class="text-2xl sm:text-4xl md:text-7xl font-black text-white tracking-widest uppercase mb-4 selection:bg-primary selection:text-black text-center px-4">Nano Banana Pro</h1>
<p class="text-secondary text-sm font-medium tracking-wide opacity-60">Create stunning, high-aesthetic images in seconds</p>
`;
container.appendChild(hero);
// ==========================================
// 2. PROMPT BAR (Tailwind Refactor)
// ==========================================
const promptWrapper = document.createElement('div');
promptWrapper.className = 'w-full max-w-4xl relative z-40 animate-fade-in-up';
promptWrapper.style.animationDelay = '0.2s';
const bar = document.createElement('div');
bar.className = 'w-full bg-[#111]/90 backdrop-blur-xl border border-white/10 rounded-[1.5rem] md:rounded-[2.5rem] p-3 md:p-5 flex flex-col gap-3 md:gap-5 shadow-3xl';
// Top Row: Input
const topRow = document.createElement('div');
topRow.className = 'flex items-start gap-5 px-2';
topRow.innerHTML = `
<div class="p-2 md:p-3 bg-white/5 rounded-xl md:rounded-2xl text-secondary mt-1 border border-white/5 hidden sm:block">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
</div>
`;
const textarea = document.createElement('textarea');
textarea.placeholder = 'Describe the scene you imagine';
textarea.className = 'flex-1 bg-transparent border-none text-white text-base md:text-xl placeholder:text-muted focus:outline-none resize-none pt-2.5 leading-relaxed min-h-[40px]';
textarea.rows = 1;
textarea.oninput = () => {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
};
topRow.appendChild(textarea);
bar.appendChild(topRow);
// Bottom Row: Controls
const bottomRow = document.createElement('div');
bottomRow.className = 'flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 px-2 pt-4 border-t border-white/5';
const controlsLeft = document.createElement('div');
controlsLeft.className = 'flex items-center gap-1.5 md:gap-2.5 relative overflow-x-auto no-scrollbar pb-1 md:pb-0';
const createControlBtn = (icon, label, id) => {
const btn = document.createElement('button');
btn.id = id;
btn.className = 'flex items-center gap-1.5 md:gap-2.5 px-3 md:px-4 py-2 md:py-2.5 bg-white/5 hover:bg-white/10 rounded-xl md:rounded-2xl transition-all border border-white/5 group whitespace-nowrap';
btn.innerHTML = `
${icon}
<span id="${id}-label" class="text-xs font-bold text-white group-hover:text-primary transition-colors">${label}</span>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" class="opacity-20 group-hover:opacity-100 transition-opacity"><path d="M6 9l6 6 6-6"/></svg>
`;
return btn;
};
const modelBtn = createControlBtn(`
<div class="w-5 h-5 bg-primary rounded-md flex items-center justify-center shadow-lg shadow-primary/20">
<span class="text-[10px] font-black text-black">G</span>
</div>
`, selectedModelName, 'model-btn');
const arBtn = createControlBtn(`
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>
`, selectedAr, 'ar-btn');
const qualityBtn = createControlBtn(`
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M6 2L3 6v15a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6z"/></svg>
`, '1K', 'quality-btn');
controlsLeft.appendChild(modelBtn);
controlsLeft.appendChild(arBtn);
controlsLeft.appendChild(qualityBtn);
// Initial Resolution Visibility (only show for models with explicit resolution/megapixels enums)
const initialModel = t2iModels[0];
const hasInitialRes = initialModel?.inputs?.resolution?.enum || initialModel?.inputs?.megapixels?.enum;
qualityBtn.style.display = hasInitialRes ? 'flex' : 'none';
const generateBtn = document.createElement('button');
generateBtn.className = 'bg-primary text-black px-6 md:px-8 py-3 md:py-3.5 rounded-xl md:rounded-[1.5rem] font-black text-sm md:text-base hover:shadow-glow hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2.5 w-full sm:w-auto shadow-lg';
generateBtn.innerHTML = `Generate ✨`;
bottomRow.appendChild(controlsLeft);
bottomRow.appendChild(generateBtn);
bar.appendChild(bottomRow);
promptWrapper.appendChild(bar);
container.appendChild(promptWrapper);
// ==========================================
// 3. DROPDOWNS (Professional implementation)
// ==========================================
const dropdown = document.createElement('div');
dropdown.className = 'absolute bottom-[102%] left-2 z-50 transition-all opacity-0 pointer-events-none scale-95 origin-bottom-left glass rounded-3xl p-3 translate-y-2 w-[calc(100vw-3rem)] max-w-xs shadow-4xl border border-white/10 flex flex-col';
const showDropdown = (type, anchorBtn) => {
dropdown.innerHTML = '';
dropdown.classList.remove('opacity-0', 'pointer-events-none');
dropdown.classList.add('opacity-100', 'pointer-events-auto');
if (type === 'model') {
dropdown.classList.add('w-[calc(100vw-3rem)]', 'max-w-xs');
dropdown.classList.remove('max-w-[240px]', 'max-w-[200px]');
dropdown.innerHTML = `
<div class="flex flex-col h-full max-h-[70vh]">
<div class="px-2 pb-3 mb-2 border-b border-white/5 shrink-0">
<div class="flex items-center gap-3 bg-white/5 rounded-xl px-4 py-2.5 border border-white/5 focus-within:border-primary/50 transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" class="text-muted"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input type="text" id="model-search" placeholder="Search models..." class="bg-transparent border-none text-xs text-white focus:ring-0 w-full p-0">
</div>
</div>
<div class="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 shrink-0">Available models</div>
<div id="model-list-container" class="flex flex-col gap-1.5 overflow-y-auto custom-scrollbar pr-1 pb-2"></div>
</div>
`;
const list = dropdown.querySelector('#model-list-container');
const renderModels = (filter = '') => {
list.innerHTML = '';
const filtered = t2iModels.filter(m => m.name.toLowerCase().includes(filter.toLowerCase()) || m.id.toLowerCase().includes(filter.toLowerCase()));
filtered.forEach(m => {
const item = document.createElement('div');
item.className = `flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all border border-transparent hover:border-white/5 ${selectedModel === m.id ? 'bg-white/5 border-white/5' : ''}`;
item.innerHTML = `
<div class="flex items-center gap-3.5">
<div class="w-10 h-10 ${m.id.includes('flux') ? 'bg-blue-500/10 text-blue-400' : 'bg-primary/10 text-primary'} border border-white/5 rounded-xl flex items-center justify-center font-black text-sm shadow-inner uppercase">${m.name.charAt(0)}</div>
<div class="flex flex-col gap-0.5">
<span class="text-xs font-bold text-white tracking-tight">${m.name}</span>
</div>
</div>
${selectedModel === m.id ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#d9ff00" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg>' : ''}
`;
item.onclick = (e) => {
e.stopPropagation();
selectedModel = m.id;
selectedModelName = m.name;
// Reset AR to first valid for model
const availableArs = getAspectRatiosForModel(selectedModel);
selectedAr = availableArs[0];
document.getElementById('model-btn-label').textContent = selectedModelName;
document.getElementById('ar-btn-label').textContent = selectedAr;
// Show/Hide quality button based on model support (only resolution/megapixels enums)
const model = t2iModels.find(mod => mod.id === selectedModel);
const hasQuality = model?.inputs?.resolution?.enum || model?.inputs?.megapixels?.enum;
qualityBtn.style.display = hasQuality ? 'flex' : 'none';
// Reset resolution label if current is not valid for new model
if (hasQuality) {
const validResolutions = getResolutionsForModel(selectedModel);
const currentRes = document.getElementById('quality-btn-label').textContent;
if (!validResolutions.includes(currentRes)) {
document.getElementById('quality-btn-label').textContent = validResolutions[0];
}
}
closeDropdown();
};
list.appendChild(item);
});
};
renderModels();
const searchInput = dropdown.querySelector('#model-search');
searchInput.onclick = (e) => e.stopPropagation();
searchInput.oninput = (e) => renderModels(e.target.value);
} else if (type === 'ar') {
dropdown.classList.add('max-w-[240px]');
dropdown.innerHTML = `<div class="text-[10px] font-bold text-muted uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Aspect Ratio</div>`;
const list = document.createElement('div');
list.className = 'flex flex-col gap-1';
const availableArs = getAspectRatiosForModel(selectedModel);
availableArs.forEach(r => {
const item = document.createElement('div');
item.className = 'flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all group';
item.innerHTML = `
<div class="flex items-center gap-4">
<div class="w-6 h-6 border-2 border-white/20 rounded-md shadow-inner flex items-center justify-center group-hover:border-primary/50 transition-colors">
<div class="w-3 h-3 bg-white/10 rounded-sm"></div>
</div>
<span class="text-xs font-bold text-white opacity-80 group-hover:opacity-100 transition-opacity">${r}</span>
</div>
${selectedAr === r ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#d9ff00" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg>' : ''}
`;
item.onclick = (e) => {
e.stopPropagation();
selectedAr = r;
document.getElementById('ar-btn-label').textContent = r;
closeDropdown();
};
list.appendChild(item);
});
dropdown.appendChild(list);
} else if (type === 'quality') {
dropdown.classList.add('max-w-[200px]');
dropdown.innerHTML = `<div class="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Resolution</div>`;
const list = document.createElement('div');
list.className = 'flex flex-col gap-1';
// Dynamic resolution options
const options = getResolutionsForModel(selectedModel);
options.forEach(opt => {
const item = document.createElement('div');
item.className = 'flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all group';
item.innerHTML = `
<span class="text-xs font-bold text-white opacity-80 group-hover:opacity-100">${opt}</span>
${document.getElementById('quality-btn-label').textContent === opt ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#d9ff00" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg>' : ''}
`;
item.onclick = (e) => {
e.stopPropagation();
document.getElementById('quality-btn-label').textContent = opt;
closeDropdown();
};
list.appendChild(item);
});
dropdown.appendChild(list);
}
// Position dropdown
const btnRect = anchorBtn.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
// Horizontal position
if (window.innerWidth < 768) {
// Center on mobile
dropdown.style.left = '50%';
dropdown.style.transform = 'translateX(-50%) translate(0, 8px)';
} else {
// Align with button on desktop
dropdown.style.left = `${btnRect.left - containerRect.left}px`;
dropdown.style.transform = 'translate(0, 8px)';
}
// Vertical position (always above button)
dropdown.style.bottom = `${containerRect.bottom - btnRect.top + 8}px`;
};
const closeDropdown = () => {
dropdown.classList.add('opacity-0', 'pointer-events-none');
dropdown.classList.remove('opacity-100', 'pointer-events-auto');
dropdownOpen = null;
};
modelBtn.onclick = (e) => {
e.stopPropagation();
if (dropdownOpen === 'model') closeDropdown();
else {
dropdownOpen = 'model';
showDropdown('model', modelBtn);
}
};
arBtn.onclick = (e) => {
e.stopPropagation();
if (dropdownOpen === 'ar') closeDropdown();
else {
dropdownOpen = 'ar';
showDropdown('ar', arBtn);
}
};
qualityBtn.onclick = (e) => {
e.stopPropagation();
if (dropdownOpen === 'quality') closeDropdown();
else {
dropdownOpen = 'quality';
showDropdown('quality', qualityBtn);
}
};
window.onclick = () => closeDropdown();
container.appendChild(dropdown);
// ==========================================
// 4. CANVAS AREA + HISTORY
// ==========================================
const generationHistory = [];
// History sidebar
const historySidebar = document.createElement('div');
historySidebar.className = 'fixed right-0 top-0 h-full w-20 md:w-24 bg-black/60 backdrop-blur-xl border-l border-white/5 z-50 flex flex-col items-center py-4 gap-3 overflow-y-auto transition-all duration-500 translate-x-full opacity-0';
historySidebar.id = 'history-sidebar';
const historyLabel = document.createElement('div');
historyLabel.className = 'text-[9px] font-bold text-muted uppercase tracking-widest mb-2 rotate-0';
historyLabel.textContent = 'History';
historySidebar.appendChild(historyLabel);
const historyList = document.createElement('div');
historyList.className = 'flex flex-col gap-2 w-full px-2';
historySidebar.appendChild(historyList);
container.appendChild(historySidebar);
// Main canvas
const canvas = document.createElement('div');
canvas.className = 'absolute inset-0 flex flex-col items-center justify-center p-4 min-[800px]:p-16 z-10 opacity-0 pointer-events-none transition-all duration-1000 translate-y-10 scale-95';
const imageContainer = document.createElement('div');
imageContainer.className = 'relative group';
const resultImg = document.createElement('img');
resultImg.className = 'max-h-[60vh] max-w-[80vw] rounded-3xl shadow-3xl border border-white/10 interactive-glow object-contain';
imageContainer.appendChild(resultImg);
// Canvas Controls
const canvasControls = document.createElement('div');
canvasControls.className = 'mt-6 flex gap-3 opacity-0 transition-opacity delay-500 duration-500 justify-center';
const regenerateBtn = document.createElement('button');
regenerateBtn.className = 'bg-white/10 hover:bg-white/20 px-6 py-2.5 rounded-2xl text-xs font-bold transition-all border border-white/5 backdrop-blur-lg text-white';
regenerateBtn.textContent = '↻ Regenerate';
const downloadBtn = document.createElement('button');
downloadBtn.className = 'bg-primary text-black px-6 py-2.5 rounded-2xl text-xs font-bold transition-all shadow-glow active:scale-95';
downloadBtn.textContent = '↓ Download';
const newPromptBtn = document.createElement('button');
newPromptBtn.className = 'bg-white/10 hover:bg-white/20 px-6 py-2.5 rounded-2xl text-xs font-bold transition-all border border-white/5 backdrop-blur-lg text-white';
newPromptBtn.textContent = '+ New';
canvasControls.appendChild(regenerateBtn);
canvasControls.appendChild(downloadBtn);
canvasControls.appendChild(newPromptBtn);
canvas.appendChild(imageContainer);
canvas.appendChild(canvasControls);
container.appendChild(canvas);
// --- Helper: Show image in canvas ---
const showImageInCanvas = (imageUrl) => {
// Fully hide hero and prompt
hero.classList.add('hidden');
promptWrapper.classList.add('hidden');
resultImg.src = imageUrl;
resultImg.onload = () => {
canvas.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
canvas.classList.add('opacity-100', 'translate-y-0', 'scale-100');
canvasControls.classList.remove('opacity-0');
canvasControls.classList.add('opacity-100');
};
};
// --- Helper: Add to history ---
const addToHistory = (entry) => {
generationHistory.unshift(entry);
// Save to localStorage
localStorage.setItem('muapi_history', JSON.stringify(generationHistory.slice(0, 50)));
// Show sidebar
historySidebar.classList.remove('translate-x-full', 'opacity-0');
historySidebar.classList.add('translate-x-0', 'opacity-100');
renderHistory();
};
const renderHistory = () => {
historyList.innerHTML = '';
generationHistory.forEach((entry, idx) => {
const thumb = document.createElement('div');
thumb.className = `relative group/thumb cursor-pointer rounded-xl overflow-hidden border-2 transition-all duration-300 ${idx === 0 ? 'border-primary shadow-glow' : 'border-white/10 hover:border-white/30'}`;
thumb.innerHTML = `
<img src="${entry.url}" alt="${entry.prompt?.substring(0, 30) || 'Generated'}" class="w-full aspect-square object-cover">
<div class="absolute inset-0 bg-black/60 opacity-0 group-hover/thumb:opacity-100 transition-opacity flex items-center justify-center gap-1">
<button class="hist-download p-1.5 bg-primary rounded-lg text-black hover:scale-110 transition-transform" title="Download">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
</button>
</div>
`;
thumb.onclick = (e) => {
if (e.target.closest('.hist-download')) {
downloadImage(entry.url, `muapi-${entry.id || idx}.jpg`);
return;
}
showImageInCanvas(entry.url);
// Update active border
historyList.querySelectorAll('div').forEach(t => {
t.classList.remove('border-primary', 'shadow-glow');
t.classList.add('border-white/10');
});
thumb.classList.remove('border-white/10');
thumb.classList.add('border-primary', 'shadow-glow');
};
historyList.appendChild(thumb);
});
};
// --- Helper: Download image ---
const downloadImage = async (url, filename) => {
try {
const response = await fetch(url);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
} catch (err) {
// Fallback: open in new tab
window.open(url, '_blank');
}
};
// --- Load history from localStorage ---
try {
const saved = JSON.parse(localStorage.getItem('muapi_history') || '[]');
if (saved.length > 0) {
saved.forEach(e => generationHistory.push(e));
historySidebar.classList.remove('translate-x-full', 'opacity-0');
historySidebar.classList.add('translate-x-0', 'opacity-100');
renderHistory();
}
} catch (e) { /* ignore */ }
// --- Button Handlers ---
downloadBtn.onclick = () => {
const current = resultImg.src;
if (current) {
const entry = generationHistory.find(e => e.url === current);
downloadImage(current, `muapi-${entry?.id || 'image'}.jpg`);
}
};
regenerateBtn.onclick = () => {
generateBtn.click();
};
newPromptBtn.onclick = () => {
// Reset to prompt view
canvas.classList.add('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
canvas.classList.remove('opacity-100', 'translate-y-0', 'scale-100');
canvasControls.classList.add('opacity-0');
canvasControls.classList.remove('opacity-100');
// Restore hero and prompt
hero.classList.remove('hidden', 'opacity-0', 'scale-95', '-translate-y-10', 'pointer-events-none');
promptWrapper.classList.remove('hidden', 'opacity-40');
textarea.value = '';
textarea.focus();
};
// ==========================================
// 5. GENERATION LOGIC
// ==========================================
generateBtn.onclick = async () => {
const prompt = textarea.value.trim();
if (!prompt) return;
// Lazy API Key Check
const apiKey = localStorage.getItem('muapi_key');
if (!apiKey) {
AuthModal(() => {
// Key saved, now trigger generation
generateBtn.click();
});
return;
}
// Animate Out Hero
hero.classList.add('opacity-0', 'scale-95', '-translate-y-10', 'pointer-events-none');
generateBtn.disabled = true;
generateBtn.innerHTML = `<span class="animate-spin inline-block mr-2 text-black">◌</span> Generating...`;
try {
const res = await muapi.generateImage({
prompt,
model: selectedModel,
aspect_ratio: selectedAr
});
console.log('[ImageStudio] Full response:', res);
if (res && res.url) {
// Add to history
addToHistory({
id: res.id || Date.now().toString(),
url: res.url,
prompt: prompt,
model: selectedModel,
aspect_ratio: selectedAr,
timestamp: new Date().toISOString()
});
// Show image
showImageInCanvas(res.url);
} else {
console.error('[ImageStudio] No image URL in response:', res);
throw new Error('No image URL returned by API');
}
} catch (e) {
console.error(e);
generateBtn.innerHTML = `Error: ${e.message.slice(0, 40)}`;
setTimeout(() => {
generateBtn.innerHTML = `Generate ✨`;
generateBtn.disabled = false;
}, 3000);
} finally {
generateBtn.disabled = false;
generateBtn.innerHTML = `Generate ✨`;
}
};
return container;
}

View file

@ -0,0 +1,92 @@
export function SettingsModal(onClose) {
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-black/80 flex items-center justify-center z-50';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.right = '0';
overlay.style.bottom = '0';
overlay.style.backgroundColor = 'rgba(0,0,0,0.8)';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.zIndex = '100';
const modal = document.createElement('div');
modal.className = 'bg-card p-6 rounded-xl border border-border-color w-96 glass';
modal.style.background = 'var(--bg-card)';
modal.style.padding = '1.5rem';
modal.style.borderRadius = 'var(--border-radius-xl)';
modal.style.border = '1px solid var(--border-color)';
modal.style.width = '24rem';
const title = document.createElement('h2');
title.textContent = 'Settings';
title.className = 'text-xl font-bold mb-4';
title.style.marginBottom = '1rem';
const label = document.createElement('label');
label.textContent = 'Muapi API Key';
label.className = 'block text-sm text-secondary mb-2';
const input = document.createElement('input');
input.type = 'password';
input.className = 'w-full mb-4 p-2 rounded bg-input border border-border-color';
input.value = localStorage.getItem('muapi_key') || '';
input.placeholder = 'sk-...';
input.style.width = '100%';
input.style.marginBottom = '1rem';
const btnContainer = document.createElement('div');
btnContainer.className = 'flex justify-end gap-2';
btnContainer.style.display = 'flex';
btnContainer.style.justifyContent = 'flex-end';
btnContainer.style.gap = '0.5rem';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.className = 'px-4 py-2 rounded hover:bg-white/5';
cancelBtn.onclick = () => {
document.body.removeChild(overlay);
if (onClose) onClose();
};
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save';
saveBtn.className = 'px-4 py-2 rounded bg-primary text-black font-medium';
saveBtn.style.backgroundColor = 'var(--color-primary)';
saveBtn.style.color = 'black';
saveBtn.style.fontWeight = '500';
saveBtn.onclick = () => {
const key = input.value.trim();
if (key) {
localStorage.setItem('muapi_key', key);
alert('API Key saved!');
document.body.removeChild(overlay);
if (onClose) onClose();
} else {
alert('Please enter a valid key');
}
};
modal.appendChild(title);
modal.appendChild(label);
modal.appendChild(input);
btnContainer.appendChild(cancelBtn);
btnContainer.appendChild(saveBtn);
modal.appendChild(btnContainer);
overlay.appendChild(modal);
// Close on outside click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
if (onClose) onClose();
}
});
return overlay;
}

83
src/components/Sidebar.js Normal file
View file

@ -0,0 +1,83 @@
export function Sidebar() {
const element = document.createElement('aside');
element.className = 'glass-panel';
element.style.width = '72px';
element.style.height = '100%';
element.style.display = 'flex';
element.style.flexDirection = 'column';
element.style.alignItems = 'center';
element.style.padding = '1.5rem 0';
element.style.zIndex = '50';
element.style.background = 'var(--bg-panel)';
// Logo
const logo = document.createElement('div');
logo.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2L2 7L12 12L22 7L12 2Z" fill="white"/><path d="M2 17L12 22L22 17" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 12L12 17L22 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
logo.className = 'mb-10 text-primary';
element.appendChild(logo);
const navItems = [
{ id: 'image', icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>', label: 'Canvas' },
{ id: 'video', icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>', label: 'Video' },
{ id: 'library', icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>', label: 'Library' },
];
const bottomItems = [
{ id: 'settings', icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>', label: 'Settings' }
];
let activeTab = 'image';
const createButton = (item) => {
const container = document.createElement('div');
container.className = 'flex flex-col items-center gap-1 mb-6 cursor-pointer group';
const iconBtn = document.createElement('button');
iconBtn.innerHTML = item.icon;
iconBtn.className = 'w-10 h-10 rounded-xl flex items-center justify-center transition-all bg-transparent text-secondary group-hover:bg-white/5 group-hover:text-white';
const label = document.createElement('span');
label.textContent = item.label;
label.className = 'text-[9px] font-bold uppercase tracking-widest text-secondary group-hover:text-white transition-colors';
if (activeTab === item.id && item.id !== 'settings') {
iconBtn.classList.add('active-nav-btn');
iconBtn.style.color = 'var(--color-primary)';
label.style.color = 'var(--color-primary)';
}
container.onclick = () => {
const event = new CustomEvent('navigate', { detail: { page: item.id } });
window.dispatchEvent(event);
if (item.id !== 'settings') {
activeTab = item.id;
element.querySelectorAll('.active-nav-btn').forEach(btn => {
btn.classList.remove('active-nav-btn');
btn.style.color = 'var(--text-secondary)';
btn.nextSibling.style.color = 'var(--text-secondary)';
});
iconBtn.classList.add('active-nav-btn');
iconBtn.style.color = 'var(--color-primary)';
label.style.color = 'var(--color-primary)';
}
};
container.appendChild(iconBtn);
container.appendChild(label);
return container;
};
const navContainer = document.createElement('div');
navContainer.className = 'flex flex-col flex-1 w-full items-center';
navItems.forEach(item => navContainer.appendChild(createButton(item)));
element.appendChild(navContainer);
const bottomContainer = document.createElement('div');
bottomContainer.className = 'flex flex-col w-full items-center mt-auto';
bottomItems.forEach(item => bottomContainer.appendChild(createButton(item)));
element.appendChild(bottomContainer);
return element;
}

2023
src/lib/models.js Normal file

File diff suppressed because it is too large Load diff

172
src/lib/muapi.js Normal file
View file

@ -0,0 +1,172 @@
import { getModelById } from './models.js';
export class MuapiClient {
constructor() {
// Ideally user provides this in settings
this.baseUrl = import.meta.env.DEV ? '' : 'https://api.muapi.ai';
}
getKey() {
const key = localStorage.getItem('muapi_key');
if (!key) throw new Error('API Key missing. Please set it in Settings.');
return key;
}
/**
* Generates an image (Text-to-Image or Image-to-Image)
* @param {Object} params
* @param {string} params.model
* @param {string} params.prompt
* @param {string} params.negative_prompt
* @param {string} params.aspect_ratio
* @param {number} params.steps
* @param {number} params.guidance_scale
* @param {number} params.seed
* @param {string} [params.image_url] - If present, treats as Image-to-Image
*/
async generateImage(params) {
const key = this.getKey();
// Resolve endpoint from model definition
const modelInfo = getModelById(params.model);
const endpoint = modelInfo?.endpoint || params.model;
const url = `${this.baseUrl}/api/v1/${endpoint}`;
// Build payload matching the API's expected format
const finalPayload = {
prompt: params.prompt,
};
// Aspect ratio (send as string, the API handles it)
if (params.aspect_ratio) {
finalPayload.aspect_ratio = params.aspect_ratio;
}
// Image-to-Image
if (params.image_url) {
finalPayload.image_url = params.image_url;
finalPayload.strength = params.strength || 0.6;
} else {
finalPayload.image_url = null;
}
// Optional params if supported by model
if (params.seed && params.seed !== -1) {
finalPayload.seed = params.seed;
}
console.log('[Muapi] Requesting:', url);
console.log('[Muapi] Payload:', finalPayload);
try {
// Step 1: Submit the task
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': key
},
body: JSON.stringify(finalPayload)
});
if (!response.ok) {
const errText = await response.text();
console.error('[Muapi] API Error Body:', errText);
throw new Error(`API Request Failed: ${response.status} ${response.statusText} - ${errText.slice(0, 100)}`);
}
const submitData = await response.json();
console.log('[Muapi] Submit Response:', submitData);
// Extract request_id for polling
const requestId = submitData.request_id || submitData.id;
if (!requestId) {
// Some endpoints return the result directly
return submitData;
}
// Step 2: Poll for results
console.log('[Muapi] Polling for results, request_id:', requestId);
const result = await this.pollForResult(requestId, key);
// Normalize: extract image URL from outputs array
const imageUrl = result.outputs?.[0] || result.url || result.output?.url;
console.log('[Muapi] Image URL:', imageUrl);
return { ...result, url: imageUrl };
} catch (error) {
console.error("Muapi Client Error:", error);
throw error;
}
}
/**
* Polls the predictions endpoint until the result is ready.
* @param {string} requestId - The request ID from the submit response
* @param {string} key - The API key
* @param {number} maxAttempts - Maximum polling attempts (default 60 = ~2 min)
* @param {number} interval - Polling interval in ms (default 2000)
*/
async pollForResult(requestId, key, maxAttempts = 60, interval = 2000) {
const pollUrl = `${this.baseUrl}/api/v1/predictions/${requestId}/result`;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await new Promise(resolve => setTimeout(resolve, interval));
console.log(`[Muapi] Polling attempt ${attempt}/${maxAttempts}...`);
try {
const response = await fetch(pollUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-api-key': key
}
});
if (!response.ok) {
const errText = await response.text();
console.warn(`[Muapi] Poll error (${response.status}):`, errText);
// Continue polling on non-fatal errors
if (response.status >= 500) continue;
throw new Error(`Poll Failed: ${response.status} - ${errText.slice(0, 100)}`);
}
const data = await response.json();
console.log('[Muapi] Poll Response:', data);
const status = data.status?.toLowerCase();
if (status === 'completed' || status === 'succeeded' || status === 'success') {
return data;
}
if (status === 'failed' || status === 'error') {
throw new Error(`Generation failed: ${data.error || 'Unknown error'}`);
}
// Otherwise (processing, pending, etc.) keep polling
} catch (error) {
if (attempt === maxAttempts) throw error;
console.warn('[Muapi] Poll attempt failed, retrying...', error.message);
}
}
throw new Error('Generation timed out after polling.');
}
getDimensionsFromAR(ar) {
// Base unit 1024 (Flux standard)
switch (ar) {
case '1:1': return [1024, 1024];
case '16:9': return [1280, 720]; // 1024*1024 area approx
case '9:16': return [720, 1280];
case '4:3': return [1152, 864];
case '3:2': return [1216, 832];
case '21:9': return [1536, 640];
default: return [1024, 1024];
}
}
}
export const muapi = new MuapiClient();

View file

@ -1,24 +1,44 @@
import './style.css'
import javascriptLogo from './javascript.svg'
import viteLogo from '/vite.svg'
import { setupCounter } from './counter.js'
import './style.css';
import { Header } from './components/Header.js';
import { ImageStudio } from './components/ImageStudio.js';
document.querySelector('#app').innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
<img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
</a>
<h1>Hello Vite!</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite logo to learn more
</p>
</div>
`
const app = document.querySelector('#app');
let contentArea; // Declare contentArea globally so navigate can access it
setupCounter(document.querySelector('#counter'))
app.innerHTML = '';
app.appendChild(Header());
contentArea = document.createElement('main'); // Assign to global contentArea
contentArea.id = 'content-area';
contentArea.className = 'flex-1 relative w-full overflow-hidden flex flex-col bg-app-bg';
app.appendChild(contentArea);
// Initial Route
navigate('image');
// Router
function navigate(page) {
contentArea.innerHTML = '';
if (page === 'image') {
contentArea.appendChild(ImageStudio());
} else if (page === 'video') {
contentArea.innerHTML = '<div class="flex items-center justify-center h-full text-secondary">Video Studio Coming Soon 🎬</div>';
} else if (page === 'cinema') {
contentArea.innerHTML = '<div class="flex items-center justify-center h-full text-secondary">Cinema Studio Coming Soon 🎥</div>';
}
}
// Initial Load
navigate('image');
// Event Listener for Navigation
window.addEventListener('navigate', (e) => {
if (e.detail.page === 'settings') {
import('./components/SettingsModal.js').then(({ SettingsModal }) => {
document.body.appendChild(SettingsModal());
});
} else {
navigate(e.detail.page);
}
});

View file

@ -1,96 +1,29 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
@import './styles/global.css';
@import './styles/studio.css';
/* App-specific layout */
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
overflow: hidden;
background: var(--bg-app);
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #f7df1eaa);
main {
flex: 1;
position: relative;
overflow: hidden;
display: flex;
}
.card {
padding: 2em;
/* Utilities */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}

75
src/styles/global.css Normal file
View file

@ -0,0 +1,75 @@
@import "tailwindcss";
@theme {
--color-primary: #d9ff00;
--color-primary-hover: #c4e600;
--color-app-bg: #050505;
--color-panel-bg: #0a0a0a;
--color-card-bg: #141414;
--color-secondary: #a1a1aa;
--color-muted: #52525b;
--font-sans: "Inter", "system-ui", "-apple-system", "sans-serif";
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
--radius-3xl: 2rem;
--shadow-glow: 0 0 20px rgba(217, 255, 0, 0.4);
--shadow-glow-accent: 0 0 20px rgba(168, 85, 247, 0.4);
--shadow-3xl: 0 35px 60px -15px rgba(0, 0, 0, 0.8);
}
@layer base {
body {
@apply bg-app-bg text-white font-sans antialiased h-screen overflow-hidden;
}
/* Custom Scrollbar Refinement */
::-webkit-scrollbar {
@apply w-1.5 h-1.5;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply bg-white/10 rounded-full hover:bg-white/20 transition-colors;
}
}
@layer components {
.glass {
@apply bg-black/80 backdrop-blur-2xl border border-white/10 shadow-3xl;
}
.glass-panel {
@apply bg-black/60 backdrop-blur-xl border border-white/5;
}
.neon-border {
@apply border border-primary/20 hover:border-primary/50 transition-colors;
}
.interactive-glow {
@apply transition-all duration-300 hover:shadow-glow hover:scale-[1.02] active:scale-[0.98];
}
}
/* Custom Animations */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fade-in-up 0.6s cubic-bezier(0.23, 1, 0.32, 1) forwards;
}

119
src/styles/studio.css Normal file
View file

@ -0,0 +1,119 @@
/* Studio Specific Styles */
.style-card {
position: relative;
border-radius: var(--border-radius-lg);
overflow: hidden;
aspect-ratio: 16/10;
cursor: pointer;
border: 1px solid var(--border-color);
transition: all var(--transition-normal);
}
.style-card:hover {
border-color: var(--color-primary);
transform: translateY(-2px);
box-shadow: var(--shadow-glow);
}
.style-card.active {
border-color: var(--color-primary);
box-shadow: var(--shadow-glow);
}
.style-card img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.7;
transition: opacity var(--transition-normal);
}
.style-card:hover img,
.style-card.active img {
opacity: 1;
}
.style-card .label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0.5rem;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
color: white;
font-size: 0.75rem;
font-weight: 600;
}
/* Prompt Bar Layout */
.prompt-bar-container {
display: flex;
gap: 1rem;
width: 100%;
}
.prompt-field {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.prompt-field label {
font-size: 0.7rem;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding-left: 0.25rem;
}
.prompt-field textarea {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--border-radius-md);
padding: 0.75rem;
color: white;
resize: none;
font-size: 0.85rem;
transition: all var(--transition-fast);
}
.prompt-field textarea:focus {
background: rgba(255, 255, 255, 0.05);
border-color: var(--color-primary);
outline: none;
}
/* Canvas Toolbar */
.canvas-toolbar {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
opacity: 0;
transition: opacity var(--transition-normal);
}
.result-wrapper:hover .canvas-toolbar {
opacity: 1;
}
.toolbar-btn {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
color: white;
padding: 0.5rem;
border-radius: var(--border-radius-md);
cursor: pointer;
transition: all var(--transition-fast);
}
.toolbar-btn:hover {
background: var(--color-primary);
color: black;
border-color: var(--color-primary);
}

49
src/styles/variables.css Normal file
View file

@ -0,0 +1,49 @@
:root {
/* Brand Colors */
--color-primary: #d9ff00;
/* Neon Yellow-Green (Higgsfield Nano) */
--color-primary-hover: #c4e600;
--color-accent: #a855f7;
/* Creative Purple */
--color-accent-hover: #9333ea;
--color-danger: #ef4444;
/* Backgrounds - Strict Dark Mode */
--bg-app: #050505;
/* Deepest Black */
--bg-panel: #0a0a0a;
/* Panel Background */
--bg-card: #141414;
/* Card/Input Background */
--bg-glass: rgba(10, 10, 10, 0.8);
/* Text */
--text-primary: #ffffff;
--text-secondary: #a1a1aa;
--text-muted: #52525b;
/* UI Elements */
--border-color: #27272a;
--border-light: rgba(255, 255, 255, 0.1);
--border-radius-sm: 6px;
--border-radius-md: 10px;
--border-radius-lg: 16px;
--border-radius-xl: 24px;
--border-radius-full: 9999px;
/* Effects */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--shadow-glow: 0 0 20px rgba(176, 251, 93, 0.4);
--shadow-glow-accent: 0 0 20px rgba(168, 85, 247, 0.4);
--backdrop-blur: blur(20px);
/* Transitions */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1);
/* Typography */
--font-family: 'Inter', system-ui, -apple-system, sans-serif;
}

36
tailwind.config.js Normal file
View file

@ -0,0 +1,36 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#d9ff00',
hover: '#c4e600',
},
'app-bg': '#050505',
'panel-bg': '#0a0a0a',
'card-bg': '#141414',
secondary: '#a1a1aa',
muted: '#52525b',
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
},
borderRadius: {
'xl': '1rem',
'2xl': '1.5rem',
'3xl': '2rem',
},
boxShadow: {
'glow': '0 0 20px rgba(217, 255, 0, 0.4)',
'glow-accent': '0 0 20px rgba(168, 85, 247, 0.4)',
'3xl': '0 35px 60px -15px rgba(0, 0, 0, 0.8)',
}
},
},
plugins: [],
}

17
vite.config.js Normal file
View file

@ -0,0 +1,17 @@
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
tailwindcss(),
],
server: {
proxy: {
'/api': {
target: 'https://api.muapi.ai',
changeOrigin: true,
secure: false
}
}
}
});