Compare commits
7 Commits
master
...
238a576cb8
@@ -63,6 +63,7 @@
|
|||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -76,7 +77,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Better touch targets */
|
/* Better touch targets */
|
||||||
button, a, [role="button"] {
|
button,
|
||||||
|
a,
|
||||||
|
[role="button"] {
|
||||||
touch-action: manipulation;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ export const metadata: Metadata = {
|
|||||||
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import LocaleProvider from "@/components/LocaleProvider";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
@@ -20,7 +22,11 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={roboto.className}>{children}</body>
|
<body className={roboto.className}>
|
||||||
|
<LocaleProvider>
|
||||||
|
{children}
|
||||||
|
</LocaleProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import PromptEnhancer from "@/components/PromptEnhancer";
|
|||||||
import PRDGenerator from "@/components/PRDGenerator";
|
import PRDGenerator from "@/components/PRDGenerator";
|
||||||
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
||||||
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
||||||
|
import SlidesGenerator from "@/components/SlidesGenerator";
|
||||||
|
import GoogleAdsGenerator from "@/components/GoogleAdsGenerator";
|
||||||
import HistoryPanel from "@/components/HistoryPanel";
|
import HistoryPanel from "@/components/HistoryPanel";
|
||||||
import SettingsPanel from "@/components/SettingsPanel";
|
import SettingsPanel from "@/components/SettingsPanel";
|
||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
@@ -29,6 +31,10 @@ export default function Home() {
|
|||||||
return <ActionPlanGenerator />;
|
return <ActionPlanGenerator />;
|
||||||
case "uxdesigner":
|
case "uxdesigner":
|
||||||
return <UXDesignerPrompt />;
|
return <UXDesignerPrompt />;
|
||||||
|
case "slides":
|
||||||
|
return <SlidesGenerator />;
|
||||||
|
case "googleads":
|
||||||
|
return <GoogleAdsGenerator />;
|
||||||
case "history":
|
case "history":
|
||||||
return <HistoryPanel />;
|
return <HistoryPanel />;
|
||||||
case "settings":
|
case "settings":
|
||||||
@@ -49,3 +55,4 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
|
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function ActionPlanGenerator() {
|
export default function ActionPlanGenerator() {
|
||||||
const {
|
const {
|
||||||
|
language,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
actionPlan,
|
actionPlan,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
@@ -28,6 +30,9 @@ export default function ActionPlanGenerator() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].actionPlan;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
@@ -126,20 +131,20 @@ export default function ActionPlanGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
Action Plan Generator
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Convert PRD into actionable implementation plan
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -155,8 +160,8 @@ export default function ActionPlanGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -171,12 +176,12 @@ export default function ActionPlanGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs lg:text-sm font-medium">PRD / Requirements</label>
|
<label className="text-xs lg:text-sm font-medium">{language === "ru" ? "PRD / Требования" : language === "he" ? "PRD / דרישות" : "PRD / Requirements"}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Paste your PRD or project requirements here..."
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,7 +191,7 @@ export default function ActionPlanGenerator() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -196,24 +201,24 @@ export default function ActionPlanGenerator() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Generating...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Generate Action Plan
|
{language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!actionPlan && "opacity-50")}>
|
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Action Plan
|
{t.generatedTitle}
|
||||||
</span>
|
</span>
|
||||||
{actionPlan && (
|
{actionPlan && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
@@ -226,36 +231,36 @@ export default function ActionPlanGenerator() {
|
|||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Task breakdown, frameworks, and architecture recommendations
|
{language === "ru" ? "Разбивка задач, фреймворки и рекомендации по архитектуре" : language === "he" ? "פירוט משימות, פרימוורקים והמלצות ארכיטקטורה" : "Task breakdown, frameworks, and architecture recommendations"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{actionPlan ? (
|
{actionPlan ? (
|
||||||
<div className="space-y-3 lg:space-y-4">
|
<div className="space-y-3 lg:space-y-4">
|
||||||
<div className="rounded-md border bg-primary/5 p-3 lg:p-4">
|
<div className="rounded-md border bg-primary/5 p-3 lg:p-4 text-start">
|
||||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||||
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Implementation Roadmap
|
{language === "ru" ? "Дорожная карта реализации" : language === "he" ? "מפת דרכים ליישום" : "Implementation Roadmap"}
|
||||||
</h4>
|
</h4>
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{actionPlan.rawContent}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||||
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Quick Notes
|
{language === "ru" ? "Быстрые заметки" : language === "he" ? "הערות מהירות" : "Quick Notes"}
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
|
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
|
||||||
<li>Review all task dependencies before starting</li>
|
<li>{language === "ru" ? "Проверьте все зависимости задач перед началом" : language === "he" ? "בדוק את כל התלות בין המשימות לפני שתתחיל" : "Review all task dependencies before starting"}</li>
|
||||||
<li>Set up recommended framework architecture</li>
|
<li>{language === "ru" ? "Настройте рекомендуемую архитектуру фреймворка" : language === "he" ? "הגדר את ארכיטקטורת הפרימוורק המומלצת" : "Set up recommended framework architecture"}</li>
|
||||||
<li>Follow best practices for security and performance</li>
|
<li>{language === "ru" ? "Следуйте лучшим практикам безопасности и производительности" : language === "he" ? "עקוב אחר שיטות עבודה מומלצות לאבטחה וביצועים" : "Follow best practices for security and performance"}</li>
|
||||||
<li>Use specified deployment strategy</li>
|
<li>{language === "ru" ? "Используйте указанную стратегию развертывания" : language === "he" ? "השתמש באסטרטגיית הפריסה המצוינת" : "Use specified deployment strategy"}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||||
Action plan will appear here
|
{t.emptyState}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
800
components/GoogleAdsGenerator.tsx
Normal file
800
components/GoogleAdsGenerator.tsx
Normal file
@@ -0,0 +1,800 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } 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 useStore from "@/lib/store";
|
||||||
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
|
import { Megaphone, Copy, Loader2, CheckCircle2, Settings, Plus, X, ChevronDown, ChevronUp, Wand2, Target, TrendingUp, ShieldAlert, BarChart3, Users, Rocket } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { GoogleAdsResult } from "@/types";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
|
export default function GoogleAdsGenerator() {
|
||||||
|
const {
|
||||||
|
googleAdsResult,
|
||||||
|
magicWandResult,
|
||||||
|
selectedProvider,
|
||||||
|
selectedModels,
|
||||||
|
availableModels,
|
||||||
|
apiKeys,
|
||||||
|
isProcessing,
|
||||||
|
error,
|
||||||
|
language,
|
||||||
|
setGoogleAdsResult,
|
||||||
|
setMagicWandResult,
|
||||||
|
setProcessing,
|
||||||
|
setError,
|
||||||
|
setAvailableModels,
|
||||||
|
setSelectedModel,
|
||||||
|
setSelectedProvider,
|
||||||
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].googleAds;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
|
// 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 [duration, setDuration] = useState("30 days");
|
||||||
|
const [industry, setIndustry] = useState("");
|
||||||
|
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [expandedSections, setExpandedSections] = useState<string[]>(["keywords"]);
|
||||||
|
|
||||||
|
const [isMagicThinking, setIsMagicThinking] = useState(false);
|
||||||
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
|
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||||
|
|
||||||
|
const toggleSection = (section: string) => {
|
||||||
|
setExpandedSections((prev) =>
|
||||||
|
prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
loadAvailableModels();
|
||||||
|
const saved = localStorage.getItem("promptarch-api-keys");
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const keys = JSON.parse(saved);
|
||||||
|
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
||||||
|
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||||
|
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load API keys:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [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 handleGenerate = async () => {
|
||||||
|
if (!websiteUrl.trim()) {
|
||||||
|
setError("Please enter a website 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);
|
||||||
|
setMagicWandResult(null);
|
||||||
|
|
||||||
|
console.log("[GoogleAdsGenerator] Starting generation...", { selectedProvider, selectedModel });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await modelAdapter.generateGoogleAds(websiteUrl, {
|
||||||
|
productsServices: filteredProducts,
|
||||||
|
targetAudience,
|
||||||
|
budgetRange: { min: parseInt(budgetMin), max: parseInt(budgetMax), currency: "USD" },
|
||||||
|
campaignDuration: duration,
|
||||||
|
industry,
|
||||||
|
language: "English"
|
||||||
|
}, selectedProvider, selectedModel);
|
||||||
|
|
||||||
|
console.log("[GoogleAdsGenerator] Generation result:", result);
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
try {
|
||||||
|
// Robust JSON extraction
|
||||||
|
const extractJson = (text: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/i) ||
|
||||||
|
text.match(/```\s*([\s\S]*?)\s*```/i);
|
||||||
|
if (jsonMatch && jsonMatch[1]) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(jsonMatch[1].trim());
|
||||||
|
} catch (e2) { /* ignore */ }
|
||||||
|
}
|
||||||
|
const braceMatch = text.match(/(\{[\s\S]*\})/);
|
||||||
|
if (braceMatch) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(braceMatch[0].trim());
|
||||||
|
} catch (e3) { /* ignore */ }
|
||||||
|
}
|
||||||
|
throw new Error("Could not parse JSON from response");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawData = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);
|
||||||
|
const parsedData = extractJson(rawData);
|
||||||
|
|
||||||
|
const adsResult: GoogleAdsResult = {
|
||||||
|
...parsedData,
|
||||||
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
|
websiteUrl,
|
||||||
|
productsServices: filteredProducts,
|
||||||
|
generatedAt: new Date(),
|
||||||
|
rawContent: rawData
|
||||||
|
};
|
||||||
|
setGoogleAdsResult(adsResult);
|
||||||
|
setExpandedSections(["keywords"]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse ads data:", e);
|
||||||
|
setError("Failed to parse the generated ads content. Please try again.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("[GoogleAdsGenerator] Generation failed:", result.error);
|
||||||
|
setError(result.error || "Failed to generate Google Ads campaign");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[GoogleAdsGenerator] Generation error:", err);
|
||||||
|
setError(err instanceof Error ? err.message : "An error occurred");
|
||||||
|
} finally {
|
||||||
|
setProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMagicWand = async () => {
|
||||||
|
if (!websiteUrl.trim()) {
|
||||||
|
setError("Please enter a website URL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstProduct = products.find(p => p.trim() !== "");
|
||||||
|
if (!firstProduct) {
|
||||||
|
setError("Please add at least one product to promote");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsMagicThinking(true);
|
||||||
|
setError(null);
|
||||||
|
setGoogleAdsResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await modelAdapter.generateMagicWand(
|
||||||
|
websiteUrl,
|
||||||
|
firstProduct,
|
||||||
|
parseInt(budgetMax),
|
||||||
|
selectedProvider,
|
||||||
|
selectedModel
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const extractJson = (text: string) => {
|
||||||
|
try { return JSON.parse(text); }
|
||||||
|
catch (e) {
|
||||||
|
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/i) || text.match(/```\s*([\s\S]*?)\s*```/i);
|
||||||
|
if (jsonMatch) return JSON.parse(jsonMatch[1].trim());
|
||||||
|
const braceMatch = text.match(/(\{[\s\S]*\})/);
|
||||||
|
if (braceMatch) return JSON.parse(braceMatch[0].trim());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = extractJson(result.data);
|
||||||
|
setMagicWandResult({
|
||||||
|
...data,
|
||||||
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
|
websiteUrl,
|
||||||
|
product: firstProduct,
|
||||||
|
budget: parseInt(budgetMax),
|
||||||
|
generatedAt: new Date(),
|
||||||
|
rawContent: result.data
|
||||||
|
});
|
||||||
|
setExpandedSections(["market", "strategies"]);
|
||||||
|
} else {
|
||||||
|
setError(result.error || "Magic Wand failed to research the market");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "An error occurred during Magic Wand research");
|
||||||
|
} finally {
|
||||||
|
setIsMagicThinking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
const content = googleAdsResult?.rawContent || magicWandResult?.rawContent;
|
||||||
|
if (content) {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{ id: "keywords", title: language === "ru" ? "Исследование ключевых слов" : language === "he" ? "מחקר מילות מפתח" : "Keywords Research" },
|
||||||
|
{ id: "adcopies", title: language === "ru" ? "Варианты объявлений" : language === "he" ? "גרסאות עותקי מודעות" : "Ad Copy Variations" },
|
||||||
|
{ id: "campaigns", title: language === "ru" ? "Структура кампании" : language === "he" ? "מבנה קמפיין" : "Campaign Structure" },
|
||||||
|
{ id: "implementation", title: language === "ru" ? "Руководство по внедрению" : language === "he" ? "מדריך יישום" : "Implementation Guide" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderSectionContent = (sectionId: string) => {
|
||||||
|
if (!googleAdsResult) return null;
|
||||||
|
|
||||||
|
switch (sectionId) {
|
||||||
|
case "keywords":
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{googleAdsResult.keywords?.primary?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Primary Keywords</h4>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{googleAdsResult.keywords.primary.map((k, i) => (
|
||||||
|
<span key={i} className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-md">
|
||||||
|
{k.keyword} {k.cpc && <span className="opacity-60">({k.cpc})</span>}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{googleAdsResult.keywords?.longTail?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Long-Tail Keywords</h4>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{googleAdsResult.keywords.longTail.map((k, i) => (
|
||||||
|
<span key={i} className="text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded-md">
|
||||||
|
{k.keyword}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{googleAdsResult.keywords?.negative?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Negative Keywords</h4>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{googleAdsResult.keywords.negative.map((k, i) => (
|
||||||
|
<span key={i} className="text-xs bg-red-50 text-red-700 px-2 py-1 rounded-md line-through">
|
||||||
|
{k.keyword}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "adcopies":
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{googleAdsResult.adCopies?.map((ad, i) => (
|
||||||
|
<div key={i} className="p-3 rounded-md border bg-muted/20">
|
||||||
|
<div className="text-[10px] uppercase text-muted-foreground mb-2">Ad Variation {i + 1}</div>
|
||||||
|
<div className="space-y-1 mb-2">
|
||||||
|
{ad.headlines?.map((h, j) => (
|
||||||
|
<div key={j} className="text-sm font-medium text-blue-600">{h}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{ad.descriptions?.map((d, j) => (
|
||||||
|
<p key={j} className="text-xs text-muted-foreground">{d}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "campaigns":
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{googleAdsResult.campaigns?.map((camp, i) => (
|
||||||
|
<div key={i} className="p-3 rounded-md border">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-sm">{camp.name}</div>
|
||||||
|
<div className="text-[10px] text-muted-foreground uppercase">{camp.type}</div>
|
||||||
|
</div>
|
||||||
|
{camp.budget && (
|
||||||
|
<div className="text-right text-xs">
|
||||||
|
<div className="font-semibold">${camp.budget.monthly}/mo</div>
|
||||||
|
<div className="text-muted-foreground">${camp.budget.daily}/day</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{camp.adGroups?.length > 0 && (
|
||||||
|
<div className="mt-2 pt-2 border-t">
|
||||||
|
<div className="text-[10px] uppercase text-muted-foreground mb-1">Ad Groups</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{camp.adGroups.map((g, j) => (
|
||||||
|
<span key={j} className="text-[10px] bg-muted px-1.5 py-0.5 rounded">{g.name}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "implementation":
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{googleAdsResult.implementation?.setupSteps?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Setup Steps</h4>
|
||||||
|
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||||
|
{googleAdsResult.implementation.setupSteps.map((step, i) => (
|
||||||
|
<li key={i}>{step}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{googleAdsResult.implementation?.qualityScoreTips?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-semibold text-muted-foreground uppercase mb-2">Quality Score Tips</h4>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-xs">
|
||||||
|
{googleAdsResult.implementation.qualityScoreTips.map((tip, i) => (
|
||||||
|
<li key={i}>{tip}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <pre className="whitespace-pre-wrap text-xs">{googleAdsResult.rawContent}</pre>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMagicWandSectionContent = (sectionId: string) => {
|
||||||
|
if (!magicWandResult) return null;
|
||||||
|
|
||||||
|
switch (sectionId) {
|
||||||
|
case "market":
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="p-3 rounded-md bg-indigo-50/50 border border-indigo-100">
|
||||||
|
<div className="text-[10px] uppercase font-bold text-indigo-600 mb-1 flex items-center gap-1">
|
||||||
|
<BarChart3 className="h-3 w-3" /> Industry Size
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold">{magicWandResult.marketAnalysis.industrySize}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-md bg-emerald-50/50 border border-emerald-100">
|
||||||
|
<div className="text-[10px] uppercase font-bold text-emerald-600 mb-1 flex items-center gap-1">
|
||||||
|
<TrendingUp className="h-3 w-3" /> Growth Rate
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold">{magicWandResult.marketAnalysis.growthRate}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-slate-700 mb-2 flex items-center gap-1.5">
|
||||||
|
<Users className="h-3.5 w-3.5" /> Market Leaders
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{magicWandResult.marketAnalysis.topCompetitors.map((c, i) => (
|
||||||
|
<span key={i} className="text-xs px-2 py-1 bg-slate-100 text-slate-700 rounded-md border border-slate-200">{c}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-slate-700 mb-2 flex items-center gap-1.5">
|
||||||
|
<Rocket className="h-3.5 w-3.5" /> Emerging Trends
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{magicWandResult.marketAnalysis.marketTrends.map((t, i) => (
|
||||||
|
<li key={i} className="text-xs text-slate-600 flex items-start gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-indigo-400 mt-1 shrink-0" />
|
||||||
|
{t}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "competitors":
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{magicWandResult.competitorInsights.map((comp, i) => (
|
||||||
|
<div key={i} className="p-4 rounded-xl border bg-white/50 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-bold text-slate-900">{comp.competitor}</h4>
|
||||||
|
<ShieldAlert className="h-4 w-4 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-[10px] font-black uppercase text-emerald-600">Strengths</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{comp.strengths.map((s, j) => (
|
||||||
|
<li key={j} className="text-xs text-slate-600 flex gap-1.5">
|
||||||
|
<span className="text-emerald-500">✓</span> {s}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-[10px] font-black uppercase text-rose-600">Weaknesses</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{comp.weaknesses.map((w, j) => (
|
||||||
|
<li key={j} className="text-xs text-slate-600 flex gap-1.5">
|
||||||
|
<span className="text-rose-500">✗</span> {w}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t border-slate-100 italic text-xs text-slate-500">
|
||||||
|
<span className="font-bold text-slate-700 not-italic uppercase text-[9px]">Spy Report:</span> {comp.adStrategy}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "strategies":
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{magicWandResult.strategies.map((strat, i) => (
|
||||||
|
<div key={i} className="relative p-5 rounded-2xl border bg-white shadow-sm hover:shadow-md transition-all group overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 h-1 w-full bg-gradient-to-r from-indigo-500 to-violet-500" />
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-black text-slate-900 tracking-tight">{strat.direction}</h4>
|
||||||
|
<p className="text-sm text-indigo-600 font-bold">{strat.targetAudience}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<span className={cn(
|
||||||
|
"text-[10px] font-black uppercase px-2 py-0.5 rounded-full",
|
||||||
|
strat.riskLevel === 'low' ? "bg-emerald-100 text-emerald-700" :
|
||||||
|
strat.riskLevel === 'medium' ? "bg-amber-100 text-amber-700" : "bg-rose-100 text-rose-700"
|
||||||
|
)}>
|
||||||
|
{strat.riskLevel} risk
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-400 mt-1 font-bold italic">{strat.timeToResults} to results</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-xs text-slate-600 leading-relaxed font-medium">
|
||||||
|
<span className="text-indigo-500 font-black uppercase text-[9px] block mb-0.5">The "Why":</span>
|
||||||
|
{strat.rationale}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="p-3 bg-slate-50 rounded-xl border border-dashed text-xs space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Target className="h-3.5 w-3.5 text-indigo-500" />
|
||||||
|
<span className="font-bold text-slate-700">Edge: {strat.competitiveAdvantage}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{strat.keyMessages.map((msg, j) => (
|
||||||
|
<span key={j} className="text-[10px] bg-white border px-1.5 py-0.5 rounded-md text-slate-500 shadow-sm">{msg}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 items-center">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-[9px] font-black text-slate-400 uppercase">Channel Mix</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{strat.recommendedChannels.map((c, j) => (
|
||||||
|
<span key={j} className="text-[9px] font-bold text-slate-600">{c}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-[9px] font-black text-slate-400 uppercase text-right">Expected ROI</div>
|
||||||
|
<div className="text-lg font-black text-emerald-600 tracking-tighter">{strat.expectedROI}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <pre className="whitespace-pre-wrap text-xs">{magicWandResult.rawContent}</pre>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||||
|
<Card className="h-fit">
|
||||||
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
|
<Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
|
{t.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{t.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
|
<Button
|
||||||
|
key={provider}
|
||||||
|
variant={selectedProvider === provider ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedProvider(provider)}
|
||||||
|
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||||
|
>
|
||||||
|
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
|
<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 lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
>
|
||||||
|
{models.map((model) => (
|
||||||
|
<option key={model} value={model}>
|
||||||
|
{model}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{t.websiteUrl}</label>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., www.your-business.com"
|
||||||
|
value={websiteUrl}
|
||||||
|
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{t.products}</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{products.map((product, index) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder={`${language === "ru" ? "Продукт" : language === "he" ? "מוצר" : "Product"} ${index + 1}`}
|
||||||
|
value={product}
|
||||||
|
onChange={(e) => updateProduct(index, e.target.value)}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
{products.length > 1 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeProduct(index)}
|
||||||
|
className="h-10 w-10 shrink-0"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
|
||||||
|
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
{language === "ru" ? "Добавить продукт" : language === "he" ? "הוסף מוצר" : "Add Product"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{t.budget}</label>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Min"
|
||||||
|
value={budgetMin}
|
||||||
|
onChange={(e) => setBudgetMin(e.target.value)}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs font-bold text-center">-</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Max"
|
||||||
|
value={budgetMax}
|
||||||
|
onChange={(e) => setBudgetMax(e.target.value)}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{t.industry}</label>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., SaaS"
|
||||||
|
value={industry}
|
||||||
|
onChange={(e) => setIndustry(e.target.value)}
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="e.g., Small business owners in USA looking for productivity tools"
|
||||||
|
value={targetAudience}
|
||||||
|
onChange={(e) => setTargetAudience(e.target.value)}
|
||||||
|
className="min-h-[80px] lg:min-h-[100px] resize-y text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||||
|
{error}
|
||||||
|
{!apiKeys[selectedProvider] && (
|
||||||
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
|
||||||
|
className="h-9 lg:h-10 text-xs lg:text-sm bg-primary/90 hover:bg-primary shadow-sm"
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
|
||||||
|
{common.generating}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5" />
|
||||||
|
{t.generateAds}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleMagicWand}
|
||||||
|
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
|
||||||
|
className="h-9 lg:h-10 text-xs lg:text-sm bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white shadow-md transition-all active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
{isMagicThinking ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
|
||||||
|
{t.researching}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Wand2 className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
{t.magicWand}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className={cn(!googleAdsResult && "opacity-50")}>
|
||||||
|
<CardHeader className="p-4 lg:p-6">
|
||||||
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{magicWandResult ? (
|
||||||
|
<Wand2 className="h-4 w-4 lg:h-5 lg:w-5 text-indigo-500" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
|
)}
|
||||||
|
{magicWandResult ? t.strategicDirections : t.generatedCampaign}
|
||||||
|
</span>
|
||||||
|
{(googleAdsResult || magicWandResult) && (
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
|
{copied ? (
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{magicWandResult
|
||||||
|
? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes")
|
||||||
|
: (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready")
|
||||||
|
}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
|
{googleAdsResult || magicWandResult ? (
|
||||||
|
<div className="space-y-2 lg:space-y-3">
|
||||||
|
{(magicWandResult
|
||||||
|
? [
|
||||||
|
{ id: "market", title: t.marketIntelligence },
|
||||||
|
{ id: "competitors", title: t.competitiveInsights },
|
||||||
|
{ id: "strategies", title: t.campaignDirections }
|
||||||
|
]
|
||||||
|
: sections
|
||||||
|
).map((section) => (
|
||||||
|
<div key={section.id} className="rounded-md border bg-muted/30">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection(section.id)}
|
||||||
|
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
|
||||||
|
>
|
||||||
|
<span>{section.title}</span>
|
||||||
|
{expandedSections.includes(section.id) ? (
|
||||||
|
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{expandedSections.includes(section.id) && (
|
||||||
|
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||||
|
{magicWandResult
|
||||||
|
? renderMagicWandSectionContent(section.id)
|
||||||
|
: renderSectionContent(section.id)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||||
|
{language === "ru" ? "Здесь появится созданная кампания" : language === "he" ? "קמפיין שחולל יופיע כאן" : "Generated campaign will appear here"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,16 +4,20 @@ import useStore from "@/lib/store";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function HistoryPanel() {
|
export default function HistoryPanel() {
|
||||||
const { history, setCurrentPrompt, clearHistory } = useStore();
|
const { language, history, setCurrentPrompt, clearHistory } = useStore();
|
||||||
|
const t = translations[language].history;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const handleRestore = (prompt: string) => {
|
const handleRestore = (prompt: string) => {
|
||||||
setCurrentPrompt(prompt);
|
setCurrentPrompt(prompt);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
if (confirm("Are you sure you want to clear all history?")) {
|
const message = language === "ru" ? "Вы уверены, что хотите очистить всю историю?" : language === "he" ? "האם אתה בטוח שברצונך למחוק את כל ההיסטוריה?" : "Are you sure you want to clear all history?";
|
||||||
|
if (confirm(message)) {
|
||||||
clearHistory();
|
clearHistory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -21,12 +25,12 @@ export default function HistoryPanel() {
|
|||||||
if (history.length === 0) {
|
if (history.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6">
|
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6 text-center">
|
||||||
<div className="text-center">
|
<div>
|
||||||
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
|
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
|
||||||
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground">No history yet</p>
|
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground font-medium">{t.empty}</p>
|
||||||
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
||||||
Start enhancing prompts to see them here
|
{language === "ru" ? "Начните использовать инструменты, чтобы увидеть историю здесь" : language === "he" ? "התחל להשתמש בכלים כדי לראות אותם כאן" : "Start using tools to see them here"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -36,12 +40,14 @@ export default function HistoryPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6">
|
<CardHeader className="flex-row items-center justify-between p-4 lg:p-6 text-start">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base lg:text-lg">History</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.title}</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">{history.length} items</CardDescription>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{history.length} {language === "ru" ? "элем." : language === "he" ? "פריטים" : "items"}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9" title={t.clear}>
|
||||||
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Trash2 className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
15
components/LocaleProvider.tsx
Normal file
15
components/LocaleProvider.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import useStore from "@/lib/store";
|
||||||
|
|
||||||
|
export default function LocaleProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const language = useStore((state) => state.language);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.lang = language;
|
||||||
|
document.documentElement.dir = language === "he" ? "rtl" : "ltr";
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
|
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function PRDGenerator() {
|
export default function PRDGenerator() {
|
||||||
const {
|
const {
|
||||||
@@ -19,6 +20,7 @@ export default function PRDGenerator() {
|
|||||||
apiKeys,
|
apiKeys,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
error,
|
error,
|
||||||
|
language,
|
||||||
setCurrentPrompt,
|
setCurrentPrompt,
|
||||||
setSelectedProvider,
|
setSelectedProvider,
|
||||||
setPRD,
|
setPRD,
|
||||||
@@ -28,6 +30,9 @@ export default function PRDGenerator() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].prdGenerator;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
||||||
|
|
||||||
@@ -131,29 +136,29 @@ export default function PRDGenerator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{ id: "overview", title: "Overview & Objectives" },
|
{ id: "overview", title: language === "ru" ? "Обзор продукта" : language === "he" ? "סקירת מוצר" : "Product Overview" },
|
||||||
{ id: "personas", title: "User Personas & Use Cases" },
|
{ id: "personas", title: language === "ru" ? "Персоны пользователей" : language === "he" ? "פרסונות משתמשים" : "User Personas & Use Cases" },
|
||||||
{ id: "functional", title: "Functional Requirements" },
|
{ id: "functional", title: language === "ru" ? "Функциональные требования" : language === "he" ? "דרישות פונקציונליות" : "Functional Requirements" },
|
||||||
{ id: "nonfunctional", title: "Non-functional Requirements" },
|
{ id: "nonfunctional", title: language === "ru" ? "Нефункциональные требования" : language === "he" ? "דרישות לא פונקציונליות" : "Non-functional Requirements" },
|
||||||
{ id: "architecture", title: "Technical Architecture" },
|
{ id: "architecture", title: language === "ru" ? "Техническая архитектура" : language === "he" ? "ארכיטקטורה טכנית" : "Technical Architecture" },
|
||||||
{ id: "metrics", title: "Success Metrics" },
|
{ id: "metrics", title: language === "ru" ? "Успешность метрик" : language === "he" ? "מדדי הצלחה" : "Success Metrics" },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
PRD Generator
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Generate comprehensive Product Requirements Document from your idea
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -169,8 +174,8 @@ export default function PRDGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -185,12 +190,11 @@ export default function PRDGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs lg:text-sm font-medium">Your Idea</label>
|
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., A task management app with real-time collaboration features"
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -200,7 +204,7 @@ export default function PRDGenerator() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -210,12 +214,12 @@ export default function PRDGenerator() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Generating PRD...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FileText className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<FileText className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Generate PRD
|
{common.generate}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -223,11 +227,11 @@ export default function PRDGenerator() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!prd && "opacity-50")}>
|
<Card className={cn(!prd && "opacity-50")}>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Generated PRD
|
{t.generatedTitle}
|
||||||
</span>
|
</span>
|
||||||
{prd && (
|
{prd && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
@@ -240,7 +244,7 @@ export default function PRDGenerator() {
|
|||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Structured requirements document ready for development
|
{language === "ru" ? "Структурированный документ требований готов к разработке" : language === "he" ? "מסמך דרישות מובנה מוכן לפיתוח" : "Structured requirements document ready for development"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
@@ -269,7 +273,7 @@ export default function PRDGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||||
PRD will appear here
|
{language === "ru" ? "Здесь появится созданный PRD" : language === "he" ? "PRD שחולל יופיע כאן" : "Generated PRD will appear here"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
|
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function PromptEnhancer() {
|
export default function PromptEnhancer() {
|
||||||
const {
|
const {
|
||||||
|
language,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
enhancedPrompt,
|
enhancedPrompt,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
@@ -28,6 +30,9 @@ export default function PromptEnhancer() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].promptEnhancer;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
@@ -117,20 +122,20 @@ export default function PromptEnhancer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
Input Prompt
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Enter your prompt and we'll enhance it for AI coding agents
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -149,8 +154,8 @@ export default function PromptEnhancer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -164,13 +169,13 @@ export default function PromptEnhancer() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Your Prompt</label>
|
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., Create a user authentication system with JWT tokens"
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -180,7 +185,7 @@ export default function PromptEnhancer() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -191,29 +196,29 @@ export default function PromptEnhancer() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Enhancing...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Enhance Prompt
|
{t.title}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||||
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="hidden sm:inline">Clear</span>
|
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!enhancedPrompt && "opacity-50")}>
|
<Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
Enhanced Prompt
|
{t.enhancedTitle}
|
||||||
</span>
|
</span>
|
||||||
{enhancedPrompt && (
|
{enhancedPrompt && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
@@ -226,17 +231,17 @@ export default function PromptEnhancer() {
|
|||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Professional prompt ready for coding agents
|
{language === "ru" ? "Профессиональный промпт, готовый для кодинг-агентов" : language === "he" ? "פרומפט מקצועי מוכן לסוכני קידוד" : "Professional prompt ready for coding agents"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{enhancedPrompt ? (
|
{enhancedPrompt ? (
|
||||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4">
|
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{enhancedPrompt}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{enhancedPrompt}</pre>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||||
Enhanced prompt will appear here
|
{language === "ru" ? "Улучшенный промпт появится здесь" : language === "he" ? "פרומפט משופר יופיע כאן" : "Enhanced prompt will appear here"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ import { Input } from "@/components/ui/input";
|
|||||||
import useStore from "@/lib/store";
|
import useStore from "@/lib/store";
|
||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function SettingsPanel() {
|
export default function SettingsPanel() {
|
||||||
const { apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
const { language, apiKeys, setApiKey, selectedProvider, setSelectedProvider, qwenTokens, setQwenTokens } = useStore();
|
||||||
|
const t = translations[language].settings;
|
||||||
|
const common = translations[language].common;
|
||||||
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
||||||
const [isAuthLoading, setIsAuthLoading] = useState(false);
|
const [isAuthLoading, setIsAuthLoading] = useState(false);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
|
localStorage.setItem("promptarch-api-keys", JSON.stringify(apiKeys));
|
||||||
alert("API keys saved successfully!");
|
alert(t.keysSaved);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +84,7 @@ export default function SettingsPanel() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Qwen OAuth failed", error);
|
console.error("Qwen OAuth failed", error);
|
||||||
window.alert(
|
window.alert(
|
||||||
error instanceof Error ? error.message : "Qwen authentication failed"
|
error instanceof Error ? error.message : t.qwenAuth + " failed"
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsAuthLoading(false);
|
setIsAuthLoading(false);
|
||||||
@@ -95,17 +98,17 @@ export default function SettingsPanel() {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
API Configuration
|
{t.apiKeys}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Configure API keys for different AI providers
|
{language === "ru" ? "Настройте ключи API для различных провайдеров ИИ" : language === "he" ? "הגדר מפתחות API עבור ספקי בינה מלאכותית שונים" : "Configure API keys for different AI providers"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Qwen Code API Key
|
Qwen Code API Key
|
||||||
@@ -113,7 +116,7 @@ export default function SettingsPanel() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.qwen ? "text" : "password"}
|
type={showApiKey.qwen ? "text" : "password"}
|
||||||
placeholder="Enter your Qwen API key"
|
placeholder={t.enterKey("Qwen")}
|
||||||
value={apiKeys.qwen || ""}
|
value={apiKeys.qwen || ""}
|
||||||
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
||||||
className="font-mono text-xs lg:text-sm pr-10"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
@@ -134,7 +137,7 @@ export default function SettingsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 lg:gap-4">
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
|
<p className="text-[10px] lg:text-xs text-muted-foreground flex-1">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://help.aliyun.com/zh/dashscope/"
|
href="https://help.aliyun.com/zh/dashscope/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -152,20 +155,20 @@ export default function SettingsPanel() {
|
|||||||
disabled={isAuthLoading}
|
disabled={isAuthLoading}
|
||||||
>
|
>
|
||||||
{isAuthLoading
|
{isAuthLoading
|
||||||
? "Signing in..."
|
? (language === "ru" ? "Вход..." : language === "he" ? "מתחבר..." : "Signing in...")
|
||||||
: qwenTokens
|
: qwenTokens
|
||||||
? "Logout from Qwen"
|
? t.logoutQwen
|
||||||
: "Login with Qwen (OAuth)"}
|
: t.loginQwen}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{qwenTokens && (
|
{qwenTokens && (
|
||||||
<p className="text-[9px] lg:text-[10px] text-green-600 dark:text-green-400 font-medium">
|
<p className="text-[9px] lg:text-[10px] text-green-600 dark:text-green-400 font-medium">
|
||||||
✓ Authenticated via OAuth (Expires: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
|
✓ {t.authenticated} ({t.expires}: {new Date(qwenTokens.expiresAt || 0).toLocaleString()})
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Ollama Cloud API Key
|
Ollama Cloud API Key
|
||||||
@@ -173,7 +176,7 @@ export default function SettingsPanel() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.ollama ? "text" : "password"}
|
type={showApiKey.ollama ? "text" : "password"}
|
||||||
placeholder="Enter your Ollama API key"
|
placeholder={t.enterKey("Ollama")}
|
||||||
value={apiKeys.ollama || ""}
|
value={apiKeys.ollama || ""}
|
||||||
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
||||||
className="font-mono text-xs lg:text-sm pr-10"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
@@ -193,7 +196,7 @@ export default function SettingsPanel() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://ollama.com/cloud"
|
href="https://ollama.com/cloud"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -205,7 +208,7 @@ export default function SettingsPanel() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Z.AI Plan API Key
|
Z.AI Plan API Key
|
||||||
@@ -213,7 +216,7 @@ export default function SettingsPanel() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showApiKey.zai ? "text" : "password"}
|
type={showApiKey.zai ? "text" : "password"}
|
||||||
placeholder="Enter your Z.AI API key"
|
placeholder={t.enterKey("Z.AI")}
|
||||||
value={apiKeys.zai || ""}
|
value={apiKeys.zai || ""}
|
||||||
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
||||||
className="font-mono text-xs lg:text-sm pr-10"
|
className="font-mono text-xs lg:text-sm pr-10"
|
||||||
@@ -233,7 +236,7 @@ export default function SettingsPanel() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Get API key from{" "}
|
{t.getApiKey}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.z.ai"
|
href="https://docs.z.ai"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -247,16 +250,16 @@ export default function SettingsPanel() {
|
|||||||
|
|
||||||
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Save API Keys
|
{t.saveKeys}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="text-base lg:text-lg">Default Provider</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.defaultProvider}</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Select your preferred AI provider
|
{t.defaultProviderDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
@@ -266,8 +269,8 @@ export default function SettingsPanel() {
|
|||||||
key={provider}
|
key={provider}
|
||||||
onClick={() => setSelectedProvider(provider)}
|
onClick={() => setSelectedProvider(provider)}
|
||||||
className={`flex items-center gap-2 lg:gap-3 rounded-lg border p-3 lg:p-4 text-left transition-colors hover:bg-muted/50 ${selectedProvider === provider
|
className={`flex items-center gap-2 lg:gap-3 rounded-lg border p-3 lg:p-4 text-left transition-colors hover:bg-muted/50 ${selectedProvider === provider
|
||||||
? "border-primary bg-primary/5"
|
? "border-primary bg-primary/5"
|
||||||
: "border-border"
|
: "border-border"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex h-8 w-8 lg:h-10 lg:w-10 items-center justify-center rounded-md bg-primary/10">
|
<div className="flex h-8 w-8 lg:h-10 lg:w-10 items-center justify-center rounded-md bg-primary/10">
|
||||||
@@ -276,9 +279,9 @@ export default function SettingsPanel() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
||||||
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
||||||
{provider === "qwen" && "Alibaba DashScope API"}
|
{provider === "qwen" && t.qwenDesc}
|
||||||
{provider === "ollama" && "Ollama Cloud API"}
|
{provider === "ollama" && t.ollamaDesc}
|
||||||
{provider === "zai" && "Z.AI Plan API"}
|
{provider === "zai" && t.zaiDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{selectedProvider === provider && (
|
{selectedProvider === provider && (
|
||||||
@@ -291,16 +294,16 @@ export default function SettingsPanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="text-base lg:text-lg">Data Privacy</CardTitle>
|
<CardTitle className="text-base lg:text-lg">{t.dataPrivacy}</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Your data handling preferences
|
{language === "ru" ? "Ваши настройки обработки данных" : language === "he" ? "העדפות הטיפול בנתונים שלך" : "Your data handling preferences"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||||
<p className="text-xs lg:text-sm">
|
<p className="text-xs lg:text-sm">
|
||||||
All API keys are stored locally in your browser. Your prompts are sent directly to the selected AI provider and are not stored by PromptArch.
|
{t.dataPrivacyDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useStore from "@/lib/store";
|
import useStore from "@/lib/store";
|
||||||
import { Sparkles, FileText, ListTodo, Palette, History, Settings, Github, Menu, X } from "lucide-react";
|
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone, Languages } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "history" | "settings";
|
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "history" | "settings";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
currentView: View;
|
currentView: View;
|
||||||
@@ -14,16 +15,20 @@ interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||||
const history = useStore((state) => state.history);
|
const { language, setLanguage, history } = useStore();
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const t = translations[language].sidebar;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
|
{ id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles },
|
||||||
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
{ id: "prd" as View, label: t.prdGenerator, icon: FileText },
|
||||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
{ id: "action" as View, label: t.actionPlan, icon: ListTodo },
|
||||||
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
|
{ id: "uxdesigner" as View, label: t.uxDesigner, icon: Palette },
|
||||||
{ id: "history" as View, label: "History", icon: History, count: history.length },
|
{ id: "slides" as View, label: t.slidesGen, icon: Presentation },
|
||||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
{ id: "googleads" as View, label: t.googleAds, icon: Megaphone },
|
||||||
|
{ id: "history" as View, label: t.history, icon: History, count: history.length },
|
||||||
|
{ id: "settings" as View, label: t.settings, icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleViewChange = (view: View) => {
|
const handleViewChange = (view: View) => {
|
||||||
@@ -36,7 +41,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
|||||||
<div className="border-b p-4 lg:p-6">
|
<div className="border-b p-4 lg:p-6">
|
||||||
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
|
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||||
<Menu className="h-3 w-3" />
|
<Menu className="h-3 w-3" />
|
||||||
Back to rommark.dev
|
<span>{language === "en" ? "Back to rommark.dev" : language === "ru" ? "Вернуться на rommark.dev" : "חזרה ל-rommark.dev"}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="block">
|
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="block">
|
||||||
<h1 className="flex items-center gap-2 text-lg lg:text-xl font-bold hover:opacity-80 transition-opacity">
|
<h1 className="flex items-center gap-2 text-lg lg:text-xl font-bold hover:opacity-80 transition-opacity">
|
||||||
@@ -76,21 +81,22 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="mt-6 lg:mt-8 p-2 lg:p-3 text-[9px] lg:text-[10px] leading-relaxed text-muted-foreground border-t border-border/50 pt-3 lg:pt-4">
|
<div className="mt-4 p-2 lg:p-3 border-t border-border/50">
|
||||||
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
|
<div className="flex items-center gap-2 mb-2 text-[10px] lg:text-xs font-semibold text-muted-foreground uppercase">
|
||||||
<div className="space-y-0.5 lg:space-y-1">
|
<Languages className="h-3 w-3" /> {language === "en" ? "Language" : language === "ru" ? "Язык" : "שפה"}
|
||||||
<p>
|
</div>
|
||||||
GitHub: <a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">roman-ryzenadvanced</a>
|
<div className="flex flex-wrap gap-1">
|
||||||
</p>
|
{(["en", "ru", "he"] as const).map((lang) => (
|
||||||
<p>
|
<Button
|
||||||
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
|
key={lang}
|
||||||
</p>
|
variant={language === lang ? "default" : "outline"}
|
||||||
<p className="mt-1 lg:mt-2 text-[8px] lg:text-[9px] opacity-80">
|
size="sm"
|
||||||
100% Developed using GLM 4.7 model on TRAE.AI IDE.
|
className="h-7 px-2 text-[10px] uppercase font-bold"
|
||||||
</p>
|
onClick={() => setLanguage(lang)}
|
||||||
<p className="text-[8px] lg:text-[9px] opacity-80">
|
>
|
||||||
Model Info: <a href="https://z.ai/subscribe?ic=R0K78RJKNW" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Learn here</a>
|
{lang}
|
||||||
</p>
|
</Button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
1296
components/SlidesGenerator.tsx
Normal file
1296
components/SlidesGenerator.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,13 @@ import useStore from "@/lib/store";
|
|||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
|
import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function UXDesignerPrompt() {
|
export default function UXDesignerPrompt() {
|
||||||
const {
|
const {
|
||||||
|
language,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
|
enhancedPrompt,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
selectedModels,
|
selectedModels,
|
||||||
availableModels,
|
availableModels,
|
||||||
@@ -27,6 +30,9 @@ export default function UXDesignerPrompt() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
|
const t = translations[language].uxDesigner;
|
||||||
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -119,20 +125,20 @@ export default function UXDesignerPrompt() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
UX Designer Prompt
|
{t.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Describe your app idea and get the BEST EVER prompt for UX design
|
{t.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
{(["ollama", "zai"] as const).map((provider) => (
|
{(["ollama", "zai"] as const).map((provider) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -151,8 +157,8 @@ export default function UXDesignerPrompt() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
@@ -166,16 +172,16 @@ export default function UXDesignerPrompt() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">App Description</label>
|
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g., A fitness tracking app with workout plans, nutrition tracking, and social features for sharing progress with friends"
|
placeholder={t.placeholder}
|
||||||
value={currentPrompt}
|
value={currentPrompt}
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
Describe what kind of app you want, target users, key features, and any specific design preferences.
|
{t.inputDesc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,7 +191,7 @@ export default function UXDesignerPrompt() {
|
|||||||
{!apiKeys[selectedProvider] && (
|
{!apiKeys[selectedProvider] && (
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -196,30 +202,30 @@ export default function UXDesignerPrompt() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
Generating...
|
{common.generating}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
Generate UX Prompt
|
{language === "ru" ? "Создать UX Промпт" : language === "he" ? "חולל פרומפט UX" : "Generate UX Prompt"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||||
<span className="hidden sm:inline">Clear</span>
|
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
|
||||||
<span className="sm:hidden">×</span>
|
<span className="sm:hidden">×</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={cn(!generatedPrompt && "opacity-50")}>
|
<Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}>
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
<span className="hidden sm:inline">Best Ever UX Prompt</span>
|
<span className="hidden sm:inline">{t.resultTitle}</span>
|
||||||
<span className="sm:hidden">UX Prompt</span>
|
<span className="sm:hidden">{language === "ru" ? "UX Промпт" : language === "he" ? "פרומפט UX" : "UX Prompt"}</span>
|
||||||
</span>
|
</span>
|
||||||
{generatedPrompt && (
|
{generatedPrompt && (
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
@@ -232,17 +238,17 @@ export default function UXDesignerPrompt() {
|
|||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
Comprehensive UX design prompt ready for designers
|
{t.resultDesc}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
{generatedPrompt ? (
|
{generatedPrompt ? (
|
||||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto">
|
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{generatedPrompt}</pre>
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{generatedPrompt}</pre>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4">
|
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4 italic">
|
||||||
Your comprehensive UX designer prompt will appear here
|
{t.emptyState}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
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 }
|
||||||
370
lib/i18n/translations.ts
Normal file
370
lib/i18n/translations.ts
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
export type Language = "en" | "ru" | "he";
|
||||||
|
|
||||||
|
export const translations = {
|
||||||
|
en: {
|
||||||
|
sidebar: {
|
||||||
|
title: "PromptArch",
|
||||||
|
subtitle: "AI Tool Suite",
|
||||||
|
promptEnhancer: "Prompt Enhancer",
|
||||||
|
prdGenerator: "PRD Generator",
|
||||||
|
actionPlan: "Action Plan",
|
||||||
|
slidesGen: "Slides Gen",
|
||||||
|
googleAds: "Google Ads",
|
||||||
|
uxDesigner: "UX Designer",
|
||||||
|
settings: "Settings",
|
||||||
|
history: "History",
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
aiProvider: "AI Provider",
|
||||||
|
model: "Model",
|
||||||
|
generate: "Generate",
|
||||||
|
generating: "Generating...",
|
||||||
|
copy: "Copy",
|
||||||
|
copied: "Copied!",
|
||||||
|
settings: "Settings",
|
||||||
|
error: "Error",
|
||||||
|
configApiKey: "Configure API key in Settings",
|
||||||
|
},
|
||||||
|
promptEnhancer: {
|
||||||
|
title: "Prompt Enhancer",
|
||||||
|
description: "Transform your simple ideas into professional, high-quality prompts",
|
||||||
|
placeholder: "Enter your prompt here...",
|
||||||
|
inputLabel: "Your Prompt",
|
||||||
|
enhancedTitle: "Enhanced Prompt",
|
||||||
|
enhancedDesc: "Your prompt has been optimized for better AI performance",
|
||||||
|
},
|
||||||
|
prdGenerator: {
|
||||||
|
title: "PRD Generator",
|
||||||
|
description: "Generate comprehensive Product Requirements Document from your idea",
|
||||||
|
placeholder: "e.g., A task management app with real-time collaboration features",
|
||||||
|
generatedTitle: "Generated PRD",
|
||||||
|
},
|
||||||
|
googleAds: {
|
||||||
|
title: "Google Ads Strategist",
|
||||||
|
description: "Generate keywords, ad copy, and campaign structure for Google Ads",
|
||||||
|
websiteUrl: "Website URL",
|
||||||
|
products: "Products / Services",
|
||||||
|
budget: "Budget (USD/mo)",
|
||||||
|
industry: "Industry",
|
||||||
|
targetAudience: "Target Audience",
|
||||||
|
generateAds: "Generate Ads",
|
||||||
|
magicWand: "Magic Wand",
|
||||||
|
researching: "Researching...",
|
||||||
|
generatedCampaign: "Generated Campaign",
|
||||||
|
strategicDirections: "Strategic Directions",
|
||||||
|
marketIntelligence: "Market Intelligence",
|
||||||
|
competitiveInsights: "Competitive Insights",
|
||||||
|
campaignDirections: "Campaign Directions",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: "System Settings",
|
||||||
|
apiKeys: "API Configuration",
|
||||||
|
qwenAuth: "Qwen Authentication",
|
||||||
|
connectQwen: "Connect Qwen Account",
|
||||||
|
connected: "Connected",
|
||||||
|
notConnected: "Not Connected",
|
||||||
|
theme: "Theme",
|
||||||
|
language: "Interface Language",
|
||||||
|
saveKeys: "Save API Keys",
|
||||||
|
keysSaved: "API keys saved successfully!",
|
||||||
|
defaultProvider: "Default Provider",
|
||||||
|
defaultProviderDesc: "Select your preferred AI provider",
|
||||||
|
dataPrivacy: "Data Privacy",
|
||||||
|
dataPrivacyDesc: "All API keys are stored locally in your browser. Your prompts are sent directly to the selected AI provider and are not stored by PromptArch.",
|
||||||
|
loginQwen: "Login with Qwen (OAuth)",
|
||||||
|
logoutQwen: "Logout from Qwen",
|
||||||
|
authenticated: "Authenticated via OAuth",
|
||||||
|
expires: "Expires",
|
||||||
|
enterKey: (provider: string) => `Enter your ${provider} API key`,
|
||||||
|
getApiKey: "Get API key from",
|
||||||
|
qwenDesc: "Alibaba DashScope API",
|
||||||
|
ollamaDesc: "Ollama Cloud API",
|
||||||
|
zaiDesc: "Z.AI Plan API",
|
||||||
|
},
|
||||||
|
uxDesigner: {
|
||||||
|
title: "UX Designer Prompt",
|
||||||
|
description: "Describe your app idea and get the BEST EVER prompt for UX design",
|
||||||
|
placeholder: "e.g., A fitness tracking app with workout plans, nutrition tracking, and social features...",
|
||||||
|
inputLabel: "App Description",
|
||||||
|
inputDesc: "Describe what kind of app you want, target users, key features, and any specific design preferences.",
|
||||||
|
resultTitle: "Ultimate UX Prompt",
|
||||||
|
resultDesc: "Comprehensive UX design prompt ready for designers",
|
||||||
|
emptyState: "Your comprehensive UX designer prompt will appear here",
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
title: "Session History",
|
||||||
|
description: "Previous prompts and generated results",
|
||||||
|
empty: "No history yet. Start exploring tools!",
|
||||||
|
clear: "Clear History",
|
||||||
|
},
|
||||||
|
actionPlan: {
|
||||||
|
title: "Action Plan Generator",
|
||||||
|
description: "Generate a logical, step-by-step implementation plan from your PRD",
|
||||||
|
placeholder: "Paste your PRD or project requirements here...",
|
||||||
|
generatedTitle: "Generated Action Plan",
|
||||||
|
architecture: "Technical Architecture",
|
||||||
|
infrastructure: "Infrastructure & Tools",
|
||||||
|
tasks: "Implementation Tasks",
|
||||||
|
riskAssessment: "Risk Assessment",
|
||||||
|
emptyState: "Generated action plan will appear here",
|
||||||
|
},
|
||||||
|
slidesGen: {
|
||||||
|
title: "AI Presentation Generator",
|
||||||
|
description: "Generate stunning, professional slides for any occasion in seconds",
|
||||||
|
placeholder: "Describe your presentation topic or paste an outline...",
|
||||||
|
language: "Presentation Language",
|
||||||
|
theme: "Aesthetic Theme",
|
||||||
|
audience: "Target Audience",
|
||||||
|
animations: "Animation Style",
|
||||||
|
numSlides: "Number of Slides",
|
||||||
|
generate: "Generate Presentation",
|
||||||
|
generating: "Crafting your story...",
|
||||||
|
emptyState: "Your presentation will appear here",
|
||||||
|
attachFiles: "Attach files for context",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
sidebar: {
|
||||||
|
title: "PromptArch",
|
||||||
|
subtitle: "Набор ИИ-инструментов",
|
||||||
|
promptEnhancer: "Улучшение промптов",
|
||||||
|
prdGenerator: "Генератор PRD",
|
||||||
|
actionPlan: "План действий",
|
||||||
|
slidesGen: "Генератор слайдов",
|
||||||
|
googleAds: "Google Реклама",
|
||||||
|
uxDesigner: "UX Дизайнер",
|
||||||
|
settings: "Настройки",
|
||||||
|
history: "История",
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
aiProvider: "Провайдер ИИ",
|
||||||
|
model: "Модель",
|
||||||
|
generate: "Генерировать",
|
||||||
|
generating: "Генерация...",
|
||||||
|
copy: "Копировать",
|
||||||
|
copied: "Скопировано!",
|
||||||
|
settings: "Настройки",
|
||||||
|
error: "Ошибка",
|
||||||
|
configApiKey: "Настройте API ключ в настройках",
|
||||||
|
},
|
||||||
|
promptEnhancer: {
|
||||||
|
title: "Улучшение промптов",
|
||||||
|
description: "Превратите ваши простые идеи в профессиональные, качественные промпты",
|
||||||
|
placeholder: "Введите ваш промпт здесь...",
|
||||||
|
inputLabel: "Ваш промпт",
|
||||||
|
enhancedTitle: "Улучшенный промпт",
|
||||||
|
enhancedDesc: "Ваш промпт оптимизирован для лучшей работы ИИ",
|
||||||
|
},
|
||||||
|
prdGenerator: {
|
||||||
|
title: "Генератор PRD",
|
||||||
|
description: "Создайте подробный документ требований к продукту на основе вашей идеи",
|
||||||
|
placeholder: "Например: Приложение для управления задачами с совместной работой",
|
||||||
|
generatedTitle: "Созданный PRD",
|
||||||
|
},
|
||||||
|
googleAds: {
|
||||||
|
title: "Стратег Google Ads",
|
||||||
|
description: "Генерация ключевых слов, объявлений и структуры кампании",
|
||||||
|
websiteUrl: "URL сайта",
|
||||||
|
products: "Продукты / Услуги",
|
||||||
|
budget: "Бюджет (USD/мес)",
|
||||||
|
industry: "Отрасль",
|
||||||
|
targetAudience: "Целевая аудитория",
|
||||||
|
generateAds: "Создать рекламу",
|
||||||
|
magicWand: "Магический жезл",
|
||||||
|
researching: "Исследование...",
|
||||||
|
generatedCampaign: "Созданная кампания",
|
||||||
|
strategicDirections: "Стратегические направления",
|
||||||
|
marketIntelligence: "Анализ рынка",
|
||||||
|
competitiveInsights: "Анализ конкурентов",
|
||||||
|
campaignDirections: "Направления кампании",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: "Настройки системы",
|
||||||
|
apiKeys: "Настройка API",
|
||||||
|
qwenAuth: "Авторизация Qwen",
|
||||||
|
connectQwen: "Подключить аккаунт Qwen",
|
||||||
|
connected: "Подключено",
|
||||||
|
notConnected: "Не подключено",
|
||||||
|
theme: "Тема",
|
||||||
|
language: "Язык интерфейса",
|
||||||
|
saveKeys: "Сохранить ключи API",
|
||||||
|
keysSaved: "API ключи успешно сохранены!",
|
||||||
|
defaultProvider: "Провайдер по умолчанию",
|
||||||
|
defaultProviderDesc: "Выберите предпочитаемого провайдера ИИ",
|
||||||
|
dataPrivacy: "Конфиденциальность данных",
|
||||||
|
dataPrivacyDesc: "Все ключи API хранятся локально в вашем браузере. Ваши запросы отправляются напрямую выбранному провайдеру ИИ и не сохраняются в PromptArch.",
|
||||||
|
loginQwen: "Войти через Qwen (OAuth)",
|
||||||
|
logoutQwen: "Выйти из Qwen",
|
||||||
|
authenticated: "Авторизовано через OAuth",
|
||||||
|
expires: "Истекает",
|
||||||
|
enterKey: (provider: string) => `Введите ваш API ключ ${provider}`,
|
||||||
|
getApiKey: "Получить API ключ здесь:",
|
||||||
|
qwenDesc: "Alibaba DashScope API",
|
||||||
|
ollamaDesc: "Ollama Cloud API",
|
||||||
|
zaiDesc: "Z.AI Plan API",
|
||||||
|
},
|
||||||
|
uxDesigner: {
|
||||||
|
title: "UX Дизайнер Промпт",
|
||||||
|
description: "Опишите идею вашего приложения и получите ЛУЧШИЙ промпт для UX-дизайна",
|
||||||
|
placeholder: "Например: Приложение для отслеживания фитнеса с планами тренировок, питанием и социальными функциями...",
|
||||||
|
inputLabel: "Описание приложения",
|
||||||
|
inputDesc: "Опишите тип приложения, целевых пользователей, ключевые функции и любые предпочтения в дизайне.",
|
||||||
|
resultTitle: "Ультимативный UX промпт",
|
||||||
|
resultDesc: "Комплексный промпт для UX-дизайна, готовый для дизайнеров",
|
||||||
|
emptyState: "Ваш комплексный промпт для UX-дизайнера появится здесь",
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
title: "История сессий",
|
||||||
|
description: "Предыдущие промпты и результаты генерации",
|
||||||
|
empty: "История пока пуста. Начните использовать инструменты!",
|
||||||
|
clear: "Очистить историю",
|
||||||
|
},
|
||||||
|
actionPlan: {
|
||||||
|
title: "Генератор плана действий",
|
||||||
|
description: "Создайте логичный пошаговый план реализации на основе вашего PRD",
|
||||||
|
placeholder: "Вставьте ваш PRD или требования к проекту здесь...",
|
||||||
|
generatedTitle: "Созданный план действий",
|
||||||
|
architecture: "Техническая архитектура",
|
||||||
|
infrastructure: "Инфраструктура и инструменты",
|
||||||
|
tasks: "Задачи по реализации",
|
||||||
|
riskAssessment: "Оценка рисков",
|
||||||
|
emptyState: "Созданный план действий появится здесь",
|
||||||
|
},
|
||||||
|
slidesGen: {
|
||||||
|
title: "Генератор презентаций",
|
||||||
|
description: "Создавайте потрясающие профессиональные слайды для любого случая за считанные секунды",
|
||||||
|
placeholder: "Опишите тему презентации или вставьте план...",
|
||||||
|
language: "Язык презентации",
|
||||||
|
theme: "Эстетическая тема",
|
||||||
|
audience: "Целевая аудитория",
|
||||||
|
animations: "Стиль анимации",
|
||||||
|
numSlides: "Количество слайдов",
|
||||||
|
generate: "Создать презентацию",
|
||||||
|
generating: "Создаем вашу историю...",
|
||||||
|
emptyState: "Ваша презентация появится здесь",
|
||||||
|
attachFiles: "Прикрепить файлы для контекста",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
he: {
|
||||||
|
sidebar: {
|
||||||
|
title: "PromptArch",
|
||||||
|
subtitle: "ערכת כלי בינה מלאכותית",
|
||||||
|
promptEnhancer: "משפר פרומפטים",
|
||||||
|
prdGenerator: "מחולל PRD",
|
||||||
|
actionPlan: "תוכנית פעולה",
|
||||||
|
slidesGen: "מחולל מצגות",
|
||||||
|
googleAds: "Google Ads",
|
||||||
|
uxDesigner: "מעצב UX",
|
||||||
|
settings: "הגדרות",
|
||||||
|
history: "היסטוריה",
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
aiProvider: "ספק בינה מלאכותית",
|
||||||
|
model: "מודל",
|
||||||
|
generate: "חולל",
|
||||||
|
generating: "מחולל...",
|
||||||
|
copy: "העתק",
|
||||||
|
copied: "הועתק!",
|
||||||
|
settings: "הגדרות",
|
||||||
|
error: "שגיאה",
|
||||||
|
configApiKey: "הגדר מפתח API בהגדרות",
|
||||||
|
},
|
||||||
|
promptEnhancer: {
|
||||||
|
title: "משפר פרומפטים",
|
||||||
|
description: "הפוך רעיונות פשוטים לפרומפטים מקצועיים באיכות גבוהה",
|
||||||
|
placeholder: "הזן את הפרומפט שלך כאן...",
|
||||||
|
inputLabel: "הפרומפט שלך",
|
||||||
|
enhancedTitle: "פרומפט משופר",
|
||||||
|
enhancedDesc: "הפרומפט שלך הותאם לביצועי בינה מלאכותית טובים יותר",
|
||||||
|
},
|
||||||
|
prdGenerator: {
|
||||||
|
title: "מחולל PRD",
|
||||||
|
description: "חולל מסמך דרישות מוצר מקיף מהרעיון שלך",
|
||||||
|
placeholder: "למשל: אפליקציית ניהול משימות עם תכונות שיתוף בזמן אמת",
|
||||||
|
generatedTitle: "PRD שחולל",
|
||||||
|
},
|
||||||
|
googleAds: {
|
||||||
|
title: "אסטרטג Google Ads",
|
||||||
|
description: "חולל מילות מפתח, עותקי מודעות ומבנה קמפיין",
|
||||||
|
websiteUrl: "כתובת אתר",
|
||||||
|
products: "מוצרים / שירותים",
|
||||||
|
budget: "תקציב (USD לחודש)",
|
||||||
|
industry: "תעשייה",
|
||||||
|
targetAudience: "קהל יעד",
|
||||||
|
generateAds: "חולל מודעות",
|
||||||
|
magicWand: "מטה קסמים",
|
||||||
|
researching: "חוקר...",
|
||||||
|
generatedCampaign: "קמפיין שחולל",
|
||||||
|
strategicDirections: "כיוונים אסטרטגיים",
|
||||||
|
marketIntelligence: "מודיעין שוק",
|
||||||
|
competitiveInsights: "תובנות תחרותיות",
|
||||||
|
campaignDirections: "כיווני קמפיין",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: "הגדרות מערכת",
|
||||||
|
apiKeys: "הגדרת API",
|
||||||
|
qwenAuth: "אימות Qwen",
|
||||||
|
connectQwen: "חבר חשבון Qwen",
|
||||||
|
connected: "מחובר",
|
||||||
|
notConnected: "לא מחובר",
|
||||||
|
theme: "עיצוב",
|
||||||
|
language: "שפת ממשק",
|
||||||
|
saveKeys: "שמור מפתחות API",
|
||||||
|
keysSaved: "מפתחות API נשמרו בהצלחה!",
|
||||||
|
defaultProvider: "ספק ברירת מחדל",
|
||||||
|
defaultProviderDesc: "בחר את ספק הבינה המלאכותית המועדף עליך",
|
||||||
|
dataPrivacy: "פרטיות נתונים",
|
||||||
|
dataPrivacyDesc: "כל מפתחות ה-API נשמרים מקומית בדפדפן שלך. הפרומפטים שלך נשלחים ישירות לספק הבינה המלאכותית הנבחר ואינם נשמרים ב-PromptArch.",
|
||||||
|
loginQwen: "התחבר עם Qwen (OAuth)",
|
||||||
|
logoutQwen: "התנתק מ-Qwen",
|
||||||
|
authenticated: "מאומת באמצעות OAuth",
|
||||||
|
expires: "פג תוקף",
|
||||||
|
enterKey: (provider: string) => `הזן את מפתח ה-API של ${provider}`,
|
||||||
|
getApiKey: "קבל מפתח API מ-",
|
||||||
|
qwenDesc: "Alibaba DashScope API",
|
||||||
|
ollamaDesc: "Ollama Cloud API",
|
||||||
|
zaiDesc: "Z.AI Plan API",
|
||||||
|
},
|
||||||
|
uxDesigner: {
|
||||||
|
title: "פרומפט מעצב UX",
|
||||||
|
description: "תאר את רעיון האפליקציה שלך וקבל את הפרומפט הטוב ביותר אי פעם לעיצוב UX",
|
||||||
|
placeholder: "למשל: אפליקציית מעקב כושר עם תוכנית אימונים, מעקב תזונה ותכונות חברתיות...",
|
||||||
|
inputLabel: "תיאור האפליקציה",
|
||||||
|
inputDesc: "תאר איזה סוג אפליקציה אתה רוצה, קהל יעד, תכונות עיקריות והעדפות עיצוב ספציפיות.",
|
||||||
|
resultTitle: "פרומפט UX אולטימטיבי",
|
||||||
|
resultDesc: "פרומפט עיצוב UX מקיף מוכן למעצבים",
|
||||||
|
emptyState: "פרומפט מעצב ה-UX המקיף שלך יופיע כאן",
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
title: "היסטוריית מפגשים",
|
||||||
|
description: "פרומפטים קודמים ותוצאות שחוללו",
|
||||||
|
empty: "אין עדיין היסטוריה. התחל לחקור את הכלים!",
|
||||||
|
clear: "נקה היסטוריה",
|
||||||
|
},
|
||||||
|
actionPlan: {
|
||||||
|
title: "מחולל תוכנית פעולה",
|
||||||
|
description: "חולל תוכנית יישום לוגית, צעד אחר צעד, ממסמך ה-PRD שלך",
|
||||||
|
placeholder: "הדבק את ה-PRD או דרישות הפרויקט שלך כאן...",
|
||||||
|
generatedTitle: "תוכנית פעולה שחוללה",
|
||||||
|
architecture: "ארכיטקטורה טכנית",
|
||||||
|
infrastructure: "תשתית וכלים",
|
||||||
|
tasks: "משימות יישום",
|
||||||
|
riskAssessment: "הערכת סיכונים",
|
||||||
|
emptyState: "תוכנית הפעולה שחוללה תופיע כאן",
|
||||||
|
},
|
||||||
|
slidesGen: {
|
||||||
|
title: "מחולל מצגות בינה מלאכותית",
|
||||||
|
description: "חולל שקופיות מרהיבות ומקצועיות לכל אירוע בשניות",
|
||||||
|
placeholder: "תאר את נושא המצגת שלך או הדבק ראשי פרקים...",
|
||||||
|
language: "שפת המצגת",
|
||||||
|
theme: "עיצוב אסתטי",
|
||||||
|
audience: "קהל יעד",
|
||||||
|
animations: "סגנון אנימציה",
|
||||||
|
numSlides: "מספר שקופיות",
|
||||||
|
generate: "חולל מצגת",
|
||||||
|
generating: "יוצר את הסיפור שלך...",
|
||||||
|
emptyState: "המצגת שלך תופיע כאן",
|
||||||
|
attachFiles: "צרף קבצים להקשר",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -186,6 +186,59 @@ export class ModelAdapter {
|
|||||||
return this.callWithFallback((service) => service.generateUXDesignerPrompt(appDescription, model), providers);
|
return this.callWithFallback((service) => service.generateUXDesignerPrompt(appDescription, model), providers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateSlides(
|
||||||
|
topic: string,
|
||||||
|
options: {
|
||||||
|
language?: string;
|
||||||
|
theme?: string;
|
||||||
|
slideCount?: number;
|
||||||
|
audience?: string;
|
||||||
|
organization?: string;
|
||||||
|
animationStyle?: string;
|
||||||
|
audienceStyle?: string;
|
||||||
|
themeColors?: string[];
|
||||||
|
brandColors?: string[];
|
||||||
|
} = {},
|
||||||
|
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.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 generateMagicWand(
|
||||||
|
websiteUrl: string,
|
||||||
|
product: string,
|
||||||
|
budget: number,
|
||||||
|
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.generateMagicWand(websiteUrl, product, budget, model), providers);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async chatCompletion(
|
async chatCompletion(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
model: string,
|
model: string,
|
||||||
|
|||||||
@@ -303,6 +303,297 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
|||||||
|
|
||||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateSlides(
|
||||||
|
topic: string,
|
||||||
|
options: {
|
||||||
|
language?: string;
|
||||||
|
theme?: string;
|
||||||
|
slideCount?: number;
|
||||||
|
audience?: string;
|
||||||
|
organization?: string;
|
||||||
|
animationStyle?: string;
|
||||||
|
audienceStyle?: string;
|
||||||
|
themeColors?: string[];
|
||||||
|
brandColors?: string[];
|
||||||
|
} = {},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const {
|
||||||
|
language = "English",
|
||||||
|
theme = "executive-dark",
|
||||||
|
slideCount = 10,
|
||||||
|
audience = "Executives & C-Suite",
|
||||||
|
organization = "",
|
||||||
|
animationStyle = "Professional",
|
||||||
|
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||||
|
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||||
|
brandColors = []
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||||
|
const brandColorStr = brandColors.length > 0
|
||||||
|
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||||
|
|
||||||
|
Your slides must be VISUALLY SPECTACULAR with:
|
||||||
|
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||||
|
- Sophisticated gradient backgrounds with depth
|
||||||
|
- SVG charts and data visualizations inline
|
||||||
|
- Glassmorphism and neumorphism effects
|
||||||
|
- Professional typography with Inter/SF Pro fonts
|
||||||
|
- Strategic use of whitespace
|
||||||
|
- Micro-animations on hover/focus states
|
||||||
|
- Progress indicators and visual hierarchy
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"title": "Presentation Title",
|
||||||
|
"subtitle": "Compelling Subtitle",
|
||||||
|
"theme": "${theme}",
|
||||||
|
"language": "${language}",
|
||||||
|
"slides": [
|
||||||
|
{
|
||||||
|
"id": "slide-1",
|
||||||
|
"title": "Slide Title",
|
||||||
|
"content": "Plain text content summary",
|
||||||
|
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||||
|
"notes": "Speaker notes",
|
||||||
|
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
DESIGN SYSTEM:
|
||||||
|
- Primary: ${brandColors[0] || primaryColor}
|
||||||
|
- Secondary: ${brandColors[1] || secondaryColor}
|
||||||
|
- Background: ${bgColor}
|
||||||
|
- Text: ${textColor}${brandColorStr}
|
||||||
|
|
||||||
|
ANIMATION STYLE: ${animationStyle}
|
||||||
|
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||||
|
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||||
|
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||||
|
|
||||||
|
CSS ANIMATIONS TO INCLUDE:
|
||||||
|
\`\`\`css
|
||||||
|
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||||
|
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||||
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
SLIDE TYPES TO CREATE:
|
||||||
|
1. TITLE SLIDE: Hero-style with animated gradient background, large typography
|
||||||
|
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||||
|
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||||
|
4. KEY METRICS: Large animated numbers with KPI cards
|
||||||
|
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||||
|
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||||
|
7. QUOTE: Large typography with decorative quote marks
|
||||||
|
8. CALL-TO-ACTION: Bold CTA with pulsing button effect
|
||||||
|
|
||||||
|
TARGET AUDIENCE: ${audience}
|
||||||
|
AUDIENCE STYLE: ${audienceStyle}
|
||||||
|
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||||
|
|
||||||
|
REQUIREMENTS:
|
||||||
|
- Create EXACTLY ${slideCount} slides
|
||||||
|
- ALL content in ${language}
|
||||||
|
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||||
|
- Use animation-delay for staggered reveal effects
|
||||||
|
- Include decorative background elements (gradients, shapes)
|
||||||
|
- Ensure text contrast meets WCAG AA standards
|
||||||
|
- Add subtle shadow/glow effects for depth`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `Create a STUNNING, ANIMATED presentation about:
|
||||||
|
|
||||||
|
${topic}
|
||||||
|
|
||||||
|
SPECIFICATIONS:
|
||||||
|
- Language: ${language}
|
||||||
|
- Theme: ${theme}
|
||||||
|
- Slides: ${slideCount}
|
||||||
|
- Audience: ${audience} (${audienceStyle})
|
||||||
|
- Animation Style: ${animationStyle}
|
||||||
|
${organization ? `- Organization: ${organization}` : ""}
|
||||||
|
${brandColors.length > 0 ? `- Brand Colors: ${brandColors.join(", ")}` : ""}
|
||||||
|
|
||||||
|
Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients, and corporate-ready design!`,
|
||||||
|
};
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateMagicWand(
|
||||||
|
websiteUrl: string,
|
||||||
|
product: string,
|
||||||
|
budget: number,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy.
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"marketAnalysis": {
|
||||||
|
"industrySize": "Estimated market size",
|
||||||
|
"growthRate": "Annual growth percentage",
|
||||||
|
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||||
|
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||||
|
},
|
||||||
|
"competitorInsights": [
|
||||||
|
{
|
||||||
|
"competitor": "Competitor Name",
|
||||||
|
"strengths": ["Strength 1", "Strength 2"],
|
||||||
|
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||||
|
"adStrategy": "Their current advertising approach"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"id": "strategy-1",
|
||||||
|
"direction": "Strategic Direction Name",
|
||||||
|
"rationale": "Why this strategy works for this product/market",
|
||||||
|
"targetAudience": "Specific audience segment",
|
||||||
|
"competitiveAdvantage": "How this beats competitors",
|
||||||
|
"keyMessages": ["Message 1", "Message 2", "Message 3"],
|
||||||
|
"recommendedChannels": ["Google Search", "Display", "YouTube"],
|
||||||
|
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||||
|
"expectedROI": "150-200%",
|
||||||
|
"riskLevel": "low",
|
||||||
|
"timeToResults": "2-3 months"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
CRITICAL REQUIREMENTS:
|
||||||
|
- Provide 5-7 DISTINCT strategic directions
|
||||||
|
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||||
|
- Include REAL competitive insights based on industry knowledge
|
||||||
|
- Risk levels: "low", "medium", or "high"`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||||
|
|
||||||
|
WEBSITE: ${websiteUrl}
|
||||||
|
PRODUCT/SERVICE: ${product}
|
||||||
|
MONTHLY BUDGET: $${budget}
|
||||||
|
|
||||||
|
Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OllamaCloudService;
|
export default OllamaCloudService;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -631,6 +631,295 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
|||||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateSlides(
|
||||||
|
topic: string,
|
||||||
|
options: {
|
||||||
|
language?: string;
|
||||||
|
theme?: string;
|
||||||
|
slideCount?: number;
|
||||||
|
audience?: string;
|
||||||
|
organization?: string;
|
||||||
|
animationStyle?: string;
|
||||||
|
audienceStyle?: string;
|
||||||
|
themeColors?: string[];
|
||||||
|
brandColors?: string[];
|
||||||
|
} = {},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const {
|
||||||
|
language = "English",
|
||||||
|
theme = "executive-dark",
|
||||||
|
slideCount = 10,
|
||||||
|
audience = "Executives & C-Suite",
|
||||||
|
organization = "",
|
||||||
|
animationStyle = "Professional",
|
||||||
|
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||||
|
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||||
|
brandColors = []
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||||
|
const brandColorStr = brandColors.length > 0
|
||||||
|
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||||
|
|
||||||
|
Your slides must be VISUALLY SPECTACULAR with:
|
||||||
|
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||||
|
- Sophisticated gradient backgrounds with depth
|
||||||
|
- SVG charts and data visualizations inline
|
||||||
|
- Glassmorphism and neumorphism effects
|
||||||
|
- Professional typography with Inter/SF Pro fonts
|
||||||
|
- Strategic use of whitespace
|
||||||
|
- Micro-animations on hover/focus states
|
||||||
|
- Progress indicators and visual hierarchy
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"title": "Presentation Title",
|
||||||
|
"subtitle": "Compelling Subtitle",
|
||||||
|
"theme": "${theme}",
|
||||||
|
"language": "${language}",
|
||||||
|
"slides": [
|
||||||
|
{
|
||||||
|
"id": "slide-1",
|
||||||
|
"title": "Slide Title",
|
||||||
|
"content": "Plain text content summary",
|
||||||
|
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||||
|
"notes": "Speaker notes",
|
||||||
|
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
DESIGN SYSTEM:
|
||||||
|
- Primary: ${brandColors[0] || primaryColor}
|
||||||
|
- Secondary: ${brandColors[1] || secondaryColor}
|
||||||
|
- Background: ${bgColor}
|
||||||
|
- Text: ${textColor}${brandColorStr}
|
||||||
|
|
||||||
|
ANIMATION STYLE: ${animationStyle}
|
||||||
|
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||||
|
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||||
|
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||||
|
|
||||||
|
CSS ANIMATIONS TO INCLUDE:
|
||||||
|
\`\`\`css
|
||||||
|
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||||
|
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||||
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
SLIDE TYPES TO CREATE:
|
||||||
|
1. TITLE SLIDE: Hero-style with animated gradient background, large typography
|
||||||
|
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||||
|
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||||
|
4. KEY METRICS: Large animated numbers with KPI cards
|
||||||
|
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||||
|
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||||
|
7. QUOTE: Large typography with decorative quote marks
|
||||||
|
8. CALL-TO-ACTION: Bold CTA with pulsing button effect
|
||||||
|
|
||||||
|
TARGET AUDIENCE: ${audience}
|
||||||
|
AUDIENCE STYLE: ${audienceStyle}
|
||||||
|
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||||
|
|
||||||
|
REQUIREMENTS:
|
||||||
|
- Create EXACTLY ${slideCount} slides
|
||||||
|
- ALL content in ${language}
|
||||||
|
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||||
|
- Use animation-delay for staggered reveal effects
|
||||||
|
- Include decorative background elements (gradients, shapes)
|
||||||
|
- Ensure text contrast meets WCAG AA standards
|
||||||
|
- Add subtle shadow/glow effects for depth`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `Create a STUNNING, ANIMATED presentation about:
|
||||||
|
|
||||||
|
${topic}
|
||||||
|
|
||||||
|
SPECIFICATIONS:
|
||||||
|
- Language: ${language}
|
||||||
|
- Theme: ${theme}
|
||||||
|
- Slides: ${slideCount}
|
||||||
|
- Audience: ${audience} (${audienceStyle})
|
||||||
|
- Animation Style: ${animationStyle}
|
||||||
|
${organization ? `- Organization: ${organization}` : ""}
|
||||||
|
${brandColors.length > 0 ? `- Brand Colors: ${brandColors.join(", ")}` : ""}
|
||||||
|
|
||||||
|
Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients, and corporate-ready design!`,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 generateMagicWand(
|
||||||
|
websiteUrl: string,
|
||||||
|
product: string,
|
||||||
|
budget: number,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy.
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"marketAnalysis": {
|
||||||
|
"industrySize": "Estimated market size",
|
||||||
|
"growthRate": "Annual growth percentage",
|
||||||
|
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||||
|
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||||
|
},
|
||||||
|
"competitorInsights": [
|
||||||
|
{
|
||||||
|
"competitor": "Competitor Name",
|
||||||
|
"strengths": ["Strength 1", "Strength 2"],
|
||||||
|
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||||
|
"adStrategy": "Their current advertising approach"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"id": "strategy-1",
|
||||||
|
"direction": "Strategic Direction Name",
|
||||||
|
"rationale": "Why this strategy works for this product/market",
|
||||||
|
"targetAudience": "Specific audience segment",
|
||||||
|
"competitiveAdvantage": "How this beats competitors",
|
||||||
|
"keyMessages": ["Message 1", "Message 2", "Message 3"],
|
||||||
|
"recommendedChannels": ["Google Search", "Display", "YouTube"],
|
||||||
|
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||||
|
"expectedROI": "150-200%",
|
||||||
|
"riskLevel": "low",
|
||||||
|
"timeToResults": "2-3 months"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
CRITICAL REQUIREMENTS:
|
||||||
|
- Provide 5-7 DISTINCT strategic directions
|
||||||
|
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||||
|
- Include REAL competitive insights based on industry knowledge
|
||||||
|
- Risk levels: "low", "medium", or "high"`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||||
|
|
||||||
|
WEBSITE: ${websiteUrl}
|
||||||
|
PRODUCT/SERVICE: ${product}
|
||||||
|
MONTHLY BUDGET: $${budget}
|
||||||
|
|
||||||
|
Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||||
|
}
|
||||||
|
|
||||||
async listModels(): Promise<APIResponse<string[]>> {
|
async listModels(): Promise<APIResponse<string[]>> {
|
||||||
const models = [
|
const models = [
|
||||||
"coder-model",
|
"coder-model",
|
||||||
@@ -648,3 +937,5 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
|||||||
const qwenOAuthService = new QwenOAuthService();
|
const qwenOAuthService = new QwenOAuthService();
|
||||||
export default qwenOAuthService;
|
export default qwenOAuthService;
|
||||||
export { qwenOAuthService };
|
export { qwenOAuthService };
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,430 @@ Make the prompt specific, inspiring, and comprehensive. Use professional UX term
|
|||||||
|
|
||||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateSlides(
|
||||||
|
topic: string,
|
||||||
|
options: {
|
||||||
|
language?: string;
|
||||||
|
theme?: string;
|
||||||
|
slideCount?: number;
|
||||||
|
audience?: string;
|
||||||
|
organization?: string;
|
||||||
|
animationStyle?: string;
|
||||||
|
audienceStyle?: string;
|
||||||
|
themeColors?: string[];
|
||||||
|
brandColors?: string[];
|
||||||
|
} = {},
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const {
|
||||||
|
language = "English",
|
||||||
|
theme = "executive-dark",
|
||||||
|
slideCount = 10,
|
||||||
|
audience = "Executives & C-Suite",
|
||||||
|
organization = "",
|
||||||
|
animationStyle = "Professional",
|
||||||
|
audienceStyle = "Sophisticated, data-driven, strategic focus",
|
||||||
|
themeColors = ["#09090b", "#6366f1", "#a855f7", "#fafafa"],
|
||||||
|
brandColors = []
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const [bgColor, primaryColor, secondaryColor, textColor] = themeColors;
|
||||||
|
const brandColorStr = brandColors.length > 0
|
||||||
|
? `\nBRAND COLORS TO USE: ${brandColors.join(", ")}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS presentation designer who creates STUNNING, AWARD-WINNING slide decks that rival McKinsey, Apple, and TED presentations.
|
||||||
|
|
||||||
|
Your slides must be VISUALLY SPECTACULAR with:
|
||||||
|
- Modern CSS3 animations (fade-in, slide-in, scale, parallax effects)
|
||||||
|
- Sophisticated gradient backgrounds with depth
|
||||||
|
- SVG charts and data visualizations inline
|
||||||
|
- Glassmorphism and neumorphism effects
|
||||||
|
- Professional typography with Inter/SF Pro fonts
|
||||||
|
- Strategic use of whitespace
|
||||||
|
- Micro-animations on hover/focus states
|
||||||
|
- Progress indicators and visual hierarchy
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"title": "Presentation Title",
|
||||||
|
"subtitle": "Compelling Subtitle",
|
||||||
|
"theme": "${theme}",
|
||||||
|
"language": "${language}",
|
||||||
|
"slides": [
|
||||||
|
{
|
||||||
|
"id": "slide-1",
|
||||||
|
"title": "Slide Title",
|
||||||
|
"content": "Plain text content summary",
|
||||||
|
"htmlContent": "<div>FULL HTML with inline CSS and animations</div>",
|
||||||
|
"notes": "Speaker notes",
|
||||||
|
"layout": "title|content|two-column|chart|statistics|timeline|quote|comparison",
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
DESIGN SYSTEM:
|
||||||
|
- Primary: ${brandColors[0] || primaryColor}
|
||||||
|
- Secondary: ${brandColors[1] || secondaryColor}
|
||||||
|
- Background: ${bgColor}
|
||||||
|
- Text: ${textColor}${brandColorStr}
|
||||||
|
|
||||||
|
ANIMATION STYLE: ${animationStyle}
|
||||||
|
- Professional: Subtle 0.3-0.5s ease transitions, fade and slide
|
||||||
|
- Dynamic: 0.5-0.8s spring animations, emphasis effects, stagger delays
|
||||||
|
- Impressive: Bold 0.8-1.2s animations, parallax, morphing, particle effects
|
||||||
|
|
||||||
|
CSS ANIMATIONS TO INCLUDE:
|
||||||
|
\`\`\`css
|
||||||
|
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } }
|
||||||
|
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||||
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||||
|
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
|
||||||
|
@keyframes gradientShift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
SLIDE TYPES TO CREATE:
|
||||||
|
1. TITLE SLIDE: Hero-style with animated gradient background, large typography, subtle floating elements
|
||||||
|
2. AGENDA/OVERVIEW: Icon grid with staggered fade-in animations
|
||||||
|
3. DATA/CHARTS: Inline SVG bar/line/pie charts with animated drawing effects
|
||||||
|
4. KEY METRICS: Large animated numbers with counting effect styling, KPI cards with glassmorphism
|
||||||
|
5. TIMELINE: Horizontal/vertical timeline with sequential reveal animations
|
||||||
|
6. COMPARISON: Side-by-side cards with hover lift effects
|
||||||
|
7. QUOTE: Large typography with decorative quote marks, subtle background pattern
|
||||||
|
8. CALL-TO-ACTION: Bold CTA with pulsing button effect, clear next steps
|
||||||
|
|
||||||
|
SVG CHART EXAMPLE:
|
||||||
|
\`\`\`html
|
||||||
|
<svg viewBox="0 0 400 200" style="width:100%;max-width:400px;">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="barGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:${primaryColor}"/>
|
||||||
|
<stop offset="100%" style="stop-color:${secondaryColor}"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="50" y="50" width="60" height="130" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.2s both; transform-origin: bottom;"/>
|
||||||
|
<rect x="130" y="80" width="60" height="100" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.4s both; transform-origin: bottom;"/>
|
||||||
|
<rect x="210" y="30" width="60" height="150" fill="url(#barGrad)" rx="8" style="animation: scaleIn 0.8s ease-out 0.6s both; transform-origin: bottom;"/>
|
||||||
|
</svg>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
TARGET AUDIENCE: ${audience}
|
||||||
|
AUDIENCE STYLE: ${audienceStyle}
|
||||||
|
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
|
||||||
|
|
||||||
|
REQUIREMENTS:
|
||||||
|
- Create EXACTLY ${slideCount} slides
|
||||||
|
- ALL content in ${language}
|
||||||
|
- Each slide MUST have complete htmlContent with inline <style> tags
|
||||||
|
- Use animation-delay for staggered reveal effects
|
||||||
|
- Include decorative background elements (gradients, shapes, patterns)
|
||||||
|
- Ensure text contrast meets WCAG AA standards
|
||||||
|
- Add subtle shadow/glow effects for depth
|
||||||
|
- Include progress/slide number indicator styling`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `Create a STUNNING, ANIMATED presentation about:
|
||||||
|
|
||||||
|
${topic}
|
||||||
|
|
||||||
|
SPECIFICATIONS:
|
||||||
|
- Language: ${language}
|
||||||
|
- Theme: ${theme}
|
||||||
|
- Slides: ${slideCount}
|
||||||
|
- Audience: ${audience} (${audienceStyle})
|
||||||
|
- Animation Style: ${animationStyle}
|
||||||
|
${organization ? `- Organization: ${organization}` : ""}
|
||||||
|
${brandColors.length > 0 ? `- Brand Colors: ${brandColors.join(", ")}` : ""}
|
||||||
|
|
||||||
|
Generate SPECTACULAR slides with:
|
||||||
|
✨ Animated CSS3 transitions and keyframes
|
||||||
|
📊 SVG charts and data visualizations where relevant
|
||||||
|
🎨 Modern gradients and glassmorphism effects
|
||||||
|
💫 Staggered reveal animations
|
||||||
|
🏢 Corporate-ready, executive-level design
|
||||||
|
|
||||||
|
Return the complete JSON with full htmlContent for each slide. Make each slide VISUALLY IMPRESSIVE and memorable!`,
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateMagicWand(
|
||||||
|
websiteUrl: string,
|
||||||
|
product: string,
|
||||||
|
budget: number,
|
||||||
|
model?: string
|
||||||
|
): Promise<APIResponse<string>> {
|
||||||
|
const systemMessage: ChatMessage = {
|
||||||
|
role: "system",
|
||||||
|
content: `You are a WORLD-CLASS marketing strategist with 20+ years of experience in competitive intelligence, market research, and Google Ads campaign strategy. You have access to deep industry knowledge and can analyze markets like a Fortune 500 CMO.
|
||||||
|
|
||||||
|
OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"marketAnalysis": {
|
||||||
|
"industrySize": "Estimated market size",
|
||||||
|
"growthRate": "Annual growth percentage",
|
||||||
|
"topCompetitors": ["Competitor 1", "Competitor 2", "Competitor 3"],
|
||||||
|
"marketTrends": ["Trend 1", "Trend 2", "Trend 3"]
|
||||||
|
},
|
||||||
|
"competitorInsights": [
|
||||||
|
{
|
||||||
|
"competitor": "Competitor Name",
|
||||||
|
"strengths": ["Strength 1", "Strength 2"],
|
||||||
|
"weaknesses": ["Weakness 1", "Weakness 2"],
|
||||||
|
"adStrategy": "Their current advertising approach"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"id": "strategy-1",
|
||||||
|
"direction": "Strategic Direction Name",
|
||||||
|
"rationale": "Why this strategy works for this product/market",
|
||||||
|
"targetAudience": "Specific audience segment",
|
||||||
|
"competitiveAdvantage": "How this beats competitors",
|
||||||
|
"keyMessages": ["Message 1", "Message 2", "Message 3"],
|
||||||
|
"recommendedChannels": ["Google Search", "Display", "YouTube"],
|
||||||
|
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
|
||||||
|
"expectedROI": "150-200%",
|
||||||
|
"riskLevel": "low",
|
||||||
|
"timeToResults": "2-3 months"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
CRITICAL REQUIREMENTS:
|
||||||
|
- Provide 5-7 DISTINCT strategic directions
|
||||||
|
- Each strategy must be ACTIONABLE and SPECIFIC
|
||||||
|
- Include REAL competitive insights based on industry knowledge
|
||||||
|
- Budget allocations must sum to 100%
|
||||||
|
- Risk levels: "low", "medium", or "high"
|
||||||
|
- Be REALISTIC with ROI and timeline estimates`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMessage: ChatMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: `🔮 MAGIC WAND ANALYSIS REQUEST 🔮
|
||||||
|
|
||||||
|
WEBSITE: ${websiteUrl}
|
||||||
|
PRODUCT/SERVICE: ${product}
|
||||||
|
MONTHLY BUDGET: $${budget}
|
||||||
|
|
||||||
|
MISSION: Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategic campaign directions that will DOMINATE this market.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ZaiPlanService;
|
export default ZaiPlanService;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
21
lib/store.ts
21
lib/store.ts
@@ -1,11 +1,15 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan } from "@/types";
|
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult } from "@/types";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentPrompt: string;
|
currentPrompt: string;
|
||||||
enhancedPrompt: string | null;
|
enhancedPrompt: string | null;
|
||||||
prd: PRD | null;
|
prd: PRD | null;
|
||||||
actionPlan: ActionPlan | null;
|
actionPlan: ActionPlan | null;
|
||||||
|
slidesPresentation: SlidesPresentation | null;
|
||||||
|
googleAdsResult: GoogleAdsResult | null;
|
||||||
|
magicWandResult: MagicWandResult | null;
|
||||||
|
language: "en" | "ru" | "he";
|
||||||
selectedProvider: ModelProvider;
|
selectedProvider: ModelProvider;
|
||||||
selectedModels: Record<ModelProvider, string>;
|
selectedModels: Record<ModelProvider, string>;
|
||||||
availableModels: Record<ModelProvider, string[]>;
|
availableModels: Record<ModelProvider, string[]>;
|
||||||
@@ -27,6 +31,10 @@ interface AppState {
|
|||||||
setEnhancedPrompt: (enhanced: string | null) => void;
|
setEnhancedPrompt: (enhanced: string | null) => void;
|
||||||
setPRD: (prd: PRD) => void;
|
setPRD: (prd: PRD) => void;
|
||||||
setActionPlan: (plan: ActionPlan) => void;
|
setActionPlan: (plan: ActionPlan) => void;
|
||||||
|
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
||||||
|
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
||||||
|
setMagicWandResult: (result: MagicWandResult | null) => void;
|
||||||
|
setLanguage: (lang: "en" | "ru" | "he") => void;
|
||||||
setSelectedProvider: (provider: ModelProvider) => void;
|
setSelectedProvider: (provider: ModelProvider) => void;
|
||||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||||
@@ -44,6 +52,10 @@ const useStore = create<AppState>((set) => ({
|
|||||||
enhancedPrompt: null,
|
enhancedPrompt: null,
|
||||||
prd: null,
|
prd: null,
|
||||||
actionPlan: null,
|
actionPlan: null,
|
||||||
|
slidesPresentation: null,
|
||||||
|
googleAdsResult: null,
|
||||||
|
magicWandResult: null,
|
||||||
|
language: "en",
|
||||||
selectedProvider: "qwen",
|
selectedProvider: "qwen",
|
||||||
selectedModels: {
|
selectedModels: {
|
||||||
qwen: "coder-model",
|
qwen: "coder-model",
|
||||||
@@ -68,6 +80,10 @@ const useStore = create<AppState>((set) => ({
|
|||||||
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
||||||
setPRD: (prd) => set({ prd }),
|
setPRD: (prd) => set({ prd }),
|
||||||
setActionPlan: (plan) => set({ actionPlan: plan }),
|
setActionPlan: (plan) => set({ actionPlan: plan }),
|
||||||
|
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
||||||
|
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
||||||
|
setMagicWandResult: (result) => set({ magicWandResult: result }),
|
||||||
|
setLanguage: (lang) => set({ language: lang }),
|
||||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||||
setSelectedModel: (provider, model) =>
|
setSelectedModel: (provider, model) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
@@ -102,6 +118,9 @@ const useStore = create<AppState>((set) => ({
|
|||||||
enhancedPrompt: null,
|
enhancedPrompt: null,
|
||||||
prd: null,
|
prd: null,
|
||||||
actionPlan: null,
|
actionPlan: null,
|
||||||
|
slidesPresentation: null,
|
||||||
|
googleAdsResult: null,
|
||||||
|
magicWandResult: null,
|
||||||
error: null,
|
error: null,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
304
package-lock.json
generated
304
package-lock.json
generated
@@ -9,10 +9,12 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-next": "^15.0.3",
|
"eslint-config-next": "^15.0.3",
|
||||||
@@ -930,6 +932,294 @@
|
|||||||
"node": ">=12.4.0"
|
"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": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.11.2",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
@@ -1145,7 +1435,7 @@
|
|||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
@@ -2044,6 +2334,18 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^19.0.1",
|
"@types/react": "^19.0.1",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-next": "^15.0.3",
|
"eslint-config-next": "^15.0.3",
|
||||||
|
|||||||
160
types/index.ts
160
types/index.ts
@@ -91,3 +91,163 @@ export interface ChatMessage {
|
|||||||
role: "system" | "user" | "assistant";
|
role: "system" | "user" | "assistant";
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Slide {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
htmlContent: string;
|
||||||
|
notes?: string;
|
||||||
|
layout: "title" | "content" | "two-column" | "image-left" | "image-right" | "quote" | "statistics" | "timeline" | "comparison";
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlidesPresentation {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
author?: string;
|
||||||
|
organization?: string;
|
||||||
|
theme: "corporate" | "modern" | "minimal" | "dark" | "vibrant" | "gradient";
|
||||||
|
language: string;
|
||||||
|
slides: Slide[];
|
||||||
|
rawContent: string;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MagicWandStrategy {
|
||||||
|
id: string;
|
||||||
|
direction: string;
|
||||||
|
rationale: string;
|
||||||
|
targetAudience: string;
|
||||||
|
competitiveAdvantage: string;
|
||||||
|
keyMessages: string[];
|
||||||
|
recommendedChannels: string[];
|
||||||
|
estimatedBudgetAllocation: {
|
||||||
|
search?: number;
|
||||||
|
display?: number;
|
||||||
|
video?: number;
|
||||||
|
social?: number;
|
||||||
|
};
|
||||||
|
expectedROI: string;
|
||||||
|
riskLevel: "low" | "medium" | "high";
|
||||||
|
timeToResults: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MagicWandResult {
|
||||||
|
id: string;
|
||||||
|
websiteUrl: string;
|
||||||
|
product: string;
|
||||||
|
budget: number;
|
||||||
|
generatedAt: Date;
|
||||||
|
|
||||||
|
// Market Intelligence
|
||||||
|
marketAnalysis: {
|
||||||
|
industrySize: string;
|
||||||
|
growthRate: string;
|
||||||
|
topCompetitors: string[];
|
||||||
|
marketTrends: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Competitive Intelligence
|
||||||
|
competitorInsights: {
|
||||||
|
competitor: string;
|
||||||
|
strengths: string[];
|
||||||
|
weaknesses: string[];
|
||||||
|
adStrategy: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
// Strategic Directions
|
||||||
|
strategies: MagicWandStrategy[];
|
||||||
|
|
||||||
|
rawContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user