IA_API / index.html
Simonc-44's picture
Update index.html
e5c0a99 verified
<!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 --- */
.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>