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:
@@ -0,0 +1,92 @@
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../lib/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
password String
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Campaign {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
status String @default("draft") // draft, running, review, approved, published
|
||||
prompt String?
|
||||
platforms String // JSON array: ["instagram","tiktok","nextdoor"]
|
||||
config String? // JSON: full campaign config from form
|
||||
outputPath String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
agentRuns AgentRun[]
|
||||
assets Asset[]
|
||||
claudeSessions ClaudeSession[]
|
||||
}
|
||||
|
||||
model AgentRun {
|
||||
id String @id @default(cuid())
|
||||
campaignId String
|
||||
campaign Campaign @relation(fields: [campaignId], references: [id])
|
||||
agentName String
|
||||
status String @default("pending") // pending, running, completed, failed
|
||||
startedAt DateTime?
|
||||
completedAt DateTime?
|
||||
durationMs Int?
|
||||
outputSummary String?
|
||||
outputPath String?
|
||||
tokenUsage Int?
|
||||
error String?
|
||||
assets Asset[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Asset {
|
||||
id String @id @default(cuid())
|
||||
campaignId String
|
||||
campaign Campaign @relation(fields: [campaignId], references: [id])
|
||||
agentRunId String?
|
||||
agentRun AgentRun? @relation(fields: [agentRunId], references: [id])
|
||||
type String // image, video, copy, research, script
|
||||
platform String? // instagram, tiktok, nextdoor, all
|
||||
format String? // png, mp4, json, txt, html, md
|
||||
filePath String
|
||||
fileName String
|
||||
dimensions String? // 1080x1080, 1080x1920, etc.
|
||||
metadata String? // JSON: caption, hashtags, hook text, scene plan
|
||||
status String @default("draft") // draft, approved, rejected, published
|
||||
publishedTo String? // JSON array of platforms published to
|
||||
postizPostId String?
|
||||
postizMediaId String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model ClaudeSession {
|
||||
id String @id @default(cuid())
|
||||
campaignId String
|
||||
campaign Campaign @relation(fields: [campaignId], references: [id])
|
||||
sessionId String?
|
||||
messages String? // JSON array of conversation history
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TrendReport {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
filePath String
|
||||
summary String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Setting {
|
||||
key String @id
|
||||
value String
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { PrismaClient } from "../lib/generated/prisma/client";
|
||||
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
|
||||
|
||||
const adapter = new PrismaBetterSqlite3({
|
||||
url: process.env.DATABASE_URL || "file:./data/marketing.db",
|
||||
});
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
async function main() {
|
||||
// Campaign 1: Completed with assets
|
||||
const campaign1 = await prisma.campaign.create({
|
||||
data: {
|
||||
name: "Spring Launch Campaign",
|
||||
status: "review",
|
||||
platforms: JSON.stringify(["instagram", "tiktok", "nextdoor"]),
|
||||
config: JSON.stringify({
|
||||
goal: "app_downloads",
|
||||
keyMessage: "Your morning routine just got an upgrade. The smartest productivity app of 2026.",
|
||||
socialProof: "50K+ downloads, 4.8 star rating, Featured in App Store",
|
||||
variations: 5,
|
||||
useTrendReport: true,
|
||||
}),
|
||||
outputPath: "outputs/spring_launch_campaign_20260323",
|
||||
prompt: "Create a campaign batch for Spring Launch...",
|
||||
},
|
||||
});
|
||||
|
||||
// Agent runs for campaign 1
|
||||
const agents = [
|
||||
{ name: "trend-scout", duration: 12000, summary: "5 trending hooks found: 'POV: you just found...', 'Why is nobody talking about...'" },
|
||||
{ name: "marketing-research-agent", duration: 128000, summary: "Market report generated. 3 competitor angles identified, 8 pain points mapped." },
|
||||
{ name: "script-writer", duration: 95000, summary: "15 scripts written — 5 hooks × 3 platform styles (polished, authentic, local)." },
|
||||
{ name: "ad-creative-designer", duration: 187000, summary: "8 static ads generated: 4 Instagram (1080x1080), 2 Nextdoor spotlight, 2 Nextdoor display." },
|
||||
{ name: "video-ad-producer", duration: 312000, summary: "6 video ads rendered: 2 Instagram Reels (polished), 2 TikTok (authentic), 2 Nextdoor (local)." },
|
||||
{ name: "copywriter-agent", duration: 67000, summary: "Platform-tuned captions written for all 14 creatives. Hashtag sets included." },
|
||||
{ name: "distribution-agent", duration: 23000, summary: "Publish manifest created. 14 assets ready for review." },
|
||||
];
|
||||
|
||||
for (const agent of agents) {
|
||||
await prisma.agentRun.create({
|
||||
data: {
|
||||
campaignId: campaign1.id,
|
||||
agentName: agent.name,
|
||||
status: "completed",
|
||||
startedAt: new Date(Date.now() - 3600000),
|
||||
completedAt: new Date(Date.now() - 3600000 + agent.duration),
|
||||
durationMs: agent.duration,
|
||||
outputSummary: agent.summary,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Assets for campaign 1
|
||||
const assets = [
|
||||
{ type: "image", platform: "instagram", format: "png", fileName: "instagram_feed_hook_a_1080x1080.png", dimensions: "1080x1080", status: "approved", metadata: { caption: "Your morning just got an upgrade. ✨\n\nThe smartest productivity app of 2026 is here — and it's free.\n\nDownload now — link in bio.\n\n#ProductivityApp #MorningRoutine #AppOfTheDay", hook: "Still doing it the hard way?" } },
|
||||
{ type: "image", platform: "instagram", format: "png", fileName: "instagram_feed_hook_b_1080x1080.png", dimensions: "1080x1080", status: "approved", metadata: { caption: "POV: you just found the app everyone's been hiding from you. 🚀\n\n50K+ downloads can't be wrong.\n\n#ProductivityTips #TechLife", hook: "POV: you just found this app" } },
|
||||
{ type: "image", platform: "instagram", format: "png", fileName: "instagram_stories_hook_a_1080x1920.png", dimensions: "1080x1920", status: "draft", metadata: { caption: "Swipe up to download free 🔥", hook: "Your workflow is about to change" } },
|
||||
{ type: "image", platform: "nextdoor", format: "png", fileName: "nextdoor_spotlight_1200x1200.png", dimensions: "1200x1200", status: "approved", metadata: { caption: "Your neighbors are already using this app to simplify their mornings. Join them — it's free.", hook: "Your neighbors love this app" } },
|
||||
{ type: "image", platform: "nextdoor", format: "png", fileName: "nextdoor_display_1200x628.png", dimensions: "1200x628", status: "draft", metadata: { caption: "The productivity app trusted by your community. Learn more.", hook: "Trusted locally" } },
|
||||
{ type: "video", platform: "instagram", format: "mp4", fileName: "instagram_reel_polished_1080x1920.mp4", dimensions: "1080x1920", status: "approved", metadata: { caption: "3 seconds to change your morning ⚡\n\nDownload free — link in bio.\n\n#Reels #ProductivityHack", hook: "Still doing it the hard way?", duration: "15s" } },
|
||||
{ type: "video", platform: "tiktok", format: "mp4", fileName: "tiktok_ad_authentic_1080x1920.mp4", dimensions: "1080x1920", status: "approved", metadata: { caption: "okay i HAVE to share this app with you guys 😭 #fyp #productivity #appoftiktok", hook: "I need to talk about this app", duration: "9s" } },
|
||||
{ type: "video", platform: "tiktok", format: "mp4", fileName: "tiktok_ad_hook_b_1080x1920.mp4", dimensions: "1080x1920", status: "draft", metadata: { caption: "why is nobody talking about this?? #fyp #lifehack", hook: "Why is nobody talking about this?", duration: "12s" } },
|
||||
{ type: "video", platform: "nextdoor", format: "mp4", fileName: "nextdoor_video_local_1080x1080.mp4", dimensions: "1080x1080", status: "draft", metadata: { caption: "See why your neighbors love this app. Simple, free, and made for busy mornings.", hook: "Your neighbors love this app", duration: "15s" } },
|
||||
{ type: "copy", platform: "instagram", format: "json", fileName: "instagram_captions.json", status: "approved", metadata: { caption: "5 caption variations for Instagram" } },
|
||||
{ type: "copy", platform: "tiktok", format: "json", fileName: "tiktok_captions.json", status: "approved", metadata: { caption: "5 caption variations for TikTok" } },
|
||||
{ type: "copy", platform: "nextdoor", format: "json", fileName: "nextdoor_posts.json", status: "approved", metadata: { caption: "3 post variations for Nextdoor" } },
|
||||
{ type: "research", platform: null, format: "html", fileName: "interactive_report.html", status: "approved", metadata: { caption: "Market research dashboard with competitor analysis" } },
|
||||
{ type: "script", platform: null, format: "md", fileName: "ad_scripts_all_platforms.md", status: "approved", metadata: { caption: "15 ad scripts — 5 hooks × 3 platform styles" } },
|
||||
];
|
||||
|
||||
for (const asset of assets) {
|
||||
await prisma.asset.create({
|
||||
data: {
|
||||
campaignId: campaign1.id,
|
||||
type: asset.type,
|
||||
platform: asset.platform,
|
||||
format: asset.format,
|
||||
filePath: `outputs/spring_launch_campaign_20260323/${asset.type === "image" ? "ads" : asset.type === "video" ? "video" : asset.type === "copy" ? "copy" : ""}/${asset.fileName}`,
|
||||
fileName: asset.fileName,
|
||||
dimensions: asset.dimensions || null,
|
||||
status: asset.status,
|
||||
metadata: JSON.stringify(asset.metadata),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Campaign 2: Running
|
||||
const campaign2 = await prisma.campaign.create({
|
||||
data: {
|
||||
name: "Summer Feature Drop",
|
||||
status: "running",
|
||||
platforms: JSON.stringify(["instagram", "tiktok"]),
|
||||
config: JSON.stringify({
|
||||
goal: "engagement",
|
||||
keyMessage: "The feature you've been asking for is finally here.",
|
||||
variations: 3,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Partial agent runs for campaign 2
|
||||
const partialAgents = [
|
||||
{ name: "trend-scout", status: "completed", duration: 9000, summary: "4 trending hooks found" },
|
||||
{ name: "marketing-research-agent", status: "completed", duration: 110000, summary: "Research complete. 5 angles identified." },
|
||||
{ name: "script-writer", status: "running", duration: null, summary: null },
|
||||
];
|
||||
|
||||
for (const agent of partialAgents) {
|
||||
await prisma.agentRun.create({
|
||||
data: {
|
||||
campaignId: campaign2.id,
|
||||
agentName: agent.name,
|
||||
status: agent.status,
|
||||
startedAt: new Date(),
|
||||
completedAt: agent.status === "completed" ? new Date() : null,
|
||||
durationMs: agent.duration,
|
||||
outputSummary: agent.summary,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Campaign 3: Draft
|
||||
await prisma.campaign.create({
|
||||
data: {
|
||||
name: "Back to School Promo",
|
||||
status: "draft",
|
||||
platforms: JSON.stringify(["instagram", "nextdoor"]),
|
||||
config: JSON.stringify({
|
||||
goal: "app_downloads",
|
||||
keyMessage: "Get organized before school starts. The app students and parents love.",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Campaign 4: Published
|
||||
const campaign4 = await prisma.campaign.create({
|
||||
data: {
|
||||
name: "Valentine's Day Push",
|
||||
status: "published",
|
||||
platforms: JSON.stringify(["instagram", "tiktok"]),
|
||||
config: JSON.stringify({
|
||||
goal: "brand_awareness",
|
||||
keyMessage: "Share the love — share the app.",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// A few published assets for campaign 4
|
||||
for (const asset of [
|
||||
{ fileName: "ig_valentines_1080x1080.png", platform: "instagram", type: "image", dimensions: "1080x1080" },
|
||||
{ fileName: "tiktok_valentines_1080x1920.mp4", platform: "tiktok", type: "video", dimensions: "1080x1920" },
|
||||
]) {
|
||||
await prisma.asset.create({
|
||||
data: {
|
||||
campaignId: campaign4.id,
|
||||
type: asset.type,
|
||||
platform: asset.platform,
|
||||
format: asset.fileName.split(".").pop()!,
|
||||
filePath: `outputs/valentines_push_20260214/ads/${asset.fileName}`,
|
||||
fileName: asset.fileName,
|
||||
dimensions: asset.dimensions,
|
||||
status: "published",
|
||||
postizPostId: `postiz_${Math.random().toString(36).slice(2, 10)}`,
|
||||
metadata: JSON.stringify({ caption: "Share the love 💕 Download free — link in bio." }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Trend report
|
||||
await prisma.trendReport.create({
|
||||
data: {
|
||||
name: "Weekly Trends — March 17-23, 2026",
|
||||
filePath: "outputs/trend_reports/weekly_20260323.html",
|
||||
summary: "Top hooks: 'POV: you just found...', 'Why is nobody talking about...'. Competitor analysis: 3 apps gaining traction with UGC-style Reels. Recommended themes: morning routine, before/after transformations.",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.trendReport.create({
|
||||
data: {
|
||||
name: "Weekly Trends — March 10-16, 2026",
|
||||
filePath: "outputs/trend_reports/weekly_20260316.html",
|
||||
summary: "Trending format: split-screen comparisons. Rising hashtag: #AppTok. Competitor spotlight: rival app launched TikTok campaign with 2M views.",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Demo data seeded:");
|
||||
console.log(" 4 campaigns (review, running, draft, published)");
|
||||
console.log(" 14 assets on Spring Launch");
|
||||
console.log(" 2 published assets on Valentine's");
|
||||
console.log(" 7 completed agent runs + 3 partial");
|
||||
console.log(" 2 trend reports");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,32 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { PrismaClient } from "../lib/generated/prisma/client";
|
||||
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
|
||||
|
||||
const adapter = new PrismaBetterSqlite3({
|
||||
url: process.env.DATABASE_URL || "file:./data/marketing.db",
|
||||
});
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
async function main() {
|
||||
const email = process.env.ADMIN_EMAIL || "admin@localhost";
|
||||
const password = process.env.ADMIN_PASSWORD || "admin123";
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { email },
|
||||
update: {},
|
||||
create: {
|
||||
email,
|
||||
password: await bcrypt.hash(password, 12),
|
||||
name: "Admin",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Admin user created: ${email}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user