mirror of
https://github.com/f/awesome-chatgpt-prompts.git
synced 2026-03-03 02:37:02 +00:00
feat(prompts): add works best with feature for prompts
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,7 +12,6 @@
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/src/app/sponsors
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "prompts" ADD COLUMN "bestWithMCP" JSONB,
|
||||
ADD COLUMN "bestWithModels" TEXT[];
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Works Best With */}
|
||||
{(bestWithModels.length > 0 || bestWithMCP.length > 0) && (
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
|
||||
{bestWithModels.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">{t("worksBestWith")}:</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{bestWithModels.map((slug) => {
|
||||
const model = AI_MODELS[slug as keyof typeof AI_MODELS];
|
||||
return (
|
||||
<Badge key={slug} variant="secondary" className="text-xs">
|
||||
{model?.name || slug}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{bestWithMCP.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">MCP:</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{bestWithMCP.flatMap((mcp, mcpIndex) =>
|
||||
mcp.tools && mcp.tools.length > 0
|
||||
? mcp.tools.map((tool, toolIndex) => (
|
||||
<Tooltip key={`${mcpIndex}-${toolIndex}`}>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="outline" className="text-xs font-mono cursor-help gap-1">
|
||||
<Wrench className="h-3 w-3" />
|
||||
{tool}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-xs">
|
||||
<code className="text-xs break-all">{mcp.command}</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))
|
||||
: [(
|
||||
<Tooltip key={mcpIndex}>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="outline" className="text-xs font-mono cursor-help">
|
||||
{mcp.command.split("/").pop()?.replace("server-", "") || mcp.command}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-xs">
|
||||
<code className="text-xs break-all">{mcp.command}</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Report & Prompt Flow */}
|
||||
<PromptFlowSection
|
||||
promptId={prompt.id}
|
||||
|
||||
105
src/app/sponsors/coderabbit/page.tsx
Normal file
105
src/app/sponsors/coderabbit/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Users, Star, TrendingUp } from "lucide-react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "CodeRabbit joins prompts.chat as a sponsor",
|
||||
description: "We're thrilled to announce CodeRabbit as our newest sponsor!",
|
||||
};
|
||||
|
||||
export default function CodeRabbitAnnouncementPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center px-4 py-16 bg-white">
|
||||
{/* Twitter-sized container (1200x675 aspect ratio) */}
|
||||
<div className="w-full max-w-[1200px] aspect-[1200/675] flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-lg bg-white p-8">
|
||||
<div className="text-center space-y-8">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
alt="prompts.chat"
|
||||
width={32}
|
||||
height={32}
|
||||
className="h-8 w-auto"
|
||||
/>
|
||||
<span className="text-xl font-medium text-gray-600">prompts.chat</span>
|
||||
</div>
|
||||
|
||||
{/* Announcement */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl md:text-4xl font-bold tracking-tight text-gray-900">
|
||||
We're Thrilled to Announce
|
||||
</h1>
|
||||
<p className="text-gray-500 text-lg">
|
||||
Our newest sponsor is here!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sponsor Logo */}
|
||||
<div className="flex justify-center">
|
||||
<Link
|
||||
href="https://coderabbit.link/fatih"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center px-12 py-8 border border-gray-200 rounded-2xl hover:border-gray-300 transition-colors bg-white shadow-lg"
|
||||
>
|
||||
<Image
|
||||
src="/sponsors/coderabbit.svg"
|
||||
alt="CodeRabbit"
|
||||
width={200}
|
||||
height={50}
|
||||
className="h-12 w-auto"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center justify-center gap-8 md:gap-12 pt-4">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-2 text-gray-400">
|
||||
<Users className="h-5 w-5" />
|
||||
<span className="text-2xl font-bold text-gray-900">5000+</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">Contributors</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-2 text-amber-500">
|
||||
<Star className="h-5 w-5 fill-current" />
|
||||
<span className="text-2xl font-bold text-gray-900">142K+</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">GitHub Stars</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-2 text-emerald-500">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
<span className="text-2xl font-bold text-gray-900">Growing</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">Every Day</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="pt-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 border border-gray-200 rounded-full text-gray-500 hover:text-gray-900 hover:border-gray-400 transition-colors"
|
||||
>
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
alt="prompts.chat"
|
||||
width={16}
|
||||
height={16}
|
||||
className="h-4 w-auto"
|
||||
/>
|
||||
<span className="font-medium">prompts.chat</span>
|
||||
<span className="hidden sm:inline">— The social platform for AI prompts</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<typeof useForm<PromptFormValues>>;
|
||||
@@ -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<ReturnType<typeof createPromptSchema>>;
|
||||
@@ -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))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Advanced Section */}
|
||||
<div className="border rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
className="flex items-center justify-between w-full p-3 text-sm font-medium text-left hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Settings2 className="h-4 w-4 text-muted-foreground" />
|
||||
{t("advancedOptions")}
|
||||
{(bestWithModels.length > 0 || bestWithMCP.length > 0) && (
|
||||
<Badge variant="secondary" className="text-[10px] h-5">{bestWithModels.length + bestWithMCP.length}</Badge>
|
||||
)}
|
||||
</span>
|
||||
<ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ${showAdvanced ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{showAdvanced && (
|
||||
<div className="p-3 space-y-4 border-t">
|
||||
{/* Works Best With Models */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium block">{t("worksBestWithModels")}</label>
|
||||
<p className="text-xs text-muted-foreground">{t("worksBestWithModelsDescription")}</p>
|
||||
{bestWithModels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{bestWithModels.map((slug) => {
|
||||
const model = AI_MODELS[slug as keyof typeof AI_MODELS];
|
||||
return (
|
||||
<Badge key={slug} variant="secondary" className="pr-1 flex items-center gap-1">
|
||||
{model?.name || slug}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => form.setValue("bestWithModels", bestWithModels.filter((s) => s !== slug))}
|
||||
className="ml-1 rounded-full hover:bg-muted p-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{bestWithModels.length < 3 && (
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(slug) => {
|
||||
if (slug && !bestWithModels.includes(slug)) {
|
||||
form.setValue("bestWithModels", [...bestWithModels, slug]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-64 h-8 text-xs">
|
||||
<SelectValue placeholder={t("selectModel")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(modelsByProvider).map(([provider, models]) => (
|
||||
<div key={provider}>
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">{provider}</div>
|
||||
{models
|
||||
.filter((m) => !bestWithModels.includes(m.slug))
|
||||
.map((model) => (
|
||||
<SelectItem key={model.slug} value={model.slug}>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Works Best With MCP */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium block">{t("worksBestWithMCP")}</label>
|
||||
<p className="text-xs text-muted-foreground">{t("worksBestWithMCPDescription")}</p>
|
||||
{bestWithMCP.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
{bestWithMCP.map((mcp, index) => (
|
||||
<div key={index} className="flex items-center gap-2 p-2 rounded border bg-muted/30 text-xs">
|
||||
<code className="flex-1 break-all">{mcp.command}</code>
|
||||
{mcp.tools && mcp.tools.length > 0 && (
|
||||
<span className="text-muted-foreground">({mcp.tools.join(", ")})</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => form.setValue("bestWithMCP", bestWithMCP.filter((_, i) => i !== index))}
|
||||
className="p-1 hover:bg-muted rounded"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t("mcpCommandPlaceholder")}
|
||||
value={newMcpCommand}
|
||||
onChange={(e) => setNewMcpCommand(e.target.value)}
|
||||
className="flex-1 text-xs h-8"
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("mcpToolsPlaceholder")}
|
||||
value={newMcpTools}
|
||||
onChange={(e) => setNewMcpTools(e.target.value)}
|
||||
className="w-28 text-xs h-8"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-xs"
|
||||
disabled={!newMcpCommand.trim()}
|
||||
onClick={() => {
|
||||
if (newMcpCommand.trim()) {
|
||||
const tools = newMcpTools.trim() ? newMcpTools.split(",").map(t => t.trim()).filter(Boolean) : undefined;
|
||||
form.setValue("bestWithMCP", [...bestWithMCP, { command: newMcpCommand.trim(), tools }]);
|
||||
setNewMcpCommand("");
|
||||
setNewMcpTools("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== INPUT SECTION ===== */}
|
||||
|
||||
136
src/lib/works-best-with.ts
Normal file
136
src/lib/works-best-with.ts
Normal file
@@ -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<string, { slug: string; name: string }[]> {
|
||||
const grouped: Record<string, { slug: string; name: string }[]> = {};
|
||||
|
||||
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<string, unknown>;
|
||||
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user