widget-generator

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Widget Generator Skill

小组件生成器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.
本Skill指导你为prompts.chat创建小组件插件。小组件会被注入到提示信息流中,用于展示推广内容、赞助商卡片或自定义交互组件。

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
    )
小组件支持两种渲染模式:
  1. 标准提示小组件 - 使用默认的
    PromptCard
    样式(例如
    coderabbit.ts
  2. 自定义渲染小组件 - 完全自定义的React组件(例如
    book.tsx

Prerequisites

前置条件

Before creating a widget, gather from the user:
ParameterRequiredDescription
Widget IDUnique identifier (kebab-case, e.g.,
my-sponsor
)
Widget NameDisplay name for the plugin
Rendering Mode
standard
or
custom
Sponsor InfoName, logo, logoDark, URL (for sponsored widgets)
在创建小组件前,需要从用户处收集以下信息:
参数是否必填描述
小组件ID唯一标识符(短横线命名法,例如
my-sponsor
小组件名称插件的显示名称
渲染模式
standard
custom
赞助商信息名称、logo、logoDark、URL(仅适用于赞助型小组件)

Step 1: Gather Widget Configuration

步骤1:收集小组件配置

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)
- id: string (唯一,短横线命名法)
- name: string (显示名称)
- slug: string (URL友好的标识符)
- title: string (卡片标题)
- description: string (卡片描述)

Content (for standard mode)

内容(标准模式)

- content: string (prompt content, can be multi-line markdown)
- type: "TEXT" | "STRUCTURED"
- structuredFormat?: "json" | "yaml" (if type is STRUCTURED)
- content: string (提示内容,支持多行markdown)
- type: "TEXT" | "STRUCTURED"
- structuredFormat?: "json" | "yaml"(当type为STRUCTURED时可选)

Categorization

分类信息

- tags?: string[] (e.g., ["AI", "Development"])
- category?: string (e.g., "Development", "Writing")
- tags?: string[](例如["AI", "Development"])
- category?: string(例如"Development", "Writing")

Action Button

操作按钮

- actionUrl?: string (CTA link)
- actionLabel?: string (CTA button text)
- actionUrl?: string(CTA链接)
- actionLabel?: string(CTA按钮文本)

Sponsor (optional)

赞助商(可选)

- sponsor?: {
    name: string
    logo: string (path to light mode logo)
    logoDark?: string (path to dark mode logo)
    url: string (sponsor website)
  }
- sponsor?: {
    name: string
    logo: string(亮色模式logo路径)
    logoDark?: string(暗色模式logo路径)
    url: string(赞助商网站)
  }

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)
  }
- positioning: {
    position: number(从0开始的起始位置,默认值:2)
    mode: "once" | "repeat"(默认值:"once")
    repeatEvery?: number(重复模式下使用,例如30)
    maxCount?: number(最大出现次数,once模式默认1,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
- shouldInject?: (context) => boolean
  Context包含:
  - filters.q: 搜索查询词
  - filters.category: 分类名称
  - filters.categorySlug: 分类标识
  - filters.tag: 标签筛选
  - filters.sort: 排序选项
  - itemCount: 信息流中的总条目数

Step 2: Create Widget File

步骤2:创建小组件文件

Standard Widget (TypeScript only)

标准小组件(仅支持TypeScript)

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;
      },
    },
  ],
};
创建文件:
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",
      // 可选赞助商
      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;
        
        // 无筛选条件时始终显示
        if (!filters?.q && !filters?.category && !filters?.tag) {
          return true;
        }
        
        // 在此添加自定义筛选逻辑
        return false;
      },
    },
  ],
};

Custom Render Widget (TSX with React)

自定义渲染小组件(使用React的TSX)

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 (
    <div className="group border rounded-[var(--radius)] overflow-hidden hover:border-foreground/20 transition-colors bg-gradient-to-br from-primary/5 via-background to-primary/10 p-5">
      {/* Custom widget content */}
      <div className="flex flex-col items-center gap-4">
        {/* Image/visual element */}
        <div className="relative w-full aspect-video">
          <Image
            src="/path/to/image.jpg"
            alt="{Alt text}"
            fill
            className="object-cover rounded-lg"
          />
        </div>
        
        {/* Content */}
        <div className="w-full text-center">
          <h3 className="font-semibold text-base mb-1.5">{Title}</h3>
          <p className="text-xs text-muted-foreground mb-4">{Description}</p>
          <Button asChild size="sm" className="w-full">
            <Link href="{action-url}">{Action Label}</Link>
          </Button>
        </div>
      </div>
    </div>
  );
}

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 />,
    },
  ],
};
创建文件:
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 (
    <div className="group border rounded-[var(--radius)] overflow-hidden hover:border-foreground/20 transition-colors bg-gradient-to-br from-primary/5 via-background to-primary/10 p-5">
      {/* 自定义小组件内容 */}
      <div className="flex flex-col items-center gap-4">
        {/* 图片/视觉元素 */}
        <div className="relative w-full aspect-video">
          <Image
            src="/path/to/image.jpg"
            alt="{Alt text}"
            fill
            className="object-cover rounded-lg"
          />
        </div>
        
        {/* 内容 */}
        <div className="w-full text-center">
          <h3 className="font-semibold text-base mb-1.5">{Title}</h3>
          <p className="text-xs text-muted-foreground mb-4">{Description}</p>
          <Button asChild size="sm" className="w-full">
            <Link href="{action-url}">{Action Label}</Link>
          </Button>
        </div>
      </div>
    </div>
  );
}

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

