--- name: widget-generator description: Generate customizable widget plugins for the prompts.chat feed system --- # Widget Generator Skill This skill guides creation of widget plugins for **prompts.chat**. Widgets are injected into prompt feeds to display promotional content, sponsor cards, or custom interactive components. ## Overview Widgets support two rendering modes: 1. **Standard prompt widget** - Uses default `PromptCard` styling (like `coderabbit.ts`) 2. **Custom render widget** - Full custom React component (like `book.tsx`) ## Prerequisites Before creating a widget, gather from the user: | Parameter | Required | Description | |-----------|----------|-------------| | Widget ID | ✅ | Unique identifier (kebab-case, e.g., `my-sponsor`) | | Widget Name | ✅ | Display name for the plugin | | Rendering Mode | ✅ | `standard` or `custom` | | Sponsor Info | ❌ | Name, logo, logoDark, URL (for sponsored widgets) | ## Step 1: Gather Widget Configuration Ask the user for the following configuration options: ### Basic Info ``` - id: string (unique, kebab-case) - name: string (display name) - slug: string (URL-friendly identifier) - title: string (card title) - description: string (card description) ``` ### Content (for standard mode) ``` - content: string (prompt content, can be multi-line markdown) - type: "TEXT" | "STRUCTURED" - structuredFormat?: "json" | "yaml" (if type is STRUCTURED) ``` ### Categorization ``` - tags?: string[] (e.g., ["AI", "Development"]) - category?: string (e.g., "Development", "Writing") ``` ### Action Button ``` - actionUrl?: string (CTA link) - actionLabel?: string (CTA button text) ``` ### Sponsor (optional) ``` - sponsor?: { name: string logo: string (path to light mode logo) logoDark?: string (path to dark mode logo) url: string (sponsor website) } ``` ### Positioning Strategy ``` - positioning: { position: number (0-indexed start position, default: 2) mode: "once" | "repeat" (default: "once") repeatEvery?: number (for repeat mode, e.g., 30) maxCount?: number (max occurrences, default: 1 for once, unlimited for repeat) } ``` ### Injection Logic ``` - shouldInject?: (context) => boolean Context contains: - filters.q: search query - filters.category: category name - filters.categorySlug: category slug - filters.tag: tag filter - filters.sort: sort option - itemCount: total items in feed ``` ## Step 2: Create Widget File ### Standard Widget (TypeScript only) Create file: `src/lib/plugins/widgets/{widget-id}.ts` ```typescript import type { WidgetPlugin } from "./types"; export const {widgetId}Widget: WidgetPlugin = { id: "{widget-id}", name: "{Widget Name}", prompts: [ { id: "{prompt-id}", slug: "{prompt-slug}", title: "{Title}", description: "{Description}", content: `{Multi-line content here}`, type: "TEXT", // Optional sponsor sponsor: { name: "{Sponsor Name}", logo: "/sponsors/{sponsor}.svg", logoDark: "/sponsors/{sponsor}-dark.svg", url: "{sponsor-url}", }, tags: ["{Tag1}", "{Tag2}"], category: "{Category}", actionUrl: "{action-url}", actionLabel: "{Action Label}", positioning: { position: 2, mode: "repeat", repeatEvery: 50, maxCount: 3, }, shouldInject: (context) => { const { filters } = context; // Always show when no filters active if (!filters?.q && !filters?.category && !filters?.tag) { return true; } // Add custom filter logic here return false; }, }, ], }; ``` ### Custom Render Widget (TSX with React) Create file: `src/lib/plugins/widgets/{widget-id}.tsx` ```tsx import Link from "next/link"; import Image from "next/image"; import { Button } from "@/components/ui/button"; import type { WidgetPlugin } from "./types"; function {WidgetName}Widget() { return (
{/* Custom widget content */}
{/* Image/visual element */}
{Alt text}
{/* Content */}

{Title}

