diff --git a/.gitignore b/.gitignore
index 9456ac76..59274de2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,6 @@
# testing
/coverage
-/src/app/sponsors
# next.js
/.next/
diff --git a/messages/en.json b/messages/en.json
index 2e28356e..70930db6 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -128,6 +128,16 @@
"noTagsFound": "No tags found",
"promptContributors": "Contributors",
"contributorsDescription": "Other users who helped write this prompt. Users whose change requests are approved are added automatically.",
+ "worksBestWithModels": "Works Best With",
+ "worksBestWithModelsDescription": "AI models this prompt works best with (max 3)",
+ "selectModel": "Select model...",
+ "worksBestWithMCP": "MCP Servers",
+ "worksBestWithMCPDescription": "MCP servers and tools this prompt works with",
+ "mcpCommandPlaceholder": "npx -y @mcp/server-name",
+ "mcpToolsPlaceholder": "tool1, tool2",
+ "add": "Add",
+ "advancedOptions": "Advanced Options",
+ "worksBestWith": "Works best with",
"searchContributors": "Search by username...",
"noUsersFound": "No users found",
"promptPrivate": "Private",
diff --git a/prisma/migrations/20260106071035/migration.sql b/prisma/migrations/20260106071035_daily_generation_limit/migration.sql
similarity index 100%
rename from prisma/migrations/20260106071035/migration.sql
rename to prisma/migrations/20260106071035_daily_generation_limit/migration.sql
diff --git a/prisma/migrations/20260109064746_add_works_best_with/migration.sql b/prisma/migrations/20260109064746_add_works_best_with/migration.sql
new file mode 100644
index 00000000..5066d67f
--- /dev/null
+++ b/prisma/migrations/20260109064746_add_works_best_with/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "prompts" ADD COLUMN "bestWithMCP" JSONB,
+ADD COLUMN "bestWithModels" TEXT[];
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index ed77c0f4..ccd56bfc 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -124,6 +124,8 @@ model Prompt {
outgoingConnections PromptConnection[] @relation("ConnectionSource")
incomingConnections PromptConnection[] @relation("ConnectionTarget")
collectedBy Collection[]
+ bestWithModels String[] // Model slugs this prompt works best with (max 3), e.g. ["gpt-4o", "claude-3-5-sonnet"]
+ bestWithMCP Json? // MCP configs array, e.g. [{command: "npx -y @mcp/server", tools: ["tool1"]}]
@@index([authorId])
@@index([categoryId])
diff --git a/src/app/api/prompts/[id]/route.ts b/src/app/api/prompts/[id]/route.ts
index 15e72acd..655588d2 100644
--- a/src/app/api/prompts/[id]/route.ts
+++ b/src/app/api/prompts/[id]/route.ts
@@ -21,6 +21,11 @@ const updatePromptSchema = z.object({
requiresMediaUpload: z.boolean().optional(),
requiredMediaType: z.enum(["IMAGE", "VIDEO", "DOCUMENT"]).optional().nullable(),
requiredMediaCount: z.number().int().min(1).max(10).optional().nullable(),
+ bestWithModels: z.array(z.string()).max(3).optional(),
+ bestWithMCP: z.array(z.object({
+ command: z.string(),
+ tools: z.array(z.string()).optional(),
+ })).optional(),
});
// Get single prompt
@@ -156,7 +161,7 @@ export async function PATCH(
);
}
- const { tagIds, contributorIds, categoryId, mediaUrl, title, ...data } = parsed.data;
+ const { tagIds, contributorIds, categoryId, mediaUrl, title, bestWithModels, bestWithMCP, ...data } = parsed.data;
// Regenerate slug if title changed
let newSlug: string | undefined;
@@ -171,6 +176,8 @@ export async function PATCH(
...(newSlug && { slug: newSlug }),
...(categoryId !== undefined && { categoryId: categoryId || null }),
...(mediaUrl !== undefined && { mediaUrl: mediaUrl || null }),
+ ...(bestWithModels !== undefined && { bestWithModels }),
+ ...(bestWithMCP !== undefined && { bestWithMCP }),
};
// Update prompt
diff --git a/src/app/api/prompts/route.ts b/src/app/api/prompts/route.ts
index d4459028..354c6da1 100644
--- a/src/app/api/prompts/route.ts
+++ b/src/app/api/prompts/route.ts
@@ -23,6 +23,11 @@ const promptSchema = z.object({
requiresMediaUpload: z.boolean().optional(),
requiredMediaType: z.enum(["IMAGE", "VIDEO", "DOCUMENT"]).optional(),
requiredMediaCount: z.number().int().min(1).max(10).optional(),
+ bestWithModels: z.array(z.string()).max(3).optional(),
+ bestWithMCP: z.array(z.object({
+ command: z.string(),
+ tools: z.array(z.string()).optional(),
+ })).optional(),
});
// Create prompt
@@ -46,7 +51,7 @@ export async function POST(request: Request) {
);
}
- const { title, description, content, type, structuredFormat, categoryId, tagIds, contributorIds, isPrivate, mediaUrl, requiresMediaUpload, requiredMediaType, requiredMediaCount } = parsed.data;
+ const { title, description, content, type, structuredFormat, categoryId, tagIds, contributorIds, isPrivate, mediaUrl, requiresMediaUpload, requiredMediaType, requiredMediaCount, bestWithModels, bestWithMCP } = parsed.data;
// Check if user is flagged (for auto-delisting and daily limit)
const currentUser = await db.user.findUnique({
@@ -176,6 +181,8 @@ export async function POST(request: Request) {
requiresMediaUpload: requiresMediaUpload || false,
requiredMediaType: requiresMediaUpload ? requiredMediaType : null,
requiredMediaCount: requiresMediaUpload ? requiredMediaCount : null,
+ bestWithModels: bestWithModels || [],
+ bestWithMCP: bestWithMCP || [],
authorId: session.user.id,
categoryId: categoryId || null,
// Auto-delist prompts from flagged users
diff --git a/src/app/prompts/[id]/edit/page.tsx b/src/app/prompts/[id]/edit/page.tsx
index db779a1d..f5e8a929 100644
--- a/src/app/prompts/[id]/edit/page.tsx
+++ b/src/app/prompts/[id]/edit/page.tsx
@@ -96,6 +96,8 @@ export default async function EditPromptPage({ params }: EditPromptPageProps) {
requiresMediaUpload: prompt.requiresMediaUpload,
requiredMediaType: (prompt.requiredMediaType as "IMAGE" | "VIDEO" | "DOCUMENT") || "IMAGE",
requiredMediaCount: prompt.requiredMediaCount || 1,
+ bestWithModels: (prompt as unknown as { bestWithModels?: string[] }).bestWithModels || [],
+ bestWithMCP: (prompt as unknown as { bestWithMCP?: { command: string; tools?: string[] }[] }).bestWithMCP || [],
};
// Check if AI generation is enabled
diff --git a/src/app/prompts/[id]/page.tsx b/src/app/prompts/[id]/page.tsx
index a2a469a2..06ee2756 100644
--- a/src/app/prompts/[id]/page.tsx
+++ b/src/app/prompts/[id]/page.tsx
@@ -3,7 +3,7 @@ import { notFound } from "next/navigation";
import Link from "next/link";
import { getTranslations, getLocale } from "next-intl/server";
import { formatDistanceToNow } from "@/lib/date";
-import { Clock, Edit, History, GitPullRequest, Check, X, Users, ImageIcon, Video, FileText, Shield, Trash2 } from "lucide-react";
+import { Clock, Edit, History, GitPullRequest, Check, X, Users, ImageIcon, Video, FileText, Shield, Trash2, Cpu, Terminal, Wrench } from "lucide-react";
import { AnimatedDate } from "@/components/ui/animated-date";
import { ShareDropdown } from "@/components/prompts/share-dropdown";
import { auth } from "@/lib/auth";
@@ -30,6 +30,7 @@ import { RelatedPrompts } from "@/components/prompts/related-prompts";
import { AddToCollectionButton } from "@/components/prompts/add-to-collection-button";
import { getConfig } from "@/lib/config";
import { StructuredData } from "@/components/seo/structured-data";
+import { AI_MODELS } from "@/lib/works-best-with";
interface PromptPageProps {
params: Promise<{ id: string }>;
@@ -258,6 +259,10 @@ export default async function PromptPage({ params }: PromptPageProps) {
const delistReason = (prompt as { delistReason?: string | null }).delistReason as
| "TOO_SHORT" | "NOT_ENGLISH" | "LOW_QUALITY" | "NOT_LLM_INSTRUCTION" | "MANUAL" | null;
+ // Get works best with fields (cast until Prisma types are regenerated)
+ const bestWithModels = (prompt as unknown as { bestWithModels?: string[] }).bestWithModels || [];
+ const bestWithMCP = (prompt as unknown as { bestWithMCP?: { command: string; tools?: string[] }[] }).bestWithMCP || [];
+
return (
<>
{/* Structured Data for Rich Results */}
@@ -575,6 +580,65 @@ export default async function PromptPage({ params }: PromptPageProps) {
/>
)}
+
+ {/* Works Best With */}
+ {(bestWithModels.length > 0 || bestWithMCP.length > 0) && (
+
+ {bestWithModels.length > 0 && (
+
+
+
{t("worksBestWith")}:
+
+ {bestWithModels.map((slug) => {
+ const model = AI_MODELS[slug as keyof typeof AI_MODELS];
+ return (
+
+ {model?.name || slug}
+
+ );
+ })}
+
+
+ )}
+ {bestWithMCP.length > 0 && (
+
+
+
MCP:
+
+ {bestWithMCP.flatMap((mcp, mcpIndex) =>
+ mcp.tools && mcp.tools.length > 0
+ ? mcp.tools.map((tool, toolIndex) => (
+
+
+
+
+ {tool}
+
+
+
+ {mcp.command}
+
+
+ ))
+ : [(
+
+
+
+ {mcp.command.split("/").pop()?.replace("server-", "") || mcp.command}
+
+
+
+ {mcp.command}
+
+
+ )]
+ )}
+
+
+ )}
+
+ )}
+
{/* Report & Prompt Flow */}
+ {/* Twitter-sized container (1200x675 aspect ratio) */}
+
+
+ {/* Logo */}
+
+
+ prompts.chat
+
+
+ {/* Announcement */}
+
+
+ We're Thrilled to Announce
+
+
+ Our newest sponsor is here!
+
+
+
+ {/* Sponsor Logo */}
+
+
+
+
+
+
+ {/* Stats */}
+
+
+
+
+ 5000+
+
+
Contributors
+
+
+
+
+
+ 142K+
+
+
GitHub Stars
+
+
+
+
+
+ Growing
+
+
Every Day
+
+
+
+ {/* CTA */}
+
+
+
+ prompts.chat
+ — The social platform for AI prompts
+
+
+
+
+
+ );
+}
diff --git a/src/components/prompts/prompt-form.tsx b/src/components/prompts/prompt-form.tsx
index 1db462fe..d6e9d2e1 100644
--- a/src/components/prompts/prompt-form.tsx
+++ b/src/components/prompts/prompt-form.tsx
@@ -6,7 +6,7 @@ import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
-import { Loader2, Upload, X, ArrowDown, Play, Image as ImageIcon, Video, Volume2, Paperclip, Search, Sparkles, BookOpen, ExternalLink, ChevronDown } from "lucide-react";
+import { Loader2, Upload, X, ArrowDown, Play, Image as ImageIcon, Video, Volume2, Paperclip, Search, Sparkles, BookOpen, ExternalLink, ChevronDown, Settings2 } from "lucide-react";
import Link from "next/link";
import { VariableToolbar } from "./variable-toolbar";
import { VariableWarning } from "./variable-warning";
@@ -47,6 +47,7 @@ import { toast } from "sonner";
import { prettifyJson } from "@/lib/format";
import { analyticsPrompt } from "@/lib/analytics";
import { getPromptUrl } from "@/lib/urls";
+import { AI_MODELS, getModelsByProvider, type PromptMCPConfig } from "@/lib/works-best-with";
interface MediaFieldProps {
form: ReturnType>;
@@ -345,6 +346,11 @@ const createPromptSchema = (t: (key: string) => string) => z.object({
requiresMediaUpload: z.boolean(),
requiredMediaType: z.enum(["IMAGE", "VIDEO", "DOCUMENT"]).optional(),
requiredMediaCount: z.coerce.number().int().min(1).max(10).optional(),
+ bestWithModels: z.array(z.string()).max(3).optional(),
+ bestWithMCP: z.array(z.object({
+ command: z.string(),
+ tools: z.array(z.string()).optional(),
+ })).optional(),
});
type PromptFormValues = z.infer>;
@@ -426,9 +432,19 @@ export function PromptForm({ categories, tags, initialData, initialContributors
requiresMediaUpload: initialData?.requiresMediaUpload || false,
requiredMediaType: initialData?.requiredMediaType || "IMAGE",
requiredMediaCount: initialData?.requiredMediaCount || 1,
+ bestWithModels: initialData?.bestWithModels || [],
+ bestWithMCP: initialData?.bestWithMCP || [],
},
});
+ // State for MCP input and advanced section
+ const [newMcpCommand, setNewMcpCommand] = useState("");
+ const [newMcpTools, setNewMcpTools] = useState("");
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ const bestWithMCP = form.watch("bestWithMCP") || [];
+ const bestWithModels = form.watch("bestWithModels") || [];
+ const modelsByProvider = getModelsByProvider();
+
const selectedTags = form.watch("tagIds");
const promptType = form.watch("type");
const structuredFormat = form.watch("structuredFormat");
@@ -889,6 +905,136 @@ export function PromptForm({ categories, tags, initialData, initialContributors
onRemove={(userId) => setContributors((prev) => prev.filter((u) => u.id !== userId))}
/>
+
+ {/* Advanced Section */}
+
+
+ {showAdvanced && (
+
+ {/* Works Best With Models */}
+
+
+
{t("worksBestWithModelsDescription")}
+ {bestWithModels.length > 0 && (
+
+ {bestWithModels.map((slug) => {
+ const model = AI_MODELS[slug as keyof typeof AI_MODELS];
+ return (
+
+ {model?.name || slug}
+
+
+ );
+ })}
+
+ )}
+ {bestWithModels.length < 3 && (
+
+ )}
+
+
+ {/* Works Best With MCP */}
+
+
+
{t("worksBestWithMCPDescription")}
+ {bestWithMCP.length > 0 && (
+
+ {bestWithMCP.map((mcp, index) => (
+
+ {mcp.command}
+ {mcp.tools && mcp.tools.length > 0 && (
+ ({mcp.tools.join(", ")})
+ )}
+
+
+ ))}
+
+ )}
+
+ setNewMcpCommand(e.target.value)}
+ className="flex-1 text-xs h-8"
+ />
+ setNewMcpTools(e.target.value)}
+ className="w-28 text-xs h-8"
+ />
+
+
+
+
+ )}
+
{/* ===== INPUT SECTION ===== */}
diff --git a/src/lib/works-best-with.ts b/src/lib/works-best-with.ts
new file mode 100644
index 00000000..86f931f8
--- /dev/null
+++ b/src/lib/works-best-with.ts
@@ -0,0 +1,136 @@
+// Types and constants for "Works Best With" feature
+
+// MCP Server configuration for a prompt
+export interface PromptMCPConfig {
+ command: string;
+ tools?: string[];
+}
+
+// Well-known AI models (slug -> display info)
+export const AI_MODELS = {
+ // OpenAI
+ "gpt-4o": { name: "GPT-4o", provider: "OpenAI" },
+ "gpt-4o-mini": { name: "GPT-4o Mini", provider: "OpenAI" },
+ "gpt-4-turbo": { name: "GPT-4 Turbo", provider: "OpenAI" },
+ "gpt-4": { name: "GPT-4", provider: "OpenAI" },
+ "o1": { name: "o1", provider: "OpenAI" },
+ "o1-mini": { name: "o1 Mini", provider: "OpenAI" },
+ "o1-pro": { name: "o1 Pro", provider: "OpenAI" },
+ "o3": { name: "o3", provider: "OpenAI" },
+ "o3-mini": { name: "o3 Mini", provider: "OpenAI" },
+ "gpt-4-5": { name: "GPT-4.5", provider: "OpenAI" },
+
+ // Anthropic
+ "claude-3-5-sonnet": { name: "Claude 3.5 Sonnet", provider: "Anthropic" },
+ "claude-3-5-haiku": { name: "Claude 3.5 Haiku", provider: "Anthropic" },
+ "claude-3-opus": { name: "Claude 3 Opus", provider: "Anthropic" },
+ "claude-4-sonnet": { name: "Claude 4 Sonnet", provider: "Anthropic" },
+ "claude-4-opus": { name: "Claude 4 Opus", provider: "Anthropic" },
+
+ // Google
+ "gemini-2-0-flash": { name: "Gemini 2.0 Flash", provider: "Google" },
+ "gemini-2-5-pro": { name: "Gemini 2.5 Pro", provider: "Google" },
+ "gemini-2-5-flash": { name: "Gemini 2.5 Flash", provider: "Google" },
+ "gemma-3": { name: "Gemma 3", provider: "Google" },
+
+ // Meta
+ "llama-4": { name: "Llama 4", provider: "Meta" },
+ "llama-4-scout": { name: "Llama 4 Scout", provider: "Meta" },
+ "llama-4-maverick": { name: "Llama 4 Maverick", provider: "Meta" },
+ "llama-3-3": { name: "Llama 3.3", provider: "Meta" },
+
+ // xAI
+ "grok-3": { name: "Grok 3", provider: "xAI" },
+ "grok-2": { name: "Grok 2", provider: "xAI" },
+
+ // DeepSeek
+ "deepseek-r1": { name: "DeepSeek R1", provider: "DeepSeek" },
+ "deepseek-v3": { name: "DeepSeek V3", provider: "DeepSeek" },
+
+ // Mistral
+ "mistral-large": { name: "Mistral Large", provider: "Mistral" },
+ "mixtral-8x22b": { name: "Mixtral 8x22B", provider: "Mistral" },
+ "codestral": { name: "Codestral", provider: "Mistral" },
+
+ // Alibaba
+ "qwen-2-5": { name: "Qwen 2.5", provider: "Alibaba" },
+ "qwen-3": { name: "Qwen 3", provider: "Alibaba" },
+
+ // Microsoft
+ "phi-4": { name: "Phi-4", provider: "Microsoft" },
+
+ // Amazon
+ "nova-pro": { name: "Nova Pro", provider: "Amazon" },
+ "nova-lite": { name: "Nova Lite", provider: "Amazon" },
+} as const;
+
+export type AIModelSlug = keyof typeof AI_MODELS;
+
+export function getModelInfo(slug: string): { name: string; provider: string } | null {
+ return AI_MODELS[slug as AIModelSlug] ?? null;
+}
+
+export function isValidModelSlug(slug: string): slug is AIModelSlug {
+ return slug in AI_MODELS;
+}
+
+// Get models grouped by provider
+export function getModelsByProvider(): Record {
+ const grouped: Record = {};
+
+ for (const [slug, info] of Object.entries(AI_MODELS)) {
+ if (!grouped[info.provider]) {
+ grouped[info.provider] = [];
+ }
+ grouped[info.provider].push({ slug, name: info.name });
+ }
+
+ return grouped;
+}
+
+// Validate bestWithModels (max 3, valid slugs)
+export function validateBestWithModels(models: string[]): { valid: boolean; errors: string[] } {
+ const errors: string[] = [];
+
+ if (models.length > 3) {
+ errors.push("Maximum 3 models allowed");
+ }
+
+ for (const slug of models) {
+ if (!isValidModelSlug(slug)) {
+ errors.push(`Unknown model: ${slug}`);
+ }
+ }
+
+ return { valid: errors.length === 0, errors };
+}
+
+// Validate bestWithMCP
+export function validateBestWithMCP(mcp: unknown): { valid: boolean; errors: string[] } {
+ const errors: string[] = [];
+
+ if (mcp === null || mcp === undefined) {
+ return { valid: true, errors: [] };
+ }
+
+ if (typeof mcp !== "object") {
+ errors.push("MCP config must be an object");
+ return { valid: false, errors };
+ }
+
+ const config = mcp as Record;
+
+ if (!("command" in config) || typeof config.command !== "string") {
+ errors.push("MCP config.command is required and must be a string");
+ }
+
+ if ("tools" in config && config.tools !== undefined) {
+ if (!Array.isArray(config.tools)) {
+ errors.push("MCP config.tools must be an array");
+ } else if (!config.tools.every((t: unknown) => typeof t === "string")) {
+ errors.push("MCP config.tools must be an array of strings");
+ }
+ }
+
+ return { valid: errors.length === 0, errors };
+}