feat: add asset preferences, video research, and Remotion ad assets
- Add thumbs-down feedback modal and preference API endpoint - Add AI UGC video platforms research doc - Add ReflectAd Remotion composition with public flow assets - Add gemini-ad-designer and poster-ad-designer pipeline skills - Add research_reflect_v1.1 pipeline script Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface LikedEntry {
|
||||
assetId: string;
|
||||
filePath: string;
|
||||
fileName: string;
|
||||
dimensions: string | null;
|
||||
platform: string | null;
|
||||
style: string | null; // "with-people" | "without-people" | null
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface DislikedEntry {
|
||||
assetId: string;
|
||||
reason: string;
|
||||
fileName: string;
|
||||
dimensions: string | null;
|
||||
platform: string | null;
|
||||
style: string | null;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface StylePreferences {
|
||||
liked: LikedEntry[];
|
||||
disliked: DislikedEntry[];
|
||||
}
|
||||
|
||||
function parsePreferences(raw: string | null): StylePreferences {
|
||||
if (!raw) return { liked: [], disliked: [] };
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
liked: Array.isArray(parsed.liked) ? parsed.liked : [],
|
||||
disliked: Array.isArray(parsed.disliked) ? parsed.disliked : [],
|
||||
};
|
||||
} catch {
|
||||
return { liked: [], disliked: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session) return new Response("Unauthorized", { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const asset = await prisma.asset.findUnique({
|
||||
where: { id },
|
||||
include: { campaign: { include: { app: true } } },
|
||||
});
|
||||
|
||||
if (!asset) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
const app = asset.campaign?.app;
|
||||
if (!app) return Response.json({ vote: null });
|
||||
|
||||
const prefs = parsePreferences(app.stylePreferences);
|
||||
const isLiked = prefs.liked.some((e) => e.assetId === id);
|
||||
const isDisliked = prefs.disliked.some((e) => e.assetId === id);
|
||||
|
||||
return Response.json({ vote: isLiked ? "up" : isDisliked ? "down" : null });
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session) return new Response("Unauthorized", { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { vote, reason } = body as { vote: "up" | "down"; reason?: string };
|
||||
|
||||
if (vote !== "up" && vote !== "down") {
|
||||
return Response.json({ error: "vote must be 'up' or 'down'" }, { status: 400 });
|
||||
}
|
||||
|
||||
const asset = await prisma.asset.findUnique({
|
||||
where: { id },
|
||||
include: { campaign: { include: { app: true } } },
|
||||
});
|
||||
|
||||
if (!asset) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
const app = asset.campaign?.app;
|
||||
if (!app) {
|
||||
return Response.json({ error: "Asset has no associated app" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Extract style from asset metadata (set by ad_manifest.json during generation)
|
||||
let assetStyle: string | null = null;
|
||||
if (asset.metadata) {
|
||||
try {
|
||||
const meta = JSON.parse(asset.metadata);
|
||||
if (meta.style === "with-people" || meta.style === "without-people") {
|
||||
assetStyle = meta.style;
|
||||
}
|
||||
} catch { /* metadata isn't JSON or has no style */ }
|
||||
}
|
||||
|
||||
const prefs = parsePreferences(app.stylePreferences);
|
||||
|
||||
// Remove from both lists first (clean slate for this asset)
|
||||
prefs.liked = prefs.liked.filter((e) => e.assetId !== id);
|
||||
prefs.disliked = prefs.disliked.filter((e) => e.assetId !== id);
|
||||
|
||||
// Check if this was already the active vote (toggle off)
|
||||
const wasLiked = parsePreferences(app.stylePreferences).liked.some((e) => e.assetId === id);
|
||||
const wasDisliked = parsePreferences(app.stylePreferences).disliked.some((e) => e.assetId === id);
|
||||
const isToggleOff = (vote === "up" && wasLiked) || (vote === "down" && wasDisliked);
|
||||
|
||||
if (!isToggleOff) {
|
||||
if (vote === "up") {
|
||||
prefs.liked.push({
|
||||
assetId: id,
|
||||
filePath: asset.filePath,
|
||||
fileName: asset.fileName,
|
||||
dimensions: asset.dimensions || null,
|
||||
platform: asset.platform || null,
|
||||
style: assetStyle,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
// Keep max ~10 liked
|
||||
if (prefs.liked.length > 10) {
|
||||
prefs.liked = prefs.liked.slice(-10);
|
||||
}
|
||||
} else {
|
||||
prefs.disliked.push({
|
||||
assetId: id,
|
||||
reason: reason || "Not preferred",
|
||||
fileName: asset.fileName,
|
||||
dimensions: asset.dimensions || null,
|
||||
platform: asset.platform || null,
|
||||
style: assetStyle,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
// Keep max ~20 disliked
|
||||
if (prefs.disliked.length > 20) {
|
||||
prefs.disliked = prefs.disliked.slice(-20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.app.update({
|
||||
where: { id: app.id },
|
||||
data: { stylePreferences: JSON.stringify(prefs) },
|
||||
});
|
||||
|
||||
const newVote = isToggleOff ? null : vote;
|
||||
return Response.json({ vote: newVote });
|
||||
}
|
||||
Reference in New Issue
Block a user