/** * 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 = ''; let favoriteTemplateKeys = new Set(); let favoriteFilterActive = false; let userTemplateFilterActive = false; function setFavoriteKeys(keys) { if (Array.isArray(keys)) { favoriteTemplateKeys = new Set(keys.filter(Boolean)); } else { favoriteTemplateKeys = new Set(); } } function getTemplateKey(template) { if (!template) return ''; if (template.userTemplateIndex !== undefined && template.userTemplateIndex !== null) { return `user-${template.userTemplateIndex}`; } const title = i18n.getText(template.title); const promptText = i18n.getText(template.prompt); const categoryText = i18n.getText(template.category); const modeText = template.mode || 'generate'; return `builtin-${title}||${promptText}||${categoryText}||${modeText}`; } function isFavoriteKey(key) { return Boolean(key) && favoriteTemplateKeys.has(key); } async function updateFavoriteState(key, favorite) { if (!key || typeof favorite !== 'boolean') return; try { const response = await fetch('/template_favorite', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, favorite }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to update favorite'); } setFavoriteKeys(data.favorites); } catch (error) { throw error; } } function resolveTagText(tag) { if (!tag) return ''; return i18n.getText(tag); } function getTemplateTags(template) { if (!template) return []; const rawTags = template.tags; if (!rawTags) return []; let normalized = []; if (Array.isArray(rawTags)) { normalized = rawTags; } else if (typeof rawTags === 'string') { normalized = rawTags.split(','); } else { return []; } return normalized .map(tag => resolveTagText(tag).trim()) .filter(Boolean); } function filterTemplates() { let filtered = allTemplates; if (currentCategory !== 'all') { filtered = filtered.filter(t => { const categoryText = i18n.getText(t.category); return categoryText === currentCategory; }); } if (currentMode !== 'all') { filtered = filtered.filter(t => { return (t.mode || 'generate') === currentMode; }); } if (favoriteFilterActive) { filtered = filtered.filter(t => { const key = getTemplateKey(t); return isFavoriteKey(key); }); } if (userTemplateFilterActive) { filtered = filtered.filter(t => Boolean(t.isUserTemplate)); } 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(); const tagText = getTemplateTags(t).join(' ').toLowerCase(); return title.includes(query) || prompt.includes(query) || category.includes(query) || tagText.includes(query); }); } return filtered; } 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(); } 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'); 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); } const previewActions = document.createElement('div'); previewActions.className = 'template-card-preview-actions'; const templateKey = getTemplateKey(template); const isFavorite = isFavoriteKey(templateKey); const favoriteBtn = document.createElement('button'); favoriteBtn.type = 'button'; favoriteBtn.className = `template-card-favorite-btn${isFavorite ? ' active' : ''}`; favoriteBtn.setAttribute('aria-pressed', isFavorite ? 'true' : 'false'); favoriteBtn.title = isFavorite ? 'Remove from favorites' : 'Mark as favorite'; favoriteBtn.innerHTML = ` `; favoriteBtn.addEventListener('click', async (event) => { event.stopPropagation(); favoriteBtn.disabled = true; try { await updateFavoriteState(templateKey, !isFavorite); render(); } catch (error) { console.error('Failed to update favorite:', error); favoriteBtn.disabled = false; } }); previewActions.appendChild(favoriteBtn); if (template.isUserTemplate || template.builtinTemplateIndex !== undefined) { const editBtn = document.createElement('button'); editBtn.className = 'template-edit-btn'; editBtn.innerHTML = ` `; editBtn.title = 'Edit Template'; editBtn.addEventListener('click', (e) => { e.stopPropagation(); if (window.openEditTemplateModal) { window.openEditTemplateModal(template); } }); previewActions.appendChild(editBtn); } preview.appendChild(previewActions); card.appendChild(preview); const content = document.createElement('div'); content.className = 'template-card-content'; const title = document.createElement('h4'); title.className = 'template-card-title'; title.textContent = i18n.getText(template.title) || 'Untitled Template'; content.appendChild(title); card.appendChild(content); card.addEventListener('click', () => { onSelectTemplate?.(template); }); return card; } async function load() { try { const response = await fetch('/prompts'); const data = await response.json(); if (data.prompts) { allTemplates = data.prompts; setFavoriteKeys(data.favorites); render(); return true; } return false; } catch (error) { console.error('Failed to load templates:', error); return false; } } function render() { if (!container) return; const filtered = filterTemplates(); const categories = getCategories(); container.innerHTML = ''; const header = document.createElement('div'); header.className = 'template-gallery-header'; const title = document.createElement('h2'); title.className = 'template-gallery-title'; title.textContent = i18n.t('promptTemplates'); header.appendChild(title); const controls = document.createElement('div'); controls.className = 'template-gallery-controls'; 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; searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { searchQuery = e.target.value; render(); } }); searchInput.addEventListener('blur', (e) => { if (searchQuery !== e.target.value) { searchQuery = e.target.value; render(); } }); searchContainer.appendChild(searchInput); controls.appendChild(searchContainer); 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); 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); const favoritesToggle = document.createElement('button'); favoritesToggle.type = 'button'; favoritesToggle.className = `template-favorites-toggle${favoriteFilterActive ? ' active' : ''}`; favoritesToggle.setAttribute('aria-pressed', favoriteFilterActive ? 'true' : 'false'); favoritesToggle.setAttribute('aria-label', favoriteFilterActive ? 'Show all templates' : 'Show favorites only'); favoritesToggle.title = favoriteFilterActive ? 'Show all templates' : 'Show favorites only'; favoritesToggle.innerHTML = ` `; favoritesToggle.addEventListener('click', () => { favoriteFilterActive = !favoriteFilterActive; render(); }); controls.appendChild(favoritesToggle); const userToggle = document.createElement('button'); userToggle.type = 'button'; userToggle.className = `template-user-toggle${userTemplateFilterActive ? ' active' : ''}`; userToggle.setAttribute('aria-pressed', userTemplateFilterActive ? 'true' : 'false'); userToggle.setAttribute('aria-label', userTemplateFilterActive ? 'Show all templates' : 'Show my templates'); userToggle.title = userTemplateFilterActive ? 'Show all templates' : 'Show my templates'; userToggle.innerHTML = ` `; userToggle.addEventListener('click', () => { userTemplateFilterActive = !userTemplateFilterActive; render(); }); controls.appendChild(userToggle); const createTemplateBtn = document.createElement('button'); createTemplateBtn.className = 'template-create-btn'; createTemplateBtn.innerHTML = ` `; createTemplateBtn.addEventListener('click', () => { if (window.openCreateTemplateModal) { window.openCreateTemplateModal(); } }); controls.appendChild(createTemplateBtn); header.appendChild(controls); container.appendChild(header); const count = document.createElement('div'); count.className = 'template-results-count'; count.textContent = i18n.t('resultsCount', filtered.length); container.appendChild(count); 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); } return { load, render, show() { if (container) { container.classList.remove('hidden'); } }, hide() { if (container) { container.classList.add('hidden'); } } }; }