Compare commits

...

2 Commits

18 changed files with 1212 additions and 245 deletions

View File

@@ -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>
); );
} }

View File

@@ -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>

View File

@@ -7,20 +7,24 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
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 { Megaphone, Copy, Loader2, CheckCircle2, Settings, Plus, X, ChevronDown, ChevronUp } from "lucide-react"; 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 { cn } from "@/lib/utils";
import { GoogleAdsResult } from "@/types"; import { GoogleAdsResult } from "@/types";
import { translations } from "@/lib/i18n/translations";
export default function GoogleAdsGenerator() { export default function GoogleAdsGenerator() {
const { const {
googleAdsResult, googleAdsResult,
magicWandResult,
selectedProvider, selectedProvider,
selectedModels, selectedModels,
availableModels, availableModels,
apiKeys, apiKeys,
isProcessing, isProcessing,
error, error,
language,
setGoogleAdsResult, setGoogleAdsResult,
setMagicWandResult,
setProcessing, setProcessing,
setError, setError,
setAvailableModels, setAvailableModels,
@@ -28,6 +32,9 @@ export default function GoogleAdsGenerator() {
setSelectedProvider, setSelectedProvider,
} = useStore(); } = useStore();
const t = translations[language].googleAds;
const common = translations[language].common;
// Input states // Input states
const [websiteUrl, setWebsiteUrl] = useState(""); const [websiteUrl, setWebsiteUrl] = useState("");
const [products, setProducts] = useState<string[]>([""]); const [products, setProducts] = useState<string[]>([""]);
@@ -40,6 +47,7 @@ export default function GoogleAdsGenerator() {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [expandedSections, setExpandedSections] = useState<string[]>(["keywords"]); const [expandedSections, setExpandedSections] = useState<string[]>(["keywords"]);
const [isMagicThinking, setIsMagicThinking] = useState(false);
const selectedModel = selectedModels[selectedProvider]; const selectedModel = selectedModels[selectedProvider];
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider); const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
@@ -112,6 +120,7 @@ export default function GoogleAdsGenerator() {
setProcessing(true); setProcessing(true);
setError(null); setError(null);
setMagicWandResult(null);
console.log("[GoogleAdsGenerator] Starting generation...", { selectedProvider, selectedModel }); console.log("[GoogleAdsGenerator] Starting generation...", { selectedProvider, selectedModel });
@@ -180,19 +189,85 @@ export default function GoogleAdsGenerator() {
} }
}; };
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 handleCopy = async () => {
if (googleAdsResult?.rawContent) { const content = googleAdsResult?.rawContent || magicWandResult?.rawContent;
await navigator.clipboard.writeText(googleAdsResult.rawContent); if (content) {
await navigator.clipboard.writeText(content);
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
} }
}; };
const sections = [ const sections = [
{ id: "keywords", title: "Keywords Research" }, { id: "keywords", title: language === "ru" ? "Исследование ключевых слов" : language === "he" ? "מחקר מילות מפתח" : "Keywords Research" },
{ id: "adcopies", title: "Ad Copy Variations" }, { id: "adcopies", title: language === "ru" ? "Варианты объявлений" : language === "he" ? "גרסאות עותקי מודעות" : "Ad Copy Variations" },
{ id: "campaigns", title: "Campaign Structure" }, { id: "campaigns", title: language === "ru" ? "Структура кампании" : language === "he" ? "מבנה קמפיין" : "Campaign Structure" },
{ id: "implementation", title: "Implementation Guide" }, { id: "implementation", title: language === "ru" ? "Руководство по внедрению" : language === "he" ? "מדריך יישום" : "Implementation Guide" },
]; ];
const renderSectionContent = (sectionId: string) => { const renderSectionContent = (sectionId: string) => {
@@ -321,21 +396,173 @@ export default function GoogleAdsGenerator() {
} }
}; };
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 ( 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">
<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">
<Megaphone className="h-4 w-4 lg:h-5 lg:w-5" /> <Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
Google Ads Generator {t.title}
</CardTitle> </CardTitle>
<CardDescription className="text-xs lg:text-sm"> <CardDescription className="text-xs lg:text-sm">
Generate keywords, ad copy, and campaign structure for Google Ads {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">
<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
@@ -352,7 +579,7 @@ export default function GoogleAdsGenerator() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<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)}
@@ -367,7 +594,7 @@ export default function GoogleAdsGenerator() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">Website URL</label> <label className="text-xs lg:text-sm font-medium">{t.websiteUrl}</label>
<Input <Input
placeholder="e.g., www.your-business.com" placeholder="e.g., www.your-business.com"
value={websiteUrl} value={websiteUrl}
@@ -377,12 +604,12 @@ export default function GoogleAdsGenerator() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">Products / Services</label> <label className="text-xs lg:text-sm font-medium">{t.products}</label>
<div className="space-y-2"> <div className="space-y-2">
{products.map((product, index) => ( {products.map((product, index) => (
<div key={index} className="flex gap-2"> <div key={index} className="flex gap-2">
<Input <Input
placeholder={`Product ${index + 1}`} placeholder={`${language === "ru" ? "Продукт" : language === "he" ? "מוצר" : "Product"} ${index + 1}`}
value={product} value={product}
onChange={(e) => updateProduct(index, e.target.value)} onChange={(e) => updateProduct(index, e.target.value)}
className="text-sm" className="text-sm"
@@ -401,14 +628,14 @@ export default function GoogleAdsGenerator() {
))} ))}
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs"> <Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
<Plus className="mr-1.5 h-3.5 w-3.5" /> <Plus className="mr-1.5 h-3.5 w-3.5" />
Add Product {language === "ru" ? "Добавить продукт" : language === "he" ? "הוסף מוצר" : "Add Product"}
</Button> </Button>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">Budget (USD/mo)</label> <label className="text-xs lg:text-sm font-medium">{t.budget}</label>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Input <Input
type="number" type="number"
@@ -417,7 +644,7 @@ export default function GoogleAdsGenerator() {
onChange={(e) => setBudgetMin(e.target.value)} onChange={(e) => setBudgetMin(e.target.value)}
className="text-sm" className="text-sm"
/> />
<span className="text-muted-foreground text-xs">-</span> <span className="text-muted-foreground text-xs font-bold text-center">-</span>
<Input <Input
type="number" type="number"
placeholder="Max" placeholder="Max"
@@ -428,7 +655,7 @@ export default function GoogleAdsGenerator() {
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">Industry</label> <label className="text-xs lg:text-sm font-medium">{t.industry}</label>
<Input <Input
placeholder="e.g., SaaS" placeholder="e.g., SaaS"
value={industry} value={industry}
@@ -439,7 +666,7 @@ export default function GoogleAdsGenerator() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">Target Audience</label> <label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
<Textarea <Textarea
placeholder="e.g., Small business owners in USA looking for productivity tools" placeholder="e.g., Small business owners in USA looking for productivity tools"
value={targetAudience} value={targetAudience}
@@ -454,25 +681,48 @@ export default function GoogleAdsGenerator() {
{!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>
)} )}
<Button onClick={handleGenerate} disabled={isProcessing || !websiteUrl.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm"> <div className="grid grid-cols-2 gap-3">
{isProcessing ? ( <Button
<> onClick={handleGenerate}
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" /> disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
Generating Ads... className="h-9 lg:h-10 text-xs lg:text-sm bg-primary/90 hover:bg-primary shadow-sm"
</> >
) : ( {isProcessing ? (
<> <>
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" /> <Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
Generate Google Ads {common.generating}
</> </>
)} ) : (
</Button> <>
<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> </CardContent>
</Card> </Card>
@@ -480,10 +730,14 @@ export default function GoogleAdsGenerator() {
<CardHeader className="p-4 lg:p-6"> <CardHeader className="p-4 lg:p-6">
<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" /> {magicWandResult ? (
Generated Campaign <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> </span>
{googleAdsResult && ( {(googleAdsResult || magicWandResult) && (
<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">
{copied ? ( {copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" /> <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
@@ -494,13 +748,23 @@ export default function GoogleAdsGenerator() {
)} )}
</CardTitle> </CardTitle>
<CardDescription className="text-xs lg:text-sm"> <CardDescription className="text-xs lg:text-sm">
Keywords, ad copy, and campaign structure ready for Google Ads {magicWandResult
? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes")
: (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready")
}
</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">
{googleAdsResult ? ( {googleAdsResult || magicWandResult ? (
<div className="space-y-2 lg:space-y-3"> <div className="space-y-2 lg:space-y-3">
{sections.map((section) => ( {(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"> <div key={section.id} className="rounded-md border bg-muted/30">
<button <button
onClick={() => toggleSection(section.id)} onClick={() => toggleSection(section.id)}
@@ -514,8 +778,11 @@ export default function GoogleAdsGenerator() {
)} )}
</button> </button>
{expandedSections.includes(section.id) && ( {expandedSections.includes(section.id) && (
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3"> <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">
{renderSectionContent(section.id)} {magicWandResult
? renderMagicWandSectionContent(section.id)
: renderSectionContent(section.id)
}
</div> </div>
)} )}
</div> </div>
@@ -523,7 +790,7 @@ export default function GoogleAdsGenerator() {
</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">
Generated campaign will appear here {language === "ru" ? "Здесь появится созданная кампания" : language === "he" ? "קמפיין שחולל יופיע כאן" : "Generated campaign will appear here"}
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -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>

View 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}</>;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -3,8 +3,9 @@
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, Presentation, History, Settings, Github, Menu, X, Megaphone } 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" | "slides" | "googleads" | "history" | "settings"; export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "history" | "settings";
@@ -14,18 +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: "slides" as View, label: "Slides Generator", icon: Presentation }, { id: "slides" as View, label: t.slidesGen, icon: Presentation },
{ id: "googleads" as View, label: "Google Ads Gen", icon: Megaphone }, { id: "googleads" as View, label: t.googleAds, icon: Megaphone },
{ id: "history" as View, label: "History", icon: History, count: history.length }, { id: "history" as View, label: t.history, icon: History, count: history.length },
{ id: "settings" as View, label: "Settings", icon: Settings }, { id: "settings" as View, label: t.settings, icon: Settings },
]; ];
const handleViewChange = (view: View) => { const handleViewChange = (view: View) => {
@@ -38,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">
@@ -78,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>

View File

@@ -36,6 +36,7 @@ import {
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
const LANGUAGES = [ const LANGUAGES = [
{ code: "en", name: "English", nativeName: "English" }, { code: "en", name: "English", nativeName: "English" },
@@ -121,8 +122,12 @@ export default function SlidesGenerator() {
setError, setError,
setAvailableModels, setAvailableModels,
setSelectedModel, setSelectedModel,
language: uiLanguage,
} = useStore(); } = useStore();
const t = translations[uiLanguage].slidesGen;
const common = translations[uiLanguage].common;
const [topic, setTopic] = useState(""); const [topic, setTopic] = useState("");
const [language, setLanguage] = useState("en"); const [language, setLanguage] = useState("en");
const [theme, setTheme] = useState("executive-dark"); const [theme, setTheme] = useState("executive-dark");
@@ -821,27 +826,27 @@ export default function SlidesGenerator() {
}; };
return ( return (
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 xl:grid-cols-2"> <div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 xl:grid-cols-2 text-start">
{/* Input Panel */} {/* Input Panel */}
<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">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 via-purple-500 to-fuchsia-500 text-white shadow-lg shadow-violet-500/25"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 via-purple-500 to-fuchsia-500 text-white shadow-lg shadow-violet-500/25">
<Sparkles className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
</div> </div>
<span>Slides Generator</span> <span>{t.title}</span>
<span className="ml-auto text-[10px] font-normal px-2 py-0.5 rounded-full bg-gradient-to-r from-amber-500/10 to-orange-500/10 text-amber-600 border border-amber-200/50"> <span className="ml-auto text-[10px] font-normal px-2 py-0.5 rounded-full bg-gradient-to-r from-amber-500/10 to-orange-500/10 text-amber-600 border border-amber-200/50">
PRO PRO
</span> </span>
</CardTitle> </CardTitle>
<CardDescription className="text-xs lg:text-sm"> <CardDescription className="text-xs lg:text-sm">
Generate stunning, animated HTML5 presentations with charts, graphics & corporate-ready design {t.description}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 lg:space-y-5 p-4 lg:p-6 pt-0 lg:pt-0"> <CardContent className="space-y-4 lg:space-y-5 p-4 lg:p-6 pt-0 lg:pt-0">
{/* AI Provider Selection */} {/* AI Provider Selection */}
<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
@@ -858,8 +863,8 @@ export default function SlidesGenerator() {
</div> </div>
{/* Model Selection */} {/* Model Selection */}
<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)}
@@ -874,10 +879,10 @@ export default function SlidesGenerator() {
</div> </div>
{/* Topic Input */} {/* Topic Input */}
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">Presentation Topic</label> <label className="text-xs lg:text-sm font-medium">{uiLanguage === "ru" ? "Тема презентации" : uiLanguage === "he" ? "נושא המצגת" : "Presentation Topic"}</label>
<Textarea <Textarea
placeholder="e.g., Q4 2024 Revenue Analysis with YoY Growth Comparison, AI Integration Roadmap for Enterprise, Product Launch Strategy with Market Positioning..." placeholder={t.placeholder}
value={topic} value={topic}
onChange={(e) => setTopic(e.target.value)} onChange={(e) => setTopic(e.target.value)}
className="min-h-[100px] lg:min-h-[120px] resize-y text-sm" className="min-h-[100px] lg:min-h-[120px] resize-y text-sm"
@@ -885,11 +890,11 @@ export default function SlidesGenerator() {
</div> </div>
{/* File Upload Zone */} {/* File Upload Zone */}
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5"> <label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
<Upload className="h-3.5 w-3.5 text-blue-500" /> <Upload className="h-3.5 w-3.5 text-blue-500" />
Attach Files for Context {t.attachFiles}
<span className="text-[10px] text-muted-foreground font-normal">(Optional)</span> <span className="text-[10px] text-muted-foreground font-normal">({uiLanguage === "ru" ? "необязательно" : uiLanguage === "he" ? "אופציונלי" : "Optional"})</span>
</label> </label>
<div <div
className={cn( className={cn(
@@ -985,10 +990,10 @@ export default function SlidesGenerator() {
{/* Language & Theme Row */} {/* Language & Theme Row */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5"> <label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
<Globe className="h-3.5 w-3.5 text-blue-500" /> <Globe className="h-3.5 w-3.5 text-blue-500" />
Language {t.language}
</label> </label>
<select <select
value={language} value={language}
@@ -1023,10 +1028,10 @@ export default function SlidesGenerator() {
</div> </div>
{/* Animation Style */} {/* Animation Style */}
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5"> <label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
<Zap className="h-3.5 w-3.5 text-amber-500" /> <Zap className="h-3.5 w-3.5 text-amber-500" />
Animation Style {t.animations}
</label> </label>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{ANIMATION_STYLES.map((style) => ( {ANIMATION_STYLES.map((style) => (
@@ -1053,17 +1058,17 @@ export default function SlidesGenerator() {
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors" className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
> >
<Settings className="h-3.5 w-3.5" /> <Settings className="h-3.5 w-3.5" />
{showAdvanced ? "Hide" : "Show"} Advanced Options {showAdvanced ? (uiLanguage === "ru" ? "Скрыть" : uiLanguage === "he" ? "הסתר" : "Hide") : (uiLanguage === "ru" ? "Показать" : uiLanguage === "he" ? "הצג" : "Show")} {uiLanguage === "ru" ? "Раширенные настройки" : uiLanguage === "he" ? "אפשרויות מתקדמות" : "Advanced Options"}
</button> </button>
{/* Advanced Options */} {/* Advanced Options */}
{showAdvanced && ( {showAdvanced && (
<div className="space-y-3 p-3 rounded-lg bg-muted/30 border"> <div className="space-y-3 p-3 rounded-lg bg-muted/30 border text-start">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-2"> <div className="space-y-2 text-start">
<label className="text-xs font-medium flex items-center gap-1.5"> <label className="text-xs font-medium flex items-center gap-1.5">
<Users className="h-3.5 w-3.5 text-green-500" /> <Users className="h-3.5 w-3.5 text-green-500" />
Target Audience {t.audience}
</label> </label>
<select <select
value={audience} value={audience}
@@ -1146,12 +1151,12 @@ export default function SlidesGenerator() {
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating Animated Slides... {t.generating}
</> </>
) : ( ) : (
<> <>
<Sparkles className="mr-2 h-4 w-4" /> <Sparkles className="mr-2 h-4 w-4" />
Generate Animated Presentation {t.generate}
</> </>
)} )}
</Button> </Button>
@@ -1164,7 +1169,7 @@ export default function SlidesGenerator() {
<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={cn("h-4 w-4 lg:h-5 lg:w-5", slidesPresentation ? "text-green-500" : "text-muted-foreground")} /> <CheckCircle2 className={cn("h-4 w-4 lg:h-5 lg:w-5", slidesPresentation ? "text-green-500" : "text-muted-foreground")} />
Slide Preview {uiLanguage === "ru" ? "Предпросмотр слайдов" : uiLanguage === "he" ? "תצוגה מקדימה של השקופיות" : "Slide Preview"}
</span> </span>
{slidesPresentation && ( {slidesPresentation && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -1270,15 +1275,15 @@ export default function SlidesGenerator() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex h-[300px] lg:h-[400px] items-center justify-center text-center"> <div className="flex h-[300px] lg:h-[400px] items-center justify-center text-center italic">
<div className="space-y-3"> <div className="space-y-3">
<div className="mx-auto w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500/20 to-fuchsia-500/20 flex items-center justify-center"> <div className="mx-auto w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500/20 to-fuchsia-500/20 flex items-center justify-center">
<Sparkles className="h-8 w-8 text-violet-500/50" /> <Sparkles className="h-8 w-8 text-violet-500/50" />
</div> </div>
<div> <div>
<p className="text-sm font-medium text-muted-foreground">No presentation yet</p> <p className="text-sm font-medium text-muted-foreground">{t.emptyState}</p>
<p className="text-xs text-muted-foreground/70 mt-1"> <p className="text-xs text-muted-foreground/70 mt-1">
Enter a topic and generate your animated slides {uiLanguage === "ru" ? "Введите тему и создайте анимированные слайды" : uiLanguage === "he" ? "הזן נושא וחולל שקופיות אקטיביות" : "Enter a topic and generate your animated slides"}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -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>

370
lib/i18n/translations.ts Normal file
View 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: "צרף קבצים להקשר",
}
}
};

View File

@@ -226,6 +226,19 @@ export class ModelAdapter {
return this.callWithFallback((service) => service.generateGoogleAds(websiteUrl, options, model), providers); 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,

View File

@@ -526,6 +526,72 @@ Generate complete Google Ads package with keywords, ad copy, campaigns, and impl
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b"); 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;

View File

@@ -854,6 +854,72 @@ Generate complete Google Ads package with keywords, ad copy, campaigns, and impl
return this.chatCompletion([systemMessage, userMessage], model || "coder-model"); 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",

View File

@@ -605,6 +605,74 @@ Make this campaign READY TO LAUNCH with copy-paste ready content!`,
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true); 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;

View File

@@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult } from "@/types"; import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult } from "@/types";
interface AppState { interface AppState {
currentPrompt: string; currentPrompt: string;
@@ -8,6 +8,8 @@ interface AppState {
actionPlan: ActionPlan | null; actionPlan: ActionPlan | null;
slidesPresentation: SlidesPresentation | null; slidesPresentation: SlidesPresentation | null;
googleAdsResult: GoogleAdsResult | 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[]>;
@@ -31,6 +33,8 @@ interface AppState {
setActionPlan: (plan: ActionPlan) => void; setActionPlan: (plan: ActionPlan) => void;
setSlidesPresentation: (slides: SlidesPresentation | null) => void; setSlidesPresentation: (slides: SlidesPresentation | null) => void;
setGoogleAdsResult: (result: GoogleAdsResult | 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;
@@ -50,6 +54,8 @@ const useStore = create<AppState>((set) => ({
actionPlan: null, actionPlan: null,
slidesPresentation: null, slidesPresentation: null,
googleAdsResult: null, googleAdsResult: null,
magicWandResult: null,
language: "en",
selectedProvider: "qwen", selectedProvider: "qwen",
selectedModels: { selectedModels: {
qwen: "coder-model", qwen: "coder-model",
@@ -76,6 +82,8 @@ const useStore = create<AppState>((set) => ({
setActionPlan: (plan) => set({ actionPlan: plan }), setActionPlan: (plan) => set({ actionPlan: plan }),
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }), setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
setGoogleAdsResult: (result) => set({ googleAdsResult: result }), 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) => ({
@@ -112,6 +120,7 @@ const useStore = create<AppState>((set) => ({
actionPlan: null, actionPlan: null,
slidesPresentation: null, slidesPresentation: null,
googleAdsResult: null, googleAdsResult: null,
magicWandResult: null,
error: null, error: null,
}), }),
})); }));

View File

@@ -202,3 +202,52 @@ export interface GoogleAdsResult {
rawContent: 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;
}