Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CygnisAI Image Studio</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <style> | |
| :root { | |
| --bg-dark: #030014; | |
| --panel-bg: #0a0a12; | |
| --primary: #8b5cf6; | |
| --primary-hover: #7c3aed; | |
| --text-main: #ffffff; | |
| --text-muted: #94a3b8; | |
| --border: rgba(255, 255, 255, 0.08); | |
| --card-bg: rgba(20, 20, 30, 0.6); | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| font-family: 'Inter', sans-serif; | |
| height: 100vh; | |
| display: flex; | |
| overflow: hidden; | |
| background-image: | |
| radial-gradient(circle at 0% 0%, rgba(188, 19, 254, 0.15), transparent 40%), | |
| radial-gradient(circle at 100% 100%, rgba(0, 243, 255, 0.1), transparent 40%); | |
| } | |
| /* --- SIDEBAR --- */ | |
| .sidebar { | |
| width: 260px; | |
| background: rgba(10, 10, 18, 0.8); | |
| backdrop-filter: blur(20px); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| padding: 2rem; | |
| gap: 2rem; | |
| z-index: 10; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.8rem; | |
| color: white; | |
| letter-spacing: -0.5px; | |
| } | |
| .logo svg { color: var(--primary); filter: drop-shadow(0 0 10px var(--primary)); } | |
| .nav-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| padding: 1rem; | |
| border-radius: 12px; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| border: 1px solid transparent; | |
| } | |
| .nav-btn:hover { | |
| background: rgba(255,255,255,0.03); | |
| color: white; | |
| border-color: rgba(255,255,255,0.05); | |
| } | |
| .nav-btn.active { | |
| background: linear-gradient(90deg, rgba(139, 92, 246, 0.1), transparent); | |
| color: var(--primary); | |
| border-left: 3px solid var(--primary); | |
| } | |
| /* --- MAIN CONTENT --- */ | |
| .main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| } | |
| .content-view { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 2rem; | |
| display: none; | |
| flex-direction: column; | |
| gap: 2rem; | |
| align-items: center; | |
| scroll-behavior: smooth; | |
| } | |
| .content-view.active { display: flex; } | |
| .welcome-message { | |
| text-align: center; | |
| margin-top: 10vh; | |
| opacity: 1; | |
| transition: opacity 0.5s; | |
| } | |
| .welcome-message h1 { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| background: linear-gradient(to right, #fff, #a5b4fc); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 1rem; | |
| } | |
| .welcome-message p { color: var(--text-muted); font-size: 1.1rem; } | |
| /* --- IMAGE CARD --- */ | |
| .image-card { | |
| background: var(--card-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 24px; | |
| padding: 1.5rem; | |
| width: 100%; | |
| max-width: 700px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1); | |
| box-shadow: 0 20px 50px rgba(0,0,0,0.3); | |
| backdrop-filter: blur(10px); | |
| transition: transform 0.3s; | |
| } | |
| .image-wrapper { | |
| width: 100%; | |
| border-radius: 16px; | |
| overflow: hidden; | |
| position: relative; | |
| background: #000; | |
| min-height: 400px; | |
| display: flex; align-items: center; justify-content: center; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| } | |
| .image-wrapper img { | |
| width: 100%; height: auto; display: block; | |
| transition: transform 0.5s; | |
| } | |
| .image-wrapper:hover img { transform: scale(1.02); } | |
| .card-footer { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| gap: 1rem; | |
| } | |
| .prompt-text { | |
| font-size: 1rem; | |
| color: #e0e0e0; | |
| font-weight: 400; | |
| line-height: 1.5; | |
| flex: 1; | |
| } | |
| .card-actions { | |
| display: flex; | |
| gap: 0.8rem; | |
| } | |
| .icon-btn { | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| border-radius: 10px; | |
| width: 42px; height: 42px; | |
| display: flex; align-items: center; justify-content: center; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .icon-btn:hover { | |
| background: rgba(255,255,255,0.1); | |
| color: white; | |
| border-color: rgba(255,255,255,0.2); | |
| transform: translateY(-2px); | |
| } | |
| /* --- GALLERY VIEW --- */ | |
| .gallery-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | |
| gap: 1.5rem; | |
| width: 100%; | |
| } | |
| .gallery-item { | |
| position: relative; | |
| border-radius: 16px; | |
| overflow: hidden; | |
| cursor: pointer; | |
| aspect-ratio: 1 / 1; | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.3); | |
| } | |
| .gallery-item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; } | |
| .gallery-item:hover img { transform: scale(1.05); } | |
| /* --- INPUT AREA --- */ | |
| .input-container { | |
| padding: 2rem; | |
| display: flex; | |
| justify-content: center; | |
| background: linear-gradient(to top, var(--bg-dark) 60%, transparent); | |
| position: relative; | |
| z-index: 20; | |
| } | |
| .input-box { | |
| width: 100%; | |
| max-width: 700px; | |
| background: rgba(20, 20, 30, 0.8); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| padding: 0.8rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.5); | |
| transition: all 0.3s; | |
| } | |
| .input-box:focus-within { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 30px rgba(0, 243, 255, 0.15); | |
| transform: translateY(-2px); | |
| } | |
| input { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| color: white; | |
| font-size: 1.1rem; | |
| font-family: inherit; | |
| padding: 0.5rem 1rem; | |
| } | |
| input:focus { outline: none; } | |
| input::placeholder { color: rgba(255,255,255,0.3); } | |
| /* --- WAOW BUTTON --- */ | |
| .generate-btn { | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: white; | |
| border: none; | |
| border-radius: 14px; | |
| padding: 0.8rem 1.5rem; | |
| font-weight: 700; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| display: flex; align-items: center; gap: 0.6rem; | |
| transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: 0 5px 15px rgba(188, 19, 254, 0.3); | |
| } | |
| .generate-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: -100%; width: 100%; height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); | |
| transition: 0.5s; | |
| } | |
| .generate-btn:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 10px 25px rgba(0, 243, 255, 0.4); | |
| } | |
| .generate-btn:hover::before { left: 100%; } | |
| .generate-btn:disabled { | |
| background: #1e293b; | |
| color: #64748b; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| /* --- LOADER --- */ | |
| .shimmer { | |
| position: absolute; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%); | |
| background-size: 200% 100%; | |
| animation: shimmer 1.5s infinite; | |
| } | |
| @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } | |
| @keyframes slideUp { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } } | |
| </style> | |
| </head> | |
| <body> | |
| <aside class="sidebar"> | |
| <div class="logo"> | |
| <svg class="w-12 h-12 text-white" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM12 19C8.13401 19 5 15.866 5 12C5 8.13401 8.13401 5 12 5C15.866 5 19 8.13401 19 12C19 15.866 15.866 19 12 19Z" fill="currentColor" fill-opacity="0.2"></path><path d="M16.5414 10.3541C15.8617 9.68729 14.974 9.24988 14 9.10241V12.1873L16.5414 10.3541Z" fill="currentColor"></path><path d="M12.0001 14.8129L9.45874 16.6461C10.1384 17.3129 11.0261 17.7503 12.0001 17.8978V14.8129Z" fill="currentColor"></path><path d="M12.0001 6.10254C12.974 6.24995 13.8617 6.68735 14.5414 7.35413L12.0001 9.18734V6.10254Z" fill="currentColor"></path><path d="M15.5 12C15.5 13.933 13.933 15.5 12 15.5C10.067 15.5 8.5 13.933 8.5 12C8.5 10.067 10.067 8.5 12 8.5C13.933 8.5 15.5 10.067 15.5 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg> | |
| CygnisAI | |
| </div> | |
| <nav style="flex:1; display:flex; flex-direction:column; gap:0.5rem;"> | |
| <div class="nav-btn active" onclick="showView('home')"><i data-lucide="sparkles"></i> Créer</div> | |
| <div class="nav-btn" onclick="showView('gallery')"><i data-lucide="grid"></i> Galerie</div> | |
| </nav> | |
| </aside> | |
| <main class="main"> | |
| <div id="view-home" class="content-view active"> | |
| <div class="gallery-container" id="gallery-home"> | |
| <div class="welcome-message" id="welcome"> | |
| <div style="width:80px; height:80px; background:linear-gradient(135deg, var(--primary), var(--secondary)); border-radius:24px; display:flex; align-items:center; justify-content:center; margin:0 auto 2rem; box-shadow:0 0 40px rgba(188,19,254,0.4);"> | |
| <i data-lucide="image" size="40" color="white"></i> | |
| </div> | |
| <h1 style="font-size:2.5rem; margin-bottom:1rem; font-weight:700;">Imaginez l'impossible</h1> | |
| <p style="color:var(--text-muted); font-size:1.1rem;">Tapez votre idée ci-dessous pour commencer la création.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="view-gallery" class="content-view"> | |
| <div class="gallery-grid" id="gallery-grid"> | |
| <!-- Gallery items will be injected here --> | |
| </div> | |
| </div> | |
| <div class="input-container"> | |
| <div class="input-box"> | |
| <input type="text" id="prompt" placeholder="Un chat cybernétique dans une ville néon..." autocomplete="off"> | |
| <button id="generate-btn" class="generate-btn"> | |
| <i data-lucide="wand-2" size="18"></i> Générer | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| lucide.createIcons(); | |
| const btn = document.getElementById('generate-btn'); | |
| const promptInput = document.getElementById('prompt'); | |
| const galleryHome = document.getElementById('gallery-home'); | |
| const galleryGrid = document.getElementById('gallery-grid'); | |
| const welcome = document.getElementById('welcome'); | |
| // --- NAVIGATION --- | |
| window.showView = (view) => { | |
| document.querySelectorAll('.content-view').forEach(el => el.classList.remove('active')); | |
| document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active')); | |
| if (view === 'home') { | |
| document.getElementById('view-home').classList.add('active'); | |
| document.querySelector('.nav-btn:nth-child(1)').classList.add('active'); | |
| } else if (view === 'gallery') { | |
| document.getElementById('view-gallery').classList.add('active'); | |
| document.querySelector('.nav-btn:nth-child(2)').classList.add('active'); | |
| renderGalleryGrid(); | |
| } | |
| }; | |
| // --- DATA & STATE --- | |
| let history = JSON.parse(localStorage.getItem('cygnis_img_history') || '[]'); | |
| if (history.length > 0) { | |
| welcome.style.display = 'none'; | |
| history.forEach(item => addImageToHome(item.url, item.prompt, false)); | |
| } | |
| function addImageToHome(url, promptText, prepend = true) { | |
| const card = document.createElement('div'); | |
| card.className = 'image-card'; | |
| card.innerHTML = ` | |
| <div class="image-wrapper"> | |
| <img src="${url}" alt="${promptText}" onload="this.style.opacity=1" style="opacity:0; transition:opacity 0.5s;"> | |
| </div> | |
| <div class="card-footer"> | |
| <div class="prompt-text">${promptText}</div> | |
| <div class="card-actions"> | |
| <button class="icon-btn" onclick="downloadImage('${url}')" title="Télécharger"><i data-lucide="download" size="18"></i></button> | |
| <button class="icon-btn" title="Copier le prompt"><i data-lucide="copy" size="18"></i></button> | |
| </div> | |
| </div> | |
| `; | |
| if (prepend) galleryHome.prepend(card); | |
| else galleryHome.appendChild(card); | |
| lucide.createIcons(); | |
| if (prepend) card.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| function renderGalleryGrid() { | |
| galleryGrid.innerHTML = history.map(item => ` | |
| <div class="gallery-item" onclick="showImageInHome('${item.url}', '${item.prompt}')"> | |
| <img src="${item.url}" alt="${item.prompt}"> | |
| </div> | |
| `).join(''); | |
| } | |
| window.showImageInHome = (url, prompt) => { | |
| showView('home'); | |
| // Remove welcome message if it's there | |
| if (welcome) welcome.style.display = 'none'; | |
| // Check if card already exists | |
| let existingCard = false; | |
| document.querySelectorAll('.image-card').forEach(card => { | |
| if (card.querySelector('img')?.src.includes(url)) { | |
| card.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| existingCard = true; | |
| } | |
| }); | |
| if (!existingCard) { | |
| addImageToHome(url, prompt, true); | |
| } | |
| }; | |
| window.downloadImage = (url) => { | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'cygnis-creation.png'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| }; | |
| btn.addEventListener('click', async () => { | |
| const text = promptInput.value; | |
| if (!text) return; | |
| welcome.style.display = 'none'; | |
| btn.disabled = true; | |
| btn.innerHTML = '<div style="width:20px;height:20px;border:3px solid white;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;"></div>'; | |
| const placeholder = document.createElement('div'); | |
| placeholder.className = 'image-card'; | |
| placeholder.innerHTML = ` | |
| <div class="image-wrapper"> | |
| <div class="shimmer"></div> | |
| </div> | |
| <div class="card-footer"> | |
| <div class="prompt-text">${text}</div> | |
| </div> | |
| `; | |
| galleryHome.prepend(placeholder); | |
| try { | |
| const res = await fetch('/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ prompt: text }) | |
| }); | |
| if (!res.ok) throw new Error("Erreur serveur"); | |
| const data = await res.json(); | |
| placeholder.innerHTML = ` | |
| <div class="image-wrapper"> | |
| <img src="${data.image_url}" alt="${text}"> | |
| </div> | |
| <div class="card-footer"> | |
| <div class="prompt-text">${text}</div> | |
| <div class="card-actions"> | |
| <button class="icon-btn" onclick="downloadImage('${data.image_url}')"><i data-lucide="download" size="18"></i></button> | |
| <button class="icon-btn"><i data-lucide="copy" size="18"></i></button> | |
| </div> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| const newHistory = [{ url: data.image_url, prompt: text }, ...history].slice(0, 50); | |
| localStorage.setItem('cygnis_img_history', JSON.stringify(newHistory)); | |
| history = newHistory; | |
| } catch (e) { | |
| placeholder.innerHTML = `<p style="color:#ef4444; text-align:center; padding:2rem;">Erreur : ${e.message}</p>`; | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = '<i data-lucide="wand-2" size="18"></i> Générer'; | |
| lucide.createIcons(); | |
| promptInput.value = ''; | |
| promptInput.focus(); | |
| } | |
| }); | |
| promptInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') btn.click(); | |
| }); | |
| const style = document.createElement('style'); | |
| style.innerHTML = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }'; | |
| document.head.appendChild(style); | |
| </script> | |
| </body> | |
| </html> | |