feat: complete marketing command center with pipeline, UI, and asset generation

- Dashboard with campaign management, asset gallery, and publishing queue
- 7-agent pipeline: trend scout, research, scripts, ad creative, video, copy, distribution
- Campaign form with screenshot upload, goal picker, platform selection
- Campaign detail view with Details/Pipeline/Assets/Chat tabs
- Two-set image generation: Gemini AI (NanoBanana MCP) + Canvas Design posters
- Remotion video rendering with phone.png frame and real screenshot alignment
- honeyDue branding: blue #0079FF, orange #FF9400, Inter font, warm off-white
- Asset cards with source badges (Gemini/Canvas/Remotion/Playwright)
- Markdown/JSON render endpoint for viewing pipeline outputs as HTML
- Settings page with Tavily, Gemini, Postiz, Nextdoor integration management
- Claude Chat for campaign feedback loop with streaming SSE
- Postiz publishing modal with scheduling
- Auth with NextAuth credentials + JWT sessions
- SQLite via Prisma with better-sqlite3 adapter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-23 21:05:26 -05:00
parent 6b08cfb73a
commit 66c2bbec8b
113 changed files with 12741 additions and 138 deletions
+67
View File
@@ -0,0 +1,67 @@
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { buildCampaignPrompt, launchPipeline } from "@/lib/claude";
import path from "path";
import { mkdirSync } from "fs";
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 campaign = await prisma.campaign.findUnique({ where: { id } });
if (!campaign) {
return Response.json({ error: "Not found" }, { status: 404 });
}
if (campaign.status !== "draft") {
return Response.json(
{ error: "Campaign is not in draft status" },
{ status: 400 }
);
}
const config = campaign.config ? JSON.parse(campaign.config) : {};
const pipelineRoot =
process.env.PIPELINE_ROOT || path.join(process.cwd(), "pipeline");
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, "");
const taskName = campaign.name.replace(/\s+/g, "_").toLowerCase();
const outputPath = `outputs/${taskName}_${dateStr}`;
// Create output directories
const dirs = ["ads", "scripts", "video", "copy"];
for (const dir of dirs) {
mkdirSync(path.join(pipelineRoot, outputPath, dir), { recursive: true });
}
const prompt = buildCampaignPrompt({
name: campaign.name,
platforms: JSON.parse(campaign.platforms),
goal: config.goal || "brand awareness",
keyMessage: config.keyMessage || campaign.name,
socialProof: config.socialProof,
targetAudience: config.targetAudience,
visualDirection: config.visualDirection,
competitorApps: config.competitorApps,
variations: config.variations,
useTrendReport: config.useTrendReport,
screenshots: config.screenshots,
});
await prisma.campaign.update({
where: { id },
data: { prompt, outputPath },
});
// Launch pipeline asynchronously — don't await
launchPipeline(id, prompt, pipelineRoot).catch((err) =>
console.error(`Pipeline failed for campaign ${id}:`, err)
);
return Response.json({ status: "launched", outputPath });
}
+44
View File
@@ -0,0 +1,44 @@
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
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 campaign = await prisma.campaign.findUnique({
where: { id },
include: {
agentRuns: { orderBy: { createdAt: "asc" } },
assets: { orderBy: { createdAt: "desc" } },
},
});
if (!campaign) {
return Response.json({ error: "Not found" }, { status: 404 });
}
return Response.json(campaign);
}
export async function PATCH(
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 campaign = await prisma.campaign.update({
where: { id },
data: body,
});
return Response.json(campaign);
}
+30
View File
@@ -0,0 +1,30 @@
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { scanOutputDirectory } from "@/lib/scanner";
import path from "path";
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 campaign = await prisma.campaign.findUnique({ where: { id } });
if (!campaign) {
return Response.json({ error: "Not found" }, { status: 404 });
}
if (!campaign.outputPath) {
return Response.json({ error: "No output path set" }, { status: 400 });
}
const pipelineRoot =
process.env.PIPELINE_ROOT || path.join(process.cwd(), "pipeline");
const result = await scanOutputDirectory(id, campaign.outputPath, pipelineRoot);
return Response.json(result);
}
+44
View File
@@ -0,0 +1,44 @@
import { auth } from "@/lib/auth";
import { pipelineEvents } from "@/lib/claude";
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 encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
const handler = (event: Record<string, unknown>) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(event)}\n\n`)
);
};
pipelineEvents.on(id, handler);
// Keep connection alive
const keepAlive = setInterval(() => {
controller.enqueue(encoder.encode(`: keepalive\n\n`));
}, 15000);
request.signal.addEventListener("abort", () => {
pipelineEvents.off(id, handler);
clearInterval(keepAlive);
controller.close();
});
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}