add template edit
This commit is contained in:
parent
9b909dae9c
commit
f6ff32a746
6 changed files with 1043 additions and 4 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,3 +6,5 @@
|
||||||
/.venv
|
/.venv
|
||||||
/static/uploads
|
/static/uploads
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
user_prompts.json
|
||||||
|
/static/preview
|
||||||
|
|
|
||||||
293
app.py
293
app.py
|
|
@ -253,21 +253,308 @@ def get_prompts():
|
||||||
category = request.args.get('category')
|
category = request.args.get('category')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
all_prompts = []
|
||||||
|
|
||||||
# Read prompts.json file
|
# Read prompts.json file
|
||||||
prompts_path = os.path.join(os.path.dirname(__file__), 'prompts.json')
|
prompts_path = os.path.join(os.path.dirname(__file__), 'prompts.json')
|
||||||
|
if os.path.exists(prompts_path):
|
||||||
with open(prompts_path, 'r', encoding='utf-8') as f:
|
with open(prompts_path, 'r', encoding='utf-8') as f:
|
||||||
prompts = json.load(f)
|
all_prompts.extend(json.load(f))
|
||||||
|
|
||||||
|
# Read user_prompts.json file and mark as user templates
|
||||||
|
user_prompts_path = os.path.join(os.path.dirname(__file__), 'user_prompts.json')
|
||||||
|
if os.path.exists(user_prompts_path):
|
||||||
|
try:
|
||||||
|
with open(user_prompts_path, 'r', encoding='utf-8') as f:
|
||||||
|
user_prompts = json.load(f)
|
||||||
|
if isinstance(user_prompts, list):
|
||||||
|
# Mark each user template and add index for editing
|
||||||
|
for idx, template in enumerate(user_prompts):
|
||||||
|
template['isUserTemplate'] = True
|
||||||
|
template['userTemplateIndex'] = idx
|
||||||
|
all_prompts.extend(user_prompts)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass # Ignore if empty or invalid
|
||||||
|
|
||||||
# Filter by category if specified
|
# Filter by category if specified
|
||||||
if category:
|
if category:
|
||||||
prompts = [p for p in prompts if p.get('category') == category]
|
all_prompts = [p for p in all_prompts if p.get('category') == category]
|
||||||
|
|
||||||
response = jsonify({'prompts': prompts})
|
response = jsonify({'prompts': all_prompts})
|
||||||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/save_template', methods=['POST'])
|
||||||
|
def save_template():
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# Handle multipart form data
|
||||||
|
title = request.form.get('title')
|
||||||
|
prompt = request.form.get('prompt')
|
||||||
|
mode = request.form.get('mode', 'generate')
|
||||||
|
category = request.form.get('category', 'User')
|
||||||
|
|
||||||
|
if not title or not prompt:
|
||||||
|
return jsonify({'error': 'Title and prompt are required'}), 400
|
||||||
|
|
||||||
|
# Handle preview image
|
||||||
|
preview_path = None
|
||||||
|
preview_dir = os.path.join(app.static_folder, 'preview')
|
||||||
|
os.makedirs(preview_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Check if file was uploaded
|
||||||
|
if 'preview' in request.files:
|
||||||
|
file = request.files['preview']
|
||||||
|
if file.filename:
|
||||||
|
ext = os.path.splitext(file.filename)[1]
|
||||||
|
if not ext:
|
||||||
|
ext = '.png'
|
||||||
|
|
||||||
|
filename = f"template_{uuid.uuid4()}{ext}"
|
||||||
|
filepath = os.path.join(preview_dir, filename)
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
|
preview_path = url_for('static', filename=f'preview/{filename}')
|
||||||
|
|
||||||
|
# If no file uploaded, check if URL/path provided
|
||||||
|
if not preview_path:
|
||||||
|
preview_url = request.form.get('preview_path')
|
||||||
|
if preview_url:
|
||||||
|
try:
|
||||||
|
# Check if it's a URL or local path
|
||||||
|
if preview_url.startswith('http://') or preview_url.startswith('https://'):
|
||||||
|
# Download from URL
|
||||||
|
response = requests.get(preview_url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Determine extension from content-type or URL
|
||||||
|
content_type = response.headers.get('content-type', '')
|
||||||
|
if 'image/png' in content_type:
|
||||||
|
ext = '.png'
|
||||||
|
elif 'image/jpeg' in content_type or 'image/jpg' in content_type:
|
||||||
|
ext = '.jpg'
|
||||||
|
elif 'image/webp' in content_type:
|
||||||
|
ext = '.webp'
|
||||||
|
else:
|
||||||
|
# Try to get from URL
|
||||||
|
parsed = urlparse(preview_url)
|
||||||
|
ext = os.path.splitext(parsed.path)[1] or '.png'
|
||||||
|
|
||||||
|
filename = f"template_{uuid.uuid4()}{ext}"
|
||||||
|
filepath = os.path.join(preview_dir, filename)
|
||||||
|
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
preview_path = url_for('static', filename=f'preview/{filename}')
|
||||||
|
|
||||||
|
elif preview_url.startswith('/static/'):
|
||||||
|
# Local path - copy to preview folder
|
||||||
|
rel_path = preview_url.split('/static/')[1]
|
||||||
|
source_path = os.path.join(app.static_folder, rel_path)
|
||||||
|
|
||||||
|
if os.path.exists(source_path):
|
||||||
|
ext = os.path.splitext(source_path)[1] or '.png'
|
||||||
|
filename = f"template_{uuid.uuid4()}{ext}"
|
||||||
|
dest_path = os.path.join(preview_dir, filename)
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(source_path, dest_path)
|
||||||
|
|
||||||
|
preview_path = url_for('static', filename=f'preview/{filename}')
|
||||||
|
else:
|
||||||
|
# File doesn't exist, use original path
|
||||||
|
preview_path = preview_url
|
||||||
|
else:
|
||||||
|
# Use as-is if it's already a valid path
|
||||||
|
preview_path = preview_url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing preview image URL: {e}")
|
||||||
|
# Use the original URL if processing fails
|
||||||
|
preview_path = preview_url
|
||||||
|
|
||||||
|
new_template = {
|
||||||
|
'title': title,
|
||||||
|
'prompt': prompt,
|
||||||
|
'mode': mode,
|
||||||
|
'category': category,
|
||||||
|
'preview': preview_path
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save to user_prompts.json
|
||||||
|
user_prompts_path = os.path.join(os.path.dirname(__file__), 'user_prompts.json')
|
||||||
|
user_prompts = []
|
||||||
|
|
||||||
|
if os.path.exists(user_prompts_path):
|
||||||
|
try:
|
||||||
|
with open(user_prompts_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
if content.strip():
|
||||||
|
user_prompts = json.loads(content)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
user_prompts.append(new_template)
|
||||||
|
|
||||||
|
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, 'template': new_template})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving template: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/update_template', methods=['POST'])
|
||||||
|
def update_template():
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# Get template index
|
||||||
|
template_index = request.form.get('template_index')
|
||||||
|
if template_index is None:
|
||||||
|
return jsonify({'error': 'Template index is required'}), 400
|
||||||
|
|
||||||
|
template_index = int(template_index)
|
||||||
|
|
||||||
|
# Handle multipart form data
|
||||||
|
title = request.form.get('title')
|
||||||
|
prompt = request.form.get('prompt')
|
||||||
|
mode = request.form.get('mode', 'generate')
|
||||||
|
category = request.form.get('category', 'User')
|
||||||
|
|
||||||
|
if not title or not prompt:
|
||||||
|
return jsonify({'error': 'Title and prompt are required'}), 400
|
||||||
|
|
||||||
|
# Handle preview image (same logic as save_template)
|
||||||
|
preview_path = None
|
||||||
|
preview_dir = os.path.join(app.static_folder, 'preview')
|
||||||
|
os.makedirs(preview_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Check if file was uploaded
|
||||||
|
if 'preview' in request.files:
|
||||||
|
file = request.files['preview']
|
||||||
|
if file.filename:
|
||||||
|
ext = os.path.splitext(file.filename)[1]
|
||||||
|
if not ext:
|
||||||
|
ext = '.png'
|
||||||
|
|
||||||
|
filename = f"template_{uuid.uuid4()}{ext}"
|
||||||
|
filepath = os.path.join(preview_dir, filename)
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
|
preview_path = url_for('static', filename=f'preview/{filename}')
|
||||||
|
|
||||||
|
# If no file uploaded, check if URL/path provided
|
||||||
|
if not preview_path:
|
||||||
|
preview_url = request.form.get('preview_path')
|
||||||
|
if preview_url:
|
||||||
|
try:
|
||||||
|
if preview_url.startswith('http://') or preview_url.startswith('https://'):
|
||||||
|
response = requests.get(preview_url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
content_type = response.headers.get('content-type', '')
|
||||||
|
if 'image/png' in content_type:
|
||||||
|
ext = '.png'
|
||||||
|
elif 'image/jpeg' in content_type or 'image/jpg' in content_type:
|
||||||
|
ext = '.jpg'
|
||||||
|
elif 'image/webp' in content_type:
|
||||||
|
ext = '.webp'
|
||||||
|
else:
|
||||||
|
parsed = urlparse(preview_url)
|
||||||
|
ext = os.path.splitext(parsed.path)[1] or '.png'
|
||||||
|
|
||||||
|
filename = f"template_{uuid.uuid4()}{ext}"
|
||||||
|
filepath = os.path.join(preview_dir, filename)
|
||||||
|
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
preview_path = url_for('static', filename=f'preview/{filename}')
|
||||||
|
|
||||||
|
elif preview_url.startswith('/static/'):
|
||||||
|
rel_path = preview_url.split('/static/')[1]
|
||||||
|
source_path = os.path.join(app.static_folder, rel_path)
|
||||||
|
|
||||||
|
if os.path.exists(source_path):
|
||||||
|
ext = os.path.splitext(source_path)[1] or '.png'
|
||||||
|
filename = f"template_{uuid.uuid4()}{ext}"
|
||||||
|
dest_path = os.path.join(preview_dir, filename)
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(source_path, dest_path)
|
||||||
|
|
||||||
|
preview_path = url_for('static', filename=f'preview/{filename}')
|
||||||
|
else:
|
||||||
|
preview_path = preview_url
|
||||||
|
else:
|
||||||
|
preview_path = preview_url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing preview image URL: {e}")
|
||||||
|
preview_path = preview_url
|
||||||
|
|
||||||
|
# Read existing user templates
|
||||||
|
user_prompts_path = os.path.join(os.path.dirname(__file__), 'user_prompts.json')
|
||||||
|
user_prompts = []
|
||||||
|
|
||||||
|
if os.path.exists(user_prompts_path):
|
||||||
|
try:
|
||||||
|
with open(user_prompts_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
if content.strip():
|
||||||
|
user_prompts = json.loads(content)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check if index is valid
|
||||||
|
if template_index < 0 or template_index >= len(user_prompts):
|
||||||
|
return jsonify({'error': 'Invalid template index'}), 400
|
||||||
|
|
||||||
|
# Get old template to check for old preview image
|
||||||
|
old_template = user_prompts[template_index]
|
||||||
|
old_preview = old_template.get('preview', '')
|
||||||
|
|
||||||
|
# Delete old preview image if it exists in the preview folder
|
||||||
|
if old_preview and '/preview/' in old_preview:
|
||||||
|
try:
|
||||||
|
# Extract filename from URL
|
||||||
|
old_filename = old_preview.split('/preview/')[-1]
|
||||||
|
old_filepath = os.path.join(preview_dir, old_filename)
|
||||||
|
|
||||||
|
# Delete old file if it exists
|
||||||
|
if os.path.exists(old_filepath):
|
||||||
|
os.remove(old_filepath)
|
||||||
|
print(f"Deleted old preview image: {old_filepath}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting old preview image: {e}")
|
||||||
|
|
||||||
|
# Update the template
|
||||||
|
user_prompts[template_index] = {
|
||||||
|
'title': title,
|
||||||
|
'prompt': prompt,
|
||||||
|
'mode': mode,
|
||||||
|
'category': category,
|
||||||
|
'preview': preview_path
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save back to file
|
||||||
|
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, 'template': user_prompts[template_index]})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating template: {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()
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,25 @@ export function createTemplateGallery({ container, onSelectTemplate }) {
|
||||||
};
|
};
|
||||||
preview.appendChild(img);
|
preview.appendChild(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit button (show on all templates)
|
||||||
|
const editBtn = document.createElement('button');
|
||||||
|
editBtn.className = 'template-edit-btn';
|
||||||
|
editBtn.innerHTML = `
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M18.5 2.50001C18.8978 2.10219 19.4374 1.87869 20 1.87869C20.5626 1.87869 21.1022 2.10219 21.5 2.50001C21.8978 2.89784 22.1213 3.43741 22.1213 4.00001C22.1213 4.56262 21.8978 5.10219 21.5 5.50001L12 15L8 16L9 12L18.5 2.50001Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
editBtn.title = 'Edit Template';
|
||||||
|
editBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (window.openEditTemplateModal) {
|
||||||
|
window.openEditTemplateModal(template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
preview.appendChild(editBtn);
|
||||||
|
|
||||||
card.appendChild(preview);
|
card.appendChild(preview);
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
|
|
@ -227,6 +246,22 @@ export function createTemplateGallery({ container, onSelectTemplate }) {
|
||||||
});
|
});
|
||||||
controls.appendChild(categorySelect);
|
controls.appendChild(categorySelect);
|
||||||
|
|
||||||
|
// Create Template button
|
||||||
|
const createTemplateBtn = document.createElement('button');
|
||||||
|
createTemplateBtn.className = 'template-create-btn';
|
||||||
|
createTemplateBtn.innerHTML = `
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 5V19M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Create Template</span>
|
||||||
|
`;
|
||||||
|
createTemplateBtn.addEventListener('click', () => {
|
||||||
|
if (window.openCreateTemplateModal) {
|
||||||
|
window.openCreateTemplateModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
controls.appendChild(createTemplateBtn);
|
||||||
|
|
||||||
header.appendChild(controls);
|
header.appendChild(controls);
|
||||||
container.appendChild(header);
|
container.appendChild(header);
|
||||||
|
|
||||||
|
|
|
||||||
440
static/script.js
440
static/script.js
|
|
@ -319,6 +319,446 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Template Logic
|
||||||
|
const createTemplateBtn = document.getElementById('create-template-btn');
|
||||||
|
const createTemplateModal = document.getElementById('create-template-modal');
|
||||||
|
const closeTemplateModalBtn = document.getElementById('close-template-modal');
|
||||||
|
const saveTemplateBtn = document.getElementById('save-template-btn');
|
||||||
|
|
||||||
|
const templateTitleInput = document.getElementById('template-title');
|
||||||
|
const templatePromptInput = document.getElementById('template-prompt');
|
||||||
|
const templateModeSelect = document.getElementById('template-mode');
|
||||||
|
const templateCategorySelect = document.getElementById('template-category-select');
|
||||||
|
const templateCategoryInput = document.getElementById('template-category-input');
|
||||||
|
const templatePreviewDropzone = document.getElementById('template-preview-dropzone');
|
||||||
|
const templatePreviewImg = document.getElementById('template-preview-img');
|
||||||
|
const dropzonePlaceholder = document.querySelector('.dropzone-placeholder');
|
||||||
|
|
||||||
|
let currentPreviewFile = null;
|
||||||
|
let currentPreviewUrl = null;
|
||||||
|
let editingTemplate = null; // Track if we're editing an existing template
|
||||||
|
|
||||||
|
// Global function for opening edit modal (called from templateGallery.js)
|
||||||
|
window.openEditTemplateModal = async function(template) {
|
||||||
|
editingTemplate = template;
|
||||||
|
|
||||||
|
// Pre-fill with template data
|
||||||
|
templateTitleInput.value = template.title || '';
|
||||||
|
templatePromptInput.value = template.prompt || '';
|
||||||
|
templateModeSelect.value = template.mode || 'generate';
|
||||||
|
templateCategoryInput.classList.add('hidden');
|
||||||
|
templateCategoryInput.value = '';
|
||||||
|
|
||||||
|
// Populate categories
|
||||||
|
try {
|
||||||
|
const response = await fetch('/prompts');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.prompts) {
|
||||||
|
const categories = new Set();
|
||||||
|
data.prompts.forEach(t => {
|
||||||
|
if (t.category) {
|
||||||
|
const categoryText = typeof t.category === 'string'
|
||||||
|
? t.category
|
||||||
|
: (t.category.vi || t.category.en || '');
|
||||||
|
if (categoryText) categories.add(categoryText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
templateCategorySelect.innerHTML = '';
|
||||||
|
const sortedCategories = Array.from(categories).sort();
|
||||||
|
sortedCategories.forEach(cat => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = cat;
|
||||||
|
option.textContent = cat;
|
||||||
|
templateCategorySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = 'new';
|
||||||
|
newOption.textContent = '+ New Category';
|
||||||
|
templateCategorySelect.appendChild(newOption);
|
||||||
|
|
||||||
|
// Set to template's category
|
||||||
|
const templateCategory = typeof template.category === 'string'
|
||||||
|
? template.category
|
||||||
|
: (template.category.vi || template.category.en || '');
|
||||||
|
templateCategorySelect.value = templateCategory || 'User';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load categories:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set preview image
|
||||||
|
if (template.preview) {
|
||||||
|
templatePreviewImg.src = template.preview;
|
||||||
|
templatePreviewImg.classList.remove('hidden');
|
||||||
|
dropzonePlaceholder.classList.add('hidden');
|
||||||
|
currentPreviewUrl = template.preview;
|
||||||
|
} else {
|
||||||
|
templatePreviewImg.src = '';
|
||||||
|
templatePreviewImg.classList.add('hidden');
|
||||||
|
dropzonePlaceholder.classList.remove('hidden');
|
||||||
|
currentPreviewUrl = null;
|
||||||
|
}
|
||||||
|
currentPreviewFile = null;
|
||||||
|
|
||||||
|
// Update button text
|
||||||
|
saveTemplateBtn.innerHTML = '<span>Update Template</span><div class="btn-shine"></div>';
|
||||||
|
|
||||||
|
createTemplateModal.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global function for opening create modal with empty values (called from templateGallery.js)
|
||||||
|
window.openCreateTemplateModal = async function() {
|
||||||
|
editingTemplate = null;
|
||||||
|
|
||||||
|
// Clear all fields
|
||||||
|
templateTitleInput.value = '';
|
||||||
|
templatePromptInput.value = '';
|
||||||
|
templateModeSelect.value = 'generate';
|
||||||
|
templateCategoryInput.classList.add('hidden');
|
||||||
|
templateCategoryInput.value = '';
|
||||||
|
|
||||||
|
// Populate categories
|
||||||
|
try {
|
||||||
|
const response = await fetch('/prompts');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.prompts) {
|
||||||
|
const categories = new Set();
|
||||||
|
data.prompts.forEach(t => {
|
||||||
|
if (t.category) {
|
||||||
|
const categoryText = typeof t.category === 'string'
|
||||||
|
? t.category
|
||||||
|
: (t.category.vi || t.category.en || '');
|
||||||
|
if (categoryText) categories.add(categoryText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
templateCategorySelect.innerHTML = '';
|
||||||
|
const sortedCategories = Array.from(categories).sort();
|
||||||
|
sortedCategories.forEach(cat => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = cat;
|
||||||
|
option.textContent = cat;
|
||||||
|
templateCategorySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = 'new';
|
||||||
|
newOption.textContent = '+ New Category';
|
||||||
|
templateCategorySelect.appendChild(newOption);
|
||||||
|
|
||||||
|
if (sortedCategories.includes('User')) {
|
||||||
|
templateCategorySelect.value = 'User';
|
||||||
|
} else if (sortedCategories.length > 0) {
|
||||||
|
templateCategorySelect.value = sortedCategories[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load categories:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear preview image
|
||||||
|
templatePreviewImg.src = '';
|
||||||
|
templatePreviewImg.classList.add('hidden');
|
||||||
|
dropzonePlaceholder.classList.remove('hidden');
|
||||||
|
currentPreviewUrl = null;
|
||||||
|
currentPreviewFile = null;
|
||||||
|
|
||||||
|
// Update button text
|
||||||
|
saveTemplateBtn.innerHTML = '<span>Save Template</span><div class="btn-shine"></div>';
|
||||||
|
|
||||||
|
createTemplateModal.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (createTemplateBtn) {
|
||||||
|
createTemplateBtn.addEventListener('click', async () => {
|
||||||
|
// Reset editing state
|
||||||
|
editingTemplate = null;
|
||||||
|
|
||||||
|
// Pre-fill data
|
||||||
|
templateTitleInput.value = '';
|
||||||
|
templatePromptInput.value = promptInput.value;
|
||||||
|
templateModeSelect.value = 'generate';
|
||||||
|
templateCategoryInput.classList.add('hidden');
|
||||||
|
templateCategoryInput.value = '';
|
||||||
|
|
||||||
|
// Populate categories dynamically from template library
|
||||||
|
try {
|
||||||
|
const response = await fetch('/prompts');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.prompts) {
|
||||||
|
// Extract unique categories
|
||||||
|
const categories = new Set();
|
||||||
|
data.prompts.forEach(template => {
|
||||||
|
if (template.category) {
|
||||||
|
// Handle both string and object categories
|
||||||
|
const categoryText = typeof template.category === 'string'
|
||||||
|
? template.category
|
||||||
|
: (template.category.vi || template.category.en || '');
|
||||||
|
if (categoryText) {
|
||||||
|
categories.add(categoryText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear existing options except "new"
|
||||||
|
templateCategorySelect.innerHTML = '';
|
||||||
|
|
||||||
|
// Add sorted categories
|
||||||
|
const sortedCategories = Array.from(categories).sort();
|
||||||
|
sortedCategories.forEach(cat => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = cat;
|
||||||
|
option.textContent = cat;
|
||||||
|
templateCategorySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add "new category" option at the end
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = 'new';
|
||||||
|
newOption.textContent = '+ New Category';
|
||||||
|
templateCategorySelect.appendChild(newOption);
|
||||||
|
|
||||||
|
// Set default to first category or "User" if it exists
|
||||||
|
if (sortedCategories.includes('User')) {
|
||||||
|
templateCategorySelect.value = 'User';
|
||||||
|
} else if (sortedCategories.length > 0) {
|
||||||
|
templateCategorySelect.value = sortedCategories[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load categories:', error);
|
||||||
|
// Fallback to default categories
|
||||||
|
templateCategorySelect.innerHTML = `
|
||||||
|
<option value="User">User</option>
|
||||||
|
<option value="new">+ New Category</option>
|
||||||
|
`;
|
||||||
|
templateCategorySelect.value = 'User';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set preview image from current generated image
|
||||||
|
if (generatedImage.src && !generatedImage.src.endsWith('placeholder.png')) {
|
||||||
|
templatePreviewImg.src = generatedImage.src;
|
||||||
|
templatePreviewImg.classList.remove('hidden');
|
||||||
|
dropzonePlaceholder.classList.add('hidden');
|
||||||
|
currentPreviewUrl = generatedImage.src;
|
||||||
|
} else {
|
||||||
|
templatePreviewImg.src = '';
|
||||||
|
templatePreviewImg.classList.add('hidden');
|
||||||
|
dropzonePlaceholder.classList.remove('hidden');
|
||||||
|
currentPreviewUrl = null;
|
||||||
|
}
|
||||||
|
currentPreviewFile = null;
|
||||||
|
|
||||||
|
// Update button text
|
||||||
|
saveTemplateBtn.innerHTML = '<span>Save Template</span><div class="btn-shine"></div>';
|
||||||
|
|
||||||
|
createTemplateModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeTemplateModalBtn) {
|
||||||
|
closeTemplateModalBtn.addEventListener('click', () => {
|
||||||
|
createTemplateModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category select logic
|
||||||
|
if (templateCategorySelect) {
|
||||||
|
templateCategorySelect.addEventListener('change', (e) => {
|
||||||
|
if (e.target.value === 'new') {
|
||||||
|
templateCategoryInput.classList.remove('hidden');
|
||||||
|
templateCategoryInput.focus();
|
||||||
|
} else {
|
||||||
|
templateCategoryInput.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag and drop for preview
|
||||||
|
const templatePreviewUrlInput = document.getElementById('template-preview-url');
|
||||||
|
let isUrlInputMode = false;
|
||||||
|
|
||||||
|
if (templatePreviewDropzone) {
|
||||||
|
// Click to toggle URL input mode
|
||||||
|
templatePreviewDropzone.addEventListener('click', (e) => {
|
||||||
|
// Don't toggle if clicking on the input itself
|
||||||
|
if (e.target === templatePreviewUrlInput) return;
|
||||||
|
|
||||||
|
if (!isUrlInputMode) {
|
||||||
|
// Switch to URL input mode
|
||||||
|
isUrlInputMode = true;
|
||||||
|
templatePreviewImg.classList.add('hidden');
|
||||||
|
dropzonePlaceholder.classList.add('hidden');
|
||||||
|
templatePreviewUrlInput.classList.remove('hidden');
|
||||||
|
templatePreviewUrlInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle URL input
|
||||||
|
if (templatePreviewUrlInput) {
|
||||||
|
templatePreviewUrlInput.addEventListener('blur', async () => {
|
||||||
|
const url = templatePreviewUrlInput.value.trim();
|
||||||
|
if (url) {
|
||||||
|
try {
|
||||||
|
// Try to load the image from URL
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
templatePreviewImg.src = url;
|
||||||
|
templatePreviewImg.classList.remove('hidden');
|
||||||
|
dropzonePlaceholder.classList.add('hidden');
|
||||||
|
templatePreviewUrlInput.classList.add('hidden');
|
||||||
|
currentPreviewUrl = url;
|
||||||
|
currentPreviewFile = null;
|
||||||
|
isUrlInputMode = false;
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
alert('Failed to load image from URL. Please check the URL and try again.');
|
||||||
|
templatePreviewUrlInput.focus();
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
} catch (error) {
|
||||||
|
alert('Invalid image URL');
|
||||||
|
templatePreviewUrlInput.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If empty, go back to placeholder
|
||||||
|
isUrlInputMode = false;
|
||||||
|
templatePreviewUrlInput.classList.add('hidden');
|
||||||
|
dropzonePlaceholder.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
templatePreviewUrlInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
templatePreviewUrlInput.blur();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
templatePreviewUrlInput.value = '';
|
||||||
|
templatePreviewUrlInput.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
templatePreviewDropzone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
templatePreviewDropzone.classList.add('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
templatePreviewDropzone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
templatePreviewDropzone.classList.remove('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
templatePreviewDropzone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
templatePreviewDropzone.classList.remove('drag-over');
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
currentPreviewFile = file;
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
templatePreviewImg.src = objectUrl;
|
||||||
|
templatePreviewImg.classList.remove('hidden');
|
||||||
|
dropzonePlaceholder.classList.add('hidden');
|
||||||
|
templatePreviewUrlInput.classList.add('hidden');
|
||||||
|
isUrlInputMode = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save template
|
||||||
|
if (saveTemplateBtn) {
|
||||||
|
saveTemplateBtn.addEventListener('click', async () => {
|
||||||
|
const title = templateTitleInput.value.trim();
|
||||||
|
const prompt = templatePromptInput.value.trim();
|
||||||
|
const mode = templateModeSelect.value;
|
||||||
|
let category = templateCategorySelect.value;
|
||||||
|
|
||||||
|
if (category === 'new') {
|
||||||
|
category = templateCategoryInput.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
alert('Please enter a title for the template.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!prompt) {
|
||||||
|
alert('Please enter a prompt.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!category) {
|
||||||
|
alert('Please select or enter a category.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTemplateBtn.disabled = true;
|
||||||
|
saveTemplateBtn.textContent = editingTemplate ? 'Updating...' : 'Saving...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('title', title);
|
||||||
|
formData.append('prompt', prompt);
|
||||||
|
formData.append('mode', mode);
|
||||||
|
formData.append('category', category);
|
||||||
|
|
||||||
|
if (currentPreviewFile) {
|
||||||
|
formData.append('preview', currentPreviewFile);
|
||||||
|
} else if (currentPreviewUrl) {
|
||||||
|
formData.append('preview_path', currentPreviewUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If editing, add the template index
|
||||||
|
const endpoint = editingTemplate ? '/update_template' : '/save_template';
|
||||||
|
if (editingTemplate) {
|
||||||
|
formData.append('template_index', editingTemplate.userTemplateIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success
|
||||||
|
createTemplateModal.classList.add('hidden');
|
||||||
|
|
||||||
|
// Reload template gallery
|
||||||
|
await templateGallery.load();
|
||||||
|
|
||||||
|
// Reset editing state
|
||||||
|
editingTemplate = null;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Failed to ${editingTemplate ? 'update' : 'save'} template: ` + error.message);
|
||||||
|
} finally {
|
||||||
|
saveTemplateBtn.disabled = false;
|
||||||
|
saveTemplateBtn.innerHTML = '<span>Save Template</span><div class="btn-shine"></div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
if (createTemplateModal) {
|
||||||
|
createTemplateModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === createTemplateModal) {
|
||||||
|
createTemplateModal.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('pointermove', handleCanvasPointerMove);
|
document.addEventListener('pointermove', handleCanvasPointerMove);
|
||||||
document.addEventListener('pointerup', () => {
|
document.addEventListener('pointerup', () => {
|
||||||
if (isPanning && imageDisplayArea) {
|
if (isPanning && imageDisplayArea) {
|
||||||
|
|
|
||||||
197
static/style.css
197
static/style.css
|
|
@ -1251,3 +1251,200 @@ button#generate-btn:disabled {
|
||||||
max-height: 140px;
|
max-height: 140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Template Create Modal */
|
||||||
|
.template-form-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row .form-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview-dropzone {
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
border: 2px dashed rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview-dropzone:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background: rgba(251, 191, 36, 0.05);
|
||||||
|
box-shadow: 0 0 15px rgba(251, 191, 36, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview-dropzone.drag-over {
|
||||||
|
border-color: var(--accent-hover);
|
||||||
|
background: rgba(251, 191, 36, 0.1);
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview-dropzone img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-placeholder::before {
|
||||||
|
content: '+';
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--accent-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#create-template-modal .popup-card {
|
||||||
|
background: rgba(10, 11, 22, 0.98);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#save-template-btn {
|
||||||
|
padding: 0.6rem 1.5rem;
|
||||||
|
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
|
||||||
|
color: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s, box-shadow 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#save-template-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 5px 15px rgba(251, 191, 36, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#save-template-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview-url-input {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 90%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--input-bg);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview-url-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-hover);
|
||||||
|
box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-edit-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card:hover .template-edit-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-edit-btn:hover {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: #000;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-edit-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-create-btn {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: transform 0.1s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-create-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 5px 15px rgba(251, 191, 36, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-create-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,17 @@
|
||||||
stroke-linejoin="round" />
|
stroke-linejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" id="create-template-btn" class="canvas-btn icon-btn"
|
||||||
|
aria-label="Create Template" title="Tạo Template">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 14H12M12 14H14M12 14V16M12 14V12" stroke="currentColor"
|
||||||
|
stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path
|
||||||
|
d="M22 11.7979C22 9.16554 22 7.84935 21.2305 6.99383C21.1598 6.91514 21.0849 6.84024 21.0062 6.76946C20.1506 6 18.8345 6 16.2021 6H15.8284C14.6747 6 14.0979 6 13.5604 5.84678C13.2651 5.7626 12.9804 5.64471 12.7121 5.49543C12.2237 5.22367 11.8158 4.81578 11 4L10.4497 3.44975C10.1763 3.17633 10.0396 3.03961 9.89594 2.92051C9.27652 2.40704 8.51665 2.09229 7.71557 2.01738C7.52976 2 7.33642 2 6.94975 2C6.06722 2 5.62595 2 5.25839 2.06935C3.64031 2.37464 2.37464 3.64031 2.06935 5.25839C2 5.62595 2 6.06722 2 6.94975M21.9913 16C21.9554 18.4796 21.7715 19.8853 20.8284 20.8284C19.6569 22 17.7712 22 14 22H10C6.22876 22 4.34315 22 3.17157 20.8284C2 19.6569 2 17.7712 2 14V11"
|
||||||
|
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button type="button" class="canvas-btn" data-action="zoom-out">−</button>
|
<button type="button" class="canvas-btn" data-action="zoom-out">−</button>
|
||||||
<button type="button" class="canvas-btn" data-action="zoom-in">+</button>
|
<button type="button" class="canvas-btn" data-action="zoom-in">+</button>
|
||||||
<button type="button" class="canvas-btn icon-btn" data-action="zoom-reset"
|
<button type="button" class="canvas-btn icon-btn" data-action="zoom-reset"
|
||||||
|
|
@ -222,6 +233,73 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Template Modal -->
|
||||||
|
<div id="create-template-modal" class="popup-overlay hidden">
|
||||||
|
<div class="popup-card" style="max-width: 600px;">
|
||||||
|
<header class="popup-header">
|
||||||
|
<h2>Create Template</h2>
|
||||||
|
<button id="close-template-modal" type="button" class="popup-close" aria-label="Close">×</button>
|
||||||
|
</header>
|
||||||
|
<div class="popup-body">
|
||||||
|
<div class="template-form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="template-title">Title</label>
|
||||||
|
<input type="text" id="template-title" placeholder="Template Name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Preview Image</label>
|
||||||
|
<div id="template-preview-dropzone" class="template-preview-dropzone">
|
||||||
|
<img id="template-preview-img" src="" alt="Preview" class="hidden">
|
||||||
|
<div class="dropzone-placeholder">
|
||||||
|
<span>Drag & Drop Image or Click to Enter URL</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="template-preview-url" class="template-preview-url-input hidden"
|
||||||
|
placeholder="Enter image URL or path">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="template-prompt">Prompt</label>
|
||||||
|
<textarea id="template-prompt" rows="3" placeholder="Template Prompt"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="template-mode">Mode</label>
|
||||||
|
<select id="template-mode">
|
||||||
|
<option value="generate">Generate</option>
|
||||||
|
<option value="edit">Edit</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="template-category">Category</label>
|
||||||
|
<div class="category-input-wrapper">
|
||||||
|
<select id="template-category-select">
|
||||||
|
<option value="User">User</option>
|
||||||
|
<option value="Cinematic">Cinematic</option>
|
||||||
|
<option value="Anime">Anime</option>
|
||||||
|
<option value="Photography">Photography</option>
|
||||||
|
<option value="Digital Art">Digital Art</option>
|
||||||
|
<option value="new">+ New Category</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="template-category-input" class="hidden"
|
||||||
|
placeholder="New Category Name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls-footer" style="margin-top: 1.5rem; justify-content: flex-end;">
|
||||||
|
<button id="save-template-btn">
|
||||||
|
<span>Save Template</span>
|
||||||
|
<div class="btn-shine"></div>
|
||||||
|
</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