feat(api): add delist reasons and quality check functionality
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<div className="container max-w-4xl py-8">
|
||||
{/* Delist Banner - shown to owner and admins when prompt is delisted */}
|
||||
{prompt.isUnlisted && delistReason && (isOwner || isAdmin) && (
|
||||
<DelistBanner
|
||||
promptId={prompt.id}
|
||||
delistReason={delistReason}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-6">
|
||||
<div className="space-y-1 min-w-0">
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AlertTriangle, Trash2, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
type DelistReason = "TOO_SHORT" | "NOT_ENGLISH" | "LOW_QUALITY" | "NOT_LLM_INSTRUCTION" | "MANUAL";
|
||||
|
||||
interface DelistBannerProps {
|
||||
promptId: string;
|
||||
delistReason: DelistReason | null;
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
export function DelistBanner({ promptId, delistReason, isOwner }: DelistBannerProps) {
|
||||
const t = useTranslations("prompts");
|
||||
const router = useRouter();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const getReasonText = (reason: DelistReason | null): string => {
|
||||
switch (reason) {
|
||||
case "TOO_SHORT":
|
||||
return t("delistReasonTooShort");
|
||||
case "NOT_ENGLISH":
|
||||
return t("delistReasonNotEnglish");
|
||||
case "LOW_QUALITY":
|
||||
return t("delistReasonLowQuality");
|
||||
case "NOT_LLM_INSTRUCTION":
|
||||
return t("delistReasonNotInstruction");
|
||||
case "MANUAL":
|
||||
return t("delistReasonManual");
|
||||
default:
|
||||
return t("delistReasonUnknown");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/prompts/${promptId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(data.message || t("deleteError"));
|
||||
}
|
||||
} catch {
|
||||
setError(t("deleteError"));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 rounded-lg border border-amber-500/30 bg-amber-500/5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold text-amber-700 dark:text-amber-400">
|
||||
{t("promptDelisted")}
|
||||
</h3>
|
||||
<p className="text-sm text-amber-600 dark:text-amber-500">
|
||||
{getReasonText(delistReason)}
|
||||
</p>
|
||||
{isOwner && delistReason && delistReason !== "MANUAL" && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{t("delistOwnerNote")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOwner && delistReason && delistReason !== "MANUAL" && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={isDeleting}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{t("deletePrompt")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("deletePromptTitle")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("deletePromptDescription")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{t("deletePrompt")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-500 mt-2">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import OpenAI from "openai";
|
||||
|
||||
// DelistReason enum values (matches Prisma schema)
|
||||
export type DelistReason = "TOO_SHORT" | "NOT_ENGLISH" | "LOW_QUALITY" | "NOT_LLM_INSTRUCTION" | "MANUAL";
|
||||
|
||||
let openai: OpenAI | null = null;
|
||||
|
||||
function getOpenAIClient(): OpenAI {
|
||||
if (!openai) {
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("OPENAI_API_KEY is not set");
|
||||
}
|
||||
openai = new OpenAI({
|
||||
apiKey,
|
||||
baseURL: process.env.OPENAI_BASE_URL || undefined,
|
||||
});
|
||||
}
|
||||
return openai;
|
||||
}
|
||||
|
||||
const GENERATIVE_MODEL = process.env.OPENAI_GENERATIVE_MODEL || "gpt-4o";
|
||||
|
||||
// Minimum character count for prompt content
|
||||
const MIN_CONTENT_LENGTH = 50;
|
||||
|
||||
// Minimum word count for prompt content
|
||||
const MIN_WORD_COUNT = 10;
|
||||
|
||||
export interface QualityCheckResult {
|
||||
shouldDelist: boolean;
|
||||
reason: DelistReason | null;
|
||||
confidence: number;
|
||||
details: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs basic length checks on prompt content
|
||||
*/
|
||||
function checkLength(content: string): QualityCheckResult | null {
|
||||
const trimmed = content.trim();
|
||||
const wordCount = trimmed.split(/\s+/).filter(w => w.length > 0).length;
|
||||
|
||||
if (trimmed.length < MIN_CONTENT_LENGTH) {
|
||||
return {
|
||||
shouldDelist: true,
|
||||
reason: "TOO_SHORT",
|
||||
confidence: 1.0,
|
||||
details: `Content is too short (${trimmed.length} chars, minimum ${MIN_CONTENT_LENGTH})`,
|
||||
};
|
||||
}
|
||||
|
||||
if (wordCount < MIN_WORD_COUNT) {
|
||||
return {
|
||||
shouldDelist: true,
|
||||
reason: "TOO_SHORT",
|
||||
confidence: 1.0,
|
||||
details: `Content has too few words (${wordCount} words, minimum ${MIN_WORD_COUNT})`,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI-powered quality check for prompt content
|
||||
* Returns quality assessment with high precision to avoid false positives
|
||||
*/
|
||||
export async function checkPromptQuality(
|
||||
title: string,
|
||||
content: string,
|
||||
description?: string | null
|
||||
): Promise<QualityCheckResult> {
|
||||
console.log(`[Quality Check] Checking: "${title}" (${content.length} chars)`);
|
||||
|
||||
// First, run basic length checks (no AI needed)
|
||||
const lengthCheck = checkLength(content);
|
||||
if (lengthCheck) {
|
||||
console.log(`[Quality Check] Length check failed:`, lengthCheck);
|
||||
return lengthCheck;
|
||||
}
|
||||
|
||||
// Check if OpenAI is available
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.log(`[Quality Check] No OpenAI API key - skipping AI check`);
|
||||
// If no AI available, pass the check (avoid false positives)
|
||||
return {
|
||||
shouldDelist: false,
|
||||
reason: null,
|
||||
confidence: 0,
|
||||
details: "AI quality check skipped - no API key configured",
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[Quality Check] Running AI check...`);
|
||||
|
||||
try {
|
||||
const client = getOpenAIClient();
|
||||
|
||||
const systemPrompt = `You are a quality assurance system for an AI prompt sharing platform.
|
||||
Your job is to evaluate if a submitted prompt meets quality standards.
|
||||
|
||||
IMPORTANT: You must be VERY conservative. Only flag prompts that are CLEARLY problematic.
|
||||
When in doubt, APPROVE the prompt. False positives are worse than false negatives.
|
||||
|
||||
Evaluate the prompt for these issues:
|
||||
|
||||
1. NOT_ENGLISH: The prompt is primarily in a language other than English.
|
||||
- Allow prompts with some non-English words if the main instructions are in English
|
||||
- Allow technical terms, code, or proper nouns in any language
|
||||
- Only flag if >70% of the meaningful text is non-English
|
||||
|
||||
2. LOW_QUALITY: The prompt is extremely low quality.
|
||||
- Random characters, keyboard mashing, or gibberish
|
||||
- Completely meaningless or incoherent text
|
||||
- NOT low quality: Simple prompts, informal language, or beginner-level prompts
|
||||
|
||||
3. NOT_LLM_INSTRUCTION: The content is NOT an instruction for an LLM.
|
||||
- This platform is for LLM prompts/instructions only
|
||||
- Flag if it's clearly NOT a prompt (e.g., a recipe, personal note, random text, advertisement)
|
||||
- APPROVE if it could reasonably be used to instruct an AI, even if unusual
|
||||
- APPROVE roleplay scenarios, persona definitions, and creative prompts
|
||||
- APPROVE technical, coding, or specialized domain prompts
|
||||
|
||||
Respond with a JSON object:
|
||||
{
|
||||
"shouldDelist": boolean,
|
||||
"reason": "NOT_ENGLISH" | "LOW_QUALITY" | "NOT_LLM_INSTRUCTION" | null,
|
||||
"confidence": number (0.0 to 1.0),
|
||||
"details": "Brief explanation"
|
||||
}
|
||||
|
||||
CRITICAL RULES:
|
||||
- Only set shouldDelist: true if confidence >= 0.85
|
||||
- When uncertain, set shouldDelist: false
|
||||
- Creative, roleplay, and unusual prompts are VALID
|
||||
- Short but clear prompts are VALID
|
||||
- Prompts in specialized domains are VALID`;
|
||||
|
||||
const userMessage = `Title: ${title}
|
||||
${description ? `Description: ${description}\n` : ""}
|
||||
Content:
|
||||
${content}`;
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model: GENERATIVE_MODEL,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userMessage }
|
||||
],
|
||||
temperature: 0.1, // Low temperature for consistent results
|
||||
max_tokens: 300,
|
||||
response_format: { type: "json_object" },
|
||||
});
|
||||
|
||||
const responseText = response.choices[0]?.message?.content || "{}";
|
||||
console.log(`[Quality Check] AI response:`, responseText);
|
||||
|
||||
try {
|
||||
const result = JSON.parse(responseText);
|
||||
|
||||
// Extra safety: only delist if confidence is high enough
|
||||
if (result.shouldDelist && result.confidence < 0.85) {
|
||||
return {
|
||||
shouldDelist: false,
|
||||
reason: null,
|
||||
confidence: result.confidence,
|
||||
details: `Below confidence threshold: ${result.details}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
shouldDelist: !!result.shouldDelist,
|
||||
reason: result.reason as DelistReason | null,
|
||||
confidence: result.confidence || 0,
|
||||
details: result.details || "Quality check completed",
|
||||
};
|
||||
} catch {
|
||||
console.error("Failed to parse AI quality check response:", responseText);
|
||||
// On parse error, don't delist (avoid false positives)
|
||||
return {
|
||||
shouldDelist: false,
|
||||
reason: null,
|
||||
confidence: 0,
|
||||
details: "Failed to parse AI response - defaulting to approve",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("AI quality check error:", error);
|
||||
// On error, don't delist (avoid false positives)
|
||||
return {
|
||||
shouldDelist: false,
|
||||
reason: null,
|
||||
confidence: 0,
|
||||
details: "AI quality check failed - defaulting to approve",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if auto-delist feature is enabled
|
||||
*/
|
||||
export async function isAutoDelistEnabled(): Promise<boolean> {
|
||||
// Auto-delist requires OpenAI API key for AI checks
|
||||
// Basic length checks will still work without it
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user