Build an AI Caption Generator 🤖
AI is perfect for creative blocks. Instead of staring at a blank screen, we can use an AI model (like OpenAI or Gemini) to generate catchy captions. This guide shows how to build the Frontend Interface that talks to an AI API.
Step 1: The Architecture 🏗️
We need a secure way to talk to the AI. Client-Side AI calls are dangerous because you expose your API keys. Instead, we use a standard Client → Server → AI pattern.
- Frontend: Collects user description & platform choice.
- Frontend: Sends
POSTrequest to/api/generate. - Backend (API Route): Calls the AI model securely.
- Backend: Returns an array of caption strings.
Step 2: The Data Fetching Logic 📡
On the client, we manage loading and error states while waiting for the server.
/* lib/api-client.js */
export const getCaptions = async (description, platform) => {
const res = await fetch('/api/ai/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'caption',
prompt: description,
platform: platform
})
});
if (!res.ok) throw new Error("API Error");
const data = await res.json();
return data.captions; // Expecting ["Caption 1", "Caption 2"]
}
Step 3: The React Component 💬
A clean UI that lets users input their prompt and see results.
"use client"
import { useState } from "react"
import { Sparkles, Copy, Loader2 } from "lucide-react"
export default function CaptionGen() {
const [desc, setDesc] = useState("")
const [captions, setCaptions] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const handleGenerate = async () => {
if(!desc) return;
setLoading(true);
// Simulate API Call delay
// In real app: const data = await getCaptions(desc, "instagram")
setTimeout(() => {
setCaptions([
"Sunset vibes & good times 🌅 #goldenhour",
"Chasing sunsets, catching dreams ✨",
"Painting the sky with colors today 🎨"
]);
setLoading(false);
}, 1500);
}
return (
<div className="max-w-xl mx-auto p-6 bg-slate-900 rounded-xl border border-slate-800 text-slate-100">
<div className="mb-6 space-y-2">
<label className="text-sm font-medium text-purple-400">What is your photo about?</label>
<textarea
value={desc}
onChange={e => setDesc(e.target.value)}
disabled={loading}
placeholder="e.g. A cute dog playing in the park..."
className="w-full h-32 p-4 rounded-lg bg-slate-800 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none resize-none transition"
/>
</div>
<button
onClick={handleGenerate}
disabled={loading || !desc}
className="w-full py-4 bg-gradient-to-r from-purple-500 to-pink-600 rounded-lg font-bold flex items-center justify-center gap-2 hover:opacity-90 disabled:opacity-50 transition"
>
{loading ? <Loader2 className="animate-spin" /> : <Sparkles />}
{loading ? 'Thinking...' : 'Generate Magic'}
</button>
{captions.length > 0 && (
<div className="mt-8 space-y-4 animate-in slide-in-from-bottom-2">
<h3 className="text-sm font-bold text-slate-400 uppercase tracking-widest">Your Captions</h3>
{captions.map((cap, i) => (
<div key={i} className="group p-4 bg-slate-800 rounded-lg border border-slate-700 flex justify-between gap-4 hover:border-purple-500/50 transition">
<p className="text-slate-200 text-sm leading-relaxed">{cap}</p>
<button
onClick={() => navigator.clipboard.writeText(cap)}
className="p-2 text-slate-500 hover:text-white transition"
title="Copy Text"
>
<Copy size={16} />
</button>
</div>
))}
</div>
)}
</div>
)
}
Step 4: Prompt Engineering 🗣️
The quality of AI output depends on the System Prompt on your backend. When sending the request to the AI, be specific about the Platform.
- Instagram: "Add relevant hashtags and emojis."
- LinkedIn: "Use a professional, insightful tone."
- Twitter: "Keep it under 280 chars, witty and concise."