| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| |
| <title>GenImage</title> |
| <style> |
| |
| :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; |
| } |
| |
| |
| .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 { |
| 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 { |
| 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); |
| } |
| |
| |
| .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; |
| |
| } |
| |
| |
| .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; |
| |
| } |
| |
| .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); |
| } |
| |
| |
| .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); |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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); |
| } |
| |
| |
| @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> |
| |
| 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", |
| |
| 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", |
| |
| 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", |
| |
| 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'); |
| }); |
| } |
| } |
| }; |
| }; |
| |
| |
| 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, ' '); |
| }; |
| |
| |
| 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', []) } |
| }; |
| }; |
| |
| |
| |
| |
| 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 |
| })); |
| 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) => { |
| |
| 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'); |
| } |
| } |
| } |
| }; |
| }; |
| |
| |
| const ImageGenerator = (props, { getState, setState, headless }) => { |
| const { t } = headless.I18nManager; |
| |
| |
| |
| |
| const generateSingleImage = async (imageId, prompt, seed, width, height, modelUsed) => { |
| try { |
| const encodedPrompt = encodeURIComponent(sanitizeInput(prompt)); |
| |
| 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; |
| |
| const [task, ...remaining] = queue; |
| setState('ui.generationQueue', remaining); |
| await generateSingleImage(...task); |
| 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"); |
| |
| 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 |
| }; |
| |
| setState(`ui.imagesById.${imageId}`, newImage); |
| setState("ui.imagesList", [imageId, ...getState("ui.imagesList", [])]); |
| headless.PromptHistoryManager.add(newImage); |
| |
| |
| 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(); |
| |
| 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 |
| }; |
| |
| setState(`ui.imagesById.${id}`, newImage); |
| setState("ui.imagesList", [...getState("ui.imagesList", []), id]); |
| |
| setState('ui.generationQueue', [...getState('ui.generationQueue', []), [id, item.prompt, item.seed, item.width, item.height, modelToUse]]); |
| }); |
| |
| generateQueued(); |
| headless.ToastSystem.showToast(t('toastRestoring', { count }), "info"); |
| } |
| } |
| }; |
| }; |
| |
| |
| 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"); |
| 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"}` } } |
| ] |
| } |
| } |
| ] |
| } |
| } |
| ] |
| } |
| }; |
| } |
| }); |
| |
| |
| 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 } } |
| ] |
| } |
| })) |
| } |
| }) |
| }); |
| |
| |
| |
| 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 }) => { |
| const { t } = headless.I18nManager; |
| |
| return { |
| div: { |
| className: "input-group", |
| children: [ |
| { |
| label: { |
| style: { |
| color: "var(--primary)", |
| display: "block", |
| marginBottom: "0.5rem", |
| }, |
| text: () => t('selectModelLabel') |
| } |
| }, |
| { |
| select: { |
| |
| style: { |
| background: "#1A1A2E", |
| color: "var(--text-primary)", |
| fontSize: "1rem", |
| border: "1px solid rgba(255,255,255,0.5)", |
| borderRadius: "5px", |
| padding: "0.5rem 1rem", |
| appearance: "none", |
| cursor: "pointer", |
| outline: "none", |
| 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%' |
| }, |
| onchange: (e) => setState("model", e.target.value), |
| value: () => getState("model", "flux"), |
| children: [ |
| |
| { |
| option: { |
| text: "Flux", |
| value: "flux" |
| } |
| }, |
| { |
| option: { |
| text: "Turbo", |
| value: "turbo" |
| } |
| }, |
| ] |
| } |
| } |
| ] |
| } |
| } |
| } |
| |
| |
| const PromptInput = (props, { getState, setState, headless }) => { |
| const { t } = headless.I18nManager; |
| return { |
| div: { |
| className: "input-group", |
| children: [ |
| |
| { |
| div: { |
| style: { display: "flex", gap: "2rem", flexDirection: "column"}, |
| } |
| }, |
| { 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" |
| }, |
| rows: "5", |
| value: () => getState("ui.prompt", ""), |
| oninput: e => setState("ui.prompt", e.target.value) |
| } |
| } |
| ] |
| } |
| } |
| }; |
| |
| 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') } } |
| ] |
| } |
| } |
| ] |
| } |
| } |
| }; |
| |
| |
| const ControlPanel = (props, { headless }) => { |
| return { |
| div: { |
| className: "control-panel", |
| children: [{ |
| form: { |
| className: "prompt-form", |
| onsubmit: (e) => { e.preventDefault(); headless.ImageGenerator?.generate(); }, |
| children: [ |
| { PromptInput: {} }, |
| { Toolbox: {} } |
| ] |
| } |
| }] |
| } |
| } |
| }; |
| |
| |
| 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 } })) |
| } |
| }; |
| } |
| }); |
| |
| |
| 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); |
| |
| } |
| } |
| })) |
| } |
| } |
| ] |
| } |
| } |
| }; |
| |
| |
| 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) |
| } |
| } |
| ] |
| } |
| } |
| } |
| }); |
| |
| |
| 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() |
| } |
| } |
| ] |
| } |
| } |
| } |
| }); |
| |
| |
| 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: {} }, |
| { Toolbar: {} }, |
| |
| { Gallery: {} }, |
| |
| |
| ] |
| } |
| } |
| } |
| }); |
| |
| |
| 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, |
| 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(); |
| |
| |
| setTimeout(() => { |
| if (app.headlessAPIs.ImageGenerator) { |
| app.headlessAPIs.ImageGenerator.regenerateFromHistory(); |
| } |
| }, 500); |
| |
| |
| 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> |
|
|