From 1430e4422de6f44cb3381eb3164f69da7ceded24 Mon Sep 17 00:00:00 2001 From: Rafael Chen Date: Thu, 26 Feb 2026 02:17:24 -0500 Subject: [PATCH 01/27] (feat): created marketing pipeline with basic functionality (generating based on rules + documents) --- src/app/api/marketing-pipeline/route.ts | 79 +++ src/app/employer/home/page.tsx | 9 +- .../tools/marketing-pipeline/page.tsx | 302 ++++++++++ src/lib/tools/marketing-pipeline/context.ts | 54 ++ src/lib/tools/marketing-pipeline/generator.ts | 58 ++ src/lib/tools/marketing-pipeline/index.ts | 14 + src/lib/tools/marketing-pipeline/research.ts | 76 +++ src/lib/tools/marketing-pipeline/run.ts | 75 +++ src/lib/tools/marketing-pipeline/types.ts | 34 ++ .../Employer/MarketingPipeline.module.css | 560 ++++++++++++++++++ 10 files changed, 1260 insertions(+), 1 deletion(-) create mode 100644 src/app/api/marketing-pipeline/route.ts create mode 100644 src/app/employer/tools/marketing-pipeline/page.tsx create mode 100644 src/lib/tools/marketing-pipeline/context.ts create mode 100644 src/lib/tools/marketing-pipeline/generator.ts create mode 100644 src/lib/tools/marketing-pipeline/index.ts create mode 100644 src/lib/tools/marketing-pipeline/research.ts create mode 100644 src/lib/tools/marketing-pipeline/run.ts create mode 100644 src/lib/tools/marketing-pipeline/types.ts create mode 100644 src/styles/Employer/MarketingPipeline.module.css diff --git a/src/app/api/marketing-pipeline/route.ts b/src/app/api/marketing-pipeline/route.ts new file mode 100644 index 00000000..94a1cc0f --- /dev/null +++ b/src/app/api/marketing-pipeline/route.ts @@ -0,0 +1,79 @@ +import { NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { eq } from "drizzle-orm"; +import { db } from "~/server/db"; +import { users } from "~/server/db/schema"; +import { MarketingPipelineInputSchema, runMarketingPipeline } from "~/lib/tools/marketing-pipeline"; + +export const runtime = "nodejs"; +export const maxDuration = 60; + +export async function POST(request: Request) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json( + { success: false, message: "Unauthorized" }, + { status: 401 }, + ); + } + + const body = (await request.json()) as unknown; + const validation = MarketingPipelineInputSchema.safeParse(body); + if (!validation.success) { + return NextResponse.json( + { + success: false, + message: "Invalid input", + errors: validation.error.flatten(), + }, + { status: 400 }, + ); + } + + const [requestingUser] = await db + .select() + .from(users) + .where(eq(users.userId, userId)) + .limit(1); + + if (!requestingUser) { + return NextResponse.json( + { success: false, message: "User not found" }, + { status: 404 }, + ); + } + + const companyId = Number(requestingUser.companyId); + if (Number.isNaN(companyId)) { + return NextResponse.json( + { success: false, message: "Invalid company ID" }, + { status: 400 }, + ); + } + + const result = await runMarketingPipeline({ + companyId, + input: validation.data, + }); + + return NextResponse.json( + { + success: true, + data: result, + }, + { status: 200 }, + ); + } catch (error) { + console.error("[marketing-pipeline] POST error:", error); + return NextResponse.json( + { + success: false, + message: "Failed to run marketing pipeline", + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 }, + ); + } +} + diff --git a/src/app/employer/home/page.tsx b/src/app/employer/home/page.tsx index fe364b7d..6eaf937b 100644 --- a/src/app/employer/home/page.tsx +++ b/src/app/employer/home/page.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useState, useEffect } from "react"; -import { Upload, FileText, BarChart, Brain, Settings, Users, HelpCircle, Clock, MousePointerClick } from "lucide-react"; +import { Upload, FileText, BarChart, Brain, Settings, Users, HelpCircle, Clock, MousePointerClick, Megaphone } from "lucide-react"; import styles from "~/styles/Employer/Home.module.css"; import { useRouter } from "next/navigation"; import ProfileDropdown from "~/app/employer/_components/ProfileDropdown"; @@ -119,6 +119,13 @@ const HomeScreen = () => { path: "/employer/employees", isBeta: false, }, + { + icon: , + title: "Marketing Pipeline", + description: "Research trends and generate campaign-ready platform messages", + path: "/employer/tools/marketing-pipeline", + isBeta: false, + }, { icon: , title: "User Settings", diff --git a/src/app/employer/tools/marketing-pipeline/page.tsx b/src/app/employer/tools/marketing-pipeline/page.tsx new file mode 100644 index 00000000..75d5fd76 --- /dev/null +++ b/src/app/employer/tools/marketing-pipeline/page.tsx @@ -0,0 +1,302 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Loader2, MessageSquareText, Rocket, Sparkles } from "lucide-react"; +import styles from "~/styles/Employer/MarketingPipeline.module.css"; + +type Platform = "x" | "linkedin" | "reddit"; + +interface PipelineResponse { + success: boolean; + message?: string; + data?: { + platform: Platform; + message: string; + "image/video": "image" | "video"; + research: Array<{ + title: string; + url: string; + snippet: string; + source: Platform; + }>; + }; +} + +const PLATFORM_OPTIONS: Array<{ id: Platform; label: string; subtitle: string; logoText: string }> = [ + { id: "reddit", label: "Reddit", subtitle: "Community-first threads", logoText: "reddit" }, + { id: "x", label: "Twitter / X", subtitle: "Fast-moving trends", logoText: "𝕏" }, + { id: "linkedin", label: "LinkedIn", subtitle: "B2B + thought leadership", logoText: "in" }, +]; + +function usePlatformLogoClassNames() { + return useMemo( + () => ({ + reddit: styles.platformLogoReddit, + x: styles.platformLogoX, + linkedin: styles.platformLogoLinkedin, + }), + [], + ); +} + +export default function MarketingPipelinePage() { + const router = useRouter(); + const [platform, setPlatform] = useState(null); + const [prompt, setPrompt] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [result, setResult] = useState(null); + + const logoClassNames = usePlatformLogoClassNames(); + const selectedPlatform = PLATFORM_OPTIONS.find((option) => option.id === platform) ?? null; + + const runPipeline = async () => { + setError(null); + setResult(null); + + if (!platform) { + setError("Choose a platform to continue."); + return; + } + + const normalizedPrompt = prompt.trim(); + if (!normalizedPrompt) { + setError("Add a short description of what you want to promote."); + return; + } + + setLoading(true); + try { + const response = await fetch("/api/marketing-pipeline", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + platform, + prompt: normalizedPrompt, + }), + }); + + const payload = (await response.json()) as PipelineResponse; + if (!response.ok || !payload.success || !payload.data) { + setError(payload.message ?? "We couldn't generate a campaign right now. Please try again."); + return; + } + + setResult(payload.data); + } catch (requestError) { + console.error("[marketing-pipeline] request error:", requestError); + setError("Something went wrong talking to the marketing engine. Try again."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+
+ +
+
+

Marketing Pipeline

+

+ Turn your company knowledge base into platform-ready marketing messages. +

+
+
+ +
+
+ + {!platform ? ( +
+
+
+

Choose a channel to start

+

+ Pick where this campaign will live. You can always come back and run again for a + different platform. +

+
+
+ {PLATFORM_OPTIONS.map((option) => ( + + ))} +
+ {error &&

{error}

} +
+
+ ) : ( +
+
+
+
+
+ + {selectedPlatform?.logoText} + + {selectedPlatform?.label} +
+ +
+ +
+
+

Describe what you want to promote

+ 1–3 sentences is perfect. +
+