{Description}

); } export const {widgetId}Widget: WidgetPlugin = { id: "{widget-id}", name: "{Widget Name}", prompts: [ { id: "{prompt-id}", slug: "{prompt-slug}", title: "{Title}", description: "{Description}", content: "", type: "TEXT", tags: ["{Tag1}", "{Tag2}"], category: "{Category}", actionUrl: "{action-url}", actionLabel: "{Action Label}", positioning: { position: 10, mode: "repeat", repeatEvery: 60, maxCount: 4, }, shouldInject: () => true, render: () => <{WidgetName}Widget />, }, ], }; ``` ## Step 3: Register Widget Edit `src/lib/plugins/widgets/index.ts`: 1. Add import at top: ```typescript import { {widgetId}Widget } from "./{widget-id}"; ``` 2. Add to `widgetPlugins` array: ```typescript const widgetPlugins: WidgetPlugin[] = [ coderabbitWidget, bookWidget, {widgetId}Widget, // Add new widget ]; ``` ## Step 4: Add Sponsor Assets (if applicable) If the widget has a sponsor: 1. Add light logo: `public/sponsors/{sponsor}.svg` 2. Add dark logo (optional): `public/sponsors/{sponsor}-dark.svg` ## Positioning Examples ### Show once at position 5 ```typescript positioning: { position: 5, mode: "once", } ``` ### Repeat every 30 items, max 5 times ```typescript positioning: { position: 3, mode: "repeat", repeatEvery: 30, maxCount: 5, } ``` ### Unlimited repeating ```typescript positioning: { position: 2, mode: "repeat", repeatEvery: 25, // No maxCount = unlimited } ``` ## shouldInject Examples ### Always show ```typescript shouldInject: () => true, ``` ### Only when no filters active ```typescript shouldInject: (context) => { const { filters } = context; return !filters?.q && !filters?.category && !filters?.tag; }, ``` ### Show for specific categories ```typescript shouldInject: (context) => { const slug = context.filters?.categorySlug?.toLowerCase(); return slug?.includes("development") || slug?.includes("coding"); }, ``` ### Show when search matches keywords ```typescript shouldInject: (context) => { const query = context.filters?.q?.toLowerCase() || ""; return ["ai", "automation", "workflow"].some(kw => query.includes(kw)); }, ``` ### Show only when enough items ```typescript shouldInject: (context) => { return (context.itemCount ?? 0) >= 10; }, ``` ## Custom Render Patterns ### Card with gradient background ```tsx
``` ### Sponsor badge ```tsx
Sponsored
``` ### Responsive image ```tsx
...
``` ### CTA button ```tsx ``` ## Verification 1. Run type check: ```bash npx tsc --noEmit ``` 2. Start dev server: ```bash npm run dev ``` 3. Navigate to `/discover` or `/feed` to verify widget appears at configured positions ## Type Reference ```typescript interface WidgetPrompt { id: string; slug: string; title: string; description: string; content: string; type: "TEXT" | "STRUCTURED"; structuredFormat?: "json" | "yaml"; sponsor?: { name: string; logo: string; logoDark?: string; url: string; }; tags?: string[]; category?: string; actionUrl?: string; actionLabel?: string; positioning?: { position?: number; // Default: 2 mode?: "once" | "repeat"; // Default: "once" repeatEvery?: number; // For repeat mode maxCount?: number; // Max occurrences }; shouldInject?: (context: WidgetContext) => boolean; render?: () => ReactNode; // For custom rendering } interface WidgetPlugin { id: string; name: string; prompts: WidgetPrompt[]; } ``` ## Common Issues | Issue | Solution | |-------|----------| | Widget not showing | Check `shouldInject` logic, verify registration in `index.ts` | | TypeScript errors | Ensure imports from `./types`, check sponsor object shape | | Styling issues | Use Tailwind classes, match existing widget patterns | | Position wrong | Remember positions are 0-indexed, check `repeatEvery` value |