diff --git a/kits/automation/blog-automation/.env.example b/kits/automation/blog-automation/.env.example new file mode 100644 index 0000000..2100138 --- /dev/null +++ b/kits/automation/blog-automation/.env.example @@ -0,0 +1,13 @@ +# Lamatic Flows +AUTOMATION_BLOG_DRAFTING="AGENTIC_GENERATE_CONTENT FLOW ID" +AUTOMATION_BLOG_SEO="AGENTIC_GENERATE_CONTENT FLOW ID" +AUTOMATION_BLOG_PUBLISH="AGENTIC_GENERATE_CONTENT FLOW ID" + +# Lamatic Core +LAMATIC_API_URL="LAMATIC_API_URL" +LAMATIC_PROJECT_ID="LAMATIC_PROJECT_ID" +LAMATIC_API_KEY="LAMATIC_API_KEY" + +# WordPress Configuration +WORDPRESS_SITE_ID="YOUR_SITE_ID" +WORDPRESS_TOKEN="YOUR_WORDPRESS_TOKEN" diff --git a/kits/automation/blog-automation/.gitignore b/kits/automation/blog-automation/.gitignore new file mode 100644 index 0000000..8ccc874 --- /dev/null +++ b/kits/automation/blog-automation/.gitignore @@ -0,0 +1,34 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/kits/automation/blog-automation/README.md b/kits/automation/blog-automation/README.md new file mode 100644 index 0000000..298d247 --- /dev/null +++ b/kits/automation/blog-automation/README.md @@ -0,0 +1,62 @@ +# Blog Writing Automation Agent Kit + +

+ Demo +

