This commit is contained in:
phamhungd 2025-11-25 05:48:55 +07:00
parent 7cfe03832c
commit 41d14dcd24
4 changed files with 203 additions and 7 deletions

56
app.py
View file

@ -12,7 +12,11 @@ from google import genai
from google.genai import types from google.genai import types
from PIL import Image, PngImagePlugin from PIL import Image, PngImagePlugin
import logging
app = Flask(__name__) app = Flask(__name__)
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
PREVIEW_MAX_DIMENSION = 1024 PREVIEW_MAX_DIMENSION = 1024
@ -200,6 +204,7 @@ def generate_image():
return jsonify({'error': 'API Key is required.'}), 401 return jsonify({'error': 'API Key is required.'}), 401
try: try:
print("Đang gửi lệnh...", flush=True)
client = genai.Client(api_key=api_key) client = genai.Client(api_key=api_key)
image_config_args = { image_config_args = {
@ -294,6 +299,7 @@ def generate_image():
continue continue
model_name = "gemini-3-pro-image-preview" model_name = "gemini-3-pro-image-preview"
print("Đang tạo...", flush=True)
response = client.models.generate_content( response = client.models.generate_content(
model=model_name, model=model_name,
contents=contents, contents=contents,
@ -302,6 +308,7 @@ def generate_image():
image_config=types.ImageConfig(**image_config_args), image_config=types.ImageConfig(**image_config_args),
) )
) )
print("Hoàn tất!", flush=True)
for part in response.parts: for part in response.parts:
if part.inline_data: if part.inline_data:
@ -795,6 +802,55 @@ def update_template():
print(f"Error updating template: {e}") print(f"Error updating template: {e}")
return jsonify({'error': str(e)}), 500 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']) @app.route('/refine_prompt', methods=['POST'])
def refine_prompt(): def refine_prompt():
data = request.get_json() data = request.get_json()

View file

@ -6,13 +6,38 @@
import { i18n } from './i18n.js'; import { i18n } from './i18n.js';
export function createTemplateGallery({ container, onSelectTemplate }) { 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 allTemplates = [];
let currentCategory = 'all'; let currentCategory = savedFilters.category || 'all';
let currentMode = 'all'; let currentMode = savedFilters.mode || 'all';
let searchQuery = ''; let searchQuery = '';
let favoriteTemplateKeys = new Set(); let favoriteTemplateKeys = new Set();
let favoriteFilterActive = false; let favoriteFilterActive = savedFilters.favorites || false;
let userTemplateFilterActive = 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) { function setFavoriteKeys(keys) {
if (Array.isArray(keys)) { if (Array.isArray(keys)) {
@ -203,6 +228,78 @@ export function createTemplateGallery({ container, onSelectTemplate }) {
} }
}); });
previewActions.appendChild(editBtn); previewActions.appendChild(editBtn);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'template-edit-btn'; // Reuse same style
deleteBtn.style.marginLeft = '4px';
deleteBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6H5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 6V4C8 3.46957 8.21071 3 8.58579 2.62513C8.96086 2.25026 9.46957 2.03967 10 2.03967H14C14.5304 2.03967 15.0391 2.25026 15.4142 2.62513C15.7893 3 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
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); preview.appendChild(previewActions);
@ -307,6 +404,7 @@ export function createTemplateGallery({ container, onSelectTemplate }) {
modeSelect.value = currentMode; modeSelect.value = currentMode;
modeSelect.addEventListener('change', (e) => { modeSelect.addEventListener('change', (e) => {
currentMode = e.target.value; currentMode = e.target.value;
persistFilters();
render(); render();
}); });
@ -325,6 +423,7 @@ export function createTemplateGallery({ container, onSelectTemplate }) {
categorySelect.value = currentCategory; categorySelect.value = currentCategory;
categorySelect.addEventListener('change', (e) => { categorySelect.addEventListener('change', (e) => {
currentCategory = e.target.value; currentCategory = e.target.value;
persistFilters();
render(); render();
}); });
@ -343,6 +442,7 @@ export function createTemplateGallery({ container, onSelectTemplate }) {
`; `;
favoritesToggle.addEventListener('click', () => { favoritesToggle.addEventListener('click', () => {
favoriteFilterActive = !favoriteFilterActive; favoriteFilterActive = !favoriteFilterActive;
persistFilters();
render(); render();
}); });
const filterRow = document.createElement('div'); const filterRow = document.createElement('div');
@ -365,6 +465,7 @@ export function createTemplateGallery({ container, onSelectTemplate }) {
`; `;
userToggle.addEventListener('click', () => { userToggle.addEventListener('click', () => {
userTemplateFilterActive = !userTemplateFilterActive; userTemplateFilterActive = !userTemplateFilterActive;
persistFilters();
render(); render();
}); });
filterRow.appendChild(userToggle); filterRow.appendChild(userToggle);

View file

@ -1080,6 +1080,16 @@ document.addEventListener('DOMContentLoaded', () => {
loadTemplateGallery(); loadTemplateGallery();
initializeSidebarResizer(sidebar, resizeHandle); 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 // Setup canvas language toggle
const canvasLangInput = document.getElementById('canvas-lang-input'); const canvasLangInput = document.getElementById('canvas-lang-input');
if (canvasLangInput) { if (canvasLangInput) {
@ -1152,6 +1162,13 @@ document.addEventListener('DOMContentLoaded', () => {
hasGeneratedImage = true; // Mark that we have an image hasGeneratedImage = true; // Mark that we have an image
setViewState('result'); 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) { async function handleCanvasDropUrl(imageUrl) {

View file

@ -47,10 +47,14 @@
<a href="https://chatgpt.com/g/g-6923d39c8efc8191be0bc3089bebc441-banana-prompt-guide" <a href="https://chatgpt.com/g/g-6923d39c8efc8191be0bc3089bebc441-banana-prompt-guide"
target="_blank" rel="noopener noreferrer" class="prompt-action-btn" target="_blank" rel="noopener noreferrer" class="prompt-action-btn"
title="Prompt Guide"> title="Prompt Guide">
<svg fill="currentColor" height="16px" width="16px" viewBox="0 0 24 24" <svg version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"> xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px"
viewBox="0 0 32 32" xml:space="preserve" fill="currentColor">
<path <path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" /> 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" />
</svg> </svg>
<span>Prompt Guide</span> <span>Prompt Guide</span>
</a> </a>
@ -370,6 +374,24 @@
</div> </div>
</div> </div>
</div> </div>
<div id="delete-confirm-modal" class="popup-overlay hidden">
<div class="popup-card" style="max-width: 400px;">
<header class="popup-header">
<h2>Xác nhận xoá</h2>
<button id="close-delete-modal" type="button" class="popup-close" aria-label="Close">&times;</button>
</header>
<div class="popup-body">
<p style="color: var(--text-secondary); margin-bottom: 1.5rem;">Bạn có chắc chắn muốn xoá template này
không?</p>
<div class="controls-footer" style="justify-content: flex-end; gap: 0.5rem;">
<button id="cancel-delete-btn" class="action-btn"
style="background: rgba(255, 255, 255, 0.1); color: var(--text-primary);">Huỷ</button>
<button id="confirm-delete-btn" class="action-btn"
style="background: var(--danger-color); color: white;">Xoá</button>
</div>
</div>
</div>
</div>
<div id="popup-overlay" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="popup-title"> <div id="popup-overlay" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="popup-title">
<div class="popup-card"> <div class="popup-card">
<header class="popup-header"> <header class="popup-header">