genimage / index.html
artydev's picture
Update index.html
a9d40c7 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Title will be set dynamically -->
<title>GenImage</title>
<style>
/* --- CONFIGURATION & SETUP --- */
:root {
--primary: #667eea;
--secondary: #764ba2;
--bg-dark: #0f0f23;
--bg-medium: #1a1a2e;
--bg-light: #2a2a3a;
--text-primary: #ffffff;
--text-secondary: #aaaaaa;
--success: #48bb78;
--error: #f56565;
--border: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* --- MAIN LAYOUT & HEADER --- */
.app-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
position: relative;
}
h1 {
font-size: 2.5rem;
font-weight: 600;
background: linear-gradient(90deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 0.5rem;
}
.tagline {
color: var(--text-secondary);
font-size: 1.1rem;
}
/* --- LANGUAGE SWITCHER --- */
.language-switcher {
position: absolute;
top: 0;
right: 0;
z-index: 100;
}
.lang-switcher-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--bg-medium);
border: 1px solid var(--border);
color: var(--text-primary);
font-weight: 600;
padding: 0.5rem 1rem;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
}
.lang-switcher-toggle:hover {
border-color: var(--primary);
}
.lang-switcher-toggle .arrow {
font-size: 0.8em;
transition: transform 0.2s;
}
.lang-switcher-toggle.open .arrow {
transform: rotate(180deg);
}
.lang-switcher-menu {
position: absolute;
top: calc(100% + 5px);
right: 0;
background: var(--bg-light);
border: 1px solid var(--border);
border-radius: 12px;
padding: 0.5rem;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
gap: 0.25rem;
opacity: 0;
transform: translateY(-10px);
pointer-events: none;
transition: all 0.2s ease-out;
}
.lang-switcher-menu.open {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
.lang-switcher-option {
background: none;
border: none;
color: var(--text-secondary);
font-weight: 600;
padding: 0.5rem 1rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
text-align: left;
}
.lang-switcher-option:hover {
background: var(--primary);
color: var(--text-primary);
}
.lang-switcher-option.active {
color: var(--primary);
}
/* --- CONTROL PANEL & FORM --- */
.control-panel {
background: var(--bg-medium);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.prompt-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.input-group {
position: relative;
}
.input-group input {
width: 100%;
background: var(--bg-light);
border: 2px solid var(--border);
border-radius: 12px;
padding: 1rem 1.5rem;
color: var(--text-primary);
font-size: 1rem;
transition: all 0.3s ease;
}
.input-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3);
}
/* --- NEW TOOLBOX STYLES --- */
.toolbox-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 1.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border);
}
.toolbox-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbox-item label {
font-size: 0.9rem;
color: var(--text-secondary);
white-space: nowrap;
}
.toolbox-item input {
width: 80px;
background: var(--bg-light);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.5rem;
color: var(--text-primary);
text-align: center;
}
.toolbox-item input:focus {
outline: none;
border-color: var(--primary);
}
.toolbox-actions {
display: flex;
gap: 0.75rem;
margin-left: auto;
/* Push buttons to the right */
}
/* --- BUTTONS --- */
.btn {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border: none;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn-outline {
background: transparent;
border: 2px solid var(--primary);
color: var(--primary);
padding: 0.625rem 1.5rem;
/* Adjust padding for border */
}
.btn-outline:hover {
background: var(--primary);
color: white;
}
.btn-danger {
background: transparent;
border: 1px solid var(--error);
color: var(--error);
padding: 0.5rem 1rem;
border-radius: 12px;
font-size: 0.9rem;
}
.btn-danger:hover {
background: var(--error);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn-action {
background: transparent;
border: 1px solid #667EEA;
color: #667EEA;
padding: 0.5rem 1rem;
border-radius: 12px;
font-size: 0.9rem;
}
.btn-action:hover {
background: #667EEA;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* --- TOOLBAR, TABS & BANNER --- */
.gallery-toolbar {
}
.banner {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(90deg, var(--primary), var(--secondary));
padding: 0.75rem 1.5rem;
border-radius: 12px;
font-size: 0.95rem;
font-weight: 500;
}
.banner-content {
display: flex;
align-items: center;
gap: 0.75rem;
}
.banner-close {
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
font-size: 1.2rem;
cursor: pointer;
transition: color 0.2s;
padding: 0.25rem;
line-height: 1;
}
.banner-close:hover {
color: white;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
}
.tab {
padding: 0.75rem 1.25rem;
border-radius: 8px 8px 0 0;
cursor: pointer;
border: none;
background: none;
color: var(--text-secondary);
transition: all 0.2s;
border-bottom: 2px solid transparent;
}
.tab:hover {
background: var(--bg-light);
}
.tab.active {
font-weight: 600;
color: var(--text-primary);
border-bottom-color: var(--primary);
}
/* --- GALLERY & IMAGES --- */
.image-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.image-card {
background: var(--bg-medium);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
display: flex;
flex-direction: column;
position: relative;
}
.image-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.image-card.featured {
grid-column: span 2;
}
.image-display {
aspect-ratio: 1/1;
position: relative;
background: var(--bg-light);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.image-display img {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s;
}
.image-info {
padding: 1rem;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.image-prompt {
font-size: 0.95rem;
margin-bottom: 0.75rem;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
flex-grow: 1;
}
.image-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-secondary);
}
.image-actions {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 0.5rem;
z-index: 10;
}
.action-btn {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
transform: scale(1.1);
background: rgba(0, 0, 0, 0.8);
}
.action-btn.delete:hover {
background: rgba(255, 0, 0, 0.8);
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-top: 4px solid var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
position: relative;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 1rem;
padding: 3rem;
grid-column: 1 / -1;
color: var(--text-secondary);
border: 2px dashed var(--border);
border-radius: 16px;
}
.empty-state-icon {
font-size: 3rem;
}
/* --- OVERLAYS (TOASTS & LIGHTBOX) --- */
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 1rem;
}
.toast {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
border-radius: 10px;
background: var(--bg-medium);
border-left: 4px solid var(--primary);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
transform: translateX(calc(100% + 20px));
opacity: 0;
animation: toast-in 0.5s forwards cubic-bezier(0.25, 1, 0.5, 1);
}
.toast.success {
border-left-color: var(--success);
}
.toast.error {
border-left-color: var(--error);
}
.lightbox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.lightbox.active {
opacity: 1;
pointer-events: all;
}
.lightbox-content {
position: relative;
max-width: 90%;
max-height: 90%;
}
.lightbox-image {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
}
.lightbox-close {
position: absolute;
top: -10px;
right: -10px;
background: var(--bg-dark);
border: 1px solid var(--border);
color: white;
font-size: 1.2rem;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s;
}
.lightbox-close:hover {
transform: scale(1.1);
}
/* --- ANIMATIONS & MEDIA QUERIES --- */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes toast-in {
to {
transform: translateX(0);
opacity: 1;
}
}
@media (max-width: 860px) {
.language-switcher {
position: static;
justify-content: center;
margin-bottom: 1.5rem;
}
.header {
padding-top: 0;
}
.toolbox-actions {
margin-left: 0;
width: 100%;
}
.toolbox-actions .btn {
flex-grow: 1;
}
}
@media (max-width: 768px) {
.app-layout {
padding: 1rem;
}
.image-gallery {
grid-template-columns: 1fr;
}
.image-card.featured {
grid-column: span 1;
}
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/juris@0.88"></script>
<script>
// --- I18N SYSTEM ---
const translations = {
it: {
langName: "Italiano",
flag: "🇮🇹 ",
appTitle: "GenImage",
headerTitle: "GenImagini",
tagline: "Trasforma la tua immaginazione in immagini straordinarie",
bannerText: "⭐ Sblocca le funzionalità Pro per generazioni illimitate e modelli avanzati!",
promptLabel: "Prompt AI",
// ADD THIS LINE
selectModelLabel: "Seleziona Modello",
promptPlaceholder: "Descrivi cosa vuoi generare...",
widthLabel: "Larghezza",
heightLabel: "Altezza",
seedLabel: "Seme",
generateButton: "Genera ✨",
randomizeButton: "Casuale",
galleryTab: "La tua Galleria",
historyTab: "Cronologia Prompt",
emptyGalleryTitle: "La tua galleria è vuota",
emptyGalleryDesc: "Inserisci un prompt e fai clic su Genera per creare la tua prima immagine.",
metaSeed: "Seme: {seed}",
metaRandom: "Casuale",
failedToLoad: "⚠️ Caricamento fallito",
toastHistoryFail: "Impossibile caricare la cronologia",
toastImageSuccess: "Immagine generata con successo!",
toastImageFail: "Impossibile generare l'immagine",
toastEnterPrompt: "Per favore, inserisci un prompt",
toastImageRemoved: "Immagine rimossa",
toastSettingsLoaded: "Impostazioni del prompt caricate",
toastRestoring: "Ripristino di {count} immagini dalla cronologia...",
toastNewSeed: "Nuovo seme: {seed}",
deleteTooltip: "Elimina Immagine",
zoomTooltip: "Visualizza a Schermo Intero",
clearAllButton: "Cancella Tutto",
clearAllConfirm: "Sei sicuro di voler svuotare l'intera galleria e la cronologia? Questa azione non può essere annullata.",
toastGalleryCleared: "Galleria e cronologia svuotate.",
clearPrompt: "Cancella Prompt"
},
en: {
langName: "English",
flag:"🇬🇧 ",
clearPrompt: "Clear Prompt",
appTitle: "GenImage",
headerTitle: "GenImages",
tagline: "Transform your imagination into stunning visuals",
bannerText: "⭐ Unlock Pro features for unlimited generations and advanced models!",
promptLabel: "AI Prompt",
// ADD THIS LINE
selectModelLabel: "Select Model",
promptPlaceholder: "Describe what you want to generate...",
widthLabel: "Width",
heightLabel: "Height",
seedLabel: "Seed",
generateButton: "Generate ✨",
randomizeButton: "Randomize",
galleryTab: "Your Gallery",
historyTab: "Prompt History",
emptyGalleryTitle: "Your gallery is empty",
emptyGalleryDesc: "Enter a prompt and click Generate to create your first image.",
metaSeed: "Seed: {seed}",
metaRandom: "Random",
failedToLoad: "⚠️ Failed to load",
toastHistoryFail: "Failed to load history",
toastImageSuccess: "Image generated successfully!",
toastImageFail: "Failed to generate image",
toastEnterPrompt: "Please enter a prompt",
toastImageRemoved: "Image removed",
toastSettingsLoaded: "Prompt settings loaded",
toastRestoring: "Restoring {count} images from history...",
toastNewSeed: "New seed: {seed}",
deleteTooltip: "Delete Image",
zoomTooltip: "View Fullscreen",
clearAllButton: "Clear All",
clearAllConfirm: "Are you sure you want to clear the entire gallery and history? This cannot be undone.",
toastGalleryCleared: "Gallery and history cleared.",
},
fr: {
langName: "Français",
flag: '🇫🇷 ',
appTitle: "GenImage",
headerTitle: "GenImages",
tagline: "Mettez vos idées en images",
bannerText: "⭐ Débloquez les fonctionnalités Pro pour des générations illimitées et des modèles avancés !",
promptLabel: "Prompt IA",
// ADD THIS LINE
selectModelLabel: "Sélectionner un modèle",
promptPlaceholder: "Décrivez ce que vous voulez générer...",
widthLabel: "Largeur",
heightLabel: "Hauteur",
seedLabel: "Graine",
generateButton: "Générer ✨",
randomizeButton: "Aléatoire",
galleryTab: "Votre Galerie",
historyTab: "Historique des Prompts",
emptyGalleryTitle: "Votre galerie est vide",
emptyGalleryDesc: "Entrez un prompt et cliquez sur Générer pour créer votre première image.",
metaSeed: "Graine : {seed}",
metaRandom: "Aléatoire",
failedToLoad: "⚠️ Échec du chargement",
toastHistoryFail: "Échec du chargement de l'historique",
toastImageSuccess: "Image générée avec succès !",
toastImageFail: "Échec de la génération de l'image",
toastEnterPrompt: "Veuillez entrer un prompt",
toastImageRemoved: "Image supprimée",
toastSettingsLoaded: "Paramètres du prompt chargés",
toastRestoring: "Restauration de {count} images de l'historique...",
toastNewSeed: "Nouvelle graine : {seed}",
deleteTooltip: "Supprimer l'image",
zoomTooltip: "Voir en plein écran",
clearAllButton: "Tout Effacer",
clearAllConfirm: "Êtes-vous sûr de vouloir vider toute la galerie et l'historique ? Cette action est irréversible.",
toastGalleryCleared: "Galerie et historique vidés.",
clearPrompt: "Effacer le Prompt"
},
};
const I18nManager = (props, { getState, setState, subscribe }) => {
const t = (key, params = {}) => {
const lang = getState('ui.lang', 'en');
let str = translations[lang]?.[key] || translations['en'][key] || key;
for (const p in params) {
str = str.replace(`{${p}}`, params[p]);
}
return str;
};
return {
api: { t },
hooks: {
onRegister: () => {
subscribe('ui.lang', (lang) => {
document.title = t('appTitle');
});
}
}
};
};
// Utility functions
const uniqueId = () => `${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
const debounce = (fn, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
};
const sanitizeInput = (str) => {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML.replace(/[/?#]/g, ' ');
};
// Toast System
const ToastSystem = (props, { getState, setState }) => {
const showToast = (message, type = 'info', duration = 5000) => {
const id = uniqueId();
const toast = { id, message, type };
setState('toasts', [...getState('toasts', []), toast]);
setTimeout(() => {
const toasts = getState('toasts', []).filter(t => t.id !== id);
setState('toasts', toasts);
}, duration);
};
return {
api: { showToast },
hooks: { onRegister: () => setState('toasts', []) }
};
};
// Prompt History Manager
const PromptHistoryManager = (props, { getState, setState, headless }) => {
const { t } = headless.I18nManager;
const save = () => {
const history = getState('promptHistory', []).map(item => ({
id: item.id, prompt: item.prompt, seed: item.seed,
width: item.width, height: item.height, createdAt: item.createdAt,
model: item.model // <--- ADD THIS LINE
}));
localStorage.setItem('juris_history', JSON.stringify(history));
};
const load = () => {
try {
const stored = localStorage.getItem('juris_history');
if (stored) setState('promptHistory', JSON.parse(stored));
} catch {
headless.ToastSystem.showToast(t('toastHistoryFail'), "error");
setState('promptHistory', []);
}
};
const clearHistory = () => {
setState('promptHistory', []);
localStorage.removeItem('juris_history');
};
return {
hooks: { onRegister: load },
api: {
add: (item) => {
// Ensure the 'item' passed here includes the model
let history = [item, ...getState('promptHistory', [])].slice(0, 50);
setState('promptHistory', history);
save();
},
remove: (id) => {
const newHistory = getState('promptHistory', []).filter(i => i.id !== id);
setState('promptHistory', newHistory);
save();
},
clearAll: () => {
if (confirm(t('clearAllConfirm'))) {
clearHistory();
setState('ui.imagesList', []);
setState('ui.imagesById', {});
headless.ToastSystem.showToast(t('toastGalleryCleared'), 'info');
}
}
}
};
};
// Image Generator Service
const ImageGenerator = (props, { getState, setState, headless }) => {
const { t } = headless.I18nManager;
// IMPORTANT: generateSingleImage needs to receive the model.
// The 'model' variable outside this function is the *current* app model,
// not necessarily the one saved with the history item.
const generateSingleImage = async (imageId, prompt, seed, width, height, modelUsed) => { // Added modelUsed parameter
try {
const encodedPrompt = encodeURIComponent(sanitizeInput(prompt));
// Use modelUsed here, which comes from the history item or current selection
let url = `https://image.pollinations.ai/prompt/${encodedPrompt}?nologo=true&enhance=true&model=${modelUsed}&width=${width}&height=${height}`;
if (seed) url += `&seed=${seed}`;
await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(url);
img.onerror = () => reject(new Error("Image generation failed"));
img.src = url;
});
setState(`ui.imagesById.${imageId}`, {
...getState(`ui.imagesById.${imageId}`),
status: "loaded",
imageUrl: url,
});
headless.ToastSystem.showToast(t('toastImageSuccess'), "success");
} catch (error) {
console.error("Image generation error:", error);
setState(`ui.imagesById.${imageId}.status`, "error");
headless.ToastSystem.showToast(t('toastImageFail'), "error");
}
};
const generateQueued = debounce(async () => {
const queue = getState('ui.generationQueue', []);
if (queue.length === 0) return;
// The task now contains all parameters including modelUsed
const [task, ...remaining] = queue;
setState('ui.generationQueue', remaining);
await generateSingleImage(...task); // Pass all task elements
generateQueued();
}, 300);
return {
api: {
generate: async () => {
const prompt = sanitizeInput(getState("ui.prompt", "").trim());
const seed = getState("ui.seed", "");
const width = Math.min(2048, Math.max(64, getState("ui.width", "512")));
const height = Math.min(2048, Math.max(64, getState("ui.height", "512")));
const currentModel = getState("model", "flux"); // Get the currently selected model
if (!prompt) {
headless.ToastSystem.showToast(t('toastEnterPrompt'), "error");
return;
}
const imageId = uniqueId();
const newImage = {
id: imageId, status: "loading", prompt, seed, width, height, imageUrl: null,
historyId: uniqueId(), createdAt: new Date().toISOString(),
model: currentModel // <--- SAVE THE MODEL HERE FOR NEW GENERATIONS
};
setState(`ui.imagesById.${imageId}`, newImage);
setState("ui.imagesList", [imageId, ...getState("ui.imagesList", [])]);
headless.PromptHistoryManager.add(newImage);
// Pass all parameters including currentModel to the queue
setState('ui.generationQueue', [...getState('ui.generationQueue', []), [imageId, prompt, seed, width, height, currentModel]]);
generateQueued();
},
regenerateFromHistory: () => {
const history = getState('promptHistory', []);
if (!history || history.length === 0) return;
const count = Math.min(50, history.length);
history.slice(0, count).forEach(item => {
const id = uniqueId();
// Ensure 'item.model' exists, default if not (for old history entries)
const modelToUse = item.model || getState("model", "flux");
const newImage = {
id, status: "loading", prompt: item.prompt, seed: item.seed, width: item.width,
height: item.height, imageUrl: null, historyId: item.id,
createdAt: item.createdAt || new Date().toISOString(),
model: modelToUse // <--- Store the model from history with the new image
};
setState(`ui.imagesById.${id}`, newImage);
setState("ui.imagesList", [...getState("ui.imagesList", []), id]);
// Pass the model from the history item to the queue
setState('ui.generationQueue', [...getState('ui.generationQueue', []), [id, item.prompt, item.seed, item.width, item.height, modelToUse]]);
});
generateQueued();
headless.ToastSystem.showToast(t('toastRestoring', { count }), "info");
}
}
};
};
// Image Display Component
// Image Display Component
const ImageDisplay = (props, { getState, setState, headless }) => ({
render: () => {
const { t } = headless.I18nManager;
const img = getState(`ui.imagesById.${props.id}`);
if (!img) return "ignore";
return {
div: {
className: { "image-card": true, "featured": props.featured },
children: [
{
div: {
className: "image-display",
children: [
{
div: {
className: "image-actions",
children: [
{
button: {
className: "action-btn delete", text: "✕", title: () => t('deleteTooltip'),
onclick: () => {
headless.PromptHistoryManager.remove(img.historyId);
const ids = getState("ui.imagesList", []);
setState("ui.imagesList", ids.filter((id) => id !== props.id));
const newMap = { ...getState("ui.imagesById") };
delete newMap[props.id];
setState("ui.imagesById", newMap);
headless.ToastSystem.showToast(t('toastImageRemoved'), "info");
}
}
},
]
}
},
{
div: {
style: { width: '100%', height: '100%' },
onclick: () => {
setState("ui.prompt", img.prompt);
setState("ui.seed", img.seed);
setState("ui.width", img.width);
setState("ui.height", img.height);
setState("model", img.model || "flux"); // <--- LOAD THE MODEL HERE
headless.ToastSystem.showToast(t('toastSettingsLoaded'), "info");
},
children: () => {
if (img.status === "loading") return [
{
div:
{
style: { display: 'grid', height: '100%', placeItems: 'center' },
children: [{ div: { className: "spinner" } }]
}
}
];
if (img.status === "error") return [{ div: { text: t('failedToLoad') } }];
return [{ img: { src: img.imageUrl, alt: img.prompt, loading: "lazy" } }];
}
}
}
]
}
},
{
div: {
className: "image-info",
children: [
{ div: { className: "image-prompt", text: img.prompt } },
{
div: {
className: "image-meta",
children: [
{ span: { text: `${img.width}x${img.height}` } },
{ span: { text: () => img.seed ? t('metaSeed', { seed: img.seed }) : t('metaRandom') } },
{ span: { text: `Model: ${img.model || "N/A"}` } } // <--- DISPLAY THE MODEL HERE
]
}
}
]
}
}
]
}
};
}
});
// Toast Display Component
const ToastDisplay = (props, { getState }) => ({
render: () => ({
div: {
className: "toast-container",
children: () => getState('toasts', []).map(toast => ({
div: {
key: toast.id, className: `toast ${toast.type}`,
children: [
{ div: { text: toast.type === 'success' ? '✓' : '⚠' } },
{ div: { text: toast.message } }
]
}
}))
}
})
});
// Gallery Controls Component
// Display : None
const GalleryControls = (props, { getState, setState, headless }) => ({
render: () => {
const { t } = headless.I18nManager;
return {
div: {
style: {display: "none"},
className: "tabs",
children: [
{
button: {
className: { "tab": true, "active": getState('ui.activeTab', 'images') === 'images' },
text: () => t('galleryTab'),
onclick: () => setState('ui.activeTab', 'images')
}
},
{
button: {
className: { "tab": true, "active": getState('ui.activeTab') === 'history' },
text: () => t('historyTab'),
onclick: () => setState('ui.activeTab', 'history')
}
}
]
}
}
}
});
const SelectModels = (props, { getState, setState, headless }) => { // Ensure 'headless' is destructured here
const { t } = headless.I18nManager; // Get the translation function
return {
div: { // Wrap the select in a div to hold the label and select
className: "input-group", // Reusing input-group for consistent styling if needed
children: [
{
label: {
style: {
color: "var(--primary)",
display: "block",
marginBottom: "0.5rem", // Add some space below the label
},
text: () => t('selectModelLabel') // Use the translated label
}
},
{
select: {
// Add inline styles here
style: {
background: "#1A1A2E", // Dark background
color: "var(--text-primary)", // Text color
fontSize: "1rem",
border: "1px solid rgba(255,255,255,0.5)", // Border like the textarea
borderRadius: "5px",
padding: "0.5rem 1rem", // Padding
appearance: "none", // Remove default select arrow for custom styling
cursor: "pointer",
outline: "none", // Remove outline on focus
backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%20197.4l-14.3-14.3c-2.1-2.1-4.7-3.3-7.7-3.3s-5.6%201.2-7.7%203.3L146.2%20275.9%2034.7%20164.4c-2.1-2.1-4.7-3.3-7.7-3.3s-5.6%201.2-7.7%203.3L5%20178.7c-2.1%202.1-3.3%204.7-3.3%207.7s1.2%205.6%203.3%207.7l136.2%20136.2c2.1%202.1%204.7%203.3%207.7%203.3s5.6-1.2%207.7-3.3L287%20212.8c2.1-2.1%203.3-4.7%203.3-7.7s-1.2-5.6-3.3-7.7z%22%2F%3E%3C%2Fsvg%3E")',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 0.7em top 50%',
backgroundSize: '0.65em auto',
paddingRight: '2.5em',
minWidth: '100px',
width: '100%' // Make it take full available width in its container
},
onchange: (e) => setState("model", e.target.value),
value: () => getState("model", "flux"),
children: [
{
option: {
text: "Flux",
value: "flux"
}
},
{
option: {
text: "Turbo",
value: "turbo"
}
},
]
}
}
]
}
}
}
// Component for just the main prompt input
// Component for just the main prompt input
const PromptInput = (props, { getState, setState, headless }) => {
const { t } = headless.I18nManager;
return {
div: {
className: "input-group",
children: [
// This div now only contains the 'AI Prompt' label
{
div: {
style: { display: "flex", gap: "2rem", flexDirection: "column"}, // Changed
}
},
{ SelectModels: {} },
{
div: {
style: {
position: "relative",
top: "0.5rem",
color: "var(--primary)",
},
text: t("promptLabel")
}
},
{
textarea: {
style: { width: "100%",
maxWidth: '100%',
minWidth: '100%',
display: "flex",
"flex-direction": "column",
"background": "#1A1A2E",
"color": "var(--text-primary)",
"font-size": "1rem",
"border": "1px solid rgba(255,255,255,0.5)",
"border-radius": "5px",
"padding": "1rem",
spellcheck: "false" ,
autocorrect:"off",
marginTop: "1rem" // Add some space above the textarea
},
rows: "5",
value: () => getState("ui.prompt", ""),
oninput: e => setState("ui.prompt", e.target.value)
}
}
]
}
}
};
// Component for the new toolbox bar
const Toolbox = (props, { getState, setState, headless }) => {
const { t } = headless.I18nManager;
return {
div: {
className: "toolbox-bar",
children: [
{ div: {
className: "toolbox-item",
style: () => ({ display: 'flex', 'flex-direction': getState("ui.isMobile") ? 'column' : 'row' }),
children: [
{ label: { text: () => t('widthLabel') } },
{ input: {
type: "number", min: "64", max: "2048", step: "16",
value: () => getState("ui.width", "512"),
oninput: e => setState("ui.width", e.target.value) } }] }
},
{ div: {
className: "toolbox-item",
style: () => ({ display: 'flex', 'flex-direction': getState("ui.isMobile") ? 'column' : 'row' }),
children: [
{ label: { text: () => t('heightLabel') } },
{ input: {
type: "number", min: "64", max: "2048", step: "16",
value: () => getState("ui.height", "512"),
oninput: e => setState("ui.height", e.target.value) } }] } },
{ div: {
className: "toolbox-item",
style: () => ({ display: 'flex', 'flex-direction': getState("ui.isMobile") ? 'column' : 'row' }),
children: [
{ label: { text: () => t('seedLabel') } },
{ input: {
type: "number",
value: () => getState("ui.seed", ""),
oninput: e => setState("ui.seed", e.target.value) } }]
} },
{
div: {
className: 'toolbox-actions',
children: [
{ button: {
type: "button",
className: "btn btn-outline",
text: () => t('randomizeButton'),
onclick: () => {
const newSeed = Math.floor(Math.random() * 1000000);
setState("ui.seed", newSeed);
headless.ToastSystem.showToast(t('toastNewSeed', { seed: newSeed }), "info"); }
} },
{ button: { type: "submit", className: "btn", text: () => t('generateButton') } }
]
}
}
]
}
}
};
// Main control panel component that composes the others
const ControlPanel = (props, { headless }) => {
return {
div: {
className: "control-panel",
children: [{
form: {
className: "prompt-form",
onsubmit: (e) => { e.preventDefault(); headless.ImageGenerator?.generate(); },
children: [
{ PromptInput: {} },
{ Toolbox: {} }
]
}
}]
}
}
};
// Gallery Component
const Gallery = (props, { getState, headless }) => ({
render: () => {
const { t } = headless.I18nManager;
const images = getState("ui.imagesList", []);
if (images.length === 0) {
return {
div: {
className: "empty-state",
children: [
{ div: { className: "empty-state-icon", text: "🖼️" } },
{ h3: { text: () => t('emptyGalleryTitle') } },
{ p: { text: () => t('emptyGalleryDesc') } }
]
}
};
}
return {
div: {
className: "image-gallery",
children: images.map((id, index) => ({ ImageDisplay: { key: id, id, featured: index === 0 } }))
}
};
}
});
// Language Switcher Component
const LanguageSwitcher = (props, { getState, setState }) => {
const currentLang = getState('ui.lang', 'en');
return {
div: {
children: [
{
div: {
style: {
display: "flex",
gap: "1rem",
justifyContent: "center",
position: "absolute",
right: 0,
top: "-0.5rem"
},
children: Object.keys(translations).map(langCode => ({
button: {
style: {
border:"none",
cursor: "pointer",
background: "rgba(255,255,255,0.121)",
border: "1px solid rgba(255, 255, 255, 0.4)",
fontSize: "2rem",
width: "3rem"
},
key: langCode,
text: translations[langCode].flag ,
onclick: () => {
setState('ui.lang', langCode);
// setState('ui.langSwitcherOpen', false);
}
}
}))
}
}
]
}
}
};
// Banner Component
const Banner = (props, { getState, setState, headless }) => ({
render: () => {
const { t } = headless.I18nManager;
if (!getState('ui.isBannerVisible', true)) return "ignore";
return {
div: {
className: 'banner',
children: [
{
div: {
className: 'banner-content',
children: [{ span: { text: () => t('bannerText') } }]
}
},
{
button: {
className: 'banner-close',
text: '✕',
onclick: () => setState('ui.isBannerVisible', false)
}
}
]
}
}
}
});
// Toolbar Component
const Toolbar = (props, { setState, headless }) => ({
render: () => {
const { t } = headless.I18nManager;
return {
div: {
className: 'gallery-toolbar',
style: {
display: "flex",
gap: "1rem",
"justify-content": "center",
"align-items": "center", padding: "0 0.5rem",
"margin-bottom": "1.5rem"
},
children: [
{
button: {
className: 'btn btn-action',
text: t("clearPrompt"),
onclick: () => setState("ui.prompt", "")
}
},
{
button: {
className: 'btn btn-danger',
text: () => t('clearAllButton'),
onclick: () => headless.PromptHistoryManager?.clearAll()
}
}
]
}
}
}
});
// App Component
const App = (props, { headless }) => ({
render: () => {
const { t } = headless.I18nManager;
return {
div: {
className: "app-layout",
children: [
{
div: {
className: "header",
children: [
{ LanguageSwitcher: {} },
{ h1:
{
style: { 'margin-top': '2rem'},
text: () => t('headerTitle')
} },
{ p: { className: "tagline", text: () => t('tagline') } }
]
}
},
{ ControlPanel: {} }, // Use the new main ControlPanel
{ Toolbar: {} },
//{ GalleryControls: {} },
{ Gallery: {} },
// { ToastDisplay: {} },
]
}
}
}
});
// Initialize Juris App
const app = new Juris({
states: {
ui: {
lang: 'fr',
isMobile: 'false',
langSwitcherOpen: false,
isBannerVisible: true,
prompt: "Oil thick past paint of Paris in winter",
seed: "57811",
width: "1024",
height: "1024",
imagesList: [],
imagesById: {},
generationQueue: [],
activeTab: "images",
lightbox: { activeImage: null },
},
model: "flux",
promptHistory: [],
toasts: [],
},
components: {
App, ControlPanel, PromptInput, Toolbox, // Register the new components
GalleryControls, Gallery, ImageDisplay, ToastDisplay,
LanguageSwitcher, Toolbar, Banner, SelectModels
},
headlessComponents: {
I18nManager: { fn: I18nManager, options: { autoInit: true } },
ImageGenerator: { fn: ImageGenerator, options: { autoInit: true } },
PromptHistoryManager: { fn: PromptHistoryManager, options: { autoInit: true } },
ToastSystem: { fn: ToastSystem, options: { autoInit: true } },
},
layout: { App: {} },
});
app.render();
// Restore history on load
setTimeout(() => {
if (app.headlessAPIs.ImageGenerator) {
app.headlessAPIs.ImageGenerator.regenerateFromHistory();
}
}, 500);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'Escape') app.setState('ui.lightbox.activeImage', null);
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
app.headlessAPIs.ImageGenerator?.generate();
}
});
if (window.matchMedia("(max-width: 640px)").matches) {
app.setState("ui.isMobile", true);
}
else {
app.setState("ui.isMobile", false);
}
</script>
</body>
</html>