+ +**Blog Writing Automation** is an AI-powered tool built with [Lamatic.ai](https://lamatic.ai) that automates the generation and publishing of blog posts. It can be triggered externally via webhooks or scheduled tasks to maintain a consistent content pipeline. + +--- + +## 🛠️ How It Works (Step-by-Step) + +1. **External Trigger**: An external webhook (e.g., from CRM, Zapier, or a scheduler) signals the agentkit to start a new blog post. +2. **Payload Extraction**: The agent fetches the topic, target keywords, and stylistic instructions from the trigger payload. +3. **AI Drafting & SEO**: The agent drafts a blog post using AI, ensuring deep SEO optimization, coherence, and technical accuracy. +4. **Review Phase**: The draft can be reviewed (optionally by a human or another AI agent) before being finalized for publishing. +5. **Multi-Platform Publishing**: The post is automatically published to a CMS (WordPress, Ghost, etc.) or a static blog platform via API. +6. **Status Monitoring**: Logs and execution status of the publishing pipeline are maintained and visible in the dashboard. + +--- + +## 🔑 Setup + +### 1. Lamatic Flows +Before running this project, you must build and deploy the following flows in Lamatic: +- **Drafting Flow**: Input (topic, keywords) -> Output (`content` or `draft`). +- **SEO Flow**: Input (draft, keywords) -> Output (`optimized_content` or `content`). +- **Publish Flow**: Input (content, title) -> Output (`publish_status`, `url`). + +### 2. Environment Variables +Create a `.env` file in this directory and set the following keys: + +```bash +# Lamatic Flow IDs +AUTOMATION_BLOG_DRAFTING = "FLOW_ID_HERE" +AUTOMATION_BLOG_SEO = "FLOW_ID_HERE" +AUTOMATION_BLOG_PUBLISH = "FLOW_ID_HERE" + +# Lamatic Connection +LAMATIC_API_URL = "https://api.lamatic.ai" # Or your project-specific GraphQL endpoint +LAMATIC_PROJECT_ID = "YOUR_PROJECT_ID" +LAMATIC_API_KEY = "YOUR_API_KEY" +``` + +### 3. Install & Run +```bash +npm install +npm run dev +``` + +--- + +## 📂 Repo Structure +- `/actions/orchestrate.ts`: Handles the multi-step orchestration logic and field mapping. +- `/app/page.tsx`: Premium dashboard for manual triggers and status monitoring. +- `/orchestrate.js`: Configuration for flow dependencies and schemas. +- `/config.json`: Metadata for the AgentKit repository. + +--- + +## 🤝 Contributing +Refer to the main [CONTRIBUTING.md](../../../CONTRIBUTING.md) for global standards and coding patterns. diff --git a/kits/automation/blog-automation/actions/orchestrate.ts b/kits/automation/blog-automation/actions/orchestrate.ts new file mode 100644 index 0000000..ac79639 --- /dev/null +++ b/kits/automation/blog-automation/actions/orchestrate.ts @@ -0,0 +1,133 @@ +"use server" + +import { lamaticClient } from "@/lib/lamatic-client" +import { config } from "../orchestrate" + +export type BlogAutomationResult = { + success: boolean + draft?: string + optimizedContent?: string + url?: string + error?: string +} + +export async function runBlogAutomation( + topic: string, + keywords: string, + instructions: string +): Promise { + try { + const { flows } = config + + // MOCK MODE: For testing without live Lamatic keys + if (process.env.NEXT_PUBLIC_MOCK_MODE === "true") { + console.log("[Blog Automation] Running in MOCK MODE") + await new Promise(resolve => setTimeout(resolve, 2000)) // Simulate network lag + return { + success: true, + draft: "This is a mock draft for: " + topic, + optimizedContent: "# " + topic + "\n\nThis is optimized content with keywords: " + keywords + "\n\nGenerated with Blog Automation Kit.", + url: "https://example.com/mock-blog-post" + } + } + + // 1. Drafting Phase + console.log("[Blog Automation] Starting Drafting...") + const draftingFlow = flows.drafting + if (!draftingFlow.workflowId) throw new Error("Drafting Flow ID missing") + + const payload = { + topic, + keywords, + instructions, + // Common variations to ensure mapping + Topic: topic, + Keywords: keywords, + Instructions: instructions + } + console.log("[Blog Automation] Sending Payload to Drafting:", JSON.stringify(payload, null, 2)) + + const draftingRes = await lamaticClient.executeFlow(draftingFlow.workflowId, payload) + console.log("[Blog Automation] Drafting Response:", JSON.stringify(draftingRes, null, 2)) + + if (draftingRes?.status === "error") { + throw new Error(`Lamatic Error: ${draftingRes.message || "Unknown error"}`) + } + + const draft = draftingRes?.result?.generatedResponse || draftingRes?.result?.content || draftingRes?.result?.draft + if (!draft) throw new Error("Drafting failed: No content generated") + + // 2. SEO Optimization Phase + const seoFlow = flows.seo + if (!seoFlow.workflowId) throw new Error("SEO Flow ID missing") + + const seoPayload = { + draft, + keywords, + // Common variations to ensure mapping + content: draft, + text: draft, + Keywords: keywords + } + console.log("[Blog Automation] Sending Payload to SEO:", JSON.stringify(seoPayload, null, 2)) + + const seoRes = await lamaticClient.executeFlow(seoFlow.workflowId, seoPayload) + console.log("[Blog Automation] SEO Response:", JSON.stringify(seoRes, null, 2)) + + if (seoRes?.status === "error") { + throw new Error(`Lamatic SEO Error: ${seoRes.message || "Unknown error"}`) + } + const optimizedContent = seoRes?.result?.generatedResponse || seoRes?.result?.optimized_content || seoRes?.result?.content || seoRes?.result?.text + if (!optimizedContent) throw new Error("SEO Optimization failed") + + // 3. Publishing Phase + console.log("[Blog Automation] Starting Publishing...") + const publishFlow = flows.publish + if (!publishFlow.workflowId) throw new Error("Publish Flow ID missing") + + const publishPayload = { + content: optimizedContent, + title: topic, + // Common variations to ensure mapping + text: optimizedContent, + Topic: topic, + Title: topic + } + console.log("[Blog Automation] Sending Payload to Publish:", JSON.stringify(publishPayload, null, 2)) + + const publishRes = await lamaticClient.executeFlow(publishFlow.workflowId, publishPayload) + console.log("[Blog Automation] Publish Response:", JSON.stringify(publishRes, null, 2)) + + if (publishRes?.status === "error") { + throw new Error(`Lamatic Publish Error: ${publishRes.message || "Unknown error"}`) + } + + const url = publishRes?.result?.url || publishRes?.result?.post_url || publishRes?.result?.link || "" + const rawStatus = publishRes?.result?.publish_status || publishRes?.result?.status || publishRes?.status + const status = (rawStatus === "success" || rawStatus === "publish") ? "success" : rawStatus + const message = publishRes?.result?.message || "" + + // Detect if the response is an HTML error page (common with WordPress 404s) + if (typeof message === "string" && (message.includes("") || message.includes("Page not found"))) { + throw new Error("Publishing failed: The CMS returned a 'Page Not Found' (404) error. Please check your CMS endpoint in Lamatic Studio.") + } + + if (status !== "success" && !url) { + throw new Error(`Publishing failed: Response status was '${rawStatus}' and no URL was found.`) + } + + return { + success: true, + draft, + optimizedContent, + url: typeof url === "string" ? url : "" + } + + } catch (error) { + console.error("[Blog Automation] Error:", error) + return { + success: false, + error: error instanceof Error ? error.message : "An unexpected error occurred" + } + } +} diff --git a/kits/automation/blog-automation/app/globals.css b/kits/automation/blog-automation/app/globals.css new file mode 100644 index 0000000..dc2aea1 --- /dev/null +++ b/kits/automation/blog-automation/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/blog-automation/app/layout.tsx b/kits/automation/blog-automation/app/layout.tsx new file mode 100644 index 0000000..f169f54 --- /dev/null +++ b/kits/automation/blog-automation/app/layout.tsx @@ -0,0 +1,45 @@ +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import { Analytics } from '@vercel/analytics/next' +import './globals.css' + +const _geist = Geist({ subsets: ["latin"] }); +const _geistMono = Geist_Mono({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: 'Blog Automation', + description: 'Automate your content pipeline with Lamatic.ai', + generator: 'v0.app', + icons: { + icon: [ + { + url: '/icon-light-32x32.png', + media: '(prefers-color-scheme: light)', + }, + { + url: '/icon-dark-32x32.png', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/icon.svg', + type: 'image/svg+xml', + }, + ], + apple: '/apple-icon.png', + }, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/kits/automation/blog-automation/app/page.tsx b/kits/automation/blog-automation/app/page.tsx new file mode 100644 index 0000000..bc99daf --- /dev/null +++ b/kits/automation/blog-automation/app/page.tsx @@ -0,0 +1,330 @@ +"use client" + +import type React from "react" +import { useState, useCallback } 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 { + Loader2, + Sparkles, + FileText, + Copy, + Check, + Home, + ExternalLink, + RefreshCw, + Zap, + Layout, + Search, + CheckCircle2, + Clock, + ArrowRight +} from "lucide-react" +import { runBlogAutomation, type BlogAutomationResult } from "@/actions/orchestrate" +import ReactMarkdown from "react-markdown" +import { Header } from "@/components/header" + +export default function BlogAutomationPage() { + const [topic, setTopic] = useState("") + const [keywords, setKeywords] = useState("") + const [instructions, setInstructions] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [step, setStep] = useState<"idle" | "drafting" | "seo" | "publishing">("idle") + const [result, setResult] = useState(null) + const [error, setError] = useState("") + const [copied, setCopied] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!topic.trim()) { + setError("Please provide a topic") + return + } + + setIsLoading(true) + setError("") + setResult(null) + setCopied(false) + setStep("drafting") + + try { + // Small artificial delays to show the step-by-step nature of the automation + // only if not in MOCK MODE which already has a delay + const response = await runBlogAutomation(topic, keywords, instructions) + + if (response.success) { + setResult(response) + } else { + setError(response.error || "Automation failed") + } + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred") + } finally { + setIsLoading(false) + setStep("idle") + } + } + + const handleReset = () => { + setResult(null) + setTopic("") + setKeywords("") + setInstructions("") + setError("") + setCopied(false) + } + + const handleCopy = useCallback(async (text: string) => { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error("Failed to copy:", err) + } + }, []) + + return ( +
+
+
+
+
+ +
+ +
+ {!result && ( +
+
+
+ + Next-Gen Content Pipeline +
+

+ Craft Your Story.
+ Automatically. +

+

+ Transform a single topic into a fully optimized, ready-to-publish blog post in seconds using multi-agent intelligence. +

+
+ + +
+
+
+ + setTopic(e.target.value)} + disabled={isLoading} + className="h-12 bg-white/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 focus:ring-rose-500 transition-all text-base px-4 rounded-xl" + /> +
+ +
+ + setKeywords(e.target.value)} + disabled={isLoading} + className="h-12 bg-white/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 focus:ring-rose-500 transition-all text-base px-4 rounded-xl" + /> +
+ +
+ +