步骤3:注册小组件

Edit
src/lib/plugins/widgets/index.ts
:
  1. Add import at top:
typescript
import { {widgetId}Widget } from "./{widget-id}";
  1. Add to
    widgetPlugins
    array:
typescript
const widgetPlugins: WidgetPlugin[] = [
  coderabbitWidget,
  bookWidget,
  {widgetId}Widget, // Add new widget
];
编辑
src/lib/plugins/widgets/index.ts
  1. 在顶部添加导入语句:
typescript
import { {widgetId}Widget } from "./{widget-id}";
  1. 添加到
    widgetPlugins
    数组:
typescript
const widgetPlugins: WidgetPlugin[] = [
  coderabbitWidget,
  bookWidget,
  {widgetId}Widget, // 添加新小组件
];

Step 4: Add Sponsor Assets (if applicable)

步骤4:添加赞助商资源(如有需要)

If the widget has a sponsor:
  1. Add light logo:
    public/sponsors/{sponsor}.svg
  2. Add dark logo (optional):
    public/sponsors/{sponsor}-dark.svg
如果小组件包含赞助商:
  1. 添加亮色logo:
    public/sponsors/{sponsor}.svg
  2. 添加暗色logo(可选):
    public/sponsors/{sponsor}-dark.svg

Positioning Examples

定位示例

Show once at position 5

在位置5显示一次

typescript
positioning: {
  position: 5,
  mode: "once",
}
typescript
positioning: {
  position: 5,
  mode: "once",
}

Repeat every 30 items, max 5 times

每30条重复显示,最多5次

typescript
positioning: {
  position: 3,
  mode: "repeat",
  repeatEvery: 30,
  maxCount: 5,
}
typescript
positioning: {
  position: 3,
  mode: "repeat",
  repeatEvery: 30,
  maxCount: 5,
}

Unlimited repeating

无限制重复显示

typescript
positioning: {
  position: 2,
  mode: "repeat",
  repeatEvery: 25,
  // No maxCount = unlimited
}
typescript
positioning: {
  position: 2,
  mode: "repeat",
  repeatEvery: 25,
  // 不设置maxCount = 无限制
}

shouldInject Examples

shouldInject示例

Always show

始终显示

typescript
shouldInject: () => true,
typescript
shouldInject: () => true,

Only when no filters active

仅在无筛选条件时显示

typescript
shouldInject: (context) => {
  const { filters } = context;
  return !filters?.q && !filters?.category && !filters?.tag;
},
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");
},
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));
},
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;
},
typescript
shouldInject: (context) => {
  return (context.itemCount ?? 0) >= 10;
},

Custom Render Patterns

自定义渲染模式

Card with gradient background

带渐变背景的卡片

tsx
<div className="border rounded-[var(--radius)] overflow-hidden bg-gradient-to-br from-primary/5 via-background to-primary/10 p-5">
tsx
<div className="border rounded-[var(--radius)] overflow-hidden bg-gradient-to-br from-primary/5 via-background to-primary/10 p-5">

Sponsor badge

赞助商标识

tsx
<div className="flex items-center gap-2 mb-2">
  <span className="text-xs font-medium text-primary">Sponsored</span>
</div>
tsx
<div className="flex items-center gap-2 mb-2">
  <span className="text-xs font-medium text-primary">Sponsored</span>
</div>

Responsive image

响应式图片

tsx
<div className="relative w-full aspect-video">
  <Image src="/image.jpg" alt="..." fill className="object-cover" />
</div>
tsx
<div className="relative w-full aspect-video">
  <Image src="/image.jpg" alt="..." fill className="object-cover" />
</div>

CTA button

CTA按钮

tsx
<Button asChild size="sm" className="w-full">
  <Link href="https://example.com">
    Learn More
    <ArrowRight className="ml-2 h-3.5 w-3.5" />
  </Link>
</Button>
tsx
<Button asChild size="sm" className="w-full">
  <Link href="https://example.com">
    Learn More
    <ArrowRight className="ml-2 h-3.5 w-3.5" />
  </Link>
</Button>

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
  1. 运行类型检查:
    bash
    npx tsc --noEmit
  2. 启动开发服务器:
    bash
    npm run dev
  3. 访问
    /discover
    /feed
    页面,验证小组件是否出现在配置的位置

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[];
}
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;      // 默认值: 2
    mode?: "once" | "repeat"; // 默认值: "once"
    repeatEvery?: number;   // 重复模式下使用
    maxCount?: number;      // 最大出现次数
  };
  shouldInject?: (context: WidgetContext) => boolean;
  render?: () => ReactNode; // 自定义渲染时使用
}

interface WidgetPlugin {
  id: string;
  name: string;
  prompts: WidgetPrompt[];
}

Common Issues

常见问题

IssueSolution
Widget not showingCheck
shouldInject
logic, verify registration in
index.ts
TypeScript errorsEnsure imports from
./types
, check sponsor object shape
Styling issuesUse Tailwind classes, match existing widget patterns
Position wrongRemember positions are 0-indexed, check
repeatEvery
value
问题解决方案
小组件不显示检查
shouldInject
逻辑,验证是否在
index.ts
中完成注册
TypeScript报错确保从
./types
导入类型,检查赞助商对象的结构
样式问题使用Tailwind类,匹配现有小组件的样式模式
位置不正确注意位置是从0开始计数,检查
repeatEvery
的值