import { useMemo, useState } from 'react'; import { useI18n, useT } from '../i18n'; import { localizePromptTemplateCategory, localizePromptTemplateSummary, } from '../i18n/content'; import type { PromptTemplateSource, PromptTemplateSummary } from '../types'; import { Icon } from './Icon'; // Stable, human-readable provider name used by the source filter and the // thumbnail badge. Anchored on `source.repo` (a small enumerated set) rather // than `source.author` (dozens of curators). Known upstream repos get a // brand-aware label; everything else falls back to the repo's last segment. const PROVIDER_LABELS: Record = { 'heygen-com/hyperframes': 'HyperFrames', 'YouMind-OpenLab/awesome-seedance-2-prompts': 'Seedance 2', 'YouMind-OpenLab/awesome-gpt-image-2': 'GPT Image 2', 'nexu-io/open-design': 'Open Design', }; function providerLabel(source: PromptTemplateSource): string { const known = PROVIDER_LABELS[source.repo]; if (known) return known; const repo = source.repo.split('/').pop() ?? source.repo; return repo; } interface Props { surface: 'image' | 'video'; templates: PromptTemplateSummary[]; onPreview: (tpl: PromptTemplateSummary) => void; } // Curated prompt-template gallery — one tab per surface (image / video). // Layout mirrors the Examples tab: a category filter row + a responsive // card grid that lazy-loads remote thumbnails (the upstream README hosts // images on CMS / Cloudflare Stream, both public). Each card opens a // preview modal with the full prompt body and attribution. export function PromptTemplatesTab({ surface, templates, onPreview }: Props) { const { locale, t } = useI18n(); const [filter, setFilter] = useState(''); const [category, setCategory] = useState('All'); const [source, setSource] = useState('All'); const surfaceScoped = useMemo( () => templates.filter((tpl) => tpl.surface === surface), [templates, surface], ); const categories = useMemo(() => { const set = new Set(); for (const tpl of surfaceScoped) set.add(tpl.category || 'General'); return ['All', ...Array.from(set).sort()]; }, [surfaceScoped]); const sources = useMemo(() => { const set = new Set(); for (const tpl of surfaceScoped) { const label = providerLabel(tpl.source); if (label) set.add(label); } return ['All', ...Array.from(set).sort()]; }, [surfaceScoped]); const filtered = useMemo(() => { const q = filter.trim().toLowerCase(); return surfaceScoped.filter((tpl) => { if (category !== 'All' && (tpl.category || 'General') !== category) { return false; } if (source !== 'All' && providerLabel(tpl.source) !== source) { return false; } if (!q) return true; const localized = localizePromptTemplateSummary(locale, tpl); return ( tpl.title.toLowerCase().includes(q) || tpl.summary.toLowerCase().includes(q) || (tpl.tags ?? []).some((tag) => tag.toLowerCase().includes(q)) || localized.title.toLowerCase().includes(q) || localized.summary.toLowerCase().includes(q) || localized.category.toLowerCase().includes(q) || (localized.tags ?? []).some((tag) => tag.toLowerCase().includes(q)) || providerLabel(tpl.source).toLowerCase().includes(q) ); }); }, [surfaceScoped, filter, category, source, locale]); if (surfaceScoped.length === 0) { return (
{surface === 'image' ? t('promptTemplates.emptyImage') : t('promptTemplates.emptyVideo')}
); } return (
setFilter(e.target.value)} /> {sources.length > 2 ? ( ) : null} {t('promptTemplates.countLabel', { n: filtered.length })}
{filtered.length === 0 ? (
{t('promptTemplates.emptyNoMatch')}
) : (
{filtered.map((tpl) => { const localized = localizePromptTemplateSummary(locale, tpl); return ( onPreview(localized)} /> ); })}
)}
{t('promptTemplates.attributionFooter')}
); } function PromptTemplateCard({ tpl, onPreview, }: { tpl: PromptTemplateSummary; onPreview: () => void; }) { const t = useT(); const provider = providerLabel(tpl.source); const isHyperFrames = tpl.source.repo === 'heygen-com/hyperframes'; const sourceLabel = tpl.source.author ? `${tpl.source.author} · ${tpl.source.repo.split('/').pop()}` : tpl.source.repo.split('/').pop(); return ( ); }