dvijaykrishnan's picture
feat: Implement an AI analysis gatekeeper modal and auto-triggering for the DiscoveryPanel component.
85f636b
Raw
History Blame Contribute Delete
9.94 kB
"use server";
import { db } from "@/lib/db";
import { youtubeVideos, detectedObjects, marketplaceMatches, adminModeration } from "@/lib/db/schema";
import { eq, and, inArray, desc } from "drizzle-orm";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { inngest } from "@/inngest/client";
import { revalidatePath } from "next/cache";
async function ensureAdmin() {
const session = await auth.api.getSession({
headers: await headers()
});
const adminEmails = process.env.ADMIN_EMAILS?.split(",") || [];
if (!session?.user?.email || !adminEmails.includes(session.user.email)) {
throw new Error("Unauthorized");
}
}
export async function getAnalysisQueue(status?: string) {
await ensureAdmin();
const statuses = status
? [status]
: ['pending_analysis', 'awaiting_approval', 'in_progress'];
const queue = await db.query.youtubeVideos.findMany({
where: inArray(youtubeVideos.scanStatus, statuses as any),
with: {
channel: true
},
orderBy: (youtubeVideos, { desc }) => [desc(youtubeVideos.createdAt)]
});
return queue;
}
/**
* Triggers AI analysis for a video that is currently in 'pending_analysis'.
*/
export async function triggerAIAnalysisAction(videoId: string) {
await ensureAdmin();
const video = await db.query.youtubeVideos.findFirst({
where: eq(youtubeVideos.id, videoId)
});
if (!video) throw new Error("Video not found");
// Send to Inngest
await inngest.send({
name: 'youtube/video.detect-objects',
data: {
videoId: video.id,
videoUrl: video.url || `https://www.youtube.com/watch?v=${video.videoId}`,
},
});
// Note: status is updated to 'in_progress' inside the Inngest function.
revalidatePath(`/admin/analysis/${videoId}`);
return { success: true };
}
/**
* Approves a single detected object (making it visible to the user).
*/
export async function approveDetectionAction(detectionId: string) {
const session = await auth.api.getSession({
headers: await headers()
});
await ensureAdmin();
const detection = await db.query.detectedObjects.findFirst({
where: eq(detectedObjects.id, detectionId),
with: {
video: {
with: {
channel: true
}
}
}
});
if (!detection) throw new Error("Detection not found");
const isDemoRecord = detection.video?.channel?.creatorId?.startsWith('demo');
await db.transaction(async (tx) => {
const updateData: any = { moderationStatus: 'APPROVED' };
// If it's a demo record, we auto-approve at the creator level too
// so it appears in the public vault immediately.
if (isDemoRecord) {
updateData.status = 'approved';
}
await tx.update(detectedObjects)
.set(updateData)
.where(eq(detectedObjects.id, detectionId));
await tx.insert(adminModeration).values({
detectionId,
adminId: session!.user.id,
action: 'approved',
originalValues: {
moderationStatus: detection.moderationStatus,
status: detection.status
},
correctedValues: updateData,
trainAiFlag: true
});
});
revalidatePath('/admin/analysis');
return { success: true };
}
/**
* Rejects a single detected object (hiding it from the user).
*/
export async function rejectDetectionAction(detectionId: string) {
const session = await auth.api.getSession({
headers: await headers()
});
await ensureAdmin();
const detection = await db.query.detectedObjects.findFirst({
where: eq(detectedObjects.id, detectionId)
});
if (!detection) throw new Error("Detection not found");
await db.transaction(async (tx) => {
await tx.update(detectedObjects)
.set({ moderationStatus: 'REJECTED' })
.where(eq(detectedObjects.id, detectionId));
await tx.insert(adminModeration).values({
detectionId,
adminId: session!.user.id,
action: 'rejected',
originalValues: { moderationStatus: detection.moderationStatus },
correctedValues: { moderationStatus: 'REJECTED' },
trainAiFlag: true
});
});
revalidatePath('/admin/analysis');
return { success: true };
}
/**
* Marks the entire video analysis as complete and removes it from the queue.
*/
export async function completeVideoReviewAction(videoId: string) {
await ensureAdmin();
await db.update(youtubeVideos)
.set({ scanStatus: 'completed' })
.where(eq(youtubeVideos.id, videoId));
// Optional: Trigger notifications here
// notifyUser(videoId, 'analysis_ready');
revalidatePath('/admin/analysis-queue');
revalidatePath(`/admin/analysis/${videoId}`);
return { success: true };
}
/**
* Manually adds a product to a video from the admin interface.
*/
export async function addManualProductAction(videoId: string, data: {
objectName: string;
category: any;
price?: number;
affiliateUrl?: string;
imageUrl?: string;
}) {
await ensureAdmin();
// Check if it's a demo record
const videoResult = await db.query.youtubeVideos.findFirst({
where: eq(youtubeVideos.id, videoId),
with: {
channel: true
}
});
const isDemoRecord = videoResult?.channel?.creatorId?.startsWith('demo');
// 1. Create Detection (AUTO-APPROVED if added by admin)
const [detection] = await db.insert(detectedObjects).values({
videoId,
objectName: data.objectName,
category: data.category,
confidenceScore: 1.0,
frameTimestamp: 0,
status: isDemoRecord ? 'approved' : 'pending_review',
moderationStatus: 'APPROVED',
thumbnailUrl: data.imageUrl || '/placeholder-product.png',
}).returning();
// 2. Create Marketplace Match if URL provided
if (data.affiliateUrl) {
// Simple productId extraction fallback
const productId = data.affiliateUrl.match(/\/dp\/([A-Z0-9]{10})/)
? data.affiliateUrl.match(/\/dp\/([A-Z0-9]{10})/)?.[1]
: `manual-${crypto.randomUUID().slice(0, 8)}`;
await db.insert(marketplaceMatches).values({
objectId: detection.id,
marketplace: 'amazon',
productId: productId || 'unknown',
productName: data.objectName,
price: data.price || 0,
availabilityStatus: 'IN_STOCK',
affiliateUrl: data.affiliateUrl,
imageUrl: data.imageUrl,
});
}
revalidatePath(`/admin/analysis/${videoId}`);
return { success: true };
}
/**
* Fetches products from other vaults for the same videoId.
*/
export async function getCrossVaultSuggestions(videoId: string, excludeCreatorId?: string) {
await ensureAdmin();
// Find other instances of this videoId
const otherVideos = await db.query.youtubeVideos.findMany({
where: eq(youtubeVideos.videoId, videoId),
});
const otherVideoIds = otherVideos.map(v => v.id);
if (otherVideoIds.length === 0) return [];
// Fetch unique products from these videos
const products = await db.query.detectedObjects.findMany({
where: and(
inArray(detectedObjects.videoId, otherVideoIds),
eq(detectedObjects.moderationStatus, 'APPROVED')
),
with: {
marketplaceMatches: true
}
});
// Deduplicate by product name/link
return products;
}
/**
* Clones a detection (and its first marketplace match) to the specified video.
*/
export async function cloneDetectionAction(videoId: string, sourceDetectionId: string) {
await ensureAdmin();
const source = await db.query.detectedObjects.findFirst({
where: eq(detectedObjects.id, sourceDetectionId),
with: {
marketplaceMatches: true
}
});
if (!source) throw new Error("Source detection not found");
// 1. Create new detection
const [newDetection] = await db.insert(detectedObjects).values({
videoId,
objectName: source.objectName,
category: source.category,
confidenceScore: 1.0,
frameTimestamp: source.frameTimestamp,
thumbnailUrl: source.thumbnailUrl,
status: 'pending_review',
moderationStatus: 'APPROVED',
}).returning();
// 2. Clone first marketplace match
const match = source.marketplaceMatches[0];
if (match) {
await db.insert(marketplaceMatches).values({
objectId: newDetection.id,
marketplace: match.marketplace,
productId: match.productId,
productName: match.productName,
price: match.price,
availabilityStatus: match.availabilityStatus,
affiliateUrl: match.affiliateUrl,
imageUrl: match.imageUrl,
});
}
revalidatePath(`/admin/analysis/${videoId}`);
return { success: true };
}
/**
* Fetches the moderation history for a specific video.
*/
export async function getAnalysisHistory(videoInternalId: string) {
await ensureAdmin();
// Get all detections for this video
const detections = await db.query.detectedObjects.findMany({
where: eq(detectedObjects.videoId, videoInternalId),
});
if (detections.length === 0) return [];
// Get audit logs for these detections
const logs = await db.query.adminModeration.findMany({
where: inArray(adminModeration.detectionId, detections.map(d => d.id)),
with: {
admin: true,
detection: true,
},
orderBy: [desc(adminModeration.createdAt)],
});
return logs;
}