template update
This commit is contained in:
parent
be77213b65
commit
9b909dae9c
10 changed files with 3059 additions and 66 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@
|
|||
.DS_Store
|
||||
/.venv
|
||||
/static/uploads
|
||||
.DS_Store
|
||||
|
|
|
|||
59
app.py
59
app.py
|
|
@ -248,5 +248,64 @@ def get_gallery():
|
|||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
return response
|
||||
|
||||
@app.route('/prompts')
|
||||
def get_prompts():
|
||||
category = request.args.get('category')
|
||||
|
||||
try:
|
||||
# Read prompts.json file
|
||||
prompts_path = os.path.join(os.path.dirname(__file__), 'prompts.json')
|
||||
with open(prompts_path, 'r', encoding='utf-8') as f:
|
||||
prompts = json.load(f)
|
||||
|
||||
# Filter by category if specified
|
||||
if category:
|
||||
prompts = [p for p in prompts if p.get('category') == category]
|
||||
|
||||
response = jsonify({'prompts': prompts})
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/refine_prompt', methods=['POST'])
|
||||
def refine_prompt():
|
||||
data = request.get_json()
|
||||
current_prompt = data.get('current_prompt')
|
||||
instruction = data.get('instruction')
|
||||
api_key = data.get('api_key') or os.environ.get('GOOGLE_API_KEY')
|
||||
|
||||
if not api_key:
|
||||
return jsonify({'error': 'API Key is required.'}), 401
|
||||
|
||||
if not instruction:
|
||||
return jsonify({'error': 'Instruction is required'}), 400
|
||||
|
||||
try:
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
system_instruction = "You are an expert prompt engineer for image generation AI. Rewrite the prompt to incorporate the user's instruction while maintaining the original intent and improving quality. Return ONLY the new prompt text, no explanations."
|
||||
|
||||
prompt_content = f"Current prompt: {current_prompt}\nUser instruction: {instruction}\nNew prompt:"
|
||||
|
||||
print(f"Refining prompt with instruction: {instruction}")
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents=[prompt_content],
|
||||
config=types.GenerateContentConfig(
|
||||
system_instruction=system_instruction,
|
||||
temperature=0.7,
|
||||
)
|
||||
)
|
||||
|
||||
if response.text:
|
||||
return jsonify({'refined_prompt': response.text.strip()})
|
||||
else:
|
||||
return jsonify({'error': 'No response from AI'}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, port=8888)
|
||||
|
|
|
|||
1965
prompts.json
Normal file
1965
prompts.json
Normal file
File diff suppressed because it is too large
Load diff
BIN
static/eror.png
Normal file
BIN
static/eror.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
70
static/modules/i18n.js
Normal file
70
static/modules/i18n.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Simple i18n utility for bilingual English-Vietnamese support
|
||||
*/
|
||||
|
||||
export const i18n = {
|
||||
currentLang: 'vi',
|
||||
|
||||
translations: {
|
||||
searchPlaceholder: {
|
||||
en: 'Search templates...',
|
||||
vi: 'Tìm kiếm mẫu...'
|
||||
},
|
||||
allCategories: {
|
||||
en: 'All Categories',
|
||||
vi: 'Tất cả danh mục'
|
||||
},
|
||||
resultsCount: {
|
||||
en: (count) => `${count} template${count !== 1 ? 's' : ''}`,
|
||||
vi: (count) => `${count} mẫu`
|
||||
},
|
||||
noResults: {
|
||||
en: 'No templates found',
|
||||
vi: 'Không tìm thấy mẫu nào'
|
||||
},
|
||||
promptTemplates: {
|
||||
en: 'Prompt Templates',
|
||||
vi: 'Mẫu Prompt'
|
||||
},
|
||||
toggleLanguage: {
|
||||
en: 'Switch to Vietnamese',
|
||||
vi: 'Chuyển sang tiếng Anh'
|
||||
},
|
||||
languageCode: {
|
||||
en: 'EN',
|
||||
vi: 'VI'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Translate a key with optional arguments
|
||||
*/
|
||||
t(key, ...args) {
|
||||
const translation = this.translations[key]?.[this.currentLang];
|
||||
if (typeof translation === 'function') {
|
||||
return translation(...args);
|
||||
}
|
||||
return translation || key;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set current language
|
||||
*/
|
||||
setLanguage(lang) {
|
||||
if (lang === 'en' || lang === 'vi') {
|
||||
this.currentLang = lang;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get language-aware text from object
|
||||
* Supports both string and object formats
|
||||
*/
|
||||
getText(obj) {
|
||||
if (typeof obj === 'string') return obj;
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
return obj[this.currentLang] || obj['en'] || obj['vi'] || '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
};
|
||||
281
static/modules/templateGallery.js
Normal file
281
static/modules/templateGallery.js
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
/**
|
||||
* Template Gallery Module
|
||||
* Displays prompt templates from prompts.json as selectable cards
|
||||
*/
|
||||
|
||||
import { i18n } from './i18n.js';
|
||||
|
||||
export function createTemplateGallery({ container, onSelectTemplate }) {
|
||||
let allTemplates = [];
|
||||
let currentCategory = 'all';
|
||||
let currentMode = 'all';
|
||||
let searchQuery = '';
|
||||
|
||||
/**
|
||||
* Fetch templates from API
|
||||
*/
|
||||
async function load() {
|
||||
try {
|
||||
const response = await fetch('/prompts');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.prompts) {
|
||||
allTemplates = data.prompts;
|
||||
render();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to load templates:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique categories from templates
|
||||
*/
|
||||
function getCategories() {
|
||||
const categories = new Set();
|
||||
allTemplates.forEach(t => {
|
||||
if (t.category) {
|
||||
const categoryText = i18n.getText(t.category);
|
||||
if (categoryText) categories.add(categoryText);
|
||||
}
|
||||
});
|
||||
return Array.from(categories).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter templates based on category and search
|
||||
*/
|
||||
function filterTemplates() {
|
||||
let filtered = allTemplates;
|
||||
|
||||
// Filter by category
|
||||
if (currentCategory !== 'all') {
|
||||
filtered = filtered.filter(t => {
|
||||
const categoryText = i18n.getText(t.category);
|
||||
return categoryText === currentCategory;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by mode
|
||||
if (currentMode !== 'all') {
|
||||
filtered = filtered.filter(t => {
|
||||
return (t.mode || 'generate') === currentMode;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(t => {
|
||||
const title = i18n.getText(t.title).toLowerCase();
|
||||
const prompt = i18n.getText(t.prompt).toLowerCase();
|
||||
const category = i18n.getText(t.category).toLowerCase();
|
||||
return title.includes(query) ||
|
||||
prompt.includes(query) ||
|
||||
category.includes(query);
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a template card element
|
||||
*/
|
||||
function createTemplateCard(template) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'template-card';
|
||||
card.setAttribute('data-category', i18n.getText(template.category) || '');
|
||||
card.setAttribute('data-mode', template.mode || 'generate');
|
||||
|
||||
// Preview image
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'template-card-preview';
|
||||
if (template.preview) {
|
||||
const img = document.createElement('img');
|
||||
img.src = template.preview;
|
||||
img.alt = i18n.getText(template.title) || 'Template preview';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = function() {
|
||||
this.onerror = null;
|
||||
this.src = '/static/eror.png';
|
||||
};
|
||||
preview.appendChild(img);
|
||||
}
|
||||
card.appendChild(preview);
|
||||
|
||||
// Content
|
||||
const content = document.createElement('div');
|
||||
content.className = 'template-card-content';
|
||||
|
||||
// Title
|
||||
const title = document.createElement('h4');
|
||||
title.className = 'template-card-title';
|
||||
title.textContent = i18n.getText(template.title) || 'Untitled Template';
|
||||
content.appendChild(title);
|
||||
|
||||
card.appendChild(content);
|
||||
|
||||
// Click handler
|
||||
card.addEventListener('click', () => {
|
||||
onSelectTemplate?.(template);
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the gallery
|
||||
*/
|
||||
function render() {
|
||||
if (!container) return;
|
||||
|
||||
const filtered = filterTemplates();
|
||||
const categories = getCategories();
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create header with controls
|
||||
const header = document.createElement('div');
|
||||
header.className = 'template-gallery-header';
|
||||
|
||||
// Title
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'template-gallery-title';
|
||||
title.textContent = i18n.t('promptTemplates');
|
||||
header.appendChild(title);
|
||||
|
||||
// Controls container
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'template-gallery-controls';
|
||||
|
||||
// Search input
|
||||
const searchContainer = document.createElement('div');
|
||||
searchContainer.className = 'template-search-container';
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.className = 'template-search-input';
|
||||
searchInput.placeholder = i18n.t('searchPlaceholder');
|
||||
searchInput.value = searchQuery;
|
||||
|
||||
// Only search on Enter
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
searchQuery = e.target.value;
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
// Also update on blur to ensure value is captured if user clicks away
|
||||
searchInput.addEventListener('blur', (e) => {
|
||||
if (searchQuery !== e.target.value) {
|
||||
searchQuery = e.target.value;
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
searchContainer.appendChild(searchInput);
|
||||
controls.appendChild(searchContainer);
|
||||
|
||||
// Mode filter
|
||||
const modeSelect = document.createElement('select');
|
||||
modeSelect.className = 'template-mode-select';
|
||||
|
||||
const modes = [
|
||||
{ value: 'all', label: 'All Modes' },
|
||||
{ value: 'edit', label: 'Edit' },
|
||||
{ value: 'generate', label: 'Generate' }
|
||||
];
|
||||
|
||||
modes.forEach(mode => {
|
||||
const option = document.createElement('option');
|
||||
option.value = mode.value;
|
||||
option.textContent = mode.label;
|
||||
modeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
modeSelect.value = currentMode;
|
||||
modeSelect.addEventListener('change', (e) => {
|
||||
currentMode = e.target.value;
|
||||
render();
|
||||
});
|
||||
controls.appendChild(modeSelect);
|
||||
|
||||
// Category filter
|
||||
const categorySelect = document.createElement('select');
|
||||
categorySelect.className = 'template-category-select';
|
||||
|
||||
const allOption = document.createElement('option');
|
||||
allOption.value = 'all';
|
||||
allOption.textContent = i18n.t('allCategories');
|
||||
categorySelect.appendChild(allOption);
|
||||
|
||||
categories.forEach(cat => {
|
||||
const option = document.createElement('option');
|
||||
option.value = cat;
|
||||
option.textContent = cat;
|
||||
categorySelect.appendChild(option);
|
||||
});
|
||||
|
||||
categorySelect.value = currentCategory;
|
||||
categorySelect.addEventListener('change', (e) => {
|
||||
currentCategory = e.target.value;
|
||||
render();
|
||||
});
|
||||
controls.appendChild(categorySelect);
|
||||
|
||||
header.appendChild(controls);
|
||||
container.appendChild(header);
|
||||
|
||||
// Results count
|
||||
const count = document.createElement('div');
|
||||
count.className = 'template-results-count';
|
||||
count.textContent = i18n.t('resultsCount', filtered.length);
|
||||
container.appendChild(count);
|
||||
|
||||
// Create grid
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'template-card-grid';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'template-empty-state';
|
||||
empty.textContent = i18n.t('noResults');
|
||||
grid.appendChild(empty);
|
||||
} else {
|
||||
filtered.forEach(template => {
|
||||
grid.appendChild(createTemplateCard(template));
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(grid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the gallery
|
||||
*/
|
||||
function show() {
|
||||
if (container) {
|
||||
container.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the gallery
|
||||
*/
|
||||
function hide() {
|
||||
if (container) {
|
||||
container.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
render,
|
||||
show,
|
||||
hide
|
||||
};
|
||||
}
|
||||
173
static/script.js
173
static/script.js
|
|
@ -3,6 +3,8 @@ import { createGallery } from './modules/gallery.js';
|
|||
import { createReferenceSlotManager } from './modules/referenceSlots.js';
|
||||
import { setupHelpPopups } from './modules/popup.js';
|
||||
import { extractMetadataFromBlob } from './modules/metadata.js';
|
||||
import { createTemplateGallery } from './modules/templateGallery.js';
|
||||
import { i18n } from './modules/i18n.js';
|
||||
|
||||
const SETTINGS_STORAGE_KEY = 'gemini-image-app-settings';
|
||||
const ZOOM_STEP = 0.1;
|
||||
|
|
@ -68,6 +70,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const placeholderState = document.getElementById('placeholder-state');
|
||||
const loadingState = document.getElementById('loading-state');
|
||||
const errorState = document.getElementById('error-state');
|
||||
const templateGalleryState = document.getElementById('template-gallery-state');
|
||||
const templateGalleryContainer = document.getElementById('template-gallery-container');
|
||||
const resultState = document.getElementById('result-state');
|
||||
const errorText = document.getElementById('error-text');
|
||||
const generatedImage = document.getElementById('generated-image');
|
||||
|
|
@ -79,15 +83,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const sidebar = document.querySelector('.sidebar');
|
||||
const resizeHandle = document.querySelector('.sidebar-resize-handle');
|
||||
|
||||
// Refine Prompt Elements
|
||||
const refinePromptBtn = document.getElementById('refine-prompt-btn');
|
||||
const refineModal = document.getElementById('refine-modal');
|
||||
const closeRefineModalBtn = document.getElementById('close-refine-modal');
|
||||
const refineInstructionInput = document.getElementById('refine-instruction');
|
||||
const confirmRefineBtn = document.getElementById('confirm-refine-btn');
|
||||
|
||||
let zoomLevel = 1;
|
||||
let panOffset = { x: 0, y: 0 };
|
||||
let isPanning = false;
|
||||
let lastPointer = { x: 0, y: 0 };
|
||||
let hasGeneratedImage = false; // Track if image exists
|
||||
|
||||
const slotManager = createReferenceSlotManager(imageInputGrid, {
|
||||
onChange: persistSettings,
|
||||
});
|
||||
|
||||
const templateGallery = createTemplateGallery({
|
||||
container: templateGalleryContainer,
|
||||
onSelectTemplate: (template) => {
|
||||
// Fill prompt field with template prompt (language-aware)
|
||||
if (template.prompt) {
|
||||
promptInput.value = i18n.getText(template.prompt);
|
||||
persistSettings();
|
||||
}
|
||||
// Stay in template gallery view - don't auto-switch
|
||||
// User will switch view by selecting image from history or generating
|
||||
}
|
||||
});
|
||||
|
||||
const gallery = createGallery({
|
||||
galleryGrid,
|
||||
onSelect: async ({ imageUrl, metadata }) => {
|
||||
|
|
@ -95,7 +120,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (metadata) {
|
||||
applyMetadata(metadata);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
setupHelpPopups({
|
||||
|
|
@ -164,6 +189,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
document.addEventListener('keydown', handleGenerateShortcut);
|
||||
document.addEventListener('keydown', handleResetShortcut);
|
||||
document.addEventListener('keydown', handleDownloadShortcut);
|
||||
document.addEventListener('keydown', handleTemplateShortcut);
|
||||
|
||||
if (imageDisplayArea) {
|
||||
imageDisplayArea.addEventListener('wheel', handleCanvasWheel, { passive: false });
|
||||
|
|
@ -217,6 +243,82 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
canvasToolbar.addEventListener('click', handleCanvasToolbarClick);
|
||||
}
|
||||
|
||||
// Refine Prompt Logic
|
||||
if (refinePromptBtn) {
|
||||
refinePromptBtn.addEventListener('click', () => {
|
||||
refineInstructionInput.value = ''; // Clear previous instruction
|
||||
refineModal.classList.remove('hidden');
|
||||
refineInstructionInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (closeRefineModalBtn) {
|
||||
closeRefineModalBtn.addEventListener('click', () => {
|
||||
refineModal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
if (refineModal) {
|
||||
refineModal.addEventListener('click', (e) => {
|
||||
if (e.target === refineModal) {
|
||||
refineModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const refineLoading = document.getElementById('refine-loading');
|
||||
|
||||
if (confirmRefineBtn && refineLoading) {
|
||||
confirmRefineBtn.addEventListener('click', async () => {
|
||||
const instruction = refineInstructionInput.value.trim();
|
||||
const currentPrompt = promptInput.value.trim();
|
||||
const apiKey = apiKeyInput.value.trim();
|
||||
|
||||
if (!instruction) return;
|
||||
if (!apiKey) {
|
||||
alert('Please enter your API Key first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
confirmRefineBtn.classList.add('hidden');
|
||||
refineLoading.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch('/refine_prompt', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
current_prompt: currentPrompt,
|
||||
instruction: instruction,
|
||||
api_key: apiKey
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (data.refined_prompt) {
|
||||
promptInput.value = data.refined_prompt;
|
||||
persistSettings(); // Save the new prompt
|
||||
refineModal.classList.add('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to refine prompt: ' + error.message);
|
||||
} finally {
|
||||
// Reset state
|
||||
confirmRefineBtn.classList.remove('hidden');
|
||||
refineLoading.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('pointermove', handleCanvasPointerMove);
|
||||
document.addEventListener('pointerup', () => {
|
||||
if (isPanning && imageDisplayArea) {
|
||||
|
|
@ -232,12 +334,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
|
||||
loadGallery();
|
||||
loadTemplateGallery();
|
||||
initializeSidebarResizer(sidebar, resizeHandle);
|
||||
|
||||
// Setup canvas language toggle
|
||||
const canvasLangInput = document.getElementById('canvas-lang-input');
|
||||
if (canvasLangInput) {
|
||||
// Set initial state
|
||||
canvasLangInput.checked = i18n.currentLang === 'en';
|
||||
|
||||
canvasLangInput.addEventListener('change', (e) => {
|
||||
i18n.setLanguage(e.target.checked ? 'en' : 'vi');
|
||||
// Update visual state
|
||||
const options = document.querySelectorAll('.canvas-lang-option');
|
||||
options.forEach(opt => {
|
||||
const isActive = opt.dataset.lang === i18n.currentLang;
|
||||
opt.classList.toggle('active', isActive);
|
||||
});
|
||||
// Reload template gallery with new language
|
||||
templateGallery.render();
|
||||
});
|
||||
}
|
||||
|
||||
function setViewState(state) {
|
||||
placeholderState.classList.add('hidden');
|
||||
loadingState.classList.add('hidden');
|
||||
errorState.classList.add('hidden');
|
||||
templateGalleryState.classList.add('hidden');
|
||||
resultState.classList.add('hidden');
|
||||
|
||||
switch (state) {
|
||||
|
|
@ -250,6 +373,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
case 'error':
|
||||
errorState.classList.remove('hidden');
|
||||
break;
|
||||
case 'template-gallery':
|
||||
templateGalleryState.classList.remove('hidden');
|
||||
break;
|
||||
case 'result':
|
||||
resultState.classList.remove('hidden');
|
||||
break;
|
||||
|
|
@ -281,6 +407,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
resetView();
|
||||
};
|
||||
|
||||
hasGeneratedImage = true; // Mark that we have an image
|
||||
setViewState('result');
|
||||
}
|
||||
|
||||
|
|
@ -320,6 +447,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadTemplateGallery() {
|
||||
try {
|
||||
await templateGallery.load();
|
||||
// Don't auto-show template gallery - let user trigger it
|
||||
// Default view will be placeholder or template gallery based on state
|
||||
if (!hasGeneratedImage) {
|
||||
setViewState('template-gallery');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unable to load template gallery', error);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeSidebarResizer(sidebar, handle) {
|
||||
if (!sidebar || !handle) return;
|
||||
const resizerQuery = window.matchMedia('(min-width: 1025px)');
|
||||
|
|
@ -432,6 +572,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
downloadLink.click();
|
||||
}
|
||||
|
||||
function handleTemplateShortcut(event) {
|
||||
if (event.key.toLowerCase() !== 't') return;
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
||||
const targetTag = event.target?.tagName;
|
||||
if (targetTag && ['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTag)) return;
|
||||
if (event.target?.isContentEditable) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// Toggle template gallery
|
||||
if (templateGalleryState.classList.contains('hidden')) {
|
||||
setViewState('template-gallery');
|
||||
} else {
|
||||
// If we have a generated image, go back to result
|
||||
if (hasGeneratedImage) {
|
||||
setViewState('result');
|
||||
} else {
|
||||
// Otherwise go to placeholder
|
||||
setViewState('placeholder');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCanvasWheel(event) {
|
||||
if (resultState.classList.contains('hidden')) return;
|
||||
event.preventDefault();
|
||||
|
|
@ -475,6 +638,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
case 'zoom-reset':
|
||||
resetView();
|
||||
break;
|
||||
case 'toggle-template':
|
||||
// Toggle between result and template gallery
|
||||
if (resultState.classList.contains('hidden')) {
|
||||
setViewState('result');
|
||||
} else {
|
||||
setViewState('template-gallery');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
353
static/style.css
353
static/style.css
|
|
@ -311,6 +311,80 @@ textarea {
|
|||
min-height: 100px;
|
||||
}
|
||||
|
||||
.prompt-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prompt-wrapper textarea {
|
||||
width: 100%;
|
||||
padding-bottom: 2.5rem; /* Make space for the button */
|
||||
}
|
||||
|
||||
.prompt-refine-btn {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
z-index: 5;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.prompt-refine-btn:hover {
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.prompt-refine-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
#confirm-refine-btn {
|
||||
padding: 0.4rem 1rem;
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
|
||||
color: #111;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(245, 197, 24, 0.3);
|
||||
transition: transform 0.1s, box-shadow 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#confirm-refine-btn:hover {
|
||||
box-shadow: 0 6px 16px rgba(245, 197, 24, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#confirm-refine-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.rotating-icon {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Reference image input grid */
|
||||
|
||||
/* Reference image input grid */
|
||||
.image-inputs {
|
||||
display: flex;
|
||||
|
|
@ -769,6 +843,285 @@ button#generate-btn:disabled {
|
|||
background: rgba(255, 68, 68, 0.9);
|
||||
}
|
||||
|
||||
/* Template Browser Box */
|
||||
.template-browser-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
background: linear-gradient(135deg, rgba(251, 191, 36, 0.15), rgba(251, 191, 36, 0.05));
|
||||
border: 2px dashed rgba(251, 191, 36, 0.6);
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.template-browser-box:hover {
|
||||
background: linear-gradient(135deg, rgba(251, 191, 36, 0.25), rgba(251, 191, 36, 0.1));
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.template-browser-box.active {
|
||||
background: linear-gradient(135deg, rgba(251, 191, 36, 0.3), rgba(251, 191, 36, 0.15));
|
||||
border-color: var(--accent-color);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.template-box-icon {
|
||||
color: var(--accent-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.template-box-icon svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.template-box-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-color);
|
||||
text-align: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Template Gallery */
|
||||
.template-gallery-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-gallery-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.template-gallery-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.template-gallery-controls {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-search-container {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.template-search-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.template-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.template-category-select {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Canvas Language Toggle (Top-Left Corner) */
|
||||
.canvas-lang-toggle {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.canvas-lang-switch {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.canvas-lang-switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.canvas-lang-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
background: rgba(10, 15, 50, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 18px;
|
||||
padding: 3px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.canvas-lang-option {
|
||||
padding: 4px 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 15px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.canvas-lang-option.active {
|
||||
background: linear-gradient(135deg, #FBC02D, #F9A825);
|
||||
color: #0A0F32;
|
||||
box-shadow: 0 2px 8px rgba(251, 192, 45, 0.4);
|
||||
}
|
||||
|
||||
.canvas-lang-switch:hover .canvas-lang-option:not(.active) {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.template-results-count {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.template-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.template-empty-state {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 10px 30px rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
.template-card-preview {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.template-card-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.template-card:hover .template-card-preview img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.template-card-content {
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.template-card-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.template-category-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.template-card-author {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.template-card-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.template-card-preview {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.template-gallery-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.template-search-container,
|
||||
.template-category-select {
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
width: 50px;
|
||||
|
|
|
|||
|
|
@ -15,73 +15,100 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="brand">
|
||||
<h1>aPix <span class="badge">by SDVN</span></h1>
|
||||
<div class="app-container">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="brand">
|
||||
<h1>aPix <span class="badge">by SDVN</span></h1>
|
||||
</div>
|
||||
<button type="button" class="toolbar-info-btn info-icon-btn" data-popup-target="help"
|
||||
aria-label="Thông tin và hướng dẫn">
|
||||
<span aria-hidden="true">i</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="controls-section">
|
||||
<div class="controls-content">
|
||||
<div class="input-group">
|
||||
<label for="api-key">API Key</label>
|
||||
<input type="password" id="api-key" placeholder="Google Cloud API Key">
|
||||
</div>
|
||||
<button type="button" class="toolbar-info-btn info-icon-btn" data-popup-target="help"
|
||||
aria-label="Thông tin và hướng dẫn">
|
||||
<span aria-hidden="true">i</span>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="prompt">Prompt</label>
|
||||
<div class="prompt-wrapper">
|
||||
<textarea id="prompt" placeholder="Describe your imagination..." rows="6"></textarea>
|
||||
<button id="refine-prompt-btn" class="prompt-refine-btn" title="Refine with AI">
|
||||
<svg fill="currentColor" height="16px" width="16px" version="1.1" id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512.422 512.422" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M41.053,223.464c2.667,1.067,5.76,1.067,8.427-0.213l83.307-37.867c5.333-2.56,7.573-8.96,5.013-14.293
|
||||
c-2.453-5.12-8.533-7.467-13.76-5.12l-58.347,26.56c27.84-83.307,105.387-138.987,194.667-138.987
|
||||
c93.547,0,175.36,62.507,198.933,152c1.493,5.653,7.36,9.067,13.013,7.573c5.653-1.493,9.067-7.36,7.573-13.013
|
||||
c-26.027-98.773-116.267-167.893-219.52-167.893c-98.453,0-184.107,61.44-215.04,153.387l-24.533-61.333
|
||||
c-1.813-5.547-7.893-8.64-13.44-6.827c-5.547,1.813-8.64,7.893-6.827,13.44c0.107,0.427,0.32,0.853,0.533,1.28l34.027,85.333
|
||||
C36.146,220.158,38.279,222.398,41.053,223.464z" />
|
||||
<path
|
||||
d="M511.773,380.904c-0.107-0.213-0.213-0.427-0.213-0.64l-34.027-85.333c-1.067-2.667-3.2-4.907-5.973-5.973
|
||||
c-2.667-1.067-5.76-0.96-8.427,0.213l-83.307,37.867c-5.44,2.24-8,8.533-5.76,13.973c2.24,5.44,8.533,8,13.973,5.76
|
||||
c0.213-0.107,0.427-0.213,0.64-0.32l58.347-26.56c-28.053,83.307-105.707,138.987-194.88,138.987
|
||||
c-93.547,0-175.36-62.507-198.933-152c-1.493-5.653-7.36-9.067-13.013-7.573c-5.653,1.493-9.067,7.36-7.573,13.013
|
||||
c25.92,98.88,116.267,167.893,219.52,167.893c98.453,0,184-61.44,215.04-153.387l24.533,61.333
|
||||
c2.027,5.547,8.107,8.427,13.653,6.4C510.919,392.531,513.799,386.451,511.773,380.904z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group image-inputs">
|
||||
<div class="image-input-header">
|
||||
<label>Reference Images</label>
|
||||
</div>
|
||||
<div id="image-input-grid" class="image-input-grid" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="aspect-ratio">Aspect Ratio</label>
|
||||
<select id="aspect-ratio">
|
||||
<option value="Auto">Auto (Default)</option>
|
||||
<option value="1:1">1:1 (Square)</option>
|
||||
<option value="16:9">16:9 (Widescreen)</option>
|
||||
<option value="4:3">4:3 (Standard)</option>
|
||||
<option value="3:4">3:4 (Portrait)</option>
|
||||
<option value="9:16">9:16 (Mobile)</option>
|
||||
<option value="2:3">2:3</option>
|
||||
<option value="3:2">3:2</option>
|
||||
<option value="4:5">4:5</option>
|
||||
<option value="5:4">5:4</option>
|
||||
<option value="21:9">21:9 (Cinema)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="resolution">Resolution</label>
|
||||
<select id="resolution">
|
||||
<option value="1K" selected>1K</option>
|
||||
<option value="2K">2K</option>
|
||||
<option value="4K">4K</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-footer">
|
||||
<button id="generate-btn">
|
||||
<span>Generate</span>
|
||||
<div class="btn-shine"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="controls-section">
|
||||
<div class="controls-content">
|
||||
<div class="input-group">
|
||||
<label for="api-key">API Key</label>
|
||||
<input type="password" id="api-key" placeholder="Google Cloud API Key">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="prompt">Prompt</label>
|
||||
<textarea id="prompt" placeholder="Describe your imagination..." rows="6"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="input-group image-inputs">
|
||||
<div class="image-input-header">
|
||||
<label>Reference Images</label>
|
||||
</div>
|
||||
<div id="image-input-grid" class="image-input-grid" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="aspect-ratio">Aspect Ratio</label>
|
||||
<select id="aspect-ratio">
|
||||
<option value="Auto">Auto (Default)</option>
|
||||
<option value="1:1">1:1 (Square)</option>
|
||||
<option value="16:9">16:9 (Widescreen)</option>
|
||||
<option value="4:3">4:3 (Standard)</option>
|
||||
<option value="3:4">3:4 (Portrait)</option>
|
||||
<option value="9:16">9:16 (Mobile)</option>
|
||||
<option value="2:3">2:3</option>
|
||||
<option value="3:2">3:2</option>
|
||||
<option value="4:5">4:5</option>
|
||||
<option value="5:4">5:4</option>
|
||||
<option value="21:9">21:9 (Cinema)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="resolution">Resolution</label>
|
||||
<select id="resolution">
|
||||
<option value="1K" selected>1K</option>
|
||||
<option value="2K">2K</option>
|
||||
<option value="4K">4K</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-footer">
|
||||
<button id="generate-btn">
|
||||
<span>Generate</span>
|
||||
<div class="btn-shine"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="sidebar-resize-handle" aria-hidden="true"></div>
|
||||
<div class="content-area">
|
||||
</div>
|
||||
</aside>
|
||||
<div class="sidebar-resize-handle" aria-hidden="true"></div>
|
||||
<div class="content-area">
|
||||
<main class="main-content">
|
||||
<div class="image-display-area">
|
||||
<div id="placeholder-state" class="state-view">
|
||||
|
|
@ -98,9 +125,37 @@
|
|||
<p id="error-text"></p>
|
||||
</div>
|
||||
|
||||
<div id="template-gallery-state" class="state-view hidden">
|
||||
<!-- Language Toggle in top-left corner -->
|
||||
<div class="canvas-lang-toggle">
|
||||
<label class="canvas-lang-switch">
|
||||
<input type="checkbox" id="canvas-lang-input">
|
||||
<span class="canvas-lang-slider">
|
||||
<span class="canvas-lang-option active" data-lang="vi">VI</span>
|
||||
<span class="canvas-lang-option" data-lang="en">EN</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="template-gallery-container" class="template-gallery-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="result-state" class="state-view hidden">
|
||||
<img id="generated-image" src="" alt="Generated Image">
|
||||
<div class="canvas-toolbar" role="toolbar">
|
||||
<button type="button" class="canvas-btn" data-action="toggle-template"
|
||||
aria-label="Toggle template view" title="Chọn mẫu prompt">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M3 9H21" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M9 21V9" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</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 icon-btn" data-action="zoom-reset"
|
||||
|
|
@ -129,6 +184,44 @@
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div id="refine-modal" class="popup-overlay hidden">
|
||||
<div class="popup-card">
|
||||
<header class="popup-header">
|
||||
<h2>Refine Prompt</h2>
|
||||
<button id="close-refine-modal" type="button" class="popup-close" aria-label="Close">×</button>
|
||||
</header>
|
||||
<div class="popup-body">
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 0.5rem;">How would you like to
|
||||
change the current prompt?</p>
|
||||
<textarea id="refine-instruction" placeholder="e.g. Make it more realistic, add dramatic lighting..."
|
||||
rows="3"></textarea>
|
||||
<div class="controls-footer" style="margin-top: 1rem; justify-content: flex-end; align-items: center;">
|
||||
<button id="confirm-refine-btn">
|
||||
<span>Refine</span>
|
||||
<div class="btn-shine"></div>
|
||||
</button>
|
||||
<div id="refine-loading" class="hidden" style="color: var(--accent-color);">
|
||||
<svg class="rotating-icon" fill="currentColor" height="32px" width="32px" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.422 512.422">
|
||||
<path d="M41.053,223.464c2.667,1.067,5.76,1.067,8.427-0.213l83.307-37.867c5.333-2.56,7.573-8.96,5.013-14.293
|
||||
c-2.453-5.12-8.533-7.467-13.76-5.12l-58.347,26.56c27.84-83.307,105.387-138.987,194.667-138.987
|
||||
c93.547,0,175.36,62.507,198.933,152c1.493,5.653,7.36,9.067,13.013,7.573c5.653-1.493,9.067-7.36,7.573-13.013
|
||||
c-26.027-98.773-116.267-167.893-219.52-167.893c-98.453,0-184.107,61.44-215.04,153.387l-24.533-61.333
|
||||
c-1.813-5.547-7.893-8.64-13.44-6.827c-5.547,1.813-8.64,7.893-6.827,13.44c0.107,0.427,0.32,0.853,0.533,1.28l34.027,85.333
|
||||
C36.146,220.158,38.279,222.398,41.053,223.464z" />
|
||||
<path
|
||||
d="M511.773,380.904c-0.107-0.213-0.213-0.427-0.213-0.64l-34.027-85.333c-1.067-2.667-3.2-4.907-5.973-5.973
|
||||
c-2.667-1.067-5.76-0.96-8.427,0.213l-83.307,37.867c-5.44,2.24-8,8.533-5.76,13.973c2.24,5.44,8.533,8,13.973,5.76
|
||||
c0.213-0.107,0.427-0.213,0.64-0.32l58.347-26.56c-28.053,83.307-105.707,138.987-194.88,138.987
|
||||
c-93.547,0-175.36-62.507-198.933-152c-1.493-5.653-7.36-9.067-13.013-7.573c-5.653,1.493-9.067,7.36-7.573,13.013
|
||||
c25.92,98.88,116.267,167.893,219.52,167.893c98.453,0,184-61.44,215.04-153.387l24.533,61.333
|
||||
c2.027,5.547,8.107,8.427,13.653,6.4C510.919,392.531,513.799,386.451,511.773,380.904z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="popup-overlay" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="popup-title">
|
||||
<div class="popup-card">
|
||||
<header class="popup-header">
|
||||
|
|
@ -141,4 +234,4 @@
|
|||
<script type="module" src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
Loading…
Reference in a new issue