diff --git a/messages/en.json b/messages/en.json index ad1179fb..2b673b5f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -140,6 +140,18 @@ "unlist": "Unlist", "relist": "Relist", "adminArea": "Admin Area", + "promptDelisted": "This prompt has been delisted", + "delistReasonTooShort": "This prompt was automatically delisted because the content is too short.", + "delistReasonNotEnglish": "This prompt was automatically delisted because it's not primarily in English.", + "delistReasonLowQuality": "This prompt was automatically delisted due to quality issues.", + "delistReasonNotInstruction": "This prompt was automatically delisted because it doesn't appear to be an LLM instruction.", + "delistReasonManual": "This prompt was manually delisted by an administrator.", + "delistReasonUnknown": "This prompt has been delisted.", + "delistOwnerNote": "Since this prompt was delisted for quality issues, you can delete it if you wish.", + "deletePrompt": "Delete Prompt", + "deletePromptTitle": "Delete this prompt?", + "deletePromptDescription": "This action cannot be undone. The prompt will be permanently removed.", + "deleteError": "Failed to delete prompt", "types": { "text": "Text", "image": "Image", diff --git a/prisma/migrations/20251217173000_add_delist_reason/migration.sql b/prisma/migrations/20251217173000_add_delist_reason/migration.sql new file mode 100644 index 00000000..bea6d869 --- /dev/null +++ b/prisma/migrations/20251217173000_add_delist_reason/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "DelistReason" AS ENUM ('TOO_SHORT', 'NOT_ENGLISH', 'LOW_QUALITY', 'NOT_LLM_INSTRUCTION', 'MANUAL'); + +-- AlterTable +ALTER TABLE "prompts" ADD COLUMN "delistReason" "DelistReason"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 68a59a61..6d00b662 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -98,6 +98,7 @@ model Prompt { isFeatured Boolean @default(false) isUnlisted Boolean @default(false) unlistedAt DateTime? + delistReason DelistReason? deletedAt DateTime? changeRequests ChangeRequest[] pinnedBy PinnedPrompt[] @@ -314,3 +315,11 @@ enum WebhookEvent { PROMPT_UPDATED PROMPT_DELETED } + +enum DelistReason { + TOO_SHORT + NOT_ENGLISH + LOW_QUALITY + NOT_LLM_INSTRUCTION + MANUAL +} diff --git a/src/app/api/prompts/[id]/route.ts b/src/app/api/prompts/[id]/route.ts index 44d67827..47177b10 100644 --- a/src/app/api/prompts/[id]/route.ts +++ b/src/app/api/prompts/[id]/route.ts @@ -4,6 +4,7 @@ import { auth } from "@/lib/auth"; import { db } from "@/lib/db"; import { generatePromptEmbedding } from "@/lib/ai/embeddings"; import { generatePromptSlug } from "@/lib/slug"; +import { checkPromptQuality } from "@/lib/ai/quality-check"; const updatePromptSchema = z.object({ title: z.string().min(1).max(200).optional(), @@ -203,6 +204,33 @@ export async function PATCH( ); } + // Run quality check for auto-delist on content changes (non-blocking) + // Only for public prompts that aren't already delisted + if (contentChanged && !prompt.isPrivate && !prompt.isUnlisted) { + const checkTitle = title || prompt.title; + const checkContent = data.content || prompt.content; + const checkDescription = data.description !== undefined ? data.description : prompt.description; + + console.log(`[Quality Check] Starting check for updated prompt ${id}`); + checkPromptQuality(checkTitle, checkContent, checkDescription).then(async (result) => { + console.log(`[Quality Check] Result for prompt ${id}:`, JSON.stringify(result)); + if (result.shouldDelist && result.reason) { + console.log(`[Quality Check] Auto-delisting prompt ${id}: ${result.reason} - ${result.details}`); + await db.prompt.update({ + where: { id }, + data: { + isUnlisted: true, + unlistedAt: new Date(), + delistReason: result.reason, + }, + }); + console.log(`[Quality Check] Prompt ${id} delisted successfully`); + } + }).catch((err) => { + console.error("[Quality Check] Failed to run quality check for prompt:", id, err); + }); + } + return NextResponse.json(prompt); } catch (error) { console.error("Update prompt error:", error); @@ -213,7 +241,9 @@ export async function PATCH( } } -// Soft delete prompt (admin only - CC0 prompts cannot be deleted by users) +// Soft delete prompt +// - Admins can delete any prompt +// - Owners can delete their own delisted prompts (auto-delisted for quality issues) export async function DELETE( request: Request, { params }: { params: Promise<{ id: string }> } @@ -229,18 +259,16 @@ export async function DELETE( ); } - // Only admins can soft-delete prompts (CC0 content is public domain) - if (session.user.role !== "ADMIN") { - return NextResponse.json( - { error: "forbidden", message: "Prompts are released under CC0 and cannot be deleted. Contact an admin if there is an issue." }, - { status: 403 } - ); - } - - // Check if prompt exists + // Check if prompt exists and get ownership/delist status const existing = await db.prompt.findUnique({ where: { id }, - select: { id: true, deletedAt: true }, + select: { + id: true, + deletedAt: true, + authorId: true, + isUnlisted: true, + delistReason: true, + }, }); if (!existing) { @@ -257,13 +285,36 @@ export async function DELETE( ); } + const isAdmin = session.user.role === "ADMIN"; + const isOwner = existing.authorId === session.user.id; + const isDelisted = existing.isUnlisted && existing.delistReason; + + // Owners can only delete their own delisted prompts (quality issues) + // Admins can delete any prompt + if (!isAdmin && !(isOwner && isDelisted)) { + return NextResponse.json( + { + error: "forbidden", + message: isOwner + ? "You can only delete prompts that have been delisted for quality issues. Contact an admin for other deletions." + : "Prompts are released under CC0 and cannot be deleted. Contact an admin if there is an issue." + }, + { status: 403 } + ); + } + // Soft delete by setting deletedAt timestamp await db.prompt.update({ where: { id }, data: { deletedAt: new Date() }, }); - return NextResponse.json({ success: true, message: "Prompt soft deleted" }); + return NextResponse.json({ + success: true, + message: isOwner && isDelisted + ? "Delisted prompt deleted successfully" + : "Prompt soft deleted" + }); } catch (error) { console.error("Delete prompt error:", error); return NextResponse.json( diff --git a/src/app/api/prompts/route.ts b/src/app/api/prompts/route.ts index 5d60b876..d3d93bb6 100644 --- a/src/app/api/prompts/route.ts +++ b/src/app/api/prompts/route.ts @@ -5,6 +5,7 @@ import { db } from "@/lib/db"; import { triggerWebhooks } from "@/lib/webhook"; import { generatePromptEmbedding } from "@/lib/ai/embeddings"; import { generatePromptSlug } from "@/lib/slug"; +import { checkPromptQuality } from "@/lib/ai/quality-check"; const promptSchema = z.object({ title: z.string().min(1).max(200), @@ -128,6 +129,31 @@ export async function POST(request: Request) { ); } + // Run quality check for auto-delist (non-blocking for public prompts) + // This runs in the background and will delist the prompt if quality issues are found + if (!isPrivate) { + console.log(`[Quality Check] Starting check for prompt ${prompt.id}`); + checkPromptQuality(title, content, description).then(async (result) => { + console.log(`[Quality Check] Result for prompt ${prompt.id}:`, JSON.stringify(result)); + if (result.shouldDelist && result.reason) { + console.log(`[Quality Check] Auto-delisting prompt ${prompt.id}: ${result.reason} - ${result.details}`); + await db.prompt.update({ + where: { id: prompt.id }, + data: { + isUnlisted: true, + unlistedAt: new Date(), + delistReason: result.reason, + }, + }); + console.log(`[Quality Check] Prompt ${prompt.id} delisted successfully`); + } + }).catch((err) => { + console.error("[Quality Check] Failed to run quality check for prompt:", prompt.id, err); + }); + } else { + console.log(`[Quality Check] Skipped - prompt ${prompt.id} is private`); + } + return NextResponse.json(prompt); } catch (error) { console.error("Create prompt error:", error); diff --git a/src/app/prompts/[id]/page.tsx b/src/app/prompts/[id]/page.tsx index f3e7bebd..2e2f275e 100644 --- a/src/app/prompts/[id]/page.tsx +++ b/src/app/prompts/[id]/page.tsx @@ -22,6 +22,7 @@ import { FeaturePromptButton } from "@/components/prompts/feature-prompt-button" import { UnlistPromptButton } from "@/components/prompts/unlist-prompt-button"; import { MediaPreview } from "@/components/prompts/media-preview"; import { ReportPromptDialog } from "@/components/prompts/report-prompt-dialog"; +import { DelistBanner } from "@/components/prompts/delist-banner"; interface PromptPageProps { params: Promise<{ id: string }>; @@ -175,8 +176,21 @@ export default async function PromptPage({ params }: PromptPageProps) { REJECTED: X, }; + // Get delist reason (cast to expected type after Prisma migration) + const delistReason = (prompt as { delistReason?: string | null }).delistReason as + | "TOO_SHORT" | "NOT_ENGLISH" | "LOW_QUALITY" | "NOT_LLM_INSTRUCTION" | "MANUAL" | null; + return (
+ {getReasonText(delistReason)} +
+ {isOwner && delistReason && delistReason !== "MANUAL" && ( ++ {t("delistOwnerNote")} +
+ )} +{error}
+ )} +