up fix
This commit is contained in:
parent
7cfe03832c
commit
41d14dcd24
4 changed files with 203 additions and 7 deletions
56
app.py
56
app.py
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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">×</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">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue