feat: Integrated Google Ads Generator feature
- Added GoogleAdsGenerator component with comprehensive keyword research and ad copy generation. - Updated Sidebar and Home page for navigation. - Added necessary UI components (Badge, Tabs) and dependencies. - Added custom animations for progress indicators.
This commit is contained in:
@@ -63,6 +63,7 @@
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -76,7 +77,9 @@
|
||||
}
|
||||
|
||||
/* Better touch targets */
|
||||
button, a, [role="button"] {
|
||||
button,
|
||||
a,
|
||||
[role="button"] {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
@@ -113,3 +116,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@keyframes progress-indeterminate {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-progress-indeterminate {
|
||||
animation: progress-indeterminate 1.5s infinite linear;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import PRDGenerator from "@/components/PRDGenerator";
|
||||
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
||||
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
||||
import SlidesGenerator from "@/components/SlidesGenerator";
|
||||
import GoogleAdsGenerator from "@/components/GoogleAdsGenerator";
|
||||
import HistoryPanel from "@/components/HistoryPanel";
|
||||
import SettingsPanel from "@/components/SettingsPanel";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
@@ -32,6 +33,8 @@ export default function Home() {
|
||||
return <UXDesignerPrompt />;
|
||||
case "slides":
|
||||
return <SlidesGenerator />;
|
||||
case "googleads":
|
||||
return <GoogleAdsGenerator />;
|
||||
case "history":
|
||||
return <HistoryPanel />;
|
||||
case "settings":
|
||||
|
||||
871
components/GoogleAdsGenerator.tsx
Normal file
871
components/GoogleAdsGenerator.tsx
Normal file
@@ -0,0 +1,871 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import {
|
||||
Search,
|
||||
Target,
|
||||
DollarSign,
|
||||
Globe,
|
||||
Calendar,
|
||||
Zap,
|
||||
BarChart3,
|
||||
Layers,
|
||||
ShieldCheck,
|
||||
Loader2,
|
||||
Copy,
|
||||
Download,
|
||||
ExternalLink,
|
||||
MousePointer2,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Megaphone,
|
||||
Briefcase,
|
||||
TrendingUp,
|
||||
X,
|
||||
Plus
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GoogleAdsResult, GoogleAdsKeyword, GoogleAdCopy, GoogleAdsCampaign } from "@/types";
|
||||
|
||||
export default function GoogleAdsGenerator() {
|
||||
const {
|
||||
googleAdsResult,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
availableModels,
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
setGoogleAdsResult,
|
||||
setProcessing,
|
||||
setError,
|
||||
setAvailableModels,
|
||||
setSelectedModel,
|
||||
setSelectedProvider,
|
||||
} = useStore();
|
||||
|
||||
// Input states
|
||||
const [websiteUrl, setWebsiteUrl] = useState("");
|
||||
const [products, setProducts] = useState<string[]>([""]);
|
||||
const [targetAudience, setTargetAudience] = useState("");
|
||||
const [budgetMin, setBudgetMin] = useState("500");
|
||||
const [budgetMax, setBudgetMax] = useState("2000");
|
||||
const [currency, setCurrency] = useState("USD");
|
||||
const [duration, setDuration] = useState("30 days");
|
||||
const [industry, setIndustry] = useState("");
|
||||
|
||||
const [activeTab, setActiveTab] = useState("input");
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
loadAvailableModels();
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load models:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const addProduct = () => setProducts([...products, ""]);
|
||||
const removeProduct = (index: number) => {
|
||||
const newProducts = products.filter((_, i) => i !== index);
|
||||
setProducts(newProducts.length ? newProducts : [""]);
|
||||
};
|
||||
const updateProduct = (index: number, value: string) => {
|
||||
const newProducts = [...products];
|
||||
newProducts[index] = value;
|
||||
setProducts(newProducts);
|
||||
};
|
||||
|
||||
const validateUrl = (url: string) => {
|
||||
try {
|
||||
new URL(url.startsWith("http") ? url : `https://${url}`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!websiteUrl.trim()) {
|
||||
setError("Please enter a website URL");
|
||||
return;
|
||||
}
|
||||
if (!validateUrl(websiteUrl)) {
|
||||
setError("Please enter a valid URL");
|
||||
return;
|
||||
}
|
||||
const filteredProducts = products.filter(p => p.trim() !== "");
|
||||
if (filteredProducts.length === 0) {
|
||||
setError("Please add at least one product or service");
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKey = apiKeys[selectedProvider];
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
setError(null);
|
||||
setActiveTab("input");
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.generateGoogleAds(websiteUrl, {
|
||||
productsServices: filteredProducts,
|
||||
targetAudience,
|
||||
budgetRange: { min: parseInt(budgetMin), max: parseInt(budgetMax), currency },
|
||||
campaignDuration: duration,
|
||||
industry,
|
||||
language: "English"
|
||||
}, selectedProvider, selectedModel);
|
||||
|
||||
if (result.success && result.data) {
|
||||
try {
|
||||
const parsedData = typeof result.data === 'string' ? JSON.parse(result.data) : result.data;
|
||||
const adsResult: GoogleAdsResult = {
|
||||
...parsedData,
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
websiteUrl,
|
||||
productsServices: filteredProducts,
|
||||
generatedAt: new Date(),
|
||||
rawContent: typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2)
|
||||
};
|
||||
setGoogleAdsResult(adsResult);
|
||||
setActiveTab("keywords");
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ads data:", e);
|
||||
setError("Failed to parse the generated ads content. Please try again.");
|
||||
}
|
||||
} else {
|
||||
setError(result.error || "Failed to generate Google Ads campaign");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Ads Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async (text: string, id: string) => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(id);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
};
|
||||
|
||||
const exportToCsv = () => {
|
||||
if (!googleAdsResult) return;
|
||||
|
||||
let csvContent = "data:text/csv;charset=utf-8,";
|
||||
csvContent += "Type,Category,Headline/Keyword,Description/Value,Details\n";
|
||||
|
||||
// Keywords
|
||||
googleAdsResult.keywords.primary.forEach(k => {
|
||||
csvContent += `Keyword,Primary,"${k.keyword}","${k.searchVolume || ''}","${k.competition || ''}"\n`;
|
||||
});
|
||||
googleAdsResult.keywords.longTail.forEach(k => {
|
||||
csvContent += `Keyword,Long-Tail,"${k.keyword}","${k.searchVolume || ''}","${k.competition || ''}"\n`;
|
||||
});
|
||||
googleAdsResult.keywords.negative.forEach(k => {
|
||||
csvContent += `Keyword,Negative,"${k.keyword}","",""\n`;
|
||||
});
|
||||
|
||||
// Ads
|
||||
googleAdsResult.adCopies.forEach((ad, i) => {
|
||||
ad.headlines.forEach((h, j) => {
|
||||
csvContent += `Ad Copy,Headline ${j + 1},"${h}","",""\n`;
|
||||
});
|
||||
ad.descriptions.forEach((d, j) => {
|
||||
csvContent += `Ad Copy,Description ${j + 1},"${d}","",""\n`;
|
||||
});
|
||||
});
|
||||
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", `google_ads_${googleAdsResult.id}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl animate-in fade-in duration-500">
|
||||
<div className="mb-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent flex items-center gap-3">
|
||||
<Megaphone className="h-8 w-8 text-blue-600" />
|
||||
Google Ads Gen
|
||||
<Badge variant="secondary" className="bg-blue-100 text-blue-700 border-blue-200">PRO</Badge>
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Generate high-converting keywords, ad copy, and campaign structures with AI.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{googleAdsResult && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={exportToCsv} className="gap-2">
|
||||
<Download className="h-4 w-4" />
|
||||
Export CSV
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setGoogleAdsResult(null)} className="gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
New Campaign
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 grid-cols-1 lg:grid-cols-12">
|
||||
{/* Input Panel */}
|
||||
<div className={cn("lg:col-span-4 space-y-6", activeTab !== "input" && googleAdsResult && "hidden lg:block")}>
|
||||
<Card className="border-blue-100 shadow-sm overflow-hidden">
|
||||
<div className="h-1.5 bg-gradient-to-r from-blue-500 to-indigo-600" />
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Target className="h-5 w-5 text-blue-600" />
|
||||
Campaign Parameters
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Define your goals and target audience.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold flex items-center gap-2 text-foreground/80">
|
||||
<Globe className="h-4 w-4" />
|
||||
Website URL
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g. https://www.your-business.com"
|
||||
value={websiteUrl}
|
||||
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||
className="bg-muted/30 focus-visible:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold flex items-center gap-2 text-foreground/80">
|
||||
<Briefcase className="h-4 w-4" />
|
||||
Products or Services
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{products.map((product, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={`Service/Product ${index + 1}`}
|
||||
value={product}
|
||||
onChange={(e) => updateProduct(index, e.target.value)}
|
||||
className="bg-muted/30 focus-visible:ring-blue-500"
|
||||
/>
|
||||
{products.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeProduct(index)}
|
||||
className="text-muted-foreground hover:text-destructive h-10 w-10"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addProduct}
|
||||
className="w-full border-dashed border-blue-200 text-blue-600 hover:bg-blue-50 hover:text-blue-700"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Another
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="p-3 bg-blue-50/50 rounded-lg border border-blue-100/50 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-semibold text-blue-800 flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4" />
|
||||
Monthly Budget
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="relative flex-1">
|
||||
<span className="absolute left-3 top-2.5 text-muted-foreground text-xs">$</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={budgetMin}
|
||||
onChange={(e) => setBudgetMin(e.target.value)}
|
||||
className="pl-7 bg-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-muted-foreground">to</span>
|
||||
<div className="relative flex-1">
|
||||
<span className="absolute left-3 top-2.5 text-muted-foreground text-xs">$</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={budgetMax}
|
||||
onChange={(e) => setBudgetMax(e.target.value)}
|
||||
className="pl-7 bg-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Industry
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g. SaaS"
|
||||
value={industry}
|
||||
onChange={(e) => setIndustry(e.target.value)}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Duration
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g. 30 days"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Target Audience
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g. Small business owners in US"
|
||||
value={targetAudience}
|
||||
onChange={(e) => setTargetAudience(e.target.value)}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-muted">
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={cn(
|
||||
"capitalize h-8 px-3 text-xs",
|
||||
selectedProvider === provider ? "bg-blue-600 hover:bg-blue-700" : ""
|
||||
)}
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs mb-4"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-xs text-destructive flex items-start gap-2 mb-4 animate-in slide-in-from-top-1 duration-300">
|
||||
<AlertCircle className="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
disabled={isProcessing || !websiteUrl}
|
||||
className="w-full h-11 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 font-semibold shadow-md shadow-blue-200 text-sm group"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Analyzing Website...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="mr-2 h-4 w-4 group-hover:fill-current transition-all" />
|
||||
Generate Google Ads Campaign
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="rounded-xl border bg-gradient-to-br from-indigo-50 to-blue-50 p-5 border-blue-100 shadow-sm">
|
||||
<h4 className="font-bold text-blue-900 flex items-center gap-2 mb-3">
|
||||
<ShieldCheck className="h-5 w-5 text-indigo-600" />
|
||||
Quality Assurance
|
||||
</h4>
|
||||
<ul className="space-y-2.5">
|
||||
{[
|
||||
"Character limit enforcement",
|
||||
"Keyword relevance matching >85%",
|
||||
"Google Ads policy compliance",
|
||||
"Mobile optimization focus"
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-2 text-sm text-blue-800/80">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Panel */}
|
||||
<div className="lg:col-span-8 flex flex-col h-full min-h-[600px]">
|
||||
{!googleAdsResult && !isProcessing && (
|
||||
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed rounded-3xl bg-muted/20 p-12 text-center">
|
||||
<div className="w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center mb-6">
|
||||
<Megaphone className="h-10 w-10 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-foreground">Ready to Launch?</h3>
|
||||
<p className="text-muted-foreground max-w-sm mt-2 mb-6 text-sm">
|
||||
Enter your website URL and products on the left to generate keyword research, ad copy, and a full campaign structure.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Badge variant="outline" className="px-3 py-1 bg-white">15+ Years Domain Knowledge</Badge>
|
||||
<Badge variant="outline" className="px-3 py-1 bg-white">Quality Score Optimization</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isProcessing && (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center animate-pulse">
|
||||
<div className="relative mb-8">
|
||||
<div className="w-24 h-24 border-4 border-blue-100 rounded-full animate-spin" style={{ borderTopColor: 'rgb(37 99 235)' }} />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Search className="h-8 w-8 text-blue-600 animate-bounce" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">Analyzing Domain Content</h3>
|
||||
<div className="max-w-md w-full bg-muted/40 rounded-full h-2 mb-4 overflow-hidden">
|
||||
<div className="bg-blue-600 h-full animate-progress-indeterminate w-[60%]" />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Scanning {websiteUrl}, fetching search volumes, and drafting high-converting copy...
|
||||
</p>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mt-12 w-full max-w-2xl px-4">
|
||||
{[
|
||||
{ label: "Keywords", delay: "0s" },
|
||||
{ label: "Ad Copy", delay: "0.2s" },
|
||||
{ label: "Targeting", delay: "0.4s" },
|
||||
{ label: "ROI Modeling", delay: "0.6s" }
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex flex-col items-center gap-2 animate-in fade-in slide-in-from-bottom-2 duration-700" style={{ animationDelay: item.delay }}>
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<Loader2 className="h-5 w-5 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-muted-foreground">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{googleAdsResult && (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col animate-in fade-in slide-in-from-right-4 duration-500">
|
||||
<div className="bg-muted/50 p-1.5 rounded-xl border mb-6 flex items-center justify-between">
|
||||
<TabsList className="bg-transparent border-0 gap-1">
|
||||
<TabsTrigger value="keywords" className="data-[state=active]:bg-white data-[state=active]:shadow-sm rounded-lg px-4 gap-2 text-sm h-9">
|
||||
<Search className="h-4 w-4 text-blue-600" />
|
||||
Keywords
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="creative" className="data-[state=active]:bg-white data-[state=active]:shadow-sm rounded-lg px-4 gap-2 text-sm h-9">
|
||||
<Megaphone className="h-4 w-4 text-pink-600" />
|
||||
Ad Templates
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="structure" className="data-[state=active]:bg-white data-[state=active]:shadow-sm rounded-lg px-4 gap-2 text-sm h-9">
|
||||
<Layers className="h-4 w-4 text-indigo-600" />
|
||||
Campaigns
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="implementation" className="data-[state=active]:bg-white data-[state=active]:shadow-sm rounded-lg px-4 gap-2 text-sm h-9">
|
||||
<Zap className="h-4 w-4 text-yellow-600" />
|
||||
Launch Guide
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{googleAdsResult.predictions && (
|
||||
<div className="hidden md:flex items-center gap-4 px-4 text-xs font-medium text-muted-foreground border-l border-muted-foreground/20">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] uppercase">Est. CTR</span>
|
||||
<span className="text-blue-600">{googleAdsResult.predictions.estimatedCtr}</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] uppercase">Conversions</span>
|
||||
<span className="text-blue-600">{googleAdsResult.predictions.estimatedConversions}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto pr-1">
|
||||
<TabsContent value="keywords" className="m-0 space-y-6 animate-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card className="border-green-100 shadow-sm hover:shadow-md transition-shadow">
|
||||
<CardHeader className="bg-green-50/50 py-4 border-b border-green-50">
|
||||
<CardTitle className="text-base flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5 text-green-600" />
|
||||
Primary Keywords
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleCopy(googleAdsResult.keywords.primary.map(k => k.keyword).join('\n'), 'primary-keys')} className="h-7 px-2">
|
||||
{copied === 'primary-keys' ? <CheckCircle2 className="h-4 w-4 text-green-600" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y text-sm">
|
||||
{googleAdsResult.keywords.primary.map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 hover:bg-muted/30 transition-colors group">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-foreground">{item.keyword}</span>
|
||||
<div className="flex gap-2 text-[10px] mt-0.5">
|
||||
<span className="text-muted-foreground">Vol: {item.searchVolume}</span>
|
||||
<span className="text-blue-600">CPC: {item.cpc}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className={cn(
|
||||
"text-[10px] capitalize",
|
||||
item.competition === 'low' ? "bg-green-100 text-green-700" :
|
||||
item.competition === 'medium' ? "bg-yellow-100 text-yellow-700" : "bg-red-100 text-red-700"
|
||||
)}>
|
||||
{item.competition}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-blue-100 shadow-sm hover:shadow-md transition-shadow">
|
||||
<CardHeader className="bg-blue-50/50 py-4 border-b border-blue-50">
|
||||
<CardTitle className="text-base flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<MousePointer2 className="h-5 w-5 text-blue-600" />
|
||||
Long-Tail Opportunities
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleCopy(googleAdsResult.keywords.longTail.map(k => k.keyword).join('\n'), 'long-keys')} className="h-7 px-2">
|
||||
{copied === 'long-keys' ? <CheckCircle2 className="h-4 w-4 text-green-600" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y text-sm">
|
||||
{googleAdsResult.keywords.longTail.map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 hover:bg-muted/30 transition-colors">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-foreground">{item.keyword}</span>
|
||||
<span className="text-[10px] text-muted-foreground mt-0.5">High Performance Match: {item.difficultyScore}%</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-blue-600">{item.cpc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="border-red-100 shadow-sm border-l-4 border-l-red-500">
|
||||
<CardHeader className="py-4">
|
||||
<CardTitle className="text-base flex items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-red-700">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
Negative Keyword List (Add these to Campaigns)
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleCopy(googleAdsResult.keywords.negative.map(k => k.keyword).join('\n'), 'neg-keys')} className="h-7 px-2 hover:bg-red-50">
|
||||
{copied === 'neg-keys' ? <CheckCircle2 className="h-4 w-4 text-green-600" /> : <Copy className="h-4 w-4 text-red-600" />}
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription>Exclude these terms to prevent wasted spend on irrelevant clicks.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{googleAdsResult.keywords.negative.map((item, i) => (
|
||||
<Badge key={i} variant="outline" className="bg-red-50 text-red-700 border-red-100 hover:bg-red-100 hover:text-red-800 transition-colors">
|
||||
{item.keyword}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="creative" className="m-0 space-y-6 animate-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{googleAdsResult.adCopies.map((ad, i) => (
|
||||
<Card key={i} className="border border-muted-foreground/10 overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300">
|
||||
<div className="bg-muted px-4 py-2 border-b flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Ad Variation {i + 1} • {ad.campaignType}</span>
|
||||
</div>
|
||||
{ad.mobileOptimized && (
|
||||
<Badge className="bg-blue-100 text-blue-700 hover:bg-blue-100 border-none text-[9px] h-4">Mobile Ready</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-5 space-y-4 bg-white">
|
||||
<div className="space-y-1">
|
||||
{ad.headlines.map((h, j) => (
|
||||
<div key={j} className="flex items-center justify-between group">
|
||||
<span className="text-blue-700 font-semibold text-base line-clamp-1">{h}</span>
|
||||
<Badge variant="outline" className="text-[9px] opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">{h.length}/30</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-green-800 font-medium flex items-center gap-1">
|
||||
Ads • <span className="underline cursor-pointer">{ad.displayUrl}</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{ad.descriptions.map((d, j) => (
|
||||
<div key={j} className="flex items-start justify-between group">
|
||||
<p className="text-sm text-foreground/80 leading-relaxed pr-4">{d}</p>
|
||||
<Badge variant="outline" className="text-[9px] opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap mt-1">{d.length}/90</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex items-center justify-between border-t border-muted">
|
||||
<Button size="sm" variant="outline" className="h-8 text-xs bg-indigo-50 text-indigo-700 border-indigo-100 hover:bg-indigo-100">
|
||||
{ad.callToAction}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleCopy(`${ad.headlines.join('\n')}\n\n${ad.descriptions.join('\n')}`, `ad-${i}`)} className="h-8 px-2 gap-2 text-xs">
|
||||
{copied === `ad-${i}` ? <CheckCircle2 className="h-3.5 w-3.5 text-green-600" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
Copy Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl p-6 bg-gradient-to-r from-blue-600 to-indigo-700 text-white shadow-lg overflow-hidden relative">
|
||||
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-bold flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 fill-yellow-400 text-yellow-400" />
|
||||
A/B Testing Recommendation
|
||||
</h4>
|
||||
<p className="text-blue-50 text-sm max-w-xl">
|
||||
Use dynamic keyword insertion for "Variation 1" and focus on emotion-based headlines for "Variation 2" to compare user engagement and CTR.
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="secondary" className="bg-white text-blue-700 hover:bg-blue-50 font-bold whitespace-nowrap" onClick={() => setActiveTab("implementation")}>
|
||||
View Optimization Guide
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute right-[-20px] top-[-20px] opacity-10 rotate-12">
|
||||
<Target className="w-48 h-48" />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="structure" className="m-0 space-y-6 animate-in slide-in-from-bottom-4 duration-500">
|
||||
{googleAdsResult.campaigns.map((camp, i) => (
|
||||
<Card key={i} className="border-indigo-100 shadow-sm overflow-hidden">
|
||||
<CardHeader className="bg-indigo-50/50 flex flex-row items-center justify-between py-4 border-b border-indigo-50">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg text-indigo-900">{camp.name}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2">
|
||||
<Badge className="bg-indigo-600 hover:bg-indigo-600">{camp.type.toUpperCase()}</Badge>
|
||||
<span>Targeting: {camp.targeting.locations?.join(', ') || 'Global'}</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs font-semibold text-muted-foreground uppercase">Monthly Budget</div>
|
||||
<div className="text-xl font-bold text-indigo-700">{camp.budget.currency} {camp.budget.monthly}</div>
|
||||
<div className="text-[10px] text-muted-foreground italic">Approx. {camp.budget.currency}{camp.budget.daily}/day</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
<h4 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-4">Ad Group Organization</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{camp.adGroups.map((group, j) => (
|
||||
<div key={j} className="p-4 rounded-xl border border-indigo-50 bg-white hover:border-indigo-200 transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h5 className="font-bold text-indigo-900">{group.name}</h5>
|
||||
<Badge variant="outline" className="text-[9px] border-indigo-200 text-indigo-600 uppercase">{group.biddingStrategy}</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-3">{group.theme}</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{group.keywords.map((kw, k) => (
|
||||
<span key={k} className="text-[10px] bg-muted px-2 py-0.5 rounded-full text-foreground/70">{kw}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4 p-4 rounded-2xl bg-muted/30">
|
||||
<div className="space-y-1">
|
||||
<div className="text-[10px] font-bold text-muted-foreground uppercase">Target Locations</div>
|
||||
<div className="text-xs font-medium">{camp.targeting.locations?.join(', ') || 'All'}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-[10px] font-bold text-muted-foreground uppercase">Devices</div>
|
||||
<div className="text-xs font-medium">{camp.targeting.devices?.join(', ') || 'All Devices'}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-[10px] font-bold text-muted-foreground uppercase">Demographics</div>
|
||||
<div className="text-xs font-medium">{camp.targeting.demographics?.join(', ') || 'All Ages'}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-[10px] font-bold text-muted-foreground uppercase">Schedule</div>
|
||||
<div className="text-xs font-medium">{camp.targeting.schedule?.join(', ') || '24/7'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 rounded-xl border border-yellow-100 bg-yellow-50/30">
|
||||
<h5 className="text-xs font-bold text-yellow-800 uppercase mb-2">Bidding Strategy</h5>
|
||||
<p className="text-xs text-yellow-900/70 leading-relaxed">
|
||||
We recommend <strong>Maximize Conversions</strong> with a target CPA to balance volume and ROI given your industry competition.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl border border-blue-100 bg-blue-50/30">
|
||||
<h5 className="text-xs font-bold text-blue-800 uppercase mb-2">Network Selection</h5>
|
||||
<p className="text-xs text-blue-900/70 leading-relaxed">
|
||||
Include <strong>Search Partners</strong> but disable Display Network for this campaign to ensure high search intent traffic.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl border border-indigo-100 bg-indigo-50/30">
|
||||
<h5 className="text-xs font-bold text-indigo-800 uppercase mb-2">Ad Assets</h5>
|
||||
<p className="text-xs text-indigo-900/70 leading-relaxed">
|
||||
Add <strong>Sitelinks, Callouts, and Image assets</strong> to improve your Ad Rank and Quality Score.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="implementation" className="m-0 space-y-6 animate-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-full bg-blue-600 text-white flex items-center justify-center text-sm">1</span>
|
||||
Launch Steps
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{googleAdsResult.implementation.setupSteps.map((step, i) => (
|
||||
<div key={i} className="flex gap-4 p-4 rounded-xl border bg-white shadow-sm group hover:border-blue-200 transition-all">
|
||||
<div className="text-2xl font-black text-muted-foreground/20 group-hover:text-blue-200 transition-colors uppercase italic">{i + 1}</div>
|
||||
<p className="text-sm font-medium pt-1 text-foreground/80">{step}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-full bg-green-600 text-white flex items-center justify-center text-sm">2</span>
|
||||
Quality Score Hacks
|
||||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{googleAdsResult.implementation.qualityScoreTips.map((tip, i) => (
|
||||
<div key={i} className="p-3 bg-green-50 text-green-800 text-xs rounded-lg border border-green-100/50 font-medium">
|
||||
{tip}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-full bg-indigo-600 text-white flex items-center justify-center text-sm">3</span>
|
||||
Tracking & Analytics
|
||||
</h3>
|
||||
<div className="p-5 rounded-xl border border-indigo-100 bg-white">
|
||||
<ul className="space-y-3">
|
||||
{googleAdsResult.implementation.trackingSetup.map((item, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-xs text-muted-foreground">
|
||||
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button className="w-full mt-6 bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border-indigo-100 gap-2" variant="outline">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Google Tag Manager Setup
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gradient-to-br from-indigo-900 to-indigo-950 border-0 shadow-2xl">
|
||||
<CardContent className="p-10 flex flex-col md:flex-row items-center gap-10">
|
||||
<div className="w-24 h-24 bg-white/10 rounded-2xl flex items-center justify-center backdrop-blur-sm border border-white/10 shrink-0">
|
||||
<TrendingUp className="h-12 w-12 text-blue-400" />
|
||||
</div>
|
||||
<div className="space-y-4 flex-1">
|
||||
<h4 className="text-2xl font-bold text-white">Scale Your Success</h4>
|
||||
<p className="text-blue-100 text-sm leading-relaxed max-w-2xl">
|
||||
Success in Google Ads is about iterative optimization. Use these initial settings to gather data for 14 days, then begin aggressive A/B testing on headlines and landing page consistency.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 pt-4">
|
||||
{googleAdsResult.implementation.optimizationTips.slice(0, 3).map((tip, i) => (
|
||||
<Badge key={i} variant="secondary" className="bg-white/10 text-blue-100 hover:bg-white/20 border-white/5 px-3 py-1 text-[10px] font-medium uppercase tracking-wider">
|
||||
{tip}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useStore from "@/lib/store";
|
||||
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X } from "lucide-react";
|
||||
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "history" | "settings";
|
||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "history" | "settings";
|
||||
|
||||
interface SidebarProps {
|
||||
currentView: View;
|
||||
@@ -23,6 +23,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
||||
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
|
||||
{ id: "slides" as View, label: "Slides Generator", icon: Presentation },
|
||||
{ id: "googleads" as View, label: "Google Ads Gen", icon: Megaphone },
|
||||
{ id: "history" as View, label: "History", icon: History, count: history.length },
|
||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
||||
];
|
||||
|
||||
36
components/ui/badge.tsx
Normal file
36
components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> { }
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
55
components/ui/tabs.tsx
Normal file
55
components/ui/tabs.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -207,6 +207,25 @@ export class ModelAdapter {
|
||||
return this.callWithFallback((service) => service.generateSlides(topic, options, model), providers);
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
} = { productsServices: [] },
|
||||
provider?: ModelProvider,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateGoogleAds(websiteUrl, options, model), providers);
|
||||
}
|
||||
|
||||
async chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
model: string,
|
||||
|
||||
@@ -434,7 +434,100 @@ Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients,
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English"
|
||||
} = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an EXPERT Google Ads strategist. Create HIGH-CONVERTING campaigns with comprehensive keyword research, compelling ad copy, and optimized campaign structures.
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"keywords": {
|
||||
"primary": [{"keyword": "term", "type": "primary", "searchVolume": 12000, "competition": "medium", "cpc": "$2.50"}],
|
||||
"longTail": [{"keyword": "specific term", "type": "long-tail", "searchVolume": 1200, "competition": "low", "cpc": "$1.25"}],
|
||||
"negative": [{"keyword": "exclude term", "type": "negative", "competition": "low"}]
|
||||
},
|
||||
"adCopies": [{
|
||||
"id": "ad-1",
|
||||
"campaignType": "search",
|
||||
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
||||
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
||||
"callToAction": "Get Started",
|
||||
"mobileOptimized": true
|
||||
}],
|
||||
"campaigns": [{
|
||||
"id": "campaign-1",
|
||||
"name": "Campaign Name",
|
||||
"type": "search",
|
||||
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
||||
"targeting": {"locations": [], "demographics": [], "devices": []},
|
||||
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Maximize conversions"}]
|
||||
}],
|
||||
"implementation": {
|
||||
"setupSteps": [],
|
||||
"qualityScoreTips": [],
|
||||
"trackingSetup": [],
|
||||
"optimizationTips": []
|
||||
},
|
||||
"predictions": {
|
||||
"estimatedClicks": "500-800/month",
|
||||
"estimatedImpressions": "15,000-25,000/month",
|
||||
"estimatedCtr": "3.2%-4.5%",
|
||||
"estimatedConversions": "25-50/month"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Requirements:
|
||||
- 10-15 primary keywords, 15-20 long-tail, 5-10 negative
|
||||
- Headlines max 30 chars, descriptions max 90 chars
|
||||
- 3-5 ad variations per campaign
|
||||
- Include budget and targeting recommendations`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a Google Ads campaign for:
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
PRODUCTS/SERVICES: ${productsServices.join(", ")}
|
||||
TARGET AUDIENCE: ${targetAudience}
|
||||
INDUSTRY: ${industry}
|
||||
LANGUAGE: ${language}
|
||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||
|
||||
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
}
|
||||
|
||||
export default OllamaCloudService;
|
||||
|
||||
|
||||
|
||||
@@ -762,6 +762,98 @@ Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients,
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English"
|
||||
} = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an EXPERT Google Ads strategist. Create HIGH-CONVERTING campaigns with comprehensive keyword research, compelling ad copy, and optimized campaign structures.
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"keywords": {
|
||||
"primary": [{"keyword": "term", "type": "primary", "searchVolume": 12000, "competition": "medium", "cpc": "$2.50"}],
|
||||
"longTail": [{"keyword": "specific term", "type": "long-tail", "searchVolume": 1200, "competition": "low", "cpc": "$1.25"}],
|
||||
"negative": [{"keyword": "exclude term", "type": "negative", "competition": "low"}]
|
||||
},
|
||||
"adCopies": [{
|
||||
"id": "ad-1",
|
||||
"campaignType": "search",
|
||||
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
|
||||
"descriptions": ["Description 1 (90 chars)", "Description 2"],
|
||||
"callToAction": "Get Started",
|
||||
"mobileOptimized": true
|
||||
}],
|
||||
"campaigns": [{
|
||||
"id": "campaign-1",
|
||||
"name": "Campaign Name",
|
||||
"type": "search",
|
||||
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
|
||||
"targeting": {"locations": [], "demographics": [], "devices": []},
|
||||
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Maximize conversions"}]
|
||||
}],
|
||||
"implementation": {
|
||||
"setupSteps": [],
|
||||
"qualityScoreTips": [],
|
||||
"trackingSetup": [],
|
||||
"optimizationTips": []
|
||||
},
|
||||
"predictions": {
|
||||
"estimatedClicks": "500-800/month",
|
||||
"estimatedImpressions": "15,000-25,000/month",
|
||||
"estimatedCtr": "3.2%-4.5%",
|
||||
"estimatedConversions": "25-50/month"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Requirements:
|
||||
- 10-15 primary keywords, 15-20 long-tail, 5-10 negative
|
||||
- Headlines max 30 chars, descriptions max 90 chars
|
||||
- 3-5 ad variations per campaign
|
||||
- Include budget and targeting recommendations`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a Google Ads campaign for:
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
PRODUCTS/SERVICES: ${productsServices.join(", ")}
|
||||
TARGET AUDIENCE: ${targetAudience}
|
||||
INDUSTRY: ${industry}
|
||||
LANGUAGE: ${language}
|
||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||
|
||||
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async listModels(): Promise<APIResponse<string[]>> {
|
||||
const models = [
|
||||
"coder-model",
|
||||
@@ -780,3 +872,4 @@ const qwenOAuthService = new QwenOAuthService();
|
||||
export default qwenOAuthService;
|
||||
export { qwenOAuthService };
|
||||
|
||||
|
||||
|
||||
@@ -407,7 +407,206 @@ Return the complete JSON with full htmlContent for each slide. Make each slide V
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generateGoogleAds(
|
||||
websiteUrl: string,
|
||||
options: {
|
||||
productsServices: string[];
|
||||
targetAudience?: string;
|
||||
budgetRange?: { min: number; max: number; currency: string };
|
||||
campaignDuration?: string;
|
||||
industry?: string;
|
||||
competitors?: string[];
|
||||
language?: string;
|
||||
} = { productsServices: [] },
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const {
|
||||
productsServices = [],
|
||||
targetAudience = "General consumers",
|
||||
budgetRange,
|
||||
campaignDuration,
|
||||
industry = "General",
|
||||
competitors = [],
|
||||
language = "English"
|
||||
} = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are an EXPERT Google Ads strategist with 15+ years of experience managing $100M+ in ad spend. You create HIGH-CONVERTING campaigns that consistently outperform industry benchmarks.
|
||||
|
||||
Your expertise includes:
|
||||
- Keyword research and competitive analysis
|
||||
- Ad copywriting that drives clicks and conversions
|
||||
- Campaign structure optimization
|
||||
- Quality Score improvement strategies
|
||||
- ROI maximization techniques
|
||||
|
||||
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||
\`\`\`json
|
||||
{
|
||||
"keywords": {
|
||||
"primary": [
|
||||
{
|
||||
"keyword": "exact keyword phrase",
|
||||
"type": "primary",
|
||||
"searchVolume": 12000,
|
||||
"competition": "medium",
|
||||
"difficultyScore": 65,
|
||||
"relevanceScore": 95,
|
||||
"cpc": "$2.50"
|
||||
}
|
||||
],
|
||||
"longTail": [
|
||||
{
|
||||
"keyword": "longer specific keyword phrase",
|
||||
"type": "long-tail",
|
||||
"searchVolume": 1200,
|
||||
"competition": "low",
|
||||
"difficultyScore": 35,
|
||||
"relevanceScore": 90,
|
||||
"cpc": "$1.25"
|
||||
}
|
||||
],
|
||||
"negative": [
|
||||
{
|
||||
"keyword": "irrelevant term to exclude",
|
||||
"type": "negative",
|
||||
"competition": "low"
|
||||
}
|
||||
]
|
||||
},
|
||||
"adCopies": [
|
||||
{
|
||||
"id": "ad-1",
|
||||
"campaignType": "search",
|
||||
"headlines": [
|
||||
"Headline 1 (max 30 chars)",
|
||||
"Headline 2 (max 30 chars)",
|
||||
"Headline 3 (max 30 chars)"
|
||||
],
|
||||
"descriptions": [
|
||||
"Description line 1 - compelling copy under 90 chars",
|
||||
"Description line 2 - call to action under 90 chars"
|
||||
],
|
||||
"callToAction": "Get Started Today",
|
||||
"displayUrl": "example.com/offers",
|
||||
"mobileOptimized": true
|
||||
}
|
||||
],
|
||||
"campaigns": [
|
||||
{
|
||||
"id": "campaign-1",
|
||||
"name": "Campaign Name",
|
||||
"type": "search",
|
||||
"budget": {
|
||||
"daily": 50,
|
||||
"monthly": 1500,
|
||||
"currency": "USD"
|
||||
},
|
||||
"targeting": {
|
||||
"locations": ["United States", "Canada"],
|
||||
"demographics": ["25-54", "All genders"],
|
||||
"devices": ["Desktop", "Mobile", "Tablet"],
|
||||
"schedule": ["Mon-Fri 8am-8pm"]
|
||||
},
|
||||
"adGroups": [
|
||||
{
|
||||
"id": "adgroup-1",
|
||||
"name": "Product Category Group",
|
||||
"theme": "Main product focus",
|
||||
"keywords": ["keyword1", "keyword2"],
|
||||
"biddingStrategy": "Maximize conversions"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"setupSteps": [
|
||||
"Step 1: Create Google Ads account...",
|
||||
"Step 2: Set up conversion tracking..."
|
||||
],
|
||||
"qualityScoreTips": [
|
||||
"Tip 1: Match keywords to ad copy...",
|
||||
"Tip 2: Optimize landing pages..."
|
||||
],
|
||||
"trackingSetup": [
|
||||
"Install Google Tag Manager...",
|
||||
"Set up conversion goals..."
|
||||
],
|
||||
"optimizationTips": [
|
||||
"Monitor search terms weekly...",
|
||||
"A/B test ad variations..."
|
||||
]
|
||||
},
|
||||
"predictions": {
|
||||
"estimatedClicks": "500-800 per month",
|
||||
"estimatedImpressions": "15,000-25,000 per month",
|
||||
"estimatedCtr": "3.2%-4.5%",
|
||||
"estimatedConversions": "25-50 per month"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
KEYWORD RESEARCH REQUIREMENTS:
|
||||
- Generate 10-15 PRIMARY keywords (high-volume, highly relevant)
|
||||
- Generate 15-20 LONG-TAIL keywords (specific, lower-competition)
|
||||
- Generate 5-10 NEGATIVE keywords (terms to exclude)
|
||||
- Include realistic search volume estimates
|
||||
- Provide competition level and CPC estimates
|
||||
|
||||
AD COPY REQUIREMENTS:
|
||||
- Headlines MUST be 30 characters or less
|
||||
- Descriptions MUST be 90 characters or less
|
||||
- Create 3-5 unique ad variations per campaign type
|
||||
- Include strong calls-to-action
|
||||
- Focus on benefits and unique value propositions
|
||||
- Mobile-optimized versions required
|
||||
|
||||
CAMPAIGN STRUCTURE:
|
||||
- Organize by product/service theme
|
||||
- Recommend appropriate bidding strategies
|
||||
- Include targeting recommendations
|
||||
- Suggest budget allocation
|
||||
|
||||
QUALITY STANDARDS:
|
||||
- All keywords must be relevant (>85% match)
|
||||
- Ad copy must comply with Google Ads policies
|
||||
- No trademark violations
|
||||
- Professional, compelling language
|
||||
- Clear value propositions`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a COMPREHENSIVE Google Ads campaign for:
|
||||
|
||||
WEBSITE: ${websiteUrl}
|
||||
|
||||
PRODUCTS/SERVICES TO PROMOTE:
|
||||
${productsServices.map((p, i) => `${i + 1}. ${p}`).join("\n")}
|
||||
|
||||
TARGET AUDIENCE: ${targetAudience}
|
||||
INDUSTRY: ${industry}
|
||||
LANGUAGE: ${language}
|
||||
${budgetRange ? `BUDGET: ${budgetRange.min}-${budgetRange.max} ${budgetRange.currency}/month` : ""}
|
||||
${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
|
||||
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
|
||||
|
||||
Generate a COMPLETE Google Ads package including:
|
||||
🔍 Comprehensive keyword research (primary, long-tail, negative)
|
||||
✍️ High-converting ad copy (multiple variations)
|
||||
📊 Optimized campaign structure
|
||||
📈 Performance predictions
|
||||
🎯 Implementation guidance
|
||||
|
||||
Make this campaign READY TO LAUNCH with copy-paste ready content!`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
}
|
||||
|
||||
export default ZaiPlanService;
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation } from "@/types";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult } from "@/types";
|
||||
|
||||
interface AppState {
|
||||
currentPrompt: string;
|
||||
@@ -7,6 +7,7 @@ interface AppState {
|
||||
prd: PRD | null;
|
||||
actionPlan: ActionPlan | null;
|
||||
slidesPresentation: SlidesPresentation | null;
|
||||
googleAdsResult: GoogleAdsResult | null;
|
||||
selectedProvider: ModelProvider;
|
||||
selectedModels: Record<ModelProvider, string>;
|
||||
availableModels: Record<ModelProvider, string[]>;
|
||||
@@ -29,6 +30,7 @@ interface AppState {
|
||||
setPRD: (prd: PRD) => void;
|
||||
setActionPlan: (plan: ActionPlan) => void;
|
||||
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
||||
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
||||
setSelectedProvider: (provider: ModelProvider) => void;
|
||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||
@@ -47,6 +49,7 @@ const useStore = create<AppState>((set) => ({
|
||||
prd: null,
|
||||
actionPlan: null,
|
||||
slidesPresentation: null,
|
||||
googleAdsResult: null,
|
||||
selectedProvider: "qwen",
|
||||
selectedModels: {
|
||||
qwen: "coder-model",
|
||||
@@ -72,6 +75,7 @@ const useStore = create<AppState>((set) => ({
|
||||
setPRD: (prd) => set({ prd }),
|
||||
setActionPlan: (plan) => set({ actionPlan: plan }),
|
||||
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
||||
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||
setSelectedModel: (provider, model) =>
|
||||
set((state) => ({
|
||||
@@ -107,6 +111,7 @@ const useStore = create<AppState>((set) => ({
|
||||
prd: null,
|
||||
actionPlan: null,
|
||||
slidesPresentation: null,
|
||||
googleAdsResult: null,
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
304
package-lock.json
generated
304
package-lock.json
generated
@@ -9,10 +9,12 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
@@ -930,6 +932,294 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
@@ -1145,7 +1435,7 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
@@ -2044,6 +2334,18 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
|
||||
@@ -115,3 +115,90 @@ export interface SlidesPresentation {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface GoogleAdsKeyword {
|
||||
keyword: string;
|
||||
type: "primary" | "long-tail" | "negative";
|
||||
searchVolume?: number;
|
||||
competition: "low" | "medium" | "high";
|
||||
difficultyScore?: number;
|
||||
relevanceScore?: number;
|
||||
cpc?: string;
|
||||
}
|
||||
|
||||
export interface GoogleAdCopy {
|
||||
id: string;
|
||||
campaignType: "search" | "display" | "shopping" | "video" | "performance-max";
|
||||
headlines: string[];
|
||||
descriptions: string[];
|
||||
callToAction: string;
|
||||
displayUrl?: string;
|
||||
finalUrl?: string;
|
||||
mobileOptimized: boolean;
|
||||
}
|
||||
|
||||
export interface GoogleAdGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
theme: string;
|
||||
keywords: string[];
|
||||
ads: GoogleAdCopy[];
|
||||
biddingStrategy?: string;
|
||||
}
|
||||
|
||||
export interface GoogleAdsCampaign {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "search" | "display" | "shopping" | "video" | "performance-max";
|
||||
budget: {
|
||||
daily?: number;
|
||||
monthly?: number;
|
||||
currency: string;
|
||||
};
|
||||
targeting: {
|
||||
locations?: string[];
|
||||
demographics?: string[];
|
||||
devices?: string[];
|
||||
schedule?: string[];
|
||||
};
|
||||
adGroups: GoogleAdGroup[];
|
||||
}
|
||||
|
||||
export interface GoogleAdsResult {
|
||||
id: string;
|
||||
websiteUrl: string;
|
||||
productsServices: string[];
|
||||
generatedAt: Date;
|
||||
|
||||
// Keyword Research Package
|
||||
keywords: {
|
||||
primary: GoogleAdsKeyword[];
|
||||
longTail: GoogleAdsKeyword[];
|
||||
negative: GoogleAdsKeyword[];
|
||||
};
|
||||
|
||||
// Ad Copy Suite
|
||||
adCopies: GoogleAdCopy[];
|
||||
|
||||
// Campaign Structure
|
||||
campaigns: GoogleAdsCampaign[];
|
||||
|
||||
// Implementation Guidance
|
||||
implementation: {
|
||||
setupSteps: string[];
|
||||
qualityScoreTips: string[];
|
||||
trackingSetup: string[];
|
||||
optimizationTips: string[];
|
||||
};
|
||||
|
||||
// Performance Predictions
|
||||
predictions?: {
|
||||
estimatedClicks?: string;
|
||||
estimatedImpressions?: string;
|
||||
estimatedCtr?: string;
|
||||
estimatedConversions?: string;
|
||||
};
|
||||
|
||||
rawContent: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user