Loading...
Loading...
Compare original and translation side by side
PromptCardcoderabbit.tsbook.tsxPromptCardcoderabbit.tsbook.tsx| 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(仅适用于赞助型小组件) |
- 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: 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时可选)- tags?: string[] (e.g., ["AI", "Development"])
- category?: string (e.g., "Development", "Writing")- tags?: string[](例如["AI", "Development"])
- category?: string(例如"Development", "Writing")- actionUrl?: string (CTA link)
- actionLabel?: string (CTA button text)- actionUrl?: string(CTA链接)
- actionLabel?: string(CTA按钮文本)- 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: {
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模式默认无限制)
}- 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: 信息流中的总条目数src/lib/plugins/widgets/{widget-id}.tsimport 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}.tsimport 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;
},
},
],
};src/lib/plugins/widgets/{widget-id}.tsximport 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}.tsximport 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 />,
},
],
};src/lib/plugins/widgets/index.tsimport { {widgetId}Widget } from "./{widget-id}";widgetPluginsconst widgetPlugins: WidgetPlugin[] = [
coderabbitWidget,
bookWidget,
{widgetId}Widget, // Add new widget
];src/lib/plugins/widgets/index.tsimport { {widgetId}Widget } from "./{widget-id}";widgetPluginsconst widgetPlugins: WidgetPlugin[] = [
coderabbitWidget,
bookWidget,
{widgetId}Widget, // 添加新小组件
];public/sponsors/{sponsor}.svgpublic/sponsors/{sponsor}-dark.svgpublic/sponsors/{sponsor}.svgpublic/sponsors/{sponsor}-dark.svgpositioning: {
position: 5,
mode: "once",
}positioning: {
position: 5,
mode: "once",
}positioning: {
position: 3,
mode: "repeat",
repeatEvery: 30,
maxCount: 5,
}positioning: {
position: 3,
mode: "repeat",
repeatEvery: 30,
maxCount: 5,
}positioning: {
position: 2,
mode: "repeat",
repeatEvery: 25,
// No maxCount = unlimited
}positioning: {
position: 2,
mode: "repeat",
repeatEvery: 25,
// 不设置maxCount = 无限制
}shouldInject: () => true,shouldInject: () => true,shouldInject: (context) => {
const { filters } = context;
return !filters?.q && !filters?.category && !filters?.tag;
},shouldInject: (context) => {
const { filters } = context;
return !filters?.q && !filters?.category && !filters?.tag;
},shouldInject: (context) => {
const slug = context.filters?.categorySlug?.toLowerCase();
return slug?.includes("development") || slug?.includes("coding");
},shouldInject: (context) => {
const slug = context.filters?.categorySlug?.toLowerCase();
return slug?.includes("development") || slug?.includes("coding");
},shouldInject: (context) => {
const query = context.filters?.q?.toLowerCase() || "";
return ["ai", "automation", "workflow"].some(kw => query.includes(kw));
},shouldInject: (context) => {
const query = context.filters?.q?.toLowerCase() || "";
return ["ai", "automation", "workflow"].some(kw => query.includes(kw));
},shouldInject: (context) => {
return (context.itemCount ?? 0) >= 10;
},shouldInject: (context) => {
return (context.itemCount ?? 0) >= 10;
},<div className="border rounded-[var(--radius)] overflow-hidden bg-gradient-to-br from-primary/5 via-background to-primary/10 p-5"><div className="border rounded-[var(--radius)] overflow-hidden bg-gradient-to-br from-primary/5 via-background to-primary/10 p-5"><div className="flex items-center gap-2 mb-2">
<span className="text-xs font-medium text-primary">Sponsored</span>
</div><div className="flex items-center gap-2 mb-2">
<span className="text-xs font-medium text-primary">Sponsored</span>
</div><div className="relative w-full aspect-video">
<Image src="/image.jpg" alt="..." fill className="object-cover" />
</div><div className="relative w-full aspect-video">
<Image src="/image.jpg" alt="..." fill className="object-cover" />
</div><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><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>npx tsc --noEmitnpm run dev/discover/feednpx tsc --noEmitnpm run dev/discover/feedinterface 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[];
}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[];
}| 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开始计数,检查 |