diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/kits/automation/cold-email-personalization/.env.example b/kits/automation/cold-email-personalization/.env.example new file mode 100644 index 00000000..d0d8106b --- /dev/null +++ b/kits/automation/cold-email-personalization/.env.example @@ -0,0 +1,5 @@ +# Deploy this flow in Lamatic Studio, then copy the Flow ID and credentials from Settings / API Docs. +AUTOMATION_COLD_EMAIL=YOUR_WORKFLOW_ID_UUID +LAMATIC_API_KEY=YOUR_LAMATIC_API_KEY +LAMATIC_API_URL=https://YOUR-ORG-YOUR-PROJECT.lamatic.dev/graphql +LAMATIC_PROJECT_ID=YOUR_PROJECT_ID diff --git a/kits/automation/cold-email-personalization/.gitignore b/kits/automation/cold-email-personalization/.gitignore new file mode 100644 index 00000000..a6067b6e --- /dev/null +++ b/kits/automation/cold-email-personalization/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env +.env.local +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/kits/automation/cold-email-personalization/.npmrc b/kits/automation/cold-email-personalization/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/kits/automation/cold-email-personalization/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/kits/automation/cold-email-personalization/README.md b/kits/automation/cold-email-personalization/README.md new file mode 100644 index 00000000..477ebf6c --- /dev/null +++ b/kits/automation/cold-email-personalization/README.md @@ -0,0 +1,99 @@ +# Cold Email Personalization — Lamatic AgentKit + +AI-assisted outreach for **college students** targeting **engineering internships**, co-ops, and new-grad roles. Paste LinkedIn-style profile text and structured fields; get a **subject line**, **email body**, and **personalization hook**. + +Flow exported from Lamatic lives in `flows/cold-email-personalisation/` (`config.json`, `inputs.json`, `meta.json`). + +--- + +## Prerequisites + +- Node.js 18+ +- [Lamatic](https://lamatic.ai) account — deploy this flow (or import from export) in Studio +- LLM credentials configured on the **Generate Text** node in Studio + +--- + +## Environment variables + +Copy `.env.example` to `.env` or `.env.local` and fill in: + +| Variable | Description | +|----------|-------------| +| `AUTOMATION_COLD_EMAIL` | Deployed workflow ID (UUID from Lamatic) | +| `LAMATIC_API_URL` | GraphQL endpoint (e.g. `https://-.lamatic.dev/graphql`) | +| `LAMATIC_PROJECT_ID` | Project ID from Lamatic Settings | +| `LAMATIC_API_KEY` | API key from Lamatic Settings → API Keys | + +--- + +## Run locally + +```bash +cd kits/automation/cold-email-personalization +npm install +cp .env.example .env.local +# Edit .env.local with your values +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). + +--- + +## Deploy (Vercel) + +1. Import this repository and set **Root Directory** to `kits/automation/cold-email-personalization`. +2. Add the same environment variables in the Vercel project settings. +3. Deploy. + +--- + +## Flow inputs (API payload) + +The app sends these keys to `executeWorkflow`: + +- `profile_data` — pasted profile / LinkedIn context +- `prospect_name`, `prospect_role`, `company_name` +- `product_description` — your student pitch +- `value_proposition` — why you’re a fit +- `call_to_action` — what you’re asking for + +**Outputs:** `subject_line`, `email_body`, `personalized_hook` (parsed from the API result or from `generatedResponse` JSON when needed). + +--- + +## Troubleshooting + +### Error: schema `type` / `properties` / `required` in the API result (no real email text) + +If `executeWorkflow` returns something like: + +```json +{ + "type": "object", + "properties": { + "subject_line": { "type": "string" }, + "email_body": { "type": "string" }, + "personalized_hook": { "type": "string" } + }, + "required": [...] +} +``` + +then the **API Response** node in Lamatic is emitting the **JSON Schema** only, not the **values** from **Generate Text**. + +**Fix in Lamatic Studio** + +1. Open the flow → **API Response** node → **Config** (and any **Mapping** / binding UI). +2. For each output field (`subject_line`, `email_body`, `personalized_hook`), bind the value to the **Generate Text** node — e.g. map from parsed LLM JSON or from `generatedResponse` (you may need a small parse/transform step in Studio if the UI requires it). +3. The schema defines the *shape*; each field must still have a *data source* from the LLM step. +4. **Save** and **redeploy** the flow. + +Until the live API returns real strings for those three keys (or a parseable `generatedResponse` blob), this app cannot show the email. + +--- + +## License + +MIT — see repository root [LICENSE](https://github.com/Lamatic/AgentKit/blob/main/LICENSE). diff --git a/kits/automation/cold-email-personalization/actions/orchestrate.ts b/kits/automation/cold-email-personalization/actions/orchestrate.ts new file mode 100644 index 00000000..40fcaedd --- /dev/null +++ b/kits/automation/cold-email-personalization/actions/orchestrate.ts @@ -0,0 +1,101 @@ +"use server" + +import { getLamaticClient } from "@/lib/lamatic-client" +import { parseColdEmailResult, type ColdEmailOutput } from "@/lib/parse-cold-email-result" +import { config } from "../orchestrate.js" + +export type ColdEmailInput = { + profile_data: string + prospect_name: string + prospect_role: string + company_name: string + product_description: string + value_proposition: string + call_to_action: string +} + +const INPUT_LIMITS: Record = { + profile_data: 3000, + prospect_name: 200, + prospect_role: 200, + company_name: 200, + product_description: 1500, + value_proposition: 1000, + call_to_action: 300, +} + +const REQUIRED_FIELDS: (keyof ColdEmailInput)[] = [ + "profile_data", + "prospect_name", + "company_name", + "product_description", + "value_proposition", + "call_to_action", +] + +const flow = config.flows.coldEmail + +export async function personalizeColdEmail(input: ColdEmailInput): Promise<{ + success: boolean + data?: ColdEmailOutput + error?: string +}> { + try { + if (!flow.workflowId) { + throw new Error("Workflow ID not configured (AUTOMATION_COLD_EMAIL).") + } + + for (const key of REQUIRED_FIELDS) { + if (!input[key]?.trim()) { + throw new Error(`Missing required field: ${key.replace(/_/g, " ")}`) + } + } + + for (const [key, max] of Object.entries(INPUT_LIMITS) as [keyof ColdEmailInput, number][]) { + if ((input[key] ?? "").length > max) { + throw new Error(`${key.replace(/_/g, " ")} exceeds the ${max}-character limit.`) + } + } + + const workflowInput = Object.keys(flow.inputSchema).reduce( + (acc, key) => { + acc[key] = input[key as keyof ColdEmailInput] + return acc + }, + {} as Record, + ) + + const client = getLamaticClient() + const response = await client.executeFlow(flow.workflowId, workflowInput) + + if (process.env.NODE_ENV === "development") { + console.log( + "[cold-email] executeFlow raw response:", + JSON.stringify(response, null, 2).slice(0, 4000), + ) + } + + const result = (response as Record).result + if ((response as Record).status === "error" || result == null) { + const msg = (response as Record).message + throw new Error(typeof msg === "string" ? msg : "Workflow returned an error or empty result.") + } + + const data = parseColdEmailResult(result) + return { success: true, data } + } catch (error) { + console.error("[cold-email] Error:", error instanceof Error ? error.message : error) + + let errorMessage = "Unknown error occurred" + if (error instanceof Error) { + errorMessage = error.message + if (error.message.includes("fetch failed")) { + errorMessage = "Network error: Unable to connect to Lamatic. Check your connection and try again." + } else if (error.message.includes("API key") || error.message.includes("401")) { + errorMessage = "Authentication error: Check LAMATIC_API_KEY and project settings." + } + } + + return { success: false, error: errorMessage } + } +} diff --git a/kits/automation/cold-email-personalization/app/globals.css b/kits/automation/cold-email-personalization/app/globals.css new file mode 100644 index 00000000..dc2aea17 --- /dev/null +++ b/kits/automation/cold-email-personalization/app/globals.css @@ -0,0 +1,125 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --font-sans: 'Geist', 'Geist Fallback'; + --font-mono: 'Geist Mono', 'Geist Mono Fallback'; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/kits/automation/cold-email-personalization/app/layout.tsx b/kits/automation/cold-email-personalization/app/layout.tsx new file mode 100644 index 00000000..07a5413c --- /dev/null +++ b/kits/automation/cold-email-personalization/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next" +import { Geist } from "next/font/google" +import { Analytics } from "@vercel/analytics/next" +import "./globals.css" + +const geist = Geist({ subsets: ["latin"] }) + +export const metadata: Metadata = { + title: "Cold Email Personalization | Lamatic AgentKit", + description: + "Generate hyper-personalized cold outreach emails for engineering internship applications using AI.", +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/kits/automation/cold-email-personalization/app/page.tsx b/kits/automation/cold-email-personalization/app/page.tsx new file mode 100644 index 00000000..4417efcf --- /dev/null +++ b/kits/automation/cold-email-personalization/app/page.tsx @@ -0,0 +1,338 @@ +"use client" + +import type React from "react" +import { useRef, useState } from "react" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Input } from "@/components/ui/input" +import { Card } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Loader2, Sparkles, Mail, Copy, Check, Home } from "lucide-react" +import { personalizeColdEmail } from "@/actions/orchestrate" +import { Header } from "@/components/header" + +const initialForm = { + profile_data: "", + prospect_name: "", + prospect_role: "", + company_name: "", + product_description: "", + value_proposition: "", + call_to_action: "", +} + +export default function ColdEmailPage() { + const [form, setForm] = useState(initialForm) + const [isLoading, setIsLoading] = useState(false) + const [result, setResult] = useState<{ + subject_line: string + email_body: string + personalized_hook: string + } | null>(null) + const [error, setError] = useState("") + const [copiedField, setCopiedField] = useState(null) + const copyTimeoutRef = useRef | null>(null) + + const INPUT_LIMITS: Record = { + profile_data: 3000, + prospect_name: 200, + prospect_role: 200, + company_name: 200, + product_description: 1500, + value_proposition: 1000, + call_to_action: 300, + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + const required = [ + "profile_data", + "prospect_name", + "company_name", + "product_description", + "value_proposition", + "call_to_action", + ] as const + for (const key of required) { + if (!form[key].trim()) { + setError(`Please fill in ${key.replace(/_/g, " ")}`) + return + } + } + for (const [key, max] of Object.entries(INPUT_LIMITS)) { + if (form[key as keyof typeof form].length > max) { + setError(`${key.replace(/_/g, " ")} exceeds the ${max}-character limit.`) + return + } + } + + setIsLoading(true) + setError("") + setResult(null) + + try { + const response = await personalizeColdEmail(form) + if (response.success && response.data) { + setResult(response.data) + } else { + setError(response.error || "Generation failed") + } + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred") + } finally { + setIsLoading(false) + } + } + + const handleReset = () => { + setResult(null) + setForm(initialForm) + setError("") + setCopiedField(null) + } + + const copyText = async (label: string, text: string) => { + try { + await navigator.clipboard.writeText(text) + } catch { + const ta = document.createElement("textarea") + ta.value = text + ta.style.position = "fixed" + ta.style.opacity = "0" + document.body.appendChild(ta) + ta.focus() + ta.select() + document.execCommand("copy") + document.body.removeChild(ta) + } + if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current) + setCopiedField(label) + copyTimeoutRef.current = setTimeout(() => setCopiedField(null), 2000) + } + + return ( +
+
+ +
+ {!result && ( +
+
+
+

+ Cold Email Personalization +

+

+ Paste a LinkedIn-style profile and your context. Get a subject line, email body, and + personalization hook for engineering internship outreach. +

+
+ + +
+
+ +