| | <!DOCTYPE html> |
| | <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 { |
| | 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 { |
| | 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 { |
| | 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-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-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); } |
| | |
| | |
| | .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; |
| | } |
| | |
| | |
| | .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"> |
| | |
| | </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'); |
| | |
| | |
| | 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(); |
| | } |
| | }; |
| | |
| | |
| | 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'); |
| | |
| | if (welcome) welcome.style.display = 'none'; |
| | |
| | |
| | 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> |
| |
|