diff --git a/app.py b/app.py index fcd53b5..1e884d3 100644 --- a/app.py +++ b/app.py @@ -12,7 +12,11 @@ from google import genai from google.genai import types from PIL import Image, PngImagePlugin +import logging + app = Flask(__name__) +log = logging.getLogger('werkzeug') +log.setLevel(logging.ERROR) app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 PREVIEW_MAX_DIMENSION = 1024 @@ -200,6 +204,7 @@ def generate_image(): return jsonify({'error': 'API Key is required.'}), 401 try: + print("Đang gửi lệnh...", flush=True) client = genai.Client(api_key=api_key) image_config_args = { @@ -294,6 +299,7 @@ def generate_image(): continue model_name = "gemini-3-pro-image-preview" + print("Đang tạo...", flush=True) response = client.models.generate_content( model=model_name, contents=contents, @@ -302,6 +308,7 @@ def generate_image(): image_config=types.ImageConfig(**image_config_args), ) ) + print("Hoàn tất!", flush=True) for part in response.parts: if part.inline_data: @@ -795,6 +802,55 @@ def update_template(): print(f"Error updating template: {e}") return jsonify({'error': str(e)}), 500 +@app.route('/delete_template', methods=['POST']) +def delete_template(): + try: + template_index = request.form.get('template_index') + if template_index is None: + return jsonify({'error': 'Template index is required'}), 400 + + try: + template_index = int(template_index) + except ValueError: + return jsonify({'error': 'Invalid template index'}), 400 + + user_prompts_path = os.path.join(os.path.dirname(__file__), 'user_prompts.json') + if not os.path.exists(user_prompts_path): + return jsonify({'error': 'User prompts file not found'}), 404 + + with open(user_prompts_path, 'r', encoding='utf-8') as f: + user_prompts = json.load(f) + + if template_index < 0 or template_index >= len(user_prompts): + return jsonify({'error': 'Template not found'}), 404 + + template_to_delete = user_prompts[template_index] + + # Delete preview image if it exists and is local + preview_path = template_to_delete.get('preview') + if preview_path and '/static/preview/' in preview_path: + # Extract filename + try: + filename = preview_path.split('/static/preview/')[1] + preview_dir = os.path.join(app.static_folder, 'preview') + filepath = os.path.join(preview_dir, filename) + if os.path.exists(filepath): + os.remove(filepath) + except Exception as e: + print(f"Error deleting preview image: {e}") + + # Remove from list + del user_prompts[template_index] + + # Save back + with open(user_prompts_path, 'w', encoding='utf-8') as f: + json.dump(user_prompts, f, indent=4, ensure_ascii=False) + + return jsonify({'success': True}) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + @app.route('/refine_prompt', methods=['POST']) def refine_prompt(): data = request.get_json() diff --git a/static/modules/templateGallery.js b/static/modules/templateGallery.js index 5d494e4..f75ae03 100644 --- a/static/modules/templateGallery.js +++ b/static/modules/templateGallery.js @@ -6,13 +6,38 @@ import { i18n } from './i18n.js'; export function createTemplateGallery({ container, onSelectTemplate }) { + const STORAGE_KEY = 'gemini-app-template-filters'; + + // Load saved filters + let savedFilters = {}; + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) savedFilters = JSON.parse(saved); + } catch (e) { + console.warn('Failed to load template filters', e); + } + let allTemplates = []; - let currentCategory = 'all'; - let currentMode = 'all'; + let currentCategory = savedFilters.category || 'all'; + let currentMode = savedFilters.mode || 'all'; let searchQuery = ''; let favoriteTemplateKeys = new Set(); - let favoriteFilterActive = false; - let userTemplateFilterActive = false; + let favoriteFilterActive = savedFilters.favorites || false; + let userTemplateFilterActive = savedFilters.userTemplates || false; + + function persistFilters() { + try { + const filters = { + category: currentCategory, + mode: currentMode, + favorites: favoriteFilterActive, + userTemplates: userTemplateFilterActive + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(filters)); + } catch (e) { + console.warn('Failed to save template filters', e); + } + } function setFavoriteKeys(keys) { if (Array.isArray(keys)) { @@ -203,6 +228,78 @@ export function createTemplateGallery({ container, onSelectTemplate }) { } }); previewActions.appendChild(editBtn); + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'template-edit-btn'; // Reuse same style + deleteBtn.style.marginLeft = '4px'; + deleteBtn.innerHTML = ` + + + + + `; + deleteBtn.title = 'Delete Template'; + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + + const deleteModal = document.getElementById('delete-confirm-modal'); + const confirmBtn = document.getElementById('confirm-delete-btn'); + const cancelBtn = document.getElementById('cancel-delete-btn'); + const closeBtn = document.getElementById('close-delete-modal'); + + if (!deleteModal || !confirmBtn) { + console.error('Delete modal elements not found'); + return; + } + + const closeModal = () => { + deleteModal.classList.add('hidden'); + confirmBtn.replaceWith(confirmBtn.cloneNode(true)); // Remove listeners + cancelBtn?.replaceWith(cancelBtn.cloneNode(true)); + closeBtn?.replaceWith(closeBtn.cloneNode(true)); + }; + + deleteModal.classList.remove('hidden'); + + // Setup new listeners + const newConfirmBtn = document.getElementById('confirm-delete-btn'); + const newCancelBtn = document.getElementById('cancel-delete-btn'); + const newCloseBtn = document.getElementById('close-delete-modal'); + + newConfirmBtn.addEventListener('click', async () => { + try { + const formData = new FormData(); + formData.append('template_index', template.userTemplateIndex); + + const response = await fetch('/delete_template', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + if (data.success) { + closeModal(); + load(); + } else { + alert('Failed to delete: ' + (data.error || 'Unknown error')); + } + } catch (error) { + console.error('Error deleting template:', error); + alert('Error deleting template'); + } + }); + + newCancelBtn?.addEventListener('click', closeModal); + newCloseBtn?.addEventListener('click', closeModal); + + // Close on click outside + deleteModal.onclick = (event) => { + if (event.target === deleteModal) { + closeModal(); + } + }; + }); + previewActions.appendChild(deleteBtn); } preview.appendChild(previewActions); @@ -307,6 +404,7 @@ export function createTemplateGallery({ container, onSelectTemplate }) { modeSelect.value = currentMode; modeSelect.addEventListener('change', (e) => { currentMode = e.target.value; + persistFilters(); render(); }); @@ -325,6 +423,7 @@ export function createTemplateGallery({ container, onSelectTemplate }) { categorySelect.value = currentCategory; categorySelect.addEventListener('change', (e) => { currentCategory = e.target.value; + persistFilters(); render(); }); @@ -343,6 +442,7 @@ export function createTemplateGallery({ container, onSelectTemplate }) { `; favoritesToggle.addEventListener('click', () => { favoriteFilterActive = !favoriteFilterActive; + persistFilters(); render(); }); const filterRow = document.createElement('div'); @@ -365,6 +465,7 @@ export function createTemplateGallery({ container, onSelectTemplate }) { `; userToggle.addEventListener('click', () => { userTemplateFilterActive = !userTemplateFilterActive; + persistFilters(); render(); }); filterRow.appendChild(userToggle); diff --git a/static/script.js b/static/script.js index 6146c2e..ef76d11 100644 --- a/static/script.js +++ b/static/script.js @@ -1080,6 +1080,16 @@ document.addEventListener('DOMContentLoaded', () => { loadTemplateGallery(); initializeSidebarResizer(sidebar, resizeHandle); + // Restore last image if available + try { + const lastImage = localStorage.getItem('gemini-app-last-image'); + if (lastImage) { + displayImage(lastImage); + } + } catch (e) { + console.warn('Failed to restore last image', e); + } + // Setup canvas language toggle const canvasLangInput = document.getElementById('canvas-lang-input'); if (canvasLangInput) { @@ -1152,6 +1162,13 @@ document.addEventListener('DOMContentLoaded', () => { hasGeneratedImage = true; // Mark that we have an image setViewState('result'); + + // Persist image URL + try { + localStorage.setItem('gemini-app-last-image', imageUrl); + } catch (e) { + console.warn('Failed to save last image URL', e); + } } async function handleCanvasDropUrl(imageUrl) { diff --git a/templates/index.html b/templates/index.html index 6a3b733..06b7260 100644 --- a/templates/index.html +++ b/templates/index.html @@ -47,10 +47,14 @@ - + + d="M21,24c0,0.552-0.447,1-1,1h-8c-0.553,0-1-0.448-1-1s0.447-1,1-1h8C20.553,23,21,23.448,21,24z + M20,26h-8c-0.553,0-1,0.448-1,1s0.447,1,1,1h8c0.553,0,1-0.448,1-1S20.553,26,20,26z M15,29v1c0,0.552,0.448,1,1,1s1-0.448,1-1v-1 + H15z M26,11c0,5-5,8-5,10c0,0.552-0.448,1-1,1h-8c-0.552,0-1-0.448-1-1c0-2-5-5-5-10C6,5.477,10.477,1,16,1S26,5.477,26,11z M17,4 + c0-0.552-0.447-1-1-1c-4.411,0-8,3.589-8,8c0,0.552,0.447,1,1,1s1-0.448,1-1c0-3.309,2.691-6,6-6C16.553,5,17,4.552,17,4z" /> Prompt Guide @@ -370,6 +374,24 @@ +