open-design/scripts/migrate-to-plugins/image-template.ts
pftom 5af84c09af feat(web): refactor PluginsHomeSection to use tag-based filtering and introduce PluginCard component
- Replaced the legacy tabbed categorization in `PluginsHomeSection` with a tag-driven approach, allowing dynamic filtering based on plugin tags.
- Introduced a new `PluginCard` component to encapsulate the rendering of individual plugin cards, improving separation of concerns and maintainability.
- Added a `usePluginCategories` hook to manage plugin visibility and filtering logic, enhancing the overall structure and testability of the component.
- Implemented a "More" pill for overflow tags in the filter row, improving user interaction with a cleaner UI.
- Updated CSS styles to support the new layout and improve visual consistency across the plugins home section.

This update significantly enhances the user experience by providing a more flexible and intuitive way to discover and interact with plugins.
2026-05-12 13:25:44 +08:00

202 lines
5.9 KiB
TypeScript

// Wrap a `prompt-templates/image/<id>.json` entry as a bundled plugin
// under `plugins/_official/image-templates/<plugin-id>/`. The wrapper
// preserves the original JSON beside the manifest so attribution,
// preview URLs, and the {argument …} placeholders stay accessible to
// the daemon's generator and to anyone auditing the plugin.
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import {
PLUGINS_ROOT,
PROMPT_TEMPLATES_DIR,
TIER_IMAGE_TEMPLATES,
buildManifest,
copyFile,
dedupeTags,
pluginName,
writeManifest,
type RunStats,
} from './lib.ts';
interface ImageTemplateJson {
id?: string;
surface?: string;
title?: string;
summary?: string;
category?: string;
tags?: string[];
model?: string;
aspect?: string;
prompt?: string;
previewImageUrl?: string;
previewVideoUrl?: string;
source?: { repo?: string; license?: string; author?: string; url?: string };
}
export interface ImageTemplateOptions {
ids?: string[];
limit?: number;
dryRun?: boolean;
}
export async function runImageTemplateGenerator(opts: ImageTemplateOptions): Promise<RunStats> {
return runJsonTemplateGenerator({
sourceDir: path.join(PROMPT_TEMPLATES_DIR, 'image'),
targetTier: TIER_IMAGE_TEMPLATES,
namePrefix: 'image-template',
mode: 'image',
surface: 'image',
atom: 'image-generate',
capability: 'media:image-generate',
aspectOptions: ['1:1', '16:9', '9:16', '4:5', '3:2'],
defaultAspect: '1:1',
previewType: 'image',
...opts,
});
}
export interface VideoTemplateOptions extends ImageTemplateOptions {}
export async function runVideoTemplateGenerator(opts: VideoTemplateOptions): Promise<RunStats> {
return runJsonTemplateGenerator({
sourceDir: path.join(PROMPT_TEMPLATES_DIR, 'video'),
targetTier: 'video-templates',
namePrefix: 'video-template',
mode: 'video',
surface: 'video',
atom: 'video-generate',
capability: 'media:video-generate',
aspectOptions: ['16:9', '9:16', '1:1', '4:5'],
defaultAspect: '16:9',
previewType: 'video',
...opts,
});
}
interface SharedConfig {
sourceDir: string;
targetTier: string;
namePrefix: string;
mode: 'image' | 'video';
surface: 'image' | 'video';
atom: string;
capability: string;
aspectOptions: string[];
defaultAspect: string;
previewType: 'image' | 'video';
ids?: string[];
limit?: number;
dryRun?: boolean;
}
async function runJsonTemplateGenerator(cfg: SharedConfig): Promise<RunStats> {
const { readdir } = await import('node:fs/promises');
const stats: RunStats = { generated: [], skipped: [] };
let entries: string[];
try {
entries = await readdir(cfg.sourceDir);
} catch {
return stats;
}
const filtered = entries
.filter((f) => f.endsWith('.json'))
.filter((f) => !cfg.ids || cfg.ids.includes(basenameNoExt(f)))
.sort();
const slice = cfg.limit !== undefined ? filtered.slice(0, cfg.limit) : filtered;
for (const file of slice) {
const filePath = path.join(cfg.sourceDir, file);
const raw = await readFile(filePath, 'utf8');
let parsed: ImageTemplateJson;
try {
parsed = JSON.parse(raw) as ImageTemplateJson;
} catch (err) {
stats.skipped.push({ id: file, reason: `invalid json: ${(err as Error).message}` });
continue;
}
if (!parsed.id || !parsed.title || !parsed.prompt) {
stats.skipped.push({ id: file, reason: 'missing id/title/prompt' });
continue;
}
if (parsed.surface !== cfg.surface) {
stats.skipped.push({ id: parsed.id, reason: `surface=${parsed.surface} mismatch` });
continue;
}
const name = pluginName(cfg.namePrefix, parsed.id);
const folder = path.join(PLUGINS_ROOT, cfg.targetTier, parsed.id);
const manifest = buildManifest({
name,
title: parsed.title,
description: parsed.summary ?? '',
license: parsed.source?.license ?? 'CC-BY-4.0',
author: {
...(parsed.source?.author ? { name: parsed.source.author } : {}),
...(parsed.source?.url ? { url: parsed.source.url } : {}),
},
...(parsed.source?.repo
? { homepage: `https://github.com/${parsed.source.repo}` }
: {}),
tags: dedupeTags([
cfg.namePrefix,
'first-party',
cfg.surface,
parsed.category,
...(parsed.tags ?? []),
]),
od: {
kind: 'scenario',
taskKind: 'new-generation',
mode: cfg.mode,
scenario: cfg.surface,
surface: cfg.surface,
preview: previewBlock(parsed, cfg.previewType),
useCase: { query: parsed.prompt },
inputs: [
...(parsed.model
? [{
name: 'model',
label: 'Model',
type: 'select' as const,
options: [parsed.model],
default: parsed.model,
}]
: []),
{
name: 'aspect',
label: 'Aspect ratio',
type: 'select' as const,
options: cfg.aspectOptions,
default: parsed.aspect ?? cfg.defaultAspect,
},
],
context: { assets: ['./template.json'] },
pipeline: {
stages: [{ id: 'generate', atoms: [cfg.atom] }],
},
capabilities: ['prompt:inject', cfg.capability],
},
});
if (cfg.dryRun) {
stats.generated.push(parsed.id);
continue;
}
await writeManifest(folder, manifest);
await copyFile(filePath, path.join(folder, 'template.json'));
stats.generated.push(parsed.id);
}
return stats;
}
function basenameNoExt(file: string): string {
return file.replace(/\.[^.]+$/, '');
}
function previewBlock(parsed: ImageTemplateJson, type: 'image' | 'video'): Record<string, unknown> | undefined {
const block: Record<string, unknown> = { type };
if (parsed.previewImageUrl) block.poster = parsed.previewImageUrl;
if (parsed.previewVideoUrl) block.video = parsed.previewVideoUrl;
return Object.keys(block).length > 1 ? block : undefined;
}