Files
open-design/apps/web/src/components/PromptTemplatesTab.tsx
marco 5dd70b5016
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
Initial import: open-design source for helix-mind.ai distribution
This repository contains the open-design daemon CLI source code, built
and packaged at https://helix-mind.ai/cli/open-design/latest.tgz for use
by the HelixMind /design slash command.

Licenses: Apache-2.0 (root) + MIT (skills/*)
2026-05-06 20:50:24 +02:00

218 lines
7.6 KiB
TypeScript

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<string, string> = {
'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<string>('All');
const [source, setSource] = useState<string>('All');
const surfaceScoped = useMemo(
() => templates.filter((tpl) => tpl.surface === surface),
[templates, surface],
);
const categories = useMemo(() => {
const set = new Set<string>();
for (const tpl of surfaceScoped) set.add(tpl.category || 'General');
return ['All', ...Array.from(set).sort()];
}, [surfaceScoped]);
const sources = useMemo(() => {
const set = new Set<string>();
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 (
<div className="tab-empty">
{surface === 'image'
? t('promptTemplates.emptyImage')
: t('promptTemplates.emptyVideo')}
</div>
);
}
return (
<div className="tab-panel prompt-templates-panel">
<div className="tab-panel-toolbar">
<input
placeholder={t('promptTemplates.searchPlaceholder')}
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
{categories.map((c) => (
<option key={c} value={c}>
{c === 'All' ? t('common.all') : localizePromptTemplateCategory(locale, c)}
</option>
))}
</select>
{sources.length > 2 ? (
<select
value={source}
onChange={(e) => setSource(e.target.value)}
aria-label={t('promptTemplates.sourceFilterAria')}
>
{sources.map((s) => (
<option key={s} value={s}>
{s === 'All' ? t('promptTemplates.allSources') : s}
</option>
))}
</select>
) : null}
<span className="prompt-templates-count">
{t('promptTemplates.countLabel', { n: filtered.length })}
</span>
</div>
{filtered.length === 0 ? (
<div className="tab-empty">{t('promptTemplates.emptyNoMatch')}</div>
) : (
<div className="prompt-templates-grid">
{filtered.map((tpl) => {
const localized = localizePromptTemplateSummary(locale, tpl);
return (
<PromptTemplateCard
key={tpl.id}
tpl={localized}
onPreview={() => onPreview(localized)}
/>
);
})}
</div>
)}
<div className="prompt-templates-footer">
{t('promptTemplates.attributionFooter')}
</div>
</div>
);
}
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 (
<button
type="button"
className="prompt-template-card"
onClick={onPreview}
title={t('promptTemplates.openPreviewTitle')}
>
<span className="prompt-template-thumb">
{tpl.previewImageUrl ? (
<img src={tpl.previewImageUrl} alt="" loading="lazy" draggable={false} />
) : tpl.surface === 'video' ? (
<span className="prompt-template-thumb-fallback" aria-hidden>
<Icon name="play" size={28} />
</span>
) : (
<span className="prompt-template-thumb-fallback" aria-hidden>
<Icon name="image" size={28} />
</span>
)}
{provider ? (
<span
className={
`prompt-template-thumb-provider${isHyperFrames ? ' is-hyperframes' : ''}`
}
>
{provider}
</span>
) : null}
{tpl.surface === 'video' && tpl.previewVideoUrl ? (
<span className="prompt-template-thumb-play" aria-hidden>
</span>
) : null}
</span>
<span className="prompt-template-meta">
<span className="prompt-template-title">{tpl.title}</span>
<span className="prompt-template-summary">{tpl.summary}</span>
<span className="prompt-template-tags">
<span className="prompt-template-category">{tpl.category}</span>
{(tpl.tags ?? []).slice(0, 3).map((tag) => (
<span key={tag} className="prompt-template-tag">
{tag}
</span>
))}
</span>
<span className="prompt-template-source">
{t('promptTemplates.sourcePrefix')} {sourceLabel}
</span>
</span>
</button>
);
}