widget-generator
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWidget 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:
- Standard prompt widget - Uses default styling (like
PromptCard)coderabbit.ts - Custom render widget - Full custom React component (like )
book.tsx
小组件支持两种渲染模式:
- 标准提示小组件 - 使用默认的样式(例如
PromptCard)coderabbit.ts - 自定义渲染小组件 - 完全自定义的React组件(例如)
book.tsx
Prerequisites
前置条件
Before creating a widget, gather from the user:
| Parameter | Required | Description |
|---|---|---|
| Widget ID | ✅ | Unique identifier (kebab-case, e.g., |
| Widget Name | ✅ | Display name for the plugin |
| Rendering Mode | ✅ | |
| Sponsor Info | ❌ | Name, logo, logoDark, URL (for sponsored widgets) |
在创建小组件前,需要从用户处收集以下信息:
| 参数 | 是否必填 | 描述 |
|---|---|---|
| 小组件ID | ✅ | 唯一标识符(短横线命名法,例如 |
| 小组件名称 | ✅ | 插件的显示名称 |
| 渲染模式 | ✅ | |
| 赞助商信息 | ❌ | 名称、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}.tstypescript
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}.tstypescript
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}.tsxtsx
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}.tsxtsx
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- Add import at top:
typescript
import { {widgetId}Widget } from "./{widget-id}";- Add to array:
widgetPlugins
typescript
const widgetPlugins: WidgetPlugin[] = [
coderabbitWidget,
bookWidget,
{widgetId}Widget, // Add new widget
];编辑:
src/lib/plugins/widgets/index.ts- 在顶部添加导入语句:
typescript
import { {widgetId}Widget } from "./{widget-id}";- 添加到数组:
widgetPlugins
typescript
const widgetPlugins: WidgetPlugin[] = [
coderabbitWidget,
bookWidget,
{widgetId}Widget, // 添加新小组件
];Step 4: Add Sponsor Assets (if applicable)
步骤4:添加赞助商资源(如有需要)
If the widget has a sponsor:
- Add light logo:
public/sponsors/{sponsor}.svg - Add dark logo (optional):
public/sponsors/{sponsor}-dark.svg
如果小组件包含赞助商:
- 添加亮色logo:
public/sponsors/{sponsor}.svg - 添加暗色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
验证
-
Run type check:bash
npx tsc --noEmit -
Start dev server:bash
npm run dev -
Navigate toor
/discoverto verify widget appears at configured positions/feed
-
运行类型检查:bash
npx tsc --noEmit -
启动开发服务器:bash
npm run dev -
访问或
/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
常见问题
| Issue | Solution |
|---|---|
| Widget not showing | Check |
| TypeScript errors | Ensure imports from |
| Styling issues | Use Tailwind classes, match existing widget patterns |
| Position wrong | Remember positions are 0-indexed, check |
| 问题 | 解决方案 |
|---|---|
| 小组件不显示 | 检查 |
| TypeScript报错 | 确保从 |
| 样式问题 | 使用Tailwind类,匹配现有小组件的样式模式 |
| 位置不正确 | 注意位置是从0开始计数,检查 |