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
+141
View File
@@ -0,0 +1,141 @@
import { prisma } from "./prisma";
// Settings keys and their env var fallbacks
const SETTINGS_KEYS = {
// Postiz
POSTIZ_URL: { envVar: "POSTIZ_URL", label: "Postiz URL", placeholder: "http://localhost:5000" },
POSTIZ_API_KEY: { envVar: "POSTIZ_API_KEY", label: "Postiz API Key", placeholder: "your-postiz-api-key", secret: true },
// Tavily (Research)
TAVILY_API_KEY: { envVar: "TAVILY_API_KEY", label: "Tavily API Key", placeholder: "tvly-...", secret: true },
// Google Gemini (NanoBanana image generation)
GEMINI_API_KEY: { envVar: "GEMINI_API_KEY", label: "Google Gemini API Key", placeholder: "AIza...", secret: true },
// Nextdoor
NEXTDOOR_API_TOKEN: { envVar: "NEXTDOOR_API_TOKEN", label: "Nextdoor API Token", placeholder: "your-nextdoor-token", secret: true },
NEXTDOOR_ADVERTISER_ID: { envVar: "NEXTDOOR_ADVERTISER_ID", label: "Nextdoor Advertiser ID", placeholder: "your-advertiser-id" },
} as const;
export type SettingKey = keyof typeof SETTINGS_KEYS;
export const SETTINGS_CONFIG = SETTINGS_KEYS;
// Grouped for UI
export const SETTINGS_GROUPS = [
{
name: "Postiz",
description: "Self-hosted social media scheduling. Handles Instagram and TikTok publishing.",
docsUrl: "https://postiz.com",
keys: ["POSTIZ_URL", "POSTIZ_API_KEY"] as SettingKey[],
},
{
name: "Tavily",
description: "AI-powered web research. Used by the Trend Scout and Research agents. Free tier: 1,000 searches/month.",
docsUrl: "https://tavily.com",
keys: ["TAVILY_API_KEY"] as SettingKey[],
},
{
name: "Gemini",
description: "Google Gemini powers NanoBanana MCP for AI image generation in static ads. ~$0.04-0.13/image.",
docsUrl: "https://aistudio.google.com/apikey",
keys: ["GEMINI_API_KEY"] as SettingKey[],
},
{
name: "Nextdoor",
description: "Direct Nextdoor Ads API integration for local advertising.",
docsUrl: "https://developer.nextdoor.com",
keys: ["NEXTDOOR_API_TOKEN", "NEXTDOOR_ADVERTISER_ID"] as SettingKey[],
},
];
/**
* Get a setting value. Checks DB first, falls back to env var.
*/
export async function getSetting(key: SettingKey): Promise<string> {
try {
const setting = await prisma.setting.findUnique({ where: { key } });
if (setting?.value) return setting.value;
} catch {
// DB not available, fall through to env
}
const config = SETTINGS_KEYS[key];
return process.env[config.envVar] || "";
}
/**
* Get all settings as a map.
*/
export async function getAllSettings(): Promise<Record<string, string>> {
const result: Record<string, string> = {};
for (const [key, config] of Object.entries(SETTINGS_KEYS)) {
try {
const setting = await prisma.setting.findUnique({ where: { key } });
result[key] = setting?.value || process.env[config.envVar] || "";
} catch {
result[key] = process.env[config.envVar] || "";
}
}
return result;
}
/**
* Save a setting to the database.
*/
export async function saveSetting(key: SettingKey, value: string) {
await prisma.setting.upsert({
where: { key },
update: { value },
create: { key, value },
});
}
/**
* Check connectivity status for each integration.
*/
export async function checkIntegrationStatus(): Promise<Record<string, { connected: boolean; error?: string }>> {
const settings = await getAllSettings();
const status: Record<string, { connected: boolean; error?: string }> = {};
// Postiz
if (settings.POSTIZ_URL && settings.POSTIZ_API_KEY) {
try {
const res = await fetch(`${settings.POSTIZ_URL}/public/v1/integrations`, {
headers: { Authorization: `Bearer ${settings.POSTIZ_API_KEY}` },
signal: AbortSignal.timeout(5000),
});
status.postiz = { connected: res.ok };
if (!res.ok) status.postiz.error = `HTTP ${res.status}`;
} catch (e) {
status.postiz = { connected: false, error: e instanceof Error ? e.message : "Connection failed" };
}
} else {
status.postiz = { connected: false, error: "Not configured" };
}
// Tavily
if (settings.TAVILY_API_KEY) {
status.tavily = { connected: true }; // No ping endpoint, just check if key exists
} else {
status.tavily = { connected: false, error: "Not configured" };
}
// Gemini
if (settings.GEMINI_API_KEY) {
status.gemini = { connected: true };
} else {
status.gemini = { connected: false, error: "Not configured" };
}
// Nextdoor
if (settings.NEXTDOOR_API_TOKEN && settings.NEXTDOOR_ADVERTISER_ID) {
status.nextdoor = { connected: true };
} else {
status.nextdoor = { connected: false, error: "Not configured" };
}
return status;
}