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 + prompts.chat +
+ + {/* Announcement */} +
+

+ We're Thrilled to Announce +

+

+ Our newest sponsor is here! +

+
+ + {/* Sponsor Logo */} +
+ + CodeRabbit + +
+ + {/* Stats */} +
+
+
+ + 5000+ +
+ Contributors +
+ +
+
+ + 142K+ +
+ GitHub Stars +
+ +
+
+ + Growing +
+ Every Day +
+
+ + {/* CTA */} +
+ + prompts.chat + 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 }; +}