diff --git a/kits/outreach-personalizer/.env.example b/kits/outreach-personalizer/.env.example new file mode 100644 index 00000000..6f6dd07e --- /dev/null +++ b/kits/outreach-personalizer/.env.example @@ -0,0 +1,3 @@ +# Flow-level API Keys (Required in Lamatic Studio / deployments) +FIRECRAWL_API_KEY=your_firecrawl_api_key +GROQ_API_KEY=your_groq_api_key diff --git a/kits/outreach-personalizer/.gitignore b/kits/outreach-personalizer/.gitignore new file mode 100644 index 00000000..e916ce5a --- /dev/null +++ b/kits/outreach-personalizer/.gitignore @@ -0,0 +1,5 @@ +.lamatic/ +node_modules/ +.next/ +.env +.env.local diff --git a/kits/outreach-personalizer/README.md b/kits/outreach-personalizer/README.md new file mode 100644 index 00000000..4576bf98 --- /dev/null +++ b/kits/outreach-personalizer/README.md @@ -0,0 +1,86 @@ +# Outreach Personalizer - Personalized Outreach Generator + +Outreach Personalizer is a web application that wraps a deployed Lamatic.ai AI flow. It automatically crawls a prospect company's website and founder's public profile, extracts concrete technical signals/milestones, connects them to a candidate's custom background, and drafts a highly personalized, high-specificity cold outreach email. + +## Key Features +- **Crawls Website Content**: Leverages Firecrawl integration inside the Lamatic flow to scrape company data. +- **Specific Signal Hook Extraction**: Uses an LLM to identify specific achievements, posts, or bugs rather than generic praises. +- **Value-Added Asset Suggestion**: Suggests a concrete asset the pitch candidate could build in under 2 hours to showcase capabilities. +- **Tailored Outreach Generation**: Drafts a clean, direct outreach email (80-120 words) with a specific call to action. +- **Premium Dark UI**: Glassmorphic, modern responsive web dashboard with step-by-step agent feedback and copy support. +- **Safe Server-Side Calls**: Never exposes secret credentials to the client side. +- **Resilient Error Interception**: Catches non-JSON / HTML gateway errors gracefully. + +--- + +## Setup Instructions + +### Prerequisites +- Node.js v18.0.0 or higher +- npm or yarn + +### 1. Configure Environment Variables +Create a file named `.env.local` in the `apps/web/` directory (or copy it from the workspace root) and define the following variables: + +```env +# The secret credential used to authenticate your client requests (do not share or commit) +LAMATIC_API_KEY=your_lamatic_api_key + +# The project identifier in the Lamatic Cloud Studio +LAMATIC_PROJECT_ID=your_lamatic_project_id + +# The base URL endpoint for your deployed project (e.g. https://-.lamatic.dev) +LAMATIC_API_URL=your_lamatic_api_url + +# The UUID indicating which specific flow to trigger (e.g. Outreach Personalizer) +FLOW_ID=your_deployed_flow_id +``` + +> [!IMPORTANT] +> Never commit `.env.local` to source control. It is already added to `.gitignore` to prevent secret exposure. + +### 2. Install Dependencies +Change into the `apps/web` directory and install the required modules: +```bash +cd apps/web +npm install +``` + +### 3. Run Locally +Start the local development server: +```bash +npm run dev +``` +Open [http://localhost:3000](http://localhost:3000) in your browser to view the application. + +--- + +## Architectural Details & Integration + +The app initiates execution of the Lamatic flow using a Next.js Server Handler at `apps/web/app/api/generate/route.ts`. + +``` + ┌────────────────────────┐ + │ Next.js Client │ + └───────────┬────────────┘ + │ POST (JSON Inputs) + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Next.js Server App Router │ +│ │ +│ ┌────────────────────────┐ ┌──────────────────┐ │ +│ │ API Route Handler ├──────────────►│ Lamatic Client │ │ +│ │ (api/generate/route) │ Executes │ (lib/lamatic.ts) │ │ +│ └────────────────────────┘ Client └─────────┬────────┘ │ +└─────────────────────────────────────────────────────┼───────────────────────────────────────┘ + │ GraphQL POST Request + ▼ + ┌──────────────────────┐ + │ Lamatic.ai Gateway │ + └──────────────────────┘ +``` + +1. **Client Trigger**: The client form sends `company_url`, `founder_linkedin_url`, and `candidate_context` to the local Next.js `/api/generate` handler. +2. **GraphQL Wrapping**: The `LamaticClient` wrapper formats the GraphQL mutation request using `FLOW_ID` as the workflow identifier. +3. **Execution**: The request is securely dispatched to the deployed endpoint (`LAMATIC_API_URL`) authorized by `LAMATIC_API_KEY`. +4. **Resilient Parsing**: The response text is scanned for HTML content (e.g. Cloudflare error pages) to ensure that raw parse exceptions are intercepted and displayed to the user as a friendly warning. diff --git a/kits/outreach-personalizer/agent.md b/kits/outreach-personalizer/agent.md new file mode 100644 index 00000000..0d4b8809 --- /dev/null +++ b/kits/outreach-personalizer/agent.md @@ -0,0 +1,60 @@ +# Agent: Outreach Personalizer + +## Purpose & Overview +The **Outreach Personalizer** agent is designed to construct hyper-personalized cold outreach pitches for job candidates. Instead of writing generic flattery, it acts as an analytical, eager candidate who does deep research to find concrete, high-specificity hooks. It crawls the target's public content, extracts technical signals, devises a small asset (which can be completed in < 2 hours), and builds a confident, direct cold email pitch. + +--- + +## Workflow Steps + +The agent's logic is defined across a multi-step chain in `flows/average-teenager.ts`: + +```mermaid +graph TD + Start([API Trigger]) --> Scrape[Firecrawl Crawl] + Scrape --> SignalLLM[LLM Node: Extract Signals] + SignalLLM --> AssetLLM[LLM Node: Devise Asset] + AssetLLM --> PitchLLM[LLM Node: Draft Email] + PitchLLM --> Response([Response Output]) +``` + +1. **Firecrawl Node (`firecrawlNode_692`)**: Scrapes both the company website (`company_url`) and the founder's LinkedIn profile (`founder_linkedin_url`) provided in the trigger inputs. +2. **Signal Extraction (`LLMNode_276`)**: Parses the crawled markdown content to extract 3-5 concrete "noticed things" (e.g. a specific feature release, a bug, a technical post, or a design decision) and rates them with a specificity score from 1 to 5. +3. **Asset Suggestion (`LLMNode_996`)**: Connects the candidate's background with one of the extracted signals and suggests one small, concrete value-add asset (e.g. a mini bug list, a UX walkthrough, a content sample, or a competitor teardown) that the candidate can build quickly. +4. **Outreach Drafting (`LLMNode_696`)**: Selects the signal with the highest specificity score and drafts a direct, short cold outreach email around it. + +--- + +## Agent Persona & Style Constraints + +- **Tone**: Conversational, confident, direct, and non-corporate. Sounds like one builder writing to another. +- **Formality**: Low-to-moderate; avoids generic professional platitudes. +- **Length**: Strict constraint of 80 to 120 words. +- **Structure**: + 1. Hook based on the highest specificity signal (quotes/paraphrases the exact metric/name/result). + 2. Connection to the candidate's background. + 3. Description of the specific value-add asset. + 4. Ends with exactly: *"Would it make sense to speak for 10 minutes?"* +- **Banned Words & Phrases**: + - *"I'd love to"* + - *"could be really useful"* + - *"I hope this finds you well"* + - *"reach out"* + - *"circle back"* + - *"leverage"* + - Em-dashes (`—` or `--`) + +--- + +## API Inputs & Outputs + +### Expected Inputs +The trigger accepts a JSON payload with the following fields: +- `company_url` (string): Target company website. +- `founder_linkedin_url` (string): Founder's LinkedIn profile. +- `candidate_context` (string): Context about the candidate's background, skills, and projects. + +### Expected Outputs +The flow execution response contains: +- `status` (string): Execution status (e.g. `success`). +- `result` (object/string): The plain text cold outreach email, along with outputs from preceding nodes (signals and asset ideas) if mapped. diff --git a/kits/outreach-personalizer/apps/.env.example b/kits/outreach-personalizer/apps/.env.example new file mode 100644 index 00000000..9e6f9c6f --- /dev/null +++ b/kits/outreach-personalizer/apps/.env.example @@ -0,0 +1,4 @@ +LAMATIC_API_KEY=lt-xxxxxx +LAMATIC_PROJECT_ID=xxxxxx +LAMATIC_API_URL=https://xxxxxx.lamatic.dev +FLOW_ID=xxxxxx diff --git a/kits/outreach-personalizer/apps/.gitignore b/kits/outreach-personalizer/apps/.gitignore new file mode 100644 index 00000000..7b8da95f --- /dev/null +++ b/kits/outreach-personalizer/apps/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/kits/outreach-personalizer/apps/AGENTS.md b/kits/outreach-personalizer/apps/AGENTS.md new file mode 100644 index 00000000..8bd0e390 --- /dev/null +++ b/kits/outreach-personalizer/apps/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/kits/outreach-personalizer/apps/CLAUDE.md b/kits/outreach-personalizer/apps/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/kits/outreach-personalizer/apps/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/kits/outreach-personalizer/apps/README.md b/kits/outreach-personalizer/apps/README.md new file mode 100644 index 00000000..a36d96ba --- /dev/null +++ b/kits/outreach-personalizer/apps/README.md @@ -0,0 +1,56 @@ +# Outreach Personalizer - Next.js Reference App + +This is the reference web application for the **Outreach Personalizer** kit. It provides a modern, responsive web dashboard where job candidates can generate hyper-personalized cold outreach emails based on a target company website URL, founder's LinkedIn URL, and the candidate's background. + +The application triggers a deployed Lamatic.ai AI flow via Next.js Server Actions, keeping all API credentials secure on the server side. + +## Features + +- **Input Form**: Clean fields for the company website, founder LinkedIn profile, and candidate background context. +- **Real-Time Step Progression**: Dynamic feedback showing the current step of the underlying agent flow. +- **Copy-to-Clipboard**: Quick copy functionality for the generated cold outreach pitch. +- **Error Interception**: Graceful handling of network failures or flow timeout errors. + +## Prerequisites + +- Node.js v18.0.0 or higher +- npm, yarn, or pnpm + +## Environment Setup + +Create a `.env.local` file in this directory (`apps/`), or copy `.env.example` to `.env.local`: + +```bash +cp .env.example .env.local +``` + +Define the following environment variables inside `.env.local`: + +```env +# The secret credential used to authenticate your client requests (do not share or commit) +LAMATIC_API_KEY=your_lamatic_api_key + +# The project identifier in the Lamatic Cloud Studio +LAMATIC_PROJECT_ID=your_lamatic_project_id + +# The base URL endpoint for your deployed project (e.g. https://-.lamatic.dev) +LAMATIC_API_URL=your_lamatic_api_url + +# The UUID indicating which specific flow to trigger (e.g. Outreach Personalizer) +FLOW_ID=your_deployed_flow_id +``` + +## Running the Application + +1. **Install dependencies**: + ```bash + npm install + ``` + +2. **Run the development server**: + ```bash + npm run dev + ``` + +3. **Access the dashboard**: + Open [http://localhost:3000](http://localhost:3000) in your browser. diff --git a/kits/outreach-personalizer/apps/actions/orchestrate.ts b/kits/outreach-personalizer/apps/actions/orchestrate.ts new file mode 100644 index 00000000..dbd75d5c --- /dev/null +++ b/kits/outreach-personalizer/apps/actions/orchestrate.ts @@ -0,0 +1,89 @@ +"use server" + +import { getLamaticClient } from "../lib/lamatic-client"; +import kitConfig from "../../lamatic.config"; + +export interface PitchInput { + company_url: string; + founder_linkedin_url: string; + candidate_context: string; +} + +const TIMEOUT_MS = 300000; // 5 minutes (Matching Vercel Hobby maxDuration) + +/** + * Wraps a promise with a timeout. + */ +async function withTimeout(promise: Promise, timeoutMs: number, errorMessage: string): Promise { + let timer: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(errorMessage)), timeoutMs); + // Ensure node can exit if the timer is the only thing keeping it alive + if (timer.unref) { + timer.unref(); + } + }); + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + if (timer) { + clearTimeout(timer); + } + } +} + +export async function generatePersonalizedPitch(input: PitchInput): Promise<{ + success: boolean; + data?: any; + error?: string; +}> { + try { + console.log("[v0] Executing Average Teenager flow for company URL:", input.company_url); + + const flowStep = kitConfig.steps.find((step) => step.id === "average-teenager"); + if (!flowStep) { + throw new Error("Step 'average-teenager' not defined in lamatic.config."); + } + const envKey = flowStep.envKey || "FLOW_ID"; + const flowId = process.env[envKey]; + if (!flowId) { + throw new Error(`${envKey} is not set in environment variables.`); + } + + const trimmedInput = { + company_url: input.company_url.trim(), + founder_linkedin_url: input.founder_linkedin_url.trim(), + candidate_context: input.candidate_context.trim(), + }; + + const resData: any = await withTimeout( + getLamaticClient().executeFlow(flowId, trimmedInput), + TIMEOUT_MS, + "The AI provider is taking longer than usual to respond. This usually means their service is overloaded. Please try again in a few minutes." + ); + + if (!resData || (resData.status && resData.status === "error")) { + throw new Error(resData.message || "Flow execution failed or returned no result."); + } + + return { + success: true, + data: resData.result, + }; + } catch (error: any) { + console.error("[v0] Generation error:", error); + + let errorMessage = "An unexpected error occurred during execution."; + if (error instanceof Error) { + errorMessage = error.message; + if (error.message.includes("fetch failed")) { + errorMessage = "Network error: Unable to connect to the Lamatic service. Please check your internet connection."; + } + } + + return { + success: false, + error: errorMessage, + }; + } +} diff --git a/kits/outreach-personalizer/apps/app/favicon.ico b/kits/outreach-personalizer/apps/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/kits/outreach-personalizer/apps/app/favicon.ico differ diff --git a/kits/outreach-personalizer/apps/app/globals.css b/kits/outreach-personalizer/apps/app/globals.css new file mode 100644 index 00000000..a2dc41ec --- /dev/null +++ b/kits/outreach-personalizer/apps/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/kits/outreach-personalizer/apps/app/layout.tsx b/kits/outreach-personalizer/apps/app/layout.tsx new file mode 100644 index 00000000..65cf88dc --- /dev/null +++ b/kits/outreach-personalizer/apps/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Outreach Personalizer | Personalized Outreach Generator", + description: "An agentic cold outreach assistant powered by Lamatic.ai that crawls targets and drafts custom value-added pitches.", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/kits/outreach-personalizer/apps/app/page.tsx b/kits/outreach-personalizer/apps/app/page.tsx new file mode 100644 index 00000000..09be3b65 --- /dev/null +++ b/kits/outreach-personalizer/apps/app/page.tsx @@ -0,0 +1,427 @@ +"use client"; + +import { useState } from "react"; +import { generatePersonalizedPitch } from "../actions/orchestrate"; +import { + Building2, + User, + Sparkles, + Copy, + Check, + Loader2, + AlertTriangle, + RefreshCw, + Mail, + Send, + ExternalLink, + ChevronRight +} from "lucide-react"; + +const LinkedInIcon = (props: React.SVGProps) => ( + + + + + +); + +export default function Home() { + const [companyUrl, setCompanyUrl] = useState(""); + const [founderLinkedinUrl, setFounderLinkedinUrl] = useState(""); + const [candidateContext, setCandidateContext] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + const [loadingStep, setLoadingStep] = useState(0); + const [error, setError] = useState(null); + const [result, setResult] = useState(null); + const [copied, setCopied] = useState(false); + + // Rotate loading messages for a premium agentic feel + const loadingMessages = [ + "Initializing Firecrawl scraper...", + "Crawling company website and LinkedIn...", + "Extracting specific features, posts, and technical decisions...", + "Analyzing noticed signals for outreach relevance...", + "Drafting value-added asset ideas in under 2h limit...", + "Generating final cold email using average-teenager guidelines...", + "Polishing email tone and adding call to action..." + ]; + + const runFlow = async (e: React.FormEvent) => { + e.preventDefault(); + if (!companyUrl || !founderLinkedinUrl || !candidateContext) return; + + setIsLoading(true); + setError(null); + setResult(null); + setLoadingStep(0); + + // Simulate step progress for user feedback + const interval = setInterval(() => { + setLoadingStep((prev) => (prev < loadingMessages.length - 1 ? prev + 1 : prev)); + }, 4000); + + try { + const response = await generatePersonalizedPitch({ + company_url: companyUrl, + founder_linkedin_url: founderLinkedinUrl, + candidate_context: candidateContext, + }); + clearInterval(interval); + + if (!response.success) { + throw new Error(response.error || "Failed to generate personalization."); + } + + setResult(response.data); + } catch (err: any) { + clearInterval(interval); + setError(err.message || "Something went wrong. Please check your connection and try again."); + } finally { + setIsLoading(false); + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + // Helper to extract the outreach email from the result object + const getEmailContent = (): string => { + if (!result) return ""; + if (typeof result === "string") return result; + + // Check various common return shapes from Lamatic LLM node + if (result.generatedResponse) return result.generatedResponse; + if (result.output) return result.output; + if (result.text) return result.text; + + // If it's a nested object structure, look for LLMNode_696 output + if (result.LLMNode_696?.output?.generatedResponse) { + return result.LLMNode_696.output.generatedResponse; + } + + // Fallback: look for any string values or JSON representation + if (typeof result === "object") { + const values = Object.values(result); + const stringVal = values.find(val => typeof val === "string"); + if (stringVal) return stringVal as string; + return JSON.stringify(result, null, 2); + } + + return String(result); + }; + + const emailText = getEmailContent(); + + return ( +
+ {/* Background gradients for premium glassmorphism effect */} +
+
+ + {/* Header */} +
+
+
+
+ +
+
+ + OUTREACH PERSONALIZER + + + Lamatic Agent + +
+
+ +
+
+ + {/* Main Content */} +
+ {/* Left Form: Column 5/12 */} +
+
+
+

+ Outreach Parameters +

+

+ Provide details about the recipient company and your own background. +

+
+ +
+ {/* Company Website URL */} +
+ +
+ setCompanyUrl(e.target.value)} + className="w-full bg-slate-950/80 border border-slate-800 rounded-xl px-4 py-3 pl-10 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all duration-300" + /> +
+ +
+
+
+ + {/* Founder LinkedIn URL */} +
+ +
+ setFounderLinkedinUrl(e.target.value)} + className="w-full bg-slate-950/80 border border-slate-800 rounded-xl px-4 py-3 pl-10 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all duration-300" + /> +
+ +
+
+
+ + {/* Candidate Background Context */} +
+ +
+