fix(i18n): Fix syntax errors and add missing translation keys - Restored broken useStore destructuring in PromptEnhancer, UXDesignerPrompt, ActionPlanGenerator, SlidesGenerator - Fixed GoogleAdsGenerator scope issue with renderMagicWandSectionContent - Added missing inputLabel keys to promptEnhancer for all languages - Fixed t.resultTitle to t.enhancedTitle in PromptEnhancer

This commit is contained in:
Gemini AI
2025-12-28 02:00:42 +04:00
Unverified
parent b859d77307
commit 238a576cb8
6 changed files with 1836 additions and 1794 deletions

View File

@@ -11,7 +11,16 @@ import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations"; import { translations } from "@/lib/i18n/translations";
export default function ActionPlanGenerator() { export default function ActionPlanGenerator() {
language, const {
language,
currentPrompt,
actionPlan,
selectedProvider,
selectedModels,
availableModels,
apiKeys,
isProcessing,
error,
setCurrentPrompt, setCurrentPrompt,
setSelectedProvider, setSelectedProvider,
setActionPlan, setActionPlan,
@@ -21,241 +30,241 @@ export default function ActionPlanGenerator() {
setSelectedModel, setSelectedModel,
} = useStore(); } = useStore();
const t = translations[language].actionPlan; const t = translations[language].actionPlan;
const common = translations[language].common; const common = translations[language].common;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const selectedModel = selectedModels[selectedProvider]; const selectedModel = selectedModels[selectedProvider];
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider); const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
loadAvailableModels(); loadAvailableModels();
const saved = localStorage.getItem("promptarch-api-keys"); const saved = localStorage.getItem("promptarch-api-keys");
if (saved) { if (saved) {
try { try {
const keys = JSON.parse(saved); const keys = JSON.parse(saved);
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen); if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama); if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai); if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
} catch (e) { } catch (e) {
console.error("Failed to load API keys:", e); console.error("Failed to load API keys:", e);
}
} }
} }
} }, [selectedProvider]);
}, [selectedProvider]);
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider); const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
setAvailableModels(selectedProvider, fallbackModels); setAvailableModels(selectedProvider, fallbackModels);
try { try {
const result = await modelAdapter.listModels(selectedProvider); const result = await modelAdapter.listModels(selectedProvider);
if (result.success && result.data) { if (result.success && result.data) {
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels); setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
}
} catch (error) {
console.error("Failed to load models:", error);
} }
} catch (error) { };
console.error("Failed to load models:", error);
}
};
const handleGenerate = async () => { const handleGenerate = async () => {
if (!currentPrompt.trim()) { if (!currentPrompt.trim()) {
setError("Please enter PRD or project requirements"); setError("Please enter PRD or project requirements");
return; return;
}
const apiKey = apiKeys[selectedProvider];
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setProcessing(true);
setError(null);
console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try {
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
console.log("[ActionPlanGenerator] Generation result:", result);
if (result.success && result.data) {
const newPlan = {
id: Math.random().toString(36).substr(2, 9),
prdId: "",
tasks: [],
frameworks: [],
architecture: {
pattern: "",
structure: "",
technologies: [],
bestPractices: [],
},
estimatedDuration: "",
createdAt: new Date(),
rawContent: result.data,
};
setActionPlan(newPlan);
} else {
console.error("[ActionPlanGenerator] Generation failed:", result.error);
setError(result.error || "Failed to generate action plan");
} }
} catch (err) {
console.error("[ActionPlanGenerator] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => { const apiKey = apiKeys[selectedProvider];
if (actionPlan?.rawContent) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
await navigator.clipboard.writeText(actionPlan.rawContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return ( if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start"> setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
<Card className="h-fit"> return;
<CardHeader className="p-4 lg:p-6 text-start"> }
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" /> setProcessing(true);
{t.title} setError(null);
</CardTitle>
<CardDescription className="text-xs lg:text-sm"> console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
{t.description}
</CardDescription> try {
</CardHeader> const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start"> console.log("[ActionPlanGenerator] Generation result:", result);
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2"> if (result.success && result.data) {
{(["qwen", "ollama", "zai"] as const).map((provider) => ( const newPlan = {
<Button id: Math.random().toString(36).substr(2, 9),
key={provider} prdId: "",
variant={selectedProvider === provider ? "default" : "outline"} tasks: [],
size="sm" frameworks: [],
onClick={() => setSelectedProvider(provider)} architecture: {
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3" pattern: "",
> structure: "",
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"} technologies: [],
</Button> bestPractices: [],
))} },
estimatedDuration: "",
createdAt: new Date(),
rawContent: result.data,
};
setActionPlan(newPlan);
} else {
console.error("[ActionPlanGenerator] Generation failed:", result.error);
setError(result.error || "Failed to generate action plan");
}
} catch (err) {
console.error("[ActionPlanGenerator] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => {
if (actionPlan?.rawContent) {
await navigator.clipboard.writeText(actionPlan.rawContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<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">
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{t.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
>
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))}
</div>
</div> </div>
</div>
<div className="space-y-2 text-start"> <div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.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)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
{models.map((model) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
{model} {model}
</option> </option>
))} ))}
</select> </select>
</div>
<div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{language === "ru" ? "PRD / Требования" : language === "he" ? "PRD / דרישות" : "PRD / Requirements"}</label>
<Textarea
placeholder={t.placeholder}
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
/>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error}
{!apiKeys[selectedProvider] && (
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div>
)}
</div> </div>
)}
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm"> <div className="space-y-2">
{isProcessing ? ( <label className="text-xs lg:text-sm font-medium">{language === "ru" ? "PRD / Требования" : language === "he" ? "PRD / דרישות" : "PRD / Requirements"}</label>
<> <Textarea
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" /> placeholder={t.placeholder}
{common.generating} value={currentPrompt}
</> onChange={(e) => setCurrentPrompt(e.target.value)}
) : ( className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
<> />
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" /> </div>
{language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
</>
)}
</Button>
</CardContent>
</Card>
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}> {error && (
<CardHeader className="p-4 lg:p-6 text-start"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
<CardTitle className="flex items-center justify-between text-base lg:text-lg"> {error}
<span className="flex items-center gap-2"> {!apiKeys[selectedProvider] && (
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" /> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
{t.generatedTitle} <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
</span> <span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
{actionPlan && ( </div>
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)} )}
</Button> </div>
)} )}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{language === "ru" ? "Разбивка задач, фреймворки и рекомендации по архитектуре" : language === "he" ? "פירוט משימות, פרימוורקים והמלצות ארכיטקטורה" : "Task breakdown, frameworks, and architecture recommendations"}
</CardDescription>
</CardHeader>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{actionPlan ? (
<div className="space-y-3 lg:space-y-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">
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
{language === "ru" ? "Дорожная карта реализации" : language === "he" ? "מפת דרכים ליישום" : "Implementation Roadmap"}
</h4>
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
</div>
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start"> <Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 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"> {isProcessing ? (
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" /> <>
{language === "ru" ? "Быстрые заметки" : language === "he" ? "הערות מהירות" : "Quick Notes"} <Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
</h4> {common.generating}
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground"> </>
<li>{language === "ru" ? "Проверьте все зависимости задач перед началом" : language === "he" ? "בדוק את כל התלות בין המשימות לפני שתתחיל" : "Review all task dependencies before starting"}</li> ) : (
<li>{language === "ru" ? "Настройте рекомендуемую архитектуру фреймворка" : language === "he" ? "הגדר את ארכיטקטורת הפרימוורק המומלצת" : "Set up recommended framework architecture"}</li> <>
<li>{language === "ru" ? "Следуйте лучшим практикам безопасности и производительности" : language === "he" ? "עקוב אחר שיטות עבודה מומלצות לאבטחה וביצועים" : "Follow best practices for security and performance"}</li> <ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
<li>{language === "ru" ? "Используйте указанную стратегию развертывания" : language === "he" ? "השתמש באסטרטגיית הפריסה המצוינת" : "Use specified deployment strategy"}</li> {language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
</ul> </>
)}
</Button>
</CardContent>
</Card>
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
{t.generatedTitle}
</span>
{actionPlan && (
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)}
</Button>
)}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{language === "ru" ? "Разбивка задач, фреймворки и рекомендации по архитектуре" : language === "he" ? "פירוט משימות, פרימוורקים והמלצות ארכיטקטורה" : "Task breakdown, frameworks, and architecture recommendations"}
</CardDescription>
</CardHeader>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{actionPlan ? (
<div className="space-y-3 lg:space-y-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">
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
{language === "ru" ? "Дорожная карта реализации" : language === "he" ? "מפת דרכים ליישום" : "Implementation Roadmap"}
</h4>
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
</div>
<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">
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
{language === "ru" ? "Быстрые заметки" : language === "he" ? "הערות מהירות" : "Quick Notes"}
</h4>
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
<li>{language === "ru" ? "Проверьте все зависимости задач перед началом" : language === "he" ? "בדוק את כל התלות בין המשימות לפני שתתחיל" : "Review all task dependencies before starting"}</li>
<li>{language === "ru" ? "Настройте рекомендуемую архитектуру фреймворка" : language === "he" ? "הגדר את ארכיטקטורת הפרימוורק המומלצת" : "Set up recommended framework architecture"}</li>
<li>{language === "ru" ? "Следуйте лучшим практикам безопасности и производительности" : language === "he" ? "עקוב אחר שיטות עבודה מומלצות לאבטחה וביצועים" : "Follow best practices for security and performance"}</li>
<li>{language === "ru" ? "Используйте указанную стратегию развертывания" : language === "he" ? "השתמש באסטרטגיית הפריסה המצוינת" : "Use specified deployment strategy"}</li>
</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 italic">
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic"> {t.emptyState}
{t.emptyState} </div>
</div> )}
)} </CardContent>
</CardContent> </Card>
</Card> </div>
</div> );
);
} }

View File

@@ -394,408 +394,407 @@ export default function GoogleAdsGenerator() {
default: default:
return <pre className="whitespace-pre-wrap text-xs">{googleAdsResult.rawContent}</pre>; return <pre className="whitespace-pre-wrap text-xs">{googleAdsResult.rawContent}</pre>;
} }
} };
};
const renderMagicWandSectionContent = (sectionId: string) => { const renderMagicWandSectionContent = (sectionId: string) => {
if (!magicWandResult) return null; if (!magicWandResult) return null;
switch (sectionId) { switch (sectionId) {
case "market": case "market":
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-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="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"> <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 <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="text-sm font-semibold">{magicWandResult.marketAnalysis.industrySize}</div>
</div> </div>
<div className="p-3 rounded-md bg-emerald-50/50 border border-emerald-100"> <div className="space-y-3">
<div className="text-[10px] uppercase font-bold text-emerald-600 mb-1 flex items-center gap-1"> <div>
<TrendingUp className="h-3 w-3" /> Growth Rate <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 className="text-sm font-semibold">{magicWandResult.marketAnalysis.growthRate}</div>
</div> </div>
</div> </div>
<div className="space-y-3"> );
<div> case "competitors":
<h4 className="text-xs font-bold text-slate-700 mb-2 flex items-center gap-1.5"> return (
<Users className="h-3.5 w-3.5" /> Market Leaders <div className="space-y-4">
</h4> {magicWandResult.competitorInsights.map((comp, i) => (
<div className="flex flex-wrap gap-2"> <div key={i} className="p-4 rounded-xl border bg-white/50 space-y-3">
{magicWandResult.marketAnalysis.topCompetitors.map((c, i) => ( <div className="flex items-center justify-between">
<span key={i} className="text-xs px-2 py-1 bg-slate-100 text-slate-700 rounded-md border border-slate-200">{c}</span> <h4 className="font-bold text-slate-900">{comp.competitor}</h4>
))} <ShieldAlert className="h-4 w-4 text-amber-500" />
</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>
<div className="space-y-1.5"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="text-[10px] font-black uppercase text-rose-600">Weaknesses</div> <div className="space-y-1.5">
<ul className="space-y-1"> <div className="text-[10px] font-black uppercase text-emerald-600">Strengths</div>
{comp.weaknesses.map((w, j) => ( <ul className="space-y-1">
<li key={j} className="text-xs text-slate-600 flex gap-1.5"> {comp.strengths.map((s, j) => (
<span className="text-rose-500"></span> {w} <li key={j} className="text-xs text-slate-600 flex gap-1.5">
</li> <span className="text-emerald-500"></span> {s}
))} </li>
</ul> ))}
</div> </ul>
</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>
<div className="flex flex-wrap gap-1.5"> <div className="space-y-1.5">
{strat.keyMessages.map((msg, j) => ( <div className="text-[10px] font-black uppercase text-rose-600">Weaknesses</div>
<span key={j} className="text-[10px] bg-white border px-1.5 py-0.5 rounded-md text-slate-500 shadow-sm">{msg}</span> <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> </div>
<div className="grid grid-cols-2 gap-4 items-center"> <div className="space-y-4">
<div className="space-y-1"> <p className="text-xs text-slate-600 leading-relaxed font-medium">
<div className="text-[9px] font-black text-slate-400 uppercase">Channel Mix</div> <span className="text-indigo-500 font-black uppercase text-[9px] block mb-0.5">The "Why":</span>
<div className="flex flex-wrap gap-1"> {strat.rationale}
{strat.recommendedChannels.map((c, j) => ( </p>
<span key={j} className="text-[9px] font-bold text-slate-600">{c}</span>
<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> </div>
<div className="text-right">
<div className="text-[9px] font-black text-slate-400 uppercase text-right">Expected ROI</div> <div className="grid grid-cols-2 gap-4 items-center">
<div className="text-lg font-black text-emerald-600 tracking-tighter">{strat.expectedROI}</div> <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>
</div> </div>
</div>
))}
</div>
);
default:
return <pre className="whitespace-pre-wrap text-xs">{magicWandResult.rawContent}</pre>;
}
};
return (
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
<Card className="h-fit">
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{t.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
>
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))} ))}
</div> </div>
</div> );
default:
return <pre className="whitespace-pre-wrap text-xs">{magicWandResult.rawContent}</pre>;
}
};
<div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{models.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
</div>
<div className="space-y-2"> return (
<label className="text-xs lg:text-sm font-medium">{t.websiteUrl}</label> <div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
<Input <Card className="h-fit">
placeholder="e.g., www.your-business.com" <CardHeader className="p-4 lg:p-6 text-start">
value={websiteUrl} <CardTitle className="flex items-center gap-2 text-base lg:text-lg">
onChange={(e) => setWebsiteUrl(e.target.value)} <Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
className="text-sm" {t.title}
/> </CardTitle>
</div> <CardDescription className="text-xs lg:text-sm">
{t.description}
<div className="space-y-2"> </CardDescription>
<label className="text-xs lg:text-sm font-medium">{t.products}</label> </CardHeader>
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2"> <div className="space-y-2">
{products.map((product, index) => ( <label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div key={index} className="flex gap-2"> <div className="flex flex-wrap gap-1.5 lg:gap-2">
<Input {(["qwen", "ollama", "zai"] as const).map((provider) => (
placeholder={`${language === "ru" ? "Продукт" : language === "he" ? "מוצר" : "Product"} ${index + 1}`} <Button
value={product} key={provider}
onChange={(e) => updateProduct(index, e.target.value)} variant={selectedProvider === provider ? "default" : "outline"}
className="text-sm" size="sm"
/> onClick={() => setSelectedProvider(provider)}
{products.length > 1 && ( className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
<Button >
variant="ghost" {provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
size="icon" </Button>
onClick={() => removeProduct(index)} ))}
className="h-10 w-10 shrink-0"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
))}
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
<Plus className="mr-1.5 h-3.5 w-3.5" />
{language === "ru" ? "Добавить продукт" : language === "he" ? "הוסף מוצר" : "Add Product"}
</Button>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{t.budget}</label>
<div className="flex items-center gap-1.5">
<Input
type="number"
placeholder="Min"
value={budgetMin}
onChange={(e) => setBudgetMin(e.target.value)}
className="text-sm"
/>
<span className="text-muted-foreground text-xs font-bold text-center">-</span>
<Input
type="number"
placeholder="Max"
value={budgetMax}
onChange={(e) => setBudgetMax(e.target.value)}
className="text-sm"
/>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{t.industry}</label> <label className="text-xs lg:text-sm font-medium">{common.model}</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{models.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{t.websiteUrl}</label>
<Input <Input
placeholder="e.g., SaaS" placeholder="e.g., www.your-business.com"
value={industry} value={websiteUrl}
onChange={(e) => setIndustry(e.target.value)} onChange={(e) => setWebsiteUrl(e.target.value)}
className="text-sm" className="text-sm"
/> />
</div> </div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label> <label className="text-xs lg:text-sm font-medium">{t.products}</label>
<Textarea <div className="space-y-2">
placeholder="e.g., Small business owners in USA looking for productivity tools" {products.map((product, index) => (
value={targetAudience} <div key={index} className="flex gap-2">
onChange={(e) => setTargetAudience(e.target.value)} <Input
className="min-h-[80px] lg:min-h-[100px] resize-y text-sm" placeholder={`${language === "ru" ? "Продукт" : language === "he" ? "מוצר" : "Product"} ${index + 1}`}
/> value={product}
</div> onChange={(e) => updateProduct(index, e.target.value)}
className="text-sm"
{error && ( />
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive"> {products.length > 1 && (
{error} <Button
{!apiKeys[selectedProvider] && ( variant="ghost"
<div className="mt-1.5 lg:mt-2 flex items-center gap-2"> size="icon"
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" /> onClick={() => removeProduct(index)}
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span> className="h-10 w-10 shrink-0"
</div> >
)} <X className="h-4 w-4" />
</Button>
)}
</div>
))}
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
<Plus className="mr-1.5 h-3.5 w-3.5" />
{language === "ru" ? "Добавить продукт" : language === "he" ? "הוסף מוצר" : "Add Product"}
</Button>
</div>
</div> </div>
)}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<Button <div className="space-y-2">
onClick={handleGenerate} <label className="text-xs lg:text-sm font-medium">{t.budget}</label>
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()} <div className="flex items-center gap-1.5">
className="h-9 lg:h-10 text-xs lg:text-sm bg-primary/90 hover:bg-primary shadow-sm" <Input
> type="number"
{isProcessing ? ( placeholder="Min"
<> value={budgetMin}
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" /> onChange={(e) => setBudgetMin(e.target.value)}
{common.generating} className="text-sm"
</> />
) : ( <span className="text-muted-foreground text-xs font-bold text-center">-</span>
<> <Input
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5" /> type="number"
{t.generateAds} placeholder="Max"
</> value={budgetMax}
)} onChange={(e) => setBudgetMax(e.target.value)}
</Button> className="text-sm"
<Button />
onClick={handleMagicWand} </div>
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()} </div>
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]" <div className="space-y-2">
> <label className="text-xs lg:text-sm font-medium">{t.industry}</label>
{isMagicThinking ? ( <Input
<> placeholder="e.g., SaaS"
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" /> value={industry}
{t.researching} onChange={(e) => setIndustry(e.target.value)}
</> className="text-sm"
) : ( />
<> </div>
<Wand2 className="mr-1.5 h-3.5 w-3.5" /> </div>
{t.magicWand}
</>
)}
</Button>
</div>
</CardContent>
</Card>
<Card className={cn(!googleAdsResult && "opacity-50")}> <div className="space-y-2">
<CardHeader className="p-4 lg:p-6"> <label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
<CardTitle className="flex items-center justify-between text-base lg:text-lg"> <Textarea
<span className="flex items-center gap-2"> placeholder="e.g., Small business owners in USA looking for productivity tools"
{magicWandResult ? ( value={targetAudience}
<Wand2 className="h-4 w-4 lg:h-5 lg:w-5 text-indigo-500" /> onChange={(e) => setTargetAudience(e.target.value)}
) : ( className="min-h-[80px] lg:min-h-[100px] resize-y text-sm"
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" /> />
)} </div>
{magicWandResult ? t.strategicDirections : t.generatedCampaign}
</span> {error && (
{(googleAdsResult || magicWandResult) && ( <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9"> {error}
{copied ? ( {!apiKeys[selectedProvider] && (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" /> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div>
)}
</div>
)}
<div className="grid grid-cols-2 gap-3">
<Button
onClick={handleGenerate}
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
className="h-9 lg:h-10 text-xs lg:text-sm bg-primary/90 hover:bg-primary shadow-sm"
>
{isProcessing ? (
<>
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
{common.generating}
</>
) : ( ) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" /> <>
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5" />
{t.generateAds}
</>
)} )}
</Button> </Button>
)} <Button
</CardTitle> onClick={handleMagicWand}
<CardDescription className="text-xs lg:text-sm"> disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
{magicWandResult 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]"
? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes") >
: (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready") {isMagicThinking ? (
} <>
</CardDescription> <Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
</CardHeader> {t.researching}
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0"> </>
{googleAdsResult || magicWandResult ? ( ) : (
<div className="space-y-2 lg:space-y-3"> <>
{(magicWandResult <Wand2 className="mr-1.5 h-3.5 w-3.5" />
? [ {t.magicWand}
{ id: "market", title: t.marketIntelligence }, </>
{ id: "competitors", title: t.competitiveInsights }, )}
{ id: "strategies", title: t.campaignDirections } </Button>
] </div>
: sections </CardContent>
).map((section) => ( </Card>
<div key={section.id} className="rounded-md border bg-muted/30">
<button <Card className={cn(!googleAdsResult && "opacity-50")}>
onClick={() => toggleSection(section.id)} <CardHeader className="p-4 lg:p-6">
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm" <CardTitle className="flex items-center justify-between text-base lg:text-lg">
> <span className="flex items-center gap-2">
<span>{section.title}</span> {magicWandResult ? (
{expandedSections.includes(section.id) ? ( <Wand2 className="h-4 w-4 lg:h-5 lg:w-5 text-indigo-500" />
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" /> ) : (
) : ( <CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" /> )}
)} {magicWandResult ? t.strategicDirections : t.generatedCampaign}
</button> </span>
{expandedSections.includes(section.id) && ( {(googleAdsResult || magicWandResult) && (
<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"> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{magicWandResult {copied ? (
? renderMagicWandSectionContent(section.id) <CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
: renderSectionContent(section.id) ) : (
} <Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
</div>
)} )}
</div> </Button>
))} )}
</div> </CardTitle>
) : ( <CardDescription className="text-xs lg:text-sm">
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground"> {magicWandResult
{language === "ru" ? "Здесь появится созданная кампания" : language === "he" ? "קמפיין שחולל יופיע כאן" : "Generated campaign will appear here"} ? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes")
</div> : (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready")
)} }
</CardContent> </CardDescription>
</Card> </CardHeader>
</div> <CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
); {googleAdsResult || magicWandResult ? (
<div className="space-y-2 lg:space-y-3">
{(magicWandResult
? [
{ id: "market", title: t.marketIntelligence },
{ id: "competitors", title: t.competitiveInsights },
{ id: "strategies", title: t.campaignDirections }
]
: sections
).map((section) => (
<div key={section.id} className="rounded-md border bg-muted/30">
<button
onClick={() => toggleSection(section.id)}
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
>
<span>{section.title}</span>
{expandedSections.includes(section.id) ? (
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
) : (
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)}
</button>
{expandedSections.includes(section.id) && (
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3 animate-in fade-in slide-in-from-top-1 duration-200">
{magicWandResult
? renderMagicWandSectionContent(section.id)
: renderSectionContent(section.id)
}
</div>
)}
</div>
))}
</div>
) : (
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
{language === "ru" ? "Здесь появится созданная кампания" : language === "he" ? "קמפיין שחולל יופיע כאן" : "Generated campaign will appear here"}
</div>
)}
</CardContent>
</Card>
</div>
);
} }

View File

@@ -11,7 +11,16 @@ import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations"; import { translations } from "@/lib/i18n/translations";
export default function PromptEnhancer() { export default function PromptEnhancer() {
language, const {
language,
currentPrompt,
enhancedPrompt,
selectedProvider,
selectedModels,
availableModels,
apiKeys,
isProcessing,
error,
setSelectedProvider, setSelectedProvider,
setCurrentPrompt, setCurrentPrompt,
setEnhancedPrompt, setEnhancedPrompt,
@@ -21,222 +30,222 @@ export default function PromptEnhancer() {
setSelectedModel, setSelectedModel,
} = useStore(); } = useStore();
const t = translations[language].promptEnhancer; const t = translations[language].promptEnhancer;
const common = translations[language].common; const common = translations[language].common;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const selectedModel = selectedModels[selectedProvider]; const selectedModel = selectedModels[selectedProvider];
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider); const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
loadAvailableModels(); loadAvailableModels();
const saved = localStorage.getItem("promptarch-api-keys"); const saved = localStorage.getItem("promptarch-api-keys");
if (saved) { if (saved) {
try { try {
const keys = JSON.parse(saved); const keys = JSON.parse(saved);
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen); if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama); if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai); if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
} catch (e) { } catch (e) {
console.error("Failed to load API keys:", e); console.error("Failed to load API keys:", e);
}
} }
} }
} }, [selectedProvider]);
}, [selectedProvider]);
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider); const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
setAvailableModels(selectedProvider, fallbackModels); setAvailableModels(selectedProvider, fallbackModels);
try { try {
const result = await modelAdapter.listModels(selectedProvider); const result = await modelAdapter.listModels(selectedProvider);
if (result.success && result.data) { if (result.success && result.data) {
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels); setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
}
} catch (error) {
console.error("Failed to load models:", error);
} }
} catch (error) { };
console.error("Failed to load models:", error);
}
};
const handleEnhance = async () => { const handleEnhance = async () => {
if (!currentPrompt.trim()) { if (!currentPrompt.trim()) {
setError("Please enter a prompt to enhance"); setError("Please enter a prompt to enhance");
return; return;
}
const apiKey = apiKeys[selectedProvider];
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setProcessing(true);
setError(null);
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try {
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
console.log("[PromptEnhancer] Enhancement result:", result);
if (result.success && result.data) {
setEnhancedPrompt(result.data);
} else {
console.error("[PromptEnhancer] Enhancement failed:", result.error);
setError(result.error || "Failed to enhance prompt");
} }
} catch (err) {
console.error("[PromptEnhancer] Enhancement error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => { const apiKey = apiKeys[selectedProvider];
if (enhancedPrompt) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
await navigator.clipboard.writeText(enhancedPrompt);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleClear = () => { if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setCurrentPrompt(""); setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
setEnhancedPrompt(null); return;
setError(null); }
};
return ( setProcessing(true);
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start"> setError(null);
<Card className="h-fit">
<CardHeader className="p-4 lg:p-6 text-start"> console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" /> try {
{t.title} const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
</CardTitle>
<CardDescription className="text-xs lg:text-sm"> console.log("[PromptEnhancer] Enhancement result:", result);
{t.description}
</CardDescription> if (result.success && result.data) {
</CardHeader> setEnhancedPrompt(result.data);
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0"> } else {
<div className="space-y-2 text-start"> console.error("[PromptEnhancer] Enhancement failed:", result.error);
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label> setError(result.error || "Failed to enhance prompt");
<div className="flex flex-wrap gap-1.5 lg:gap-2"> }
{(["qwen", "ollama", "zai"] as const).map((provider) => ( } catch (err) {
<Button console.error("[PromptEnhancer] Enhancement error:", err);
key={provider} setError(err instanceof Error ? err.message : "An error occurred");
variant={selectedProvider === provider ? "default" : "outline"} } finally {
size="sm" setProcessing(false);
onClick={() => setSelectedProvider(provider)} }
className={cn( };
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
selectedProvider === provider && "bg-primary text-primary-foreground" const handleCopy = async () => {
)} if (enhancedPrompt) {
> await navigator.clipboard.writeText(enhancedPrompt);
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"} setCopied(true);
</Button> setTimeout(() => setCopied(false), 2000);
))} }
};
const handleClear = () => {
setCurrentPrompt("");
setEnhancedPrompt(null);
setError(null);
};
return (
<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">
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{t.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className={cn(
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
selectedProvider === provider && "bg-primary text-primary-foreground"
)}
>
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))}
</div>
</div> </div>
</div>
<div className="space-y-2 text-start"> <div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.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)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
{models.map((model) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
{model} {model}
</option> </option>
))} ))}
</select> </select>
</div>
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
<Textarea
placeholder={t.placeholder}
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
/>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error}
{!apiKeys[selectedProvider] && (
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div>
)}
</div> </div>
)}
<div className="flex gap-2"> <div className="space-y-2 text-start">
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm"> <label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
{isProcessing ? ( <Textarea
<> placeholder={t.placeholder}
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" /> value={currentPrompt}
{common.generating} onChange={(e) => setCurrentPrompt(e.target.value)}
</> className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
) : ( />
<> </div>
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
{t.title}
</>
)}
</Button>
<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" />
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
</Button>
</div>
</CardContent>
</Card>
<Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}> {error && (
<CardHeader className="p-4 lg:p-6 text-start"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
<CardTitle className="flex items-center justify-between text-base lg:text-lg"> {error}
<span className="flex items-center gap-2"> {!apiKeys[selectedProvider] && (
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" /> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
{t.resultTitle} <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
</span> <span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
{enhancedPrompt && ( </div>
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9"> )}
{copied ? ( </div>
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" /> )}
<div className="flex gap-2">
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? (
<>
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
{common.generating}
</>
) : ( ) : (
<Copy className="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" />
{t.title}
</>
)} )}
</Button> </Button>
<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" />
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
</Button>
</div>
</CardContent>
</Card>
<Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}>
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
{t.enhancedTitle}
</span>
{enhancedPrompt && (
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)}
</Button>
)}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{language === "ru" ? "Профессиональный промпт, готовый для кодинг-агентов" : language === "he" ? "פרומפט מקצועי מוכן לסוכני קידוד" : "Professional prompt ready for coding agents"}
</CardDescription>
</CardHeader>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{enhancedPrompt ? (
<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 leading-relaxed">{enhancedPrompt}</pre>
</div>
) : (
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
{language === "ru" ? "Улучшенный промпт появится здесь" : language === "he" ? "פרומפט משופר יופיע כאן" : "Enhanced prompt will appear here"}
</div>
)} )}
</CardTitle> </CardContent>
<CardDescription className="text-xs lg:text-sm"> </Card>
{language === "ru" ? "Профессиональный промпт, готовый для кодинг-агентов" : language === "he" ? "פרומפט מקצועי מוכן לסוכני קידוד" : "Professional prompt ready for coding agents"} </div>
</CardDescription> );
</CardHeader>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{enhancedPrompt ? (
<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 leading-relaxed">{enhancedPrompt}</pre>
</div>
) : (
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
{language === "ru" ? "Улучшенный промпт появится здесь" : language === "he" ? "פרומפט משופר יופיע כאן" : "Enhanced prompt will appear here"}
</div>
)}
</CardContent>
</Card>
</div>
);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,16 @@ import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations"; import { translations } from "@/lib/i18n/translations";
export default function UXDesignerPrompt() { export default function UXDesignerPrompt() {
language, const {
language,
currentPrompt,
enhancedPrompt,
selectedProvider,
selectedModels,
availableModels,
apiKeys,
isProcessing,
error,
setSelectedProvider, setSelectedProvider,
setCurrentPrompt, setCurrentPrompt,
setEnhancedPrompt, setEnhancedPrompt,
@@ -21,229 +30,229 @@ export default function UXDesignerPrompt() {
setSelectedModel, setSelectedModel,
} = useStore(); } = useStore();
const t = translations[language].uxDesigner; const t = translations[language].uxDesigner;
const common = translations[language].common; 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);
const selectedModel = selectedModels[selectedProvider]; const selectedModel = selectedModels[selectedProvider];
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider); const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
loadAvailableModels(); loadAvailableModels();
const saved = localStorage.getItem("promptarch-api-keys"); const saved = localStorage.getItem("promptarch-api-keys");
if (saved) { if (saved) {
try { try {
const keys = JSON.parse(saved); const keys = JSON.parse(saved);
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama); if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai); if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
} catch (e) { } catch (e) {
console.error("Failed to load API keys:", e); console.error("Failed to load API keys:", e);
}
} }
} }
} }, [selectedProvider]);
}, [selectedProvider]);
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider); const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
setAvailableModels(selectedProvider, fallbackModels); setAvailableModels(selectedProvider, fallbackModels);
try { try {
const result = await modelAdapter.listModels(selectedProvider); const result = await modelAdapter.listModels(selectedProvider);
if (result.success && result.data) { if (result.success && result.data) {
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels); setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
}
} catch (error) {
console.error("Failed to load models:", error);
} }
} catch (error) { };
console.error("Failed to load models:", error);
}
};
const handleGenerate = async () => { const handleGenerate = async () => {
if (!currentPrompt.trim()) { if (!currentPrompt.trim()) {
setError("Please enter an app description"); setError("Please enter an app description");
return; return;
}
const apiKey = apiKeys[selectedProvider];
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setProcessing(true);
setError(null);
setGeneratedPrompt(null);
console.log("[UXDesignerPrompt] Starting generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
try {
const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel);
console.log("[UXDesignerPrompt] Generation result:", result);
if (result.success && result.data) {
setGeneratedPrompt(result.data);
setEnhancedPrompt(result.data);
} else {
console.error("[UXDesignerPrompt] Generation failed:", result.error);
setError(result.error || "Failed to generate UX designer prompt");
} }
} catch (err) {
console.error("[UXDesignerPrompt] Generation error:", err);
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setProcessing(false);
}
};
const handleCopy = async () => { const apiKey = apiKeys[selectedProvider];
if (generatedPrompt) { const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
await navigator.clipboard.writeText(generatedPrompt);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleClear = () => { if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setCurrentPrompt(""); setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
setGeneratedPrompt(null); return;
setEnhancedPrompt(null); }
setError(null);
};
return ( setProcessing(true);
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start"> setError(null);
<Card className="h-fit"> setGeneratedPrompt(null);
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg"> console.log("[UXDesignerPrompt] Starting generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title} try {
</CardTitle> const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel);
<CardDescription className="text-xs lg:text-sm">
{t.description} console.log("[UXDesignerPrompt] Generation result:", result);
</CardDescription>
</CardHeader> if (result.success && result.data) {
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0"> setGeneratedPrompt(result.data);
<div className="space-y-2 text-start"> setEnhancedPrompt(result.data);
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label> } else {
<div className="flex flex-wrap gap-1.5 lg:gap-2"> console.error("[UXDesignerPrompt] Generation failed:", result.error);
{(["ollama", "zai"] as const).map((provider) => ( setError(result.error || "Failed to generate UX designer prompt");
<Button }
key={provider} } catch (err) {
variant={selectedProvider === provider ? "default" : "outline"} console.error("[UXDesignerPrompt] Generation error:", err);
size="sm" setError(err instanceof Error ? err.message : "An error occurred");
onClick={() => setSelectedProvider(provider)} } finally {
className={cn( setProcessing(false);
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3", }
selectedProvider === provider && "bg-primary text-primary-foreground" };
)}
> const handleCopy = async () => {
{provider === "ollama" ? "Ollama" : "Z.AI"} if (generatedPrompt) {
</Button> await navigator.clipboard.writeText(generatedPrompt);
))} setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleClear = () => {
setCurrentPrompt("");
setGeneratedPrompt(null);
setEnhancedPrompt(null);
setError(null);
};
return (
<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">
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
{t.title}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{t.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
<div className="flex flex-wrap gap-1.5 lg:gap-2">
{(["ollama", "zai"] as const).map((provider) => (
<Button
key={provider}
variant={selectedProvider === provider ? "default" : "outline"}
size="sm"
onClick={() => setSelectedProvider(provider)}
className={cn(
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
selectedProvider === provider && "bg-primary text-primary-foreground"
)}
>
{provider === "ollama" ? "Ollama" : "Z.AI"}
</Button>
))}
</div>
</div> </div>
</div>
<div className="space-y-2 text-start"> <div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{common.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)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
{models.map((model) => ( {models.map((model) => (
<option key={model} value={model}> <option key={model} value={model}>
{model} {model}
</option> </option>
))} ))}
</select> </select>
</div>
<div className="space-y-2 text-start">
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
<Textarea
placeholder={t.placeholder}
value={currentPrompt}
onChange={(e) => setCurrentPrompt(e.target.value)}
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">
{t.inputDesc}
</p>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
{error}
{!apiKeys[selectedProvider] && (
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</div>
)}
</div> </div>
)}
<div className="flex gap-2"> <div className="space-y-2 text-start">
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm"> <label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
{isProcessing ? ( <Textarea
<> placeholder={t.placeholder}
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" /> value={currentPrompt}
{common.generating} onChange={(e) => setCurrentPrompt(e.target.value)}
</> 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">
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" /> {t.inputDesc}
{language === "ru" ? "Создать UX Промпт" : language === "he" ? "חולל פרומפט UX" : "Generate UX Prompt"} </p>
</> </div>
)}
</Button>
<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">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
<span className="sm:hidden">×</span>
</Button>
</div>
</CardContent>
</Card>
<Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}> {error && (
<CardHeader className="p-4 lg:p-6 text-start"> <div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
<CardTitle className="flex items-center justify-between text-base lg:text-lg"> {error}
<span className="flex items-center gap-2"> {!apiKeys[selectedProvider] && (
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" /> <div className="mt-1.5 lg:mt-2 flex items-center gap-2">
<span className="hidden sm:inline">{t.resultTitle}</span> <Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
<span className="sm:hidden">{language === "ru" ? "UX Промпт" : language === "he" ? "פרומפט UX" : "UX Prompt"}</span> <span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
</span> </div>
{generatedPrompt && ( )}
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9"> </div>
{copied ? ( )}
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
<div className="flex gap-2">
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
{isProcessing ? (
<>
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
{common.generating}
</>
) : ( ) : (
<Copy className="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" />
{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">
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
<span className="sm:hidden">×</span>
</Button>
</div>
</CardContent>
</Card>
<Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}>
<CardHeader className="p-4 lg:p-6 text-start">
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
<span className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
<span className="hidden sm:inline">{t.resultTitle}</span>
<span className="sm:hidden">{language === "ru" ? "UX Промпт" : language === "he" ? "פרומפט UX" : "UX Prompt"}</span>
</span>
{generatedPrompt && (
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)}
</Button>
)}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{t.resultDesc}
</CardDescription>
</CardHeader>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{generatedPrompt ? (
<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 leading-relaxed">{generatedPrompt}</pre>
</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 italic">
{t.emptyState}
</div>
)} )}
</CardTitle> </CardContent>
<CardDescription className="text-xs lg:text-sm"> </Card>
{t.resultDesc} </div>
</CardDescription> );
</CardHeader>
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
{generatedPrompt ? (
<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 leading-relaxed">{generatedPrompt}</pre>
</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 italic">
{t.emptyState}
</div>
)}
</CardContent>
</Card>
</div>
);
} }

View File

@@ -29,6 +29,7 @@ export const translations = {
title: "Prompt Enhancer", title: "Prompt Enhancer",
description: "Transform your simple ideas into professional, high-quality prompts", description: "Transform your simple ideas into professional, high-quality prompts",
placeholder: "Enter your prompt here...", placeholder: "Enter your prompt here...",
inputLabel: "Your Prompt",
enhancedTitle: "Enhanced Prompt", enhancedTitle: "Enhanced Prompt",
enhancedDesc: "Your prompt has been optimized for better AI performance", enhancedDesc: "Your prompt has been optimized for better AI performance",
}, },
@@ -150,6 +151,7 @@ export const translations = {
title: "Улучшение промптов", title: "Улучшение промптов",
description: "Превратите ваши простые идеи в профессиональные, качественные промпты", description: "Превратите ваши простые идеи в профессиональные, качественные промпты",
placeholder: "Введите ваш промпт здесь...", placeholder: "Введите ваш промпт здесь...",
inputLabel: "Ваш промпт",
enhancedTitle: "Улучшенный промпт", enhancedTitle: "Улучшенный промпт",
enhancedDesc: "Ваш промпт оптимизирован для лучшей работы ИИ", enhancedDesc: "Ваш промпт оптимизирован для лучшей работы ИИ",
}, },
@@ -271,6 +273,7 @@ export const translations = {
title: "משפר פרומפטים", title: "משפר פרומפטים",
description: "הפוך רעיונות פשוטים לפרומפטים מקצועיים באיכות גבוהה", description: "הפוך רעיונות פשוטים לפרומפטים מקצועיים באיכות גבוהה",
placeholder: "הזן את הפרומפט שלך כאן...", placeholder: "הזן את הפרומפט שלך כאן...",
inputLabel: "הפרומפט שלך",
enhancedTitle: "פרומפט משופר", enhancedTitle: "פרומפט משופר",
enhancedDesc: "הפרומפט שלך הותאם לביצועי בינה מלאכותית טובים יותר", enhancedDesc: "הפרומפט שלך הותאם לביצועי בינה מלאכותית טובים יותר",
}, },