feat(i18n): Complete localization for Settings, Action Plan, Slides, and Google Ads generators - Added actionPlan and slidesGen translation sections for EN/RU/HE - Localized ActionPlanGenerator with dynamic titles, labels, and quick notes - Localized SlidesGenerator with theme, audience, and animation settings - Added RTL support and text-start alignment for Hebrew - Standardized AI Provider and Model labels across all generators
This commit is contained in:
@@ -8,17 +8,10 @@ import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { ListTodo, Copy, Loader2, CheckCircle2, Clock, AlertTriangle, Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function ActionPlanGenerator() {
|
||||
const {
|
||||
currentPrompt,
|
||||
actionPlan,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
availableModels,
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
language,
|
||||
setCurrentPrompt,
|
||||
setSelectedProvider,
|
||||
setActionPlan,
|
||||
@@ -28,238 +21,241 @@ export default function ActionPlanGenerator() {
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const t = translations[language].actionPlan;
|
||||
const common = translations[language].common;
|
||||
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
loadAvailableModels();
|
||||
const saved = localStorage.getItem("promptarch-api-keys");
|
||||
if (saved) {
|
||||
try {
|
||||
const keys = JSON.parse(saved);
|
||||
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||
} catch (e) {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
loadAvailableModels();
|
||||
const saved = localStorage.getItem("promptarch-api-keys");
|
||||
if (saved) {
|
||||
try {
|
||||
const keys = JSON.parse(saved);
|
||||
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||
} catch (e) {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load models:", error);
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load models:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter PRD or project requirements");
|
||||
return;
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter PRD or project requirements");
|
||||
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 apiKey = apiKeys[selectedProvider];
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
const handleCopy = async () => {
|
||||
if (actionPlan?.rawContent) {
|
||||
await navigator.clipboard.writeText(actionPlan.rawContent);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
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 () => {
|
||||
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">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Action Plan Generator
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Convert PRD into actionable implementation plan
|
||||
</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">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">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">PRD / Requirements</label>
|
||||
<Textarea
|
||||
placeholder="Paste your PRD or project requirements here..."
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full 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" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Generate Action Plan
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!actionPlan && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Action Plan
|
||||
</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" />
|
||||
)}
|
||||
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>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
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">
|
||||
<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" />
|
||||
Implementation Roadmap
|
||||
</h4>
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{actionPlan.rawContent}</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4">
|
||||
<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" />
|
||||
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>Review all task dependencies before starting</li>
|
||||
<li>Set up recommended framework architecture</li>
|
||||
<li>Follow best practices for security and performance</li>
|
||||
<li>Use specified deployment strategy</li>
|
||||
</ul>
|
||||
<div className="space-y-2 text-start">
|
||||
<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">{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">
|
||||
{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}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||
Action plan will appear here
|
||||
</div>
|
||||
<>
|
||||
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
{language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
</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 className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||
{t.emptyState}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,20 +7,24 @@ import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Megaphone, Copy, Loader2, CheckCircle2, Settings, Plus, X, ChevronDown, ChevronUp } 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 { GoogleAdsResult } from "@/types";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function GoogleAdsGenerator() {
|
||||
const {
|
||||
googleAdsResult,
|
||||
magicWandResult,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
availableModels,
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
language,
|
||||
setGoogleAdsResult,
|
||||
setMagicWandResult,
|
||||
setProcessing,
|
||||
setError,
|
||||
setAvailableModels,
|
||||
@@ -28,6 +32,9 @@ export default function GoogleAdsGenerator() {
|
||||
setSelectedProvider,
|
||||
} = useStore();
|
||||
|
||||
const t = translations[language].googleAds;
|
||||
const common = translations[language].common;
|
||||
|
||||
// Input states
|
||||
const [websiteUrl, setWebsiteUrl] = useState("");
|
||||
const [products, setProducts] = useState<string[]>([""]);
|
||||
@@ -40,6 +47,7 @@ export default function GoogleAdsGenerator() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [expandedSections, setExpandedSections] = useState<string[]>(["keywords"]);
|
||||
|
||||
const [isMagicThinking, setIsMagicThinking] = useState(false);
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
|
||||
@@ -112,6 +120,7 @@ export default function GoogleAdsGenerator() {
|
||||
|
||||
setProcessing(true);
|
||||
setError(null);
|
||||
setMagicWandResult(null);
|
||||
|
||||
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 () => {
|
||||
if (googleAdsResult?.rawContent) {
|
||||
await navigator.clipboard.writeText(googleAdsResult.rawContent);
|
||||
const content = googleAdsResult?.rawContent || magicWandResult?.rawContent;
|
||||
if (content) {
|
||||
await navigator.clipboard.writeText(content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const sections = [
|
||||
{ id: "keywords", title: "Keywords Research" },
|
||||
{ id: "adcopies", title: "Ad Copy Variations" },
|
||||
{ id: "campaigns", title: "Campaign Structure" },
|
||||
{ id: "implementation", title: "Implementation Guide" },
|
||||
{ id: "keywords", title: language === "ru" ? "Исследование ключевых слов" : language === "he" ? "מחקר מילות מפתח" : "Keywords Research" },
|
||||
{ id: "adcopies", title: language === "ru" ? "Варианты объявлений" : language === "he" ? "גרסאות עותקי מודעות" : "Ad Copy Variations" },
|
||||
{ id: "campaigns", title: language === "ru" ? "Структура кампании" : language === "he" ? "מבנה קמפיין" : "Campaign Structure" },
|
||||
{ id: "implementation", title: language === "ru" ? "Руководство по внедрению" : language === "he" ? "מדריך יישום" : "Implementation Guide" },
|
||||
];
|
||||
|
||||
const renderSectionContent = (sectionId: string) => {
|
||||
@@ -319,215 +394,408 @@ export default function GoogleAdsGenerator() {
|
||||
default:
|
||||
return <pre className="whitespace-pre-wrap text-xs">{googleAdsResult.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">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Google Ads Generator
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Generate keywords, ad copy, and campaign structure for Google Ads
|
||||
</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">AI Provider</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>
|
||||
))}
|
||||
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-2">
|
||||
<label className="text-xs lg:text-sm font-medium">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">Website URL</label>
|
||||
<Input
|
||||
placeholder="e.g., www.your-business.com"
|
||||
value={websiteUrl}
|
||||
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Products / Services</label>
|
||||
<div className="space-y-2">
|
||||
{products.map((product, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={`Product ${index + 1}`}
|
||||
value={product}
|
||||
onChange={(e) => updateProduct(index, e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
{products.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeProduct(index)}
|
||||
className="h-10 w-10 shrink-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||
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">Budget (USD/mo)</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">-</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Max"
|
||||
value={budgetMax}
|
||||
onChange={(e) => setBudgetMax(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
<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 className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Industry</label>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-slate-700 mb-2 flex items-center gap-1.5">
|
||||
<Rocket className="h-3.5 w-3.5" /> Emerging Trends
|
||||
</h4>
|
||||
<ul className="space-y-1">
|
||||
{magicWandResult.marketAnalysis.marketTrends.map((t, i) => (
|
||||
<li key={i} className="text-xs text-slate-600 flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-indigo-400 mt-1 shrink-0" />
|
||||
{t}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "competitors":
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{magicWandResult.competitorInsights.map((comp, i) => (
|
||||
<div key={i} className="p-4 rounded-xl border bg-white/50 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-bold text-slate-900">{comp.competitor}</h4>
|
||||
<ShieldAlert className="h-4 w-4 text-amber-500" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[10px] font-black uppercase text-emerald-600">Strengths</div>
|
||||
<ul className="space-y-1">
|
||||
{comp.strengths.map((s, j) => (
|
||||
<li key={j} className="text-xs text-slate-600 flex gap-1.5">
|
||||
<span className="text-emerald-500">✓</span> {s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[10px] font-black uppercase text-rose-600">Weaknesses</div>
|
||||
<ul className="space-y-1">
|
||||
{comp.weaknesses.map((w, j) => (
|
||||
<li key={j} className="text-xs text-slate-600 flex gap-1.5">
|
||||
<span className="text-rose-500">✗</span> {w}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-slate-100 italic text-xs text-slate-500">
|
||||
<span className="font-bold text-slate-700 not-italic uppercase text-[9px]">Spy Report:</span> {comp.adStrategy}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
case "strategies":
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{magicWandResult.strategies.map((strat, i) => (
|
||||
<div key={i} className="relative p-5 rounded-2xl border bg-white shadow-sm hover:shadow-md transition-all group overflow-hidden">
|
||||
<div className="absolute top-0 right-0 h-1 w-full bg-gradient-to-r from-indigo-500 to-violet-500" />
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h4 className="text-lg font-black text-slate-900 tracking-tight">{strat.direction}</h4>
|
||||
<p className="text-sm text-indigo-600 font-bold">{strat.targetAudience}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<span className={cn(
|
||||
"text-[10px] font-black uppercase px-2 py-0.5 rounded-full",
|
||||
strat.riskLevel === 'low' ? "bg-emerald-100 text-emerald-700" :
|
||||
strat.riskLevel === 'medium' ? "bg-amber-100 text-amber-700" : "bg-rose-100 text-rose-700"
|
||||
)}>
|
||||
{strat.riskLevel} risk
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 mt-1 font-bold italic">{strat.timeToResults} to results</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-slate-600 leading-relaxed font-medium">
|
||||
<span className="text-indigo-500 font-black uppercase text-[9px] block mb-0.5">The "Why":</span>
|
||||
{strat.rationale}
|
||||
</p>
|
||||
|
||||
<div className="p-3 bg-slate-50 rounded-xl border border-dashed text-xs space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="h-3.5 w-3.5 text-indigo-500" />
|
||||
<span className="font-bold text-slate-700">Edge: {strat.competitiveAdvantage}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{strat.keyMessages.map((msg, j) => (
|
||||
<span key={j} className="text-[10px] bg-white border px-1.5 py-0.5 rounded-md text-slate-500 shadow-sm">{msg}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 items-center">
|
||||
<div className="space-y-1">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase">Channel Mix</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{strat.recommendedChannels.map((c, j) => (
|
||||
<span key={j} className="text-[9px] font-bold text-slate-600">{c}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase text-right">Expected ROI</div>
|
||||
<div className="text-lg font-black text-emerald-600 tracking-tighter">{strat.expectedROI}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <pre className="whitespace-pre-wrap text-xs">{magicWandResult.rawContent}</pre>;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
{t.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{t.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.websiteUrl}</label>
|
||||
<Input
|
||||
placeholder="e.g., www.your-business.com"
|
||||
value={websiteUrl}
|
||||
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.products}</label>
|
||||
<div className="space-y-2">
|
||||
{products.map((product, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={`${language === "ru" ? "Продукт" : language === "he" ? "מוצר" : "Product"} ${index + 1}`}
|
||||
value={product}
|
||||
onChange={(e) => updateProduct(index, e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
{products.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeProduct(index)}
|
||||
className="h-10 w-10 shrink-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||
{language === "ru" ? "Добавить продукт" : language === "he" ? "הוסף מוצר" : "Add Product"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.budget}</label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="e.g., SaaS"
|
||||
value={industry}
|
||||
onChange={(e) => setIndustry(e.target.value)}
|
||||
type="number"
|
||||
placeholder="Min"
|
||||
value={budgetMin}
|
||||
onChange={(e) => setBudgetMin(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs font-bold text-center">-</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Max"
|
||||
value={budgetMax}
|
||||
onChange={(e) => setBudgetMax(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Target Audience</label>
|
||||
<Textarea
|
||||
placeholder="e.g., Small business owners in USA looking for productivity tools"
|
||||
value={targetAudience}
|
||||
onChange={(e) => setTargetAudience(e.target.value)}
|
||||
className="min-h-[80px] lg:min-h-[100px] resize-y text-sm"
|
||||
<label className="text-xs lg:text-sm font-medium">{t.industry}</label>
|
||||
<Input
|
||||
placeholder="e.g., SaaS"
|
||||
value={industry}
|
||||
onChange={(e) => setIndustry(e.target.value)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
|
||||
<Textarea
|
||||
placeholder="e.g., Small business owners in USA looking for productivity tools"
|
||||
value={targetAudience}
|
||||
onChange={(e) => setTargetAudience(e.target.value)}
|
||||
className="min-h-[80px] lg:min-h-[100px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleGenerate} disabled={isProcessing || !websiteUrl.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
|
||||
className="h-9 lg:h-10 text-xs lg:text-sm bg-primary/90 hover:bg-primary shadow-sm"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||
Generating Ads...
|
||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
|
||||
{common.generating}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Generate Google Ads
|
||||
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5" />
|
||||
{t.generateAds}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!googleAdsResult && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Generated Campaign
|
||||
</span>
|
||||
{googleAdsResult && (
|
||||
<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>
|
||||
<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}
|
||||
</>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Keywords, ad copy, and campaign structure ready for Google Ads
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{googleAdsResult ? (
|
||||
<div className="space-y-2 lg:space-y-3">
|
||||
{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">
|
||||
{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">
|
||||
Generated campaign will appear here
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!googleAdsResult && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
{magicWandResult ? (
|
||||
<Wand2 className="h-4 w-4 lg:h-5 lg:w-5 text-indigo-500" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
)}
|
||||
{magicWandResult ? t.strategicDirections : t.generatedCampaign}
|
||||
</span>
|
||||
{(googleAdsResult || magicWandResult) && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{magicWandResult
|
||||
? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes")
|
||||
: (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready")
|
||||
}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{googleAdsResult || magicWandResult ? (
|
||||
<div className="space-y-2 lg:space-y-3">
|
||||
{(magicWandResult
|
||||
? [
|
||||
{ id: "market", title: t.marketIntelligence },
|
||||
{ id: "competitors", title: t.competitiveInsights },
|
||||
{ id: "strategies", title: t.campaignDirections }
|
||||
]
|
||||
: sections
|
||||
).map((section) => (
|
||||
<div key={section.id} className="rounded-md border bg-muted/30">
|
||||
<button
|
||||
onClick={() => toggleSection(section.id)}
|
||||
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
|
||||
>
|
||||
<span>{section.title}</span>
|
||||
{expandedSections.includes(section.id) ? (
|
||||
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
)}
|
||||
</button>
|
||||
{expandedSections.includes(section.id) && (
|
||||
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
{magicWandResult
|
||||
? renderMagicWandSectionContent(section.id)
|
||||
: renderSectionContent(section.id)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||
{language === "ru" ? "Здесь появится созданная кампания" : language === "he" ? "קמפיין שחולל יופיע כאן" : "Generated campaign will appear here"}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,16 +4,20 @@ import useStore from "@/lib/store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Clock, Trash2, RotateCcw } from "lucide-react";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
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) => {
|
||||
setCurrentPrompt(prompt);
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -21,12 +25,12 @@ export default function HistoryPanel() {
|
||||
if (history.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6">
|
||||
<div className="text-center">
|
||||
<CardContent className="flex h-[300px] lg:h-[400px] items-center justify-center p-4 lg:p-6 text-center">
|
||||
<div>
|
||||
<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">
|
||||
Start enhancing prompts to see them here
|
||||
{language === "ru" ? "Начните использовать инструменты, чтобы увидеть историю здесь" : language === "he" ? "התחל להשתמש בכלים כדי לראות אותם כאן" : "Start using tools to see them here"}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -36,12 +40,14 @@ export default function HistoryPanel() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<CardTitle className="text-base lg:text-lg">History</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">{history.length} items</CardDescription>
|
||||
<CardTitle className="text-base lg:text-lg">{t.title}</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{history.length} {language === "ru" ? "элем." : language === "he" ? "פריטים" : "items"}
|
||||
</CardDescription>
|
||||
</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" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
|
||||
15
components/LocaleProvider.tsx
Normal file
15
components/LocaleProvider.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import useStore from "@/lib/store";
|
||||
|
||||
export default function LocaleProvider({ children }: { children: React.ReactNode }) {
|
||||
const language = useStore((state) => state.language);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = language;
|
||||
document.documentElement.dir = language === "he" ? "rtl" : "ltr";
|
||||
}, [language]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { FileText, Copy, Loader2, CheckCircle2, ChevronDown, ChevronUp, Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function PRDGenerator() {
|
||||
const {
|
||||
@@ -19,6 +20,7 @@ export default function PRDGenerator() {
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
language,
|
||||
setCurrentPrompt,
|
||||
setSelectedProvider,
|
||||
setPRD,
|
||||
@@ -28,6 +30,9 @@ export default function PRDGenerator() {
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
const t = translations[language].prdGenerator;
|
||||
const common = translations[language].common;
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
||||
|
||||
@@ -131,29 +136,29 @@ export default function PRDGenerator() {
|
||||
};
|
||||
|
||||
const sections = [
|
||||
{ id: "overview", title: "Overview & Objectives" },
|
||||
{ id: "personas", title: "User Personas & Use Cases" },
|
||||
{ id: "functional", title: "Functional Requirements" },
|
||||
{ id: "nonfunctional", title: "Non-functional Requirements" },
|
||||
{ id: "architecture", title: "Technical Architecture" },
|
||||
{ id: "metrics", title: "Success Metrics" },
|
||||
{ id: "overview", title: language === "ru" ? "Обзор продукта" : language === "he" ? "סקירת מוצר" : "Product Overview" },
|
||||
{ id: "personas", title: language === "ru" ? "Персоны пользователей" : language === "he" ? "פרסונות משתמשים" : "User Personas & Use Cases" },
|
||||
{ id: "functional", title: language === "ru" ? "Функциональные требования" : language === "he" ? "דרישות פונקציונליות" : "Functional Requirements" },
|
||||
{ id: "nonfunctional", title: language === "ru" ? "Нефункциональные требования" : language === "he" ? "דרישות לא פונקציונליות" : "Non-functional Requirements" },
|
||||
{ id: "architecture", title: language === "ru" ? "Техническая архитектура" : language === "he" ? "ארכיטקטורה טכנית" : "Technical Architecture" },
|
||||
{ id: "metrics", title: language === "ru" ? "Успешность метрик" : language === "he" ? "מדדי הצלחה" : "Success Metrics" },
|
||||
];
|
||||
|
||||
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">
|
||||
<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">
|
||||
<FileText className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
PRD Generator
|
||||
{t.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Generate comprehensive Product Requirements Document from your idea
|
||||
{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">AI Provider</label>
|
||||
<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
|
||||
@@ -169,8 +174,8 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
@@ -185,12 +190,11 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Your Idea</label>
|
||||
<Textarea
|
||||
placeholder="e.g., A task management app with real-time collaboration features"
|
||||
placeholder={t.placeholder}
|
||||
value={currentPrompt}
|
||||
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>
|
||||
|
||||
@@ -200,7 +204,7 @@ export default function PRDGenerator() {
|
||||
{!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">Configure API key in Settings</span>
|
||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -210,12 +214,12 @@ export default function PRDGenerator() {
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<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" />
|
||||
Generate PRD
|
||||
{common.generate}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -223,11 +227,11 @@ export default function PRDGenerator() {
|
||||
</Card>
|
||||
|
||||
<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">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Generated PRD
|
||||
{t.generatedTitle}
|
||||
</span>
|
||||
{prd && (
|
||||
<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>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Structured requirements document ready for development
|
||||
{language === "ru" ? "Структурированный документ требований готов к разработке" : language === "he" ? "מסמך דרישות מובנה מוכן לפיתוח" : "Structured requirements document ready for development"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
@@ -269,7 +273,7 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -8,17 +8,10 @@ import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Sparkles, Copy, RefreshCw, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function PromptEnhancer() {
|
||||
const {
|
||||
currentPrompt,
|
||||
enhancedPrompt,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
availableModels,
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
language,
|
||||
setSelectedProvider,
|
||||
setCurrentPrompt,
|
||||
setEnhancedPrompt,
|
||||
@@ -28,219 +21,222 @@ export default function PromptEnhancer() {
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const t = translations[language].promptEnhancer;
|
||||
const common = translations[language].common;
|
||||
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
loadAvailableModels();
|
||||
const saved = localStorage.getItem("promptarch-api-keys");
|
||||
if (saved) {
|
||||
try {
|
||||
const keys = JSON.parse(saved);
|
||||
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||
} catch (e) {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
loadAvailableModels();
|
||||
const saved = localStorage.getItem("promptarch-api-keys");
|
||||
if (saved) {
|
||||
try {
|
||||
const keys = JSON.parse(saved);
|
||||
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||
} catch (e) {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load models:", error);
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load models:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnhance = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter a prompt to enhance");
|
||||
return;
|
||||
const handleEnhance = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter a prompt to enhance");
|
||||
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 apiKey = apiKeys[selectedProvider];
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
const handleCopy = async () => {
|
||||
if (enhancedPrompt) {
|
||||
await navigator.clipboard.writeText(enhancedPrompt);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
return;
|
||||
}
|
||||
const handleClear = () => {
|
||||
setCurrentPrompt("");
|
||||
setEnhancedPrompt(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
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 () => {
|
||||
if (enhancedPrompt) {
|
||||
await navigator.clipboard.writeText(enhancedPrompt);
|
||||
setCopied(true);
|
||||
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">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
Input Prompt
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Enter your prompt and we'll enhance it for AI coding agents
|
||||
</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">AI Provider</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 className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">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">Your Prompt</label>
|
||||
<Textarea
|
||||
placeholder="e.g., Create a user authentication system with JWT tokens"
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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" />
|
||||
Enhancing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Enhance Prompt
|
||||
</>
|
||||
)}
|
||||
</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">Clear</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!enhancedPrompt && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
Enhanced Prompt
|
||||
</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" />
|
||||
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 className="space-y-2 text-start">
|
||||
<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 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>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
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">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{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">
|
||||
Enhanced prompt will appear here
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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")}>
|
||||
<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.resultTitle}
|
||||
</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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
</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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,16 +7,19 @@ import { Input } from "@/components/ui/input";
|
||||
import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Save, Key, Server, Eye, EyeOff } from "lucide-react";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
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 [isAuthLoading, setIsAuthLoading] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
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) {
|
||||
console.error("Qwen OAuth failed", error);
|
||||
window.alert(
|
||||
error instanceof Error ? error.message : "Qwen authentication failed"
|
||||
error instanceof Error ? error.message : t.qwenAuth + " failed"
|
||||
);
|
||||
} finally {
|
||||
setIsAuthLoading(false);
|
||||
@@ -95,17 +98,17 @@ export default function SettingsPanel() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-4 lg:space-y-6">
|
||||
<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">
|
||||
<Key className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
API Configuration
|
||||
{t.apiKeys}
|
||||
</CardTitle>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<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">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Qwen Code API Key
|
||||
@@ -113,7 +116,7 @@ export default function SettingsPanel() {
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey.qwen ? "text" : "password"}
|
||||
placeholder="Enter your Qwen API key"
|
||||
placeholder={t.enterKey("Qwen")}
|
||||
value={apiKeys.qwen || ""}
|
||||
onChange={(e) => handleApiKeyChange("qwen", e.target.value)}
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
@@ -134,7 +137,7 @@ export default function SettingsPanel() {
|
||||
</div>
|
||||
<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">
|
||||
Get API key from{" "}
|
||||
{t.getApiKey}{" "}
|
||||
<a
|
||||
href="https://help.aliyun.com/zh/dashscope/"
|
||||
target="_blank"
|
||||
@@ -152,20 +155,20 @@ export default function SettingsPanel() {
|
||||
disabled={isAuthLoading}
|
||||
>
|
||||
{isAuthLoading
|
||||
? "Signing in..."
|
||||
? (language === "ru" ? "Вход..." : language === "he" ? "מתחבר..." : "Signing in...")
|
||||
: qwenTokens
|
||||
? "Logout from Qwen"
|
||||
: "Login with Qwen (OAuth)"}
|
||||
? t.logoutQwen
|
||||
: t.loginQwen}
|
||||
</Button>
|
||||
</div>
|
||||
{qwenTokens && (
|
||||
<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>
|
||||
)}
|
||||
</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">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Ollama Cloud API Key
|
||||
@@ -173,7 +176,7 @@ export default function SettingsPanel() {
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey.ollama ? "text" : "password"}
|
||||
placeholder="Enter your Ollama API key"
|
||||
placeholder={t.enterKey("Ollama")}
|
||||
value={apiKeys.ollama || ""}
|
||||
onChange={(e) => handleApiKeyChange("ollama", e.target.value)}
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
@@ -193,7 +196,7 @@ export default function SettingsPanel() {
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Get API key from{" "}
|
||||
{t.getApiKey}{" "}
|
||||
<a
|
||||
href="https://ollama.com/cloud"
|
||||
target="_blank"
|
||||
@@ -205,7 +208,7 @@ export default function SettingsPanel() {
|
||||
</p>
|
||||
</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">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Z.AI Plan API Key
|
||||
@@ -213,7 +216,7 @@ export default function SettingsPanel() {
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey.zai ? "text" : "password"}
|
||||
placeholder="Enter your Z.AI API key"
|
||||
placeholder={t.enterKey("Z.AI")}
|
||||
value={apiKeys.zai || ""}
|
||||
onChange={(e) => handleApiKeyChange("zai", e.target.value)}
|
||||
className="font-mono text-xs lg:text-sm pr-10"
|
||||
@@ -233,7 +236,7 @@ export default function SettingsPanel() {
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
Get API key from{" "}
|
||||
{t.getApiKey}{" "}
|
||||
<a
|
||||
href="https://docs.z.ai"
|
||||
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">
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="text-base lg:text-lg">Default Provider</CardTitle>
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="text-base lg:text-lg">{t.defaultProvider}</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Select your preferred AI provider
|
||||
{t.defaultProviderDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<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}
|
||||
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
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border"
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border"
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
||||
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
||||
{provider === "qwen" && "Alibaba DashScope API"}
|
||||
{provider === "ollama" && "Ollama Cloud API"}
|
||||
{provider === "zai" && "Z.AI Plan API"}
|
||||
{provider === "qwen" && t.qwenDesc}
|
||||
{provider === "ollama" && t.ollamaDesc}
|
||||
{provider === "zai" && t.zaiDesc}
|
||||
</p>
|
||||
</div>
|
||||
{selectedProvider === provider && (
|
||||
@@ -291,16 +294,16 @@ export default function SettingsPanel() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="text-base lg:text-lg">Data Privacy</CardTitle>
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="text-base lg:text-lg">{t.dataPrivacy}</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Your data handling preferences
|
||||
{language === "ru" ? "Ваши настройки обработки данных" : language === "he" ? "העדפות הטיפול בנתונים שלך" : "Your data handling preferences"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<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">
|
||||
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>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useStore from "@/lib/store";
|
||||
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, 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 { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "history" | "settings";
|
||||
|
||||
@@ -14,18 +15,20 @@ interface 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 t = translations[language].sidebar;
|
||||
const common = translations[language].common;
|
||||
|
||||
const menuItems = [
|
||||
{ id: "enhance" as View, label: "Prompt Enhancer", icon: Sparkles },
|
||||
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
||||
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
|
||||
{ id: "slides" as View, label: "Slides Generator", icon: Presentation },
|
||||
{ id: "googleads" as View, label: "Google Ads Gen", icon: Megaphone },
|
||||
{ id: "history" as View, label: "History", icon: History, count: history.length },
|
||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
||||
{ id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles },
|
||||
{ id: "prd" as View, label: t.prdGenerator, icon: FileText },
|
||||
{ id: "action" as View, label: t.actionPlan, icon: ListTodo },
|
||||
{ id: "uxdesigner" as View, label: t.uxDesigner, icon: Palette },
|
||||
{ id: "slides" as View, label: t.slidesGen, icon: Presentation },
|
||||
{ id: "googleads" as View, label: t.googleAds, icon: Megaphone },
|
||||
{ id: "history" as View, label: t.history, icon: History, count: history.length },
|
||||
{ id: "settings" as View, label: t.settings, icon: Settings },
|
||||
];
|
||||
|
||||
const handleViewChange = (view: View) => {
|
||||
@@ -38,7 +41,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
<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">
|
||||
<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 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">
|
||||
@@ -78,21 +81,22 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
</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">
|
||||
<p className="font-semibold text-foreground mb-1">Developed by Roman | RyzenAdvanced</p>
|
||||
<div className="space-y-0.5 lg:space-y-1">
|
||||
<p>
|
||||
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>
|
||||
</p>
|
||||
<p>
|
||||
Telegram: <a href="https://t.me/VibeCodePrompterSystem" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@VibeCodePrompterSystem</a>
|
||||
</p>
|
||||
<p className="mt-1 lg:mt-2 text-[8px] lg:text-[9px] opacity-80">
|
||||
100% Developed using GLM 4.7 model on TRAE.AI IDE.
|
||||
</p>
|
||||
<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>
|
||||
</p>
|
||||
<div className="mt-4 p-2 lg:p-3 border-t border-border/50">
|
||||
<div className="flex items-center gap-2 mb-2 text-[10px] lg:text-xs font-semibold text-muted-foreground uppercase">
|
||||
<Languages className="h-3 w-3" /> {language === "en" ? "Language" : language === "ru" ? "Язык" : "שפה"}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(["en", "ru", "he"] as const).map((lang) => (
|
||||
<Button
|
||||
key={lang}
|
||||
variant={language === lang ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-7 px-2 text-[10px] uppercase font-bold"
|
||||
onClick={() => setLanguage(lang)}
|
||||
>
|
||||
{lang}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,16 +8,10 @@ import useStore from "@/lib/store";
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
import { Palette, Copy, Loader2, CheckCircle2, Settings } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { translations } from "@/lib/i18n/translations";
|
||||
|
||||
export default function UXDesignerPrompt() {
|
||||
const {
|
||||
currentPrompt,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
availableModels,
|
||||
apiKeys,
|
||||
isProcessing,
|
||||
error,
|
||||
language,
|
||||
setSelectedProvider,
|
||||
setCurrentPrompt,
|
||||
setEnhancedPrompt,
|
||||
@@ -27,226 +21,229 @@ export default function UXDesignerPrompt() {
|
||||
setSelectedModel,
|
||||
} = useStore();
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
||||
const t = translations[language].uxDesigner;
|
||||
const common = translations[language].common;
|
||||
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
loadAvailableModels();
|
||||
const saved = localStorage.getItem("promptarch-api-keys");
|
||||
if (saved) {
|
||||
try {
|
||||
const keys = JSON.parse(saved);
|
||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||
} catch (e) {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
const selectedModel = selectedModels[selectedProvider];
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
loadAvailableModels();
|
||||
const saved = localStorage.getItem("promptarch-api-keys");
|
||||
if (saved) {
|
||||
try {
|
||||
const keys = JSON.parse(saved);
|
||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||
} catch (e) {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
const loadAvailableModels = async () => {
|
||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||
setAvailableModels(selectedProvider, fallbackModels);
|
||||
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load models:", error);
|
||||
try {
|
||||
const result = await modelAdapter.listModels(selectedProvider);
|
||||
if (result.success && result.data) {
|
||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load models:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter an app description");
|
||||
return;
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter an app description");
|
||||
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 apiKey = apiKeys[selectedProvider];
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
const handleCopy = async () => {
|
||||
if (generatedPrompt) {
|
||||
await navigator.clipboard.writeText(generatedPrompt);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
return;
|
||||
}
|
||||
const handleClear = () => {
|
||||
setCurrentPrompt("");
|
||||
setGeneratedPrompt(null);
|
||||
setEnhancedPrompt(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
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 () => {
|
||||
if (generatedPrompt) {
|
||||
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">
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||
UX Designer Prompt
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Describe your app idea and get the BEST EVER prompt for UX design
|
||||
</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">AI Provider</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 className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">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">App Description</label>
|
||||
<Textarea
|
||||
placeholder="e.g., A fitness tracking app with workout plans, nutrition tracking, and social features for sharing progress with friends"
|
||||
value={currentPrompt}
|
||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm"
|
||||
/>
|
||||
<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.
|
||||
</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">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
Generate UX Prompt
|
||||
</>
|
||||
)}
|
||||
</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">Clear</span>
|
||||
<span className="sm:hidden">×</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={cn(!generatedPrompt && "opacity-50")}>
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<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="sm:hidden">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" />
|
||||
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 className="space-y-2 text-start">
|
||||
<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 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>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Comprehensive UX design prompt ready for designers
|
||||
</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">
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm">{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">
|
||||
Your comprehensive UX designer prompt will appear here
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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 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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
</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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user