mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-05-07 01:17:18 +00:00
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:
parent
5db9f98e99
commit
e0efb745d5
19 changed files with 7397 additions and 439 deletions
|
|
@ -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
2006
models_dump.json
Normal file
File diff suppressed because it is too large
Load diff
2189
package-lock.json
generated
2189
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -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
5
postcss.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
60
src/components/AuthModal.js
Normal file
60
src/components/AuthModal.js
Normal 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
75
src/components/Header.js
Normal 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;
|
||||
}
|
||||
578
src/components/ImageStudio.js
Normal file
578
src/components/ImageStudio.js
Normal 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;
|
||||
}
|
||||
92
src/components/SettingsModal.js
Normal file
92
src/components/SettingsModal.js
Normal 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
83
src/components/Sidebar.js
Normal 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
2023
src/lib/models.js
Normal file
File diff suppressed because it is too large
Load diff
172
src/lib/muapi.js
Normal file
172
src/lib/muapi.js
Normal 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();
|
||||
64
src/main.js
64
src/main.js
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
109
src/style.css
109
src/style.css
|
|
@ -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
75
src/styles/global.css
Normal 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
119
src/styles/studio.css
Normal 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
49
src/styles/variables.css
Normal 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
36
tailwind.config.js
Normal 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
17
vite.config.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue