|
|
<!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> |
|
|
|