fix(i18n): Fix syntax errors and add missing translation keys - Restored broken useStore destructuring in PromptEnhancer, UXDesignerPrompt, ActionPlanGenerator, SlidesGenerator - Fixed GoogleAdsGenerator scope issue with renderMagicWandSectionContent - Added missing inputLabel keys to promptEnhancer for all languages - Fixed t.resultTitle to t.enhancedTitle in PromptEnhancer
This commit is contained in:
@@ -11,7 +11,16 @@ import { cn } from "@/lib/utils";
|
|||||||
import { translations } from "@/lib/i18n/translations";
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function ActionPlanGenerator() {
|
export default function ActionPlanGenerator() {
|
||||||
language,
|
const {
|
||||||
|
language,
|
||||||
|
currentPrompt,
|
||||||
|
actionPlan,
|
||||||
|
selectedProvider,
|
||||||
|
selectedModels,
|
||||||
|
availableModels,
|
||||||
|
apiKeys,
|
||||||
|
isProcessing,
|
||||||
|
error,
|
||||||
setCurrentPrompt,
|
setCurrentPrompt,
|
||||||
setSelectedProvider,
|
setSelectedProvider,
|
||||||
setActionPlan,
|
setActionPlan,
|
||||||
@@ -21,241 +30,241 @@ export default function ActionPlanGenerator() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
const t = translations[language].actionPlan;
|
const t = translations[language].actionPlan;
|
||||||
const common = translations[language].common;
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
loadAvailableModels();
|
loadAvailableModels();
|
||||||
const saved = localStorage.getItem("promptarch-api-keys");
|
const saved = localStorage.getItem("promptarch-api-keys");
|
||||||
if (saved) {
|
if (saved) {
|
||||||
try {
|
try {
|
||||||
const keys = JSON.parse(saved);
|
const keys = JSON.parse(saved);
|
||||||
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
||||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load API keys:", e);
|
console.error("Failed to load API keys:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [selectedProvider]);
|
||||||
}, [selectedProvider]);
|
|
||||||
|
|
||||||
const loadAvailableModels = async () => {
|
const loadAvailableModels = async () => {
|
||||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||||
setAvailableModels(selectedProvider, fallbackModels);
|
setAvailableModels(selectedProvider, fallbackModels);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.listModels(selectedProvider);
|
const result = await modelAdapter.listModels(selectedProvider);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load models:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error("Failed to load models:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!currentPrompt.trim()) {
|
if (!currentPrompt.trim()) {
|
||||||
setError("Please enter PRD or project requirements");
|
setError("Please enter PRD or project requirements");
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = apiKeys[selectedProvider];
|
|
||||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
|
||||||
|
|
||||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
|
||||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProcessing(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
|
|
||||||
|
|
||||||
console.log("[ActionPlanGenerator] Generation result:", result);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
const newPlan = {
|
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
|
||||||
prdId: "",
|
|
||||||
tasks: [],
|
|
||||||
frameworks: [],
|
|
||||||
architecture: {
|
|
||||||
pattern: "",
|
|
||||||
structure: "",
|
|
||||||
technologies: [],
|
|
||||||
bestPractices: [],
|
|
||||||
},
|
|
||||||
estimatedDuration: "",
|
|
||||||
createdAt: new Date(),
|
|
||||||
rawContent: result.data,
|
|
||||||
};
|
|
||||||
setActionPlan(newPlan);
|
|
||||||
} else {
|
|
||||||
console.error("[ActionPlanGenerator] Generation failed:", result.error);
|
|
||||||
setError(result.error || "Failed to generate action plan");
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("[ActionPlanGenerator] Generation error:", err);
|
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const apiKey = apiKeys[selectedProvider];
|
||||||
if (actionPlan?.rawContent) {
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
await navigator.clipboard.writeText(actionPlan.rawContent);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||||
<Card className="h-fit">
|
return;
|
||||||
<CardHeader className="p-4 lg:p-6 text-start">
|
}
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
|
||||||
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
setProcessing(true);
|
||||||
{t.title}
|
setError(null);
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
console.log("[ActionPlanGenerator] Starting action plan generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||||
{t.description}
|
|
||||||
</CardDescription>
|
try {
|
||||||
</CardHeader>
|
const result = await modelAdapter.generateActionPlan(currentPrompt, selectedProvider, selectedModel);
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
|
||||||
<div className="space-y-2 text-start">
|
console.log("[ActionPlanGenerator] Generation result:", result);
|
||||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
if (result.success && result.data) {
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
const newPlan = {
|
||||||
<Button
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
key={provider}
|
prdId: "",
|
||||||
variant={selectedProvider === provider ? "default" : "outline"}
|
tasks: [],
|
||||||
size="sm"
|
frameworks: [],
|
||||||
onClick={() => setSelectedProvider(provider)}
|
architecture: {
|
||||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
pattern: "",
|
||||||
>
|
structure: "",
|
||||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
technologies: [],
|
||||||
</Button>
|
bestPractices: [],
|
||||||
))}
|
},
|
||||||
|
estimatedDuration: "",
|
||||||
|
createdAt: new Date(),
|
||||||
|
rawContent: result.data,
|
||||||
|
};
|
||||||
|
setActionPlan(newPlan);
|
||||||
|
} else {
|
||||||
|
console.error("[ActionPlanGenerator] Generation failed:", result.error);
|
||||||
|
setError(result.error || "Failed to generate action plan");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ActionPlanGenerator] Generation error:", err);
|
||||||
|
setError(err instanceof Error ? err.message : "An error occurred");
|
||||||
|
} finally {
|
||||||
|
setProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (actionPlan?.rawContent) {
|
||||||
|
await navigator.clipboard.writeText(actionPlan.rawContent);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
|
<Card className="h-fit">
|
||||||
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
|
<ListTodo className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
|
{t.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{t.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
|
<div className="space-y-2 text-start">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
|
<Button
|
||||||
|
key={provider}
|
||||||
|
variant={selectedProvider === provider ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedProvider(provider)}
|
||||||
|
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||||
|
>
|
||||||
|
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-start">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<option key={model} value={model}>
|
<option key={model} value={model}>
|
||||||
{model}
|
{model}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs lg:text-sm font-medium">{language === "ru" ? "PRD / Требования" : language === "he" ? "PRD / דרישות" : "PRD / Requirements"}</label>
|
|
||||||
<Textarea
|
|
||||||
placeholder={t.placeholder}
|
|
||||||
value={currentPrompt}
|
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
|
||||||
{error}
|
|
||||||
{!apiKeys[selectedProvider] && (
|
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
|
||||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
<div className="space-y-2">
|
||||||
{isProcessing ? (
|
<label className="text-xs lg:text-sm font-medium">{language === "ru" ? "PRD / Требования" : language === "he" ? "PRD / דרישות" : "PRD / Requirements"}</label>
|
||||||
<>
|
<Textarea
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
placeholder={t.placeholder}
|
||||||
{common.generating}
|
value={currentPrompt}
|
||||||
</>
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
) : (
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
<>
|
/>
|
||||||
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
</div>
|
||||||
{language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
|
{error && (
|
||||||
<CardHeader className="p-4 lg:p-6 text-start">
|
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
{error}
|
||||||
<span className="flex items-center gap-2">
|
{!apiKeys[selectedProvider] && (
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
{t.generatedTitle}
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
{actionPlan && (
|
</div>
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
|
||||||
{copied ? (
|
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
|
||||||
{language === "ru" ? "Разбивка задач, фреймворки и рекомендации по архитектуре" : language === "he" ? "פירוט משימות, פרימוורקים והמלצות ארכיטקטורה" : "Task breakdown, frameworks, and architecture recommendations"}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
|
||||||
{actionPlan ? (
|
|
||||||
<div className="space-y-3 lg:space-y-4">
|
|
||||||
<div className="rounded-md border bg-primary/5 p-3 lg:p-4 text-start">
|
|
||||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
|
||||||
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
|
||||||
{language === "ru" ? "Дорожная карта реализации" : language === "he" ? "מפת דרכים ליישום" : "Implementation Roadmap"}
|
|
||||||
</h4>
|
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
{isProcessing ? (
|
||||||
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<>
|
||||||
{language === "ru" ? "Быстрые заметки" : language === "he" ? "הערות מהירות" : "Quick Notes"}
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
</h4>
|
{common.generating}
|
||||||
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
|
</>
|
||||||
<li>{language === "ru" ? "Проверьте все зависимости задач перед началом" : language === "he" ? "בדוק את כל התלות בין המשימות לפני שתתחיל" : "Review all task dependencies before starting"}</li>
|
) : (
|
||||||
<li>{language === "ru" ? "Настройте рекомендуемую архитектуру фреймворка" : language === "he" ? "הגדר את ארכיטקטורת הפרימוורק המומלצת" : "Set up recommended framework architecture"}</li>
|
<>
|
||||||
<li>{language === "ru" ? "Следуйте лучшим практикам безопасности и производительности" : language === "he" ? "עקוב אחר שיטות עבודה מומלצות לאבטחה וביצועים" : "Follow best practices for security and performance"}</li>
|
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<li>{language === "ru" ? "Используйте указанную стратегию развертывания" : language === "he" ? "השתמש באסטרטגיית הפריסה המצוינת" : "Use specified deployment strategy"}</li>
|
{language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
|
||||||
</ul>
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className={cn("flex flex-col", !actionPlan && "opacity-50")}>
|
||||||
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
|
{t.generatedTitle}
|
||||||
|
</span>
|
||||||
|
{actionPlan && (
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
|
{copied ? (
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{language === "ru" ? "Разбивка задач, фреймворки и рекомендации по архитектуре" : language === "he" ? "פירוט משימות, פרימוורקים והמלצות ארכיטקטורה" : "Task breakdown, frameworks, and architecture recommendations"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
|
{actionPlan ? (
|
||||||
|
<div className="space-y-3 lg:space-y-4">
|
||||||
|
<div className="rounded-md border bg-primary/5 p-3 lg:p-4 text-start">
|
||||||
|
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||||
|
<Clock className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
{language === "ru" ? "Дорожная карта реализации" : language === "he" ? "מפת דרכים ליישום" : "Implementation Roadmap"}
|
||||||
|
</h4>
|
||||||
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border bg-muted/30 p-3 lg:p-4 text-start">
|
||||||
|
<h4 className="mb-1.5 lg:mb-2 flex items-center gap-2 font-semibold text-xs lg:text-sm">
|
||||||
|
<AlertTriangle className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
{language === "ru" ? "Быстрые заметки" : language === "he" ? "הערות מהירות" : "Quick Notes"}
|
||||||
|
</h4>
|
||||||
|
<ul className="list-inside list-disc space-y-0.5 lg:space-y-1 text-[10px] lg:text-xs text-muted-foreground">
|
||||||
|
<li>{language === "ru" ? "Проверьте все зависимости задач перед началом" : language === "he" ? "בדוק את כל התלות בין המשימות לפני שתתחיל" : "Review all task dependencies before starting"}</li>
|
||||||
|
<li>{language === "ru" ? "Настройте рекомендуемую архитектуру фреймворка" : language === "he" ? "הגדר את ארכיטקטורת הפרימוורק המומלצת" : "Set up recommended framework architecture"}</li>
|
||||||
|
<li>{language === "ru" ? "Следуйте лучшим практикам безопасности и производительности" : language === "he" ? "עקוב אחר שיטות עבודה מומלצות לאבטחה וביצועים" : "Follow best practices for security and performance"}</li>
|
||||||
|
<li>{language === "ru" ? "Используйте указанную стратегию развертывания" : language === "he" ? "השתמש באסטרטגיית הפריסה המצוינת" : "Use specified deployment strategy"}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
{t.emptyState}
|
||||||
{t.emptyState}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,408 +394,407 @@ export default function GoogleAdsGenerator() {
|
|||||||
default:
|
default:
|
||||||
return <pre className="whitespace-pre-wrap text-xs">{googleAdsResult.rawContent}</pre>;
|
return <pre className="whitespace-pre-wrap text-xs">{googleAdsResult.rawContent}</pre>;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const renderMagicWandSectionContent = (sectionId: string) => {
|
const renderMagicWandSectionContent = (sectionId: string) => {
|
||||||
if (!magicWandResult) return null;
|
if (!magicWandResult) return null;
|
||||||
|
|
||||||
switch (sectionId) {
|
switch (sectionId) {
|
||||||
case "market":
|
case "market":
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="p-3 rounded-md bg-indigo-50/50 border border-indigo-100">
|
<div className="p-3 rounded-md bg-indigo-50/50 border border-indigo-100">
|
||||||
<div className="text-[10px] uppercase font-bold text-indigo-600 mb-1 flex items-center gap-1">
|
<div className="text-[10px] uppercase font-bold text-indigo-600 mb-1 flex items-center gap-1">
|
||||||
<BarChart3 className="h-3 w-3" /> Industry Size
|
<BarChart3 className="h-3 w-3" /> Industry Size
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold">{magicWandResult.marketAnalysis.industrySize}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-md bg-emerald-50/50 border border-emerald-100">
|
||||||
|
<div className="text-[10px] uppercase font-bold text-emerald-600 mb-1 flex items-center gap-1">
|
||||||
|
<TrendingUp className="h-3 w-3" /> Growth Rate
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold">{magicWandResult.marketAnalysis.growthRate}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold">{magicWandResult.marketAnalysis.industrySize}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-md bg-emerald-50/50 border border-emerald-100">
|
<div className="space-y-3">
|
||||||
<div className="text-[10px] uppercase font-bold text-emerald-600 mb-1 flex items-center gap-1">
|
<div>
|
||||||
<TrendingUp className="h-3 w-3" /> Growth Rate
|
<h4 className="text-xs font-bold text-slate-700 mb-2 flex items-center gap-1.5">
|
||||||
|
<Users className="h-3.5 w-3.5" /> Market Leaders
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{magicWandResult.marketAnalysis.topCompetitors.map((c, i) => (
|
||||||
|
<span key={i} className="text-xs px-2 py-1 bg-slate-100 text-slate-700 rounded-md border border-slate-200">{c}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-slate-700 mb-2 flex items-center gap-1.5">
|
||||||
|
<Rocket className="h-3.5 w-3.5" /> Emerging Trends
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{magicWandResult.marketAnalysis.marketTrends.map((t, i) => (
|
||||||
|
<li key={i} className="text-xs text-slate-600 flex items-start gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-indigo-400 mt-1 shrink-0" />
|
||||||
|
{t}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold">{magicWandResult.marketAnalysis.growthRate}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
);
|
||||||
<div>
|
case "competitors":
|
||||||
<h4 className="text-xs font-bold text-slate-700 mb-2 flex items-center gap-1.5">
|
return (
|
||||||
<Users className="h-3.5 w-3.5" /> Market Leaders
|
<div className="space-y-4">
|
||||||
</h4>
|
{magicWandResult.competitorInsights.map((comp, i) => (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div key={i} className="p-4 rounded-xl border bg-white/50 space-y-3">
|
||||||
{magicWandResult.marketAnalysis.topCompetitors.map((c, i) => (
|
<div className="flex items-center justify-between">
|
||||||
<span key={i} className="text-xs px-2 py-1 bg-slate-100 text-slate-700 rounded-md border border-slate-200">{c}</span>
|
<h4 className="font-bold text-slate-900">{comp.competitor}</h4>
|
||||||
))}
|
<ShieldAlert className="h-4 w-4 text-amber-500" />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-xs font-bold text-slate-700 mb-2 flex items-center gap-1.5">
|
|
||||||
<Rocket className="h-3.5 w-3.5" /> Emerging Trends
|
|
||||||
</h4>
|
|
||||||
<ul className="space-y-1">
|
|
||||||
{magicWandResult.marketAnalysis.marketTrends.map((t, i) => (
|
|
||||||
<li key={i} className="text-xs text-slate-600 flex items-start gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-indigo-400 mt-1 shrink-0" />
|
|
||||||
{t}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "competitors":
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{magicWandResult.competitorInsights.map((comp, i) => (
|
|
||||||
<div key={i} className="p-4 rounded-xl border bg-white/50 space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="font-bold text-slate-900">{comp.competitor}</h4>
|
|
||||||
<ShieldAlert className="h-4 w-4 text-amber-500" />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="text-[10px] font-black uppercase text-emerald-600">Strengths</div>
|
|
||||||
<ul className="space-y-1">
|
|
||||||
{comp.strengths.map((s, j) => (
|
|
||||||
<li key={j} className="text-xs text-slate-600 flex gap-1.5">
|
|
||||||
<span className="text-emerald-500">✓</span> {s}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div className="text-[10px] font-black uppercase text-rose-600">Weaknesses</div>
|
<div className="space-y-1.5">
|
||||||
<ul className="space-y-1">
|
<div className="text-[10px] font-black uppercase text-emerald-600">Strengths</div>
|
||||||
{comp.weaknesses.map((w, j) => (
|
<ul className="space-y-1">
|
||||||
<li key={j} className="text-xs text-slate-600 flex gap-1.5">
|
{comp.strengths.map((s, j) => (
|
||||||
<span className="text-rose-500">✗</span> {w}
|
<li key={j} className="text-xs text-slate-600 flex gap-1.5">
|
||||||
</li>
|
<span className="text-emerald-500">✓</span> {s}
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
|
||||||
<div className="pt-2 border-t border-slate-100 italic text-xs text-slate-500">
|
|
||||||
<span className="font-bold text-slate-700 not-italic uppercase text-[9px]">Spy Report:</span> {comp.adStrategy}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "strategies":
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{magicWandResult.strategies.map((strat, i) => (
|
|
||||||
<div key={i} className="relative p-5 rounded-2xl border bg-white shadow-sm hover:shadow-md transition-all group overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 h-1 w-full bg-gradient-to-r from-indigo-500 to-violet-500" />
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-lg font-black text-slate-900 tracking-tight">{strat.direction}</h4>
|
|
||||||
<p className="text-sm text-indigo-600 font-bold">{strat.targetAudience}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-end">
|
|
||||||
<span className={cn(
|
|
||||||
"text-[10px] font-black uppercase px-2 py-0.5 rounded-full",
|
|
||||||
strat.riskLevel === 'low' ? "bg-emerald-100 text-emerald-700" :
|
|
||||||
strat.riskLevel === 'medium' ? "bg-amber-100 text-amber-700" : "bg-rose-100 text-rose-700"
|
|
||||||
)}>
|
|
||||||
{strat.riskLevel} risk
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-slate-400 mt-1 font-bold italic">{strat.timeToResults} to results</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-xs text-slate-600 leading-relaxed font-medium">
|
|
||||||
<span className="text-indigo-500 font-black uppercase text-[9px] block mb-0.5">The "Why":</span>
|
|
||||||
{strat.rationale}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="p-3 bg-slate-50 rounded-xl border border-dashed text-xs space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Target className="h-3.5 w-3.5 text-indigo-500" />
|
|
||||||
<span className="font-bold text-slate-700">Edge: {strat.competitiveAdvantage}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="space-y-1.5">
|
||||||
{strat.keyMessages.map((msg, j) => (
|
<div className="text-[10px] font-black uppercase text-rose-600">Weaknesses</div>
|
||||||
<span key={j} className="text-[10px] bg-white border px-1.5 py-0.5 rounded-md text-slate-500 shadow-sm">{msg}</span>
|
<ul className="space-y-1">
|
||||||
))}
|
{comp.weaknesses.map((w, j) => (
|
||||||
|
<li key={j} className="text-xs text-slate-600 flex gap-1.5">
|
||||||
|
<span className="text-rose-500">✗</span> {w}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t border-slate-100 italic text-xs text-slate-500">
|
||||||
|
<span className="font-bold text-slate-700 not-italic uppercase text-[9px]">Spy Report:</span> {comp.adStrategy}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "strategies":
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{magicWandResult.strategies.map((strat, i) => (
|
||||||
|
<div key={i} className="relative p-5 rounded-2xl border bg-white shadow-sm hover:shadow-md transition-all group overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 h-1 w-full bg-gradient-to-r from-indigo-500 to-violet-500" />
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-black text-slate-900 tracking-tight">{strat.direction}</h4>
|
||||||
|
<p className="text-sm text-indigo-600 font-bold">{strat.targetAudience}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<span className={cn(
|
||||||
|
"text-[10px] font-black uppercase px-2 py-0.5 rounded-full",
|
||||||
|
strat.riskLevel === 'low' ? "bg-emerald-100 text-emerald-700" :
|
||||||
|
strat.riskLevel === 'medium' ? "bg-amber-100 text-amber-700" : "bg-rose-100 text-rose-700"
|
||||||
|
)}>
|
||||||
|
{strat.riskLevel} risk
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-400 mt-1 font-bold italic">{strat.timeToResults} to results</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 items-center">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1">
|
<p className="text-xs text-slate-600 leading-relaxed font-medium">
|
||||||
<div className="text-[9px] font-black text-slate-400 uppercase">Channel Mix</div>
|
<span className="text-indigo-500 font-black uppercase text-[9px] block mb-0.5">The "Why":</span>
|
||||||
<div className="flex flex-wrap gap-1">
|
{strat.rationale}
|
||||||
{strat.recommendedChannels.map((c, j) => (
|
</p>
|
||||||
<span key={j} className="text-[9px] font-bold text-slate-600">{c}</span>
|
|
||||||
|
<div className="p-3 bg-slate-50 rounded-xl border border-dashed text-xs space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Target className="h-3.5 w-3.5 text-indigo-500" />
|
||||||
|
<span className="font-bold text-slate-700">Edge: {strat.competitiveAdvantage}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{strat.keyMessages.map((msg, j) => (
|
||||||
|
<span key={j} className="text-[10px] bg-white border px-1.5 py-0.5 rounded-md text-slate-500 shadow-sm">{msg}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-[9px] font-black text-slate-400 uppercase text-right">Expected ROI</div>
|
<div className="grid grid-cols-2 gap-4 items-center">
|
||||||
<div className="text-lg font-black text-emerald-600 tracking-tighter">{strat.expectedROI}</div>
|
<div className="space-y-1">
|
||||||
|
<div className="text-[9px] font-black text-slate-400 uppercase">Channel Mix</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{strat.recommendedChannels.map((c, j) => (
|
||||||
|
<span key={j} className="text-[9px] font-bold text-slate-600">{c}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-[9px] font-black text-slate-400 uppercase text-right">Expected ROI</div>
|
||||||
|
<div className="text-lg font-black text-emerald-600 tracking-tighter">{strat.expectedROI}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <pre className="whitespace-pre-wrap text-xs">{magicWandResult.rawContent}</pre>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
|
||||||
<Card className="h-fit">
|
|
||||||
<CardHeader className="p-4 lg:p-6 text-start">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
|
||||||
<Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
|
|
||||||
{t.title}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
|
||||||
{t.description}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
|
||||||
<Button
|
|
||||||
key={provider}
|
|
||||||
variant={selectedProvider === provider ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSelectedProvider(provider)}
|
|
||||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
|
||||||
>
|
|
||||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
|
||||||
</Button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
|
default:
|
||||||
|
return <pre className="whitespace-pre-wrap text-xs">{magicWandResult.rawContent}</pre>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
|
||||||
<select
|
|
||||||
value={selectedModel}
|
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
>
|
|
||||||
{models.map((model) => (
|
|
||||||
<option key={model} value={model}>
|
|
||||||
{model}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
return (
|
||||||
<label className="text-xs lg:text-sm font-medium">{t.websiteUrl}</label>
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2">
|
||||||
<Input
|
<Card className="h-fit">
|
||||||
placeholder="e.g., www.your-business.com"
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
value={websiteUrl}
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
onChange={(e) => setWebsiteUrl(e.target.value)}
|
<Megaphone className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
className="text-sm"
|
{t.title}
|
||||||
/>
|
</CardTitle>
|
||||||
</div>
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{t.description}
|
||||||
<div className="space-y-2">
|
</CardDescription>
|
||||||
<label className="text-xs lg:text-sm font-medium">{t.products}</label>
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{products.map((product, index) => (
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
<div key={index} className="flex gap-2">
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
<Input
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
placeholder={`${language === "ru" ? "Продукт" : language === "he" ? "מוצר" : "Product"} ${index + 1}`}
|
<Button
|
||||||
value={product}
|
key={provider}
|
||||||
onChange={(e) => updateProduct(index, e.target.value)}
|
variant={selectedProvider === provider ? "default" : "outline"}
|
||||||
className="text-sm"
|
size="sm"
|
||||||
/>
|
onClick={() => setSelectedProvider(provider)}
|
||||||
{products.length > 1 && (
|
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||||
size="icon"
|
</Button>
|
||||||
onClick={() => removeProduct(index)}
|
))}
|
||||||
className="h-10 w-10 shrink-0"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
|
|
||||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
|
||||||
{language === "ru" ? "Добавить продукт" : language === "he" ? "הוסף מוצר" : "Add Product"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs lg:text-sm font-medium">{t.budget}</label>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="Min"
|
|
||||||
value={budgetMin}
|
|
||||||
onChange={(e) => setBudgetMin(e.target.value)}
|
|
||||||
className="text-sm"
|
|
||||||
/>
|
|
||||||
<span className="text-muted-foreground text-xs font-bold text-center">-</span>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="Max"
|
|
||||||
value={budgetMax}
|
|
||||||
onChange={(e) => setBudgetMax(e.target.value)}
|
|
||||||
className="text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs lg:text-sm font-medium">{t.industry}</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
|
<select
|
||||||
|
value={selectedModel}
|
||||||
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
>
|
||||||
|
{models.map((model) => (
|
||||||
|
<option key={model} value={model}>
|
||||||
|
{model}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{t.websiteUrl}</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g., SaaS"
|
placeholder="e.g., www.your-business.com"
|
||||||
value={industry}
|
value={websiteUrl}
|
||||||
onChange={(e) => setIndustry(e.target.value)}
|
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
|
<label className="text-xs lg:text-sm font-medium">{t.products}</label>
|
||||||
<Textarea
|
<div className="space-y-2">
|
||||||
placeholder="e.g., Small business owners in USA looking for productivity tools"
|
{products.map((product, index) => (
|
||||||
value={targetAudience}
|
<div key={index} className="flex gap-2">
|
||||||
onChange={(e) => setTargetAudience(e.target.value)}
|
<Input
|
||||||
className="min-h-[80px] lg:min-h-[100px] resize-y text-sm"
|
placeholder={`${language === "ru" ? "Продукт" : language === "he" ? "מוצר" : "Product"} ${index + 1}`}
|
||||||
/>
|
value={product}
|
||||||
</div>
|
onChange={(e) => updateProduct(index, e.target.value)}
|
||||||
|
className="text-sm"
|
||||||
{error && (
|
/>
|
||||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
{products.length > 1 && (
|
||||||
{error}
|
<Button
|
||||||
{!apiKeys[selectedProvider] && (
|
variant="ghost"
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
size="icon"
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
onClick={() => removeProduct(index)}
|
||||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
className="h-10 w-10 shrink-0"
|
||||||
</div>
|
>
|
||||||
)}
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button variant="outline" size="sm" onClick={addProduct} className="w-full text-xs">
|
||||||
|
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
{language === "ru" ? "Добавить продукт" : language === "he" ? "הוסף מוצר" : "Add Product"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<Button
|
<div className="space-y-2">
|
||||||
onClick={handleGenerate}
|
<label className="text-xs lg:text-sm font-medium">{t.budget}</label>
|
||||||
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
|
<div className="flex items-center gap-1.5">
|
||||||
className="h-9 lg:h-10 text-xs lg:text-sm bg-primary/90 hover:bg-primary shadow-sm"
|
<Input
|
||||||
>
|
type="number"
|
||||||
{isProcessing ? (
|
placeholder="Min"
|
||||||
<>
|
value={budgetMin}
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
|
onChange={(e) => setBudgetMin(e.target.value)}
|
||||||
{common.generating}
|
className="text-sm"
|
||||||
</>
|
/>
|
||||||
) : (
|
<span className="text-muted-foreground text-xs font-bold text-center">-</span>
|
||||||
<>
|
<Input
|
||||||
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5" />
|
type="number"
|
||||||
{t.generateAds}
|
placeholder="Max"
|
||||||
</>
|
value={budgetMax}
|
||||||
)}
|
onChange={(e) => setBudgetMax(e.target.value)}
|
||||||
</Button>
|
className="text-sm"
|
||||||
<Button
|
/>
|
||||||
onClick={handleMagicWand}
|
</div>
|
||||||
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
|
</div>
|
||||||
className="h-9 lg:h-10 text-xs lg:text-sm bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white shadow-md transition-all active:scale-[0.98]"
|
<div className="space-y-2">
|
||||||
>
|
<label className="text-xs lg:text-sm font-medium">{t.industry}</label>
|
||||||
{isMagicThinking ? (
|
<Input
|
||||||
<>
|
placeholder="e.g., SaaS"
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
|
value={industry}
|
||||||
{t.researching}
|
onChange={(e) => setIndustry(e.target.value)}
|
||||||
</>
|
className="text-sm"
|
||||||
) : (
|
/>
|
||||||
<>
|
</div>
|
||||||
<Wand2 className="mr-1.5 h-3.5 w-3.5" />
|
</div>
|
||||||
{t.magicWand}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className={cn(!googleAdsResult && "opacity-50")}>
|
<div className="space-y-2">
|
||||||
<CardHeader className="p-4 lg:p-6">
|
<label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
<Textarea
|
||||||
<span className="flex items-center gap-2">
|
placeholder="e.g., Small business owners in USA looking for productivity tools"
|
||||||
{magicWandResult ? (
|
value={targetAudience}
|
||||||
<Wand2 className="h-4 w-4 lg:h-5 lg:w-5 text-indigo-500" />
|
onChange={(e) => setTargetAudience(e.target.value)}
|
||||||
) : (
|
className="min-h-[80px] lg:min-h-[100px] resize-y text-sm"
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
/>
|
||||||
)}
|
</div>
|
||||||
{magicWandResult ? t.strategicDirections : t.generatedCampaign}
|
|
||||||
</span>
|
{error && (
|
||||||
{(googleAdsResult || magicWandResult) && (
|
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
{error}
|
||||||
{copied ? (
|
{!apiKeys[selectedProvider] && (
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
|
||||||
|
className="h-9 lg:h-10 text-xs lg:text-sm bg-primary/90 hover:bg-primary shadow-sm"
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
|
||||||
|
{common.generating}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<>
|
||||||
|
<Megaphone className="mr-1.5 lg:mr-2 h-3.5 w-3.5" />
|
||||||
|
{t.generateAds}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
<Button
|
||||||
</CardTitle>
|
onClick={handleMagicWand}
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
disabled={isProcessing || isMagicThinking || !websiteUrl.trim()}
|
||||||
{magicWandResult
|
className="h-9 lg:h-10 text-xs lg:text-sm bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white shadow-md transition-all active:scale-[0.98]"
|
||||||
? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes")
|
>
|
||||||
: (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready")
|
{isMagicThinking ? (
|
||||||
}
|
<>
|
||||||
</CardDescription>
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 animate-spin" />
|
||||||
</CardHeader>
|
{t.researching}
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
</>
|
||||||
{googleAdsResult || magicWandResult ? (
|
) : (
|
||||||
<div className="space-y-2 lg:space-y-3">
|
<>
|
||||||
{(magicWandResult
|
<Wand2 className="mr-1.5 h-3.5 w-3.5" />
|
||||||
? [
|
{t.magicWand}
|
||||||
{ id: "market", title: t.marketIntelligence },
|
</>
|
||||||
{ id: "competitors", title: t.competitiveInsights },
|
)}
|
||||||
{ id: "strategies", title: t.campaignDirections }
|
</Button>
|
||||||
]
|
</div>
|
||||||
: sections
|
</CardContent>
|
||||||
).map((section) => (
|
</Card>
|
||||||
<div key={section.id} className="rounded-md border bg-muted/30">
|
|
||||||
<button
|
<Card className={cn(!googleAdsResult && "opacity-50")}>
|
||||||
onClick={() => toggleSection(section.id)}
|
<CardHeader className="p-4 lg:p-6">
|
||||||
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
>
|
<span className="flex items-center gap-2">
|
||||||
<span>{section.title}</span>
|
{magicWandResult ? (
|
||||||
{expandedSections.includes(section.id) ? (
|
<Wand2 className="h-4 w-4 lg:h-5 lg:w-5 text-indigo-500" />
|
||||||
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
) : (
|
||||||
) : (
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
)}
|
||||||
)}
|
{magicWandResult ? t.strategicDirections : t.generatedCampaign}
|
||||||
</button>
|
</span>
|
||||||
{expandedSections.includes(section.id) && (
|
{(googleAdsResult || magicWandResult) && (
|
||||||
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3 animate-in fade-in slide-in-from-top-1 duration-200">
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
{magicWandResult
|
{copied ? (
|
||||||
? renderMagicWandSectionContent(section.id)
|
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||||
: renderSectionContent(section.id)
|
) : (
|
||||||
}
|
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</Button>
|
||||||
))}
|
)}
|
||||||
</div>
|
</CardTitle>
|
||||||
) : (
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
{magicWandResult
|
||||||
{language === "ru" ? "Здесь появится созданная кампания" : language === "he" ? "קמפיין שחולל יופיע כאן" : "Generated campaign will appear here"}
|
? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes")
|
||||||
</div>
|
: (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready")
|
||||||
)}
|
}
|
||||||
</CardContent>
|
</CardDescription>
|
||||||
</Card>
|
</CardHeader>
|
||||||
</div>
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
);
|
{googleAdsResult || magicWandResult ? (
|
||||||
|
<div className="space-y-2 lg:space-y-3">
|
||||||
|
{(magicWandResult
|
||||||
|
? [
|
||||||
|
{ id: "market", title: t.marketIntelligence },
|
||||||
|
{ id: "competitors", title: t.competitiveInsights },
|
||||||
|
{ id: "strategies", title: t.campaignDirections }
|
||||||
|
]
|
||||||
|
: sections
|
||||||
|
).map((section) => (
|
||||||
|
<div key={section.id} className="rounded-md border bg-muted/30">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection(section.id)}
|
||||||
|
className="flex w-full items-center justify-between px-3 lg:px-4 py-2.5 lg:py-3 text-left font-medium transition-colors hover:bg-muted/50 text-xs lg:text-sm"
|
||||||
|
>
|
||||||
|
<span>{section.title}</span>
|
||||||
|
{expandedSections.includes(section.id) ? (
|
||||||
|
<ChevronUp className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{expandedSections.includes(section.id) && (
|
||||||
|
<div className="border-t bg-background px-3 lg:px-4 py-2.5 lg:py-3 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||||
|
{magicWandResult
|
||||||
|
? renderMagicWandSectionContent(section.id)
|
||||||
|
: renderSectionContent(section.id)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||||
|
{language === "ru" ? "Здесь появится созданная кампания" : language === "he" ? "קמפיין שחולל יופיע כאן" : "Generated campaign will appear here"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ import { cn } from "@/lib/utils";
|
|||||||
import { translations } from "@/lib/i18n/translations";
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function PromptEnhancer() {
|
export default function PromptEnhancer() {
|
||||||
language,
|
const {
|
||||||
|
language,
|
||||||
|
currentPrompt,
|
||||||
|
enhancedPrompt,
|
||||||
|
selectedProvider,
|
||||||
|
selectedModels,
|
||||||
|
availableModels,
|
||||||
|
apiKeys,
|
||||||
|
isProcessing,
|
||||||
|
error,
|
||||||
setSelectedProvider,
|
setSelectedProvider,
|
||||||
setCurrentPrompt,
|
setCurrentPrompt,
|
||||||
setEnhancedPrompt,
|
setEnhancedPrompt,
|
||||||
@@ -21,222 +30,222 @@ export default function PromptEnhancer() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
const t = translations[language].promptEnhancer;
|
const t = translations[language].promptEnhancer;
|
||||||
const common = translations[language].common;
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
loadAvailableModels();
|
loadAvailableModels();
|
||||||
const saved = localStorage.getItem("promptarch-api-keys");
|
const saved = localStorage.getItem("promptarch-api-keys");
|
||||||
if (saved) {
|
if (saved) {
|
||||||
try {
|
try {
|
||||||
const keys = JSON.parse(saved);
|
const keys = JSON.parse(saved);
|
||||||
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen);
|
||||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load API keys:", e);
|
console.error("Failed to load API keys:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [selectedProvider]);
|
||||||
}, [selectedProvider]);
|
|
||||||
|
|
||||||
const loadAvailableModels = async () => {
|
const loadAvailableModels = async () => {
|
||||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||||
setAvailableModels(selectedProvider, fallbackModels);
|
setAvailableModels(selectedProvider, fallbackModels);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.listModels(selectedProvider);
|
const result = await modelAdapter.listModels(selectedProvider);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load models:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error("Failed to load models:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnhance = async () => {
|
const handleEnhance = async () => {
|
||||||
if (!currentPrompt.trim()) {
|
if (!currentPrompt.trim()) {
|
||||||
setError("Please enter a prompt to enhance");
|
setError("Please enter a prompt to enhance");
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = apiKeys[selectedProvider];
|
|
||||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
|
||||||
|
|
||||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
|
||||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProcessing(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
|
|
||||||
|
|
||||||
console.log("[PromptEnhancer] Enhancement result:", result);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setEnhancedPrompt(result.data);
|
|
||||||
} else {
|
|
||||||
console.error("[PromptEnhancer] Enhancement failed:", result.error);
|
|
||||||
setError(result.error || "Failed to enhance prompt");
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("[PromptEnhancer] Enhancement error:", err);
|
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const apiKey = apiKeys[selectedProvider];
|
||||||
if (enhancedPrompt) {
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
await navigator.clipboard.writeText(enhancedPrompt);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClear = () => {
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
setCurrentPrompt("");
|
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||||
setEnhancedPrompt(null);
|
return;
|
||||||
setError(null);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
setProcessing(true);
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
setError(null);
|
||||||
<Card className="h-fit">
|
|
||||||
<CardHeader className="p-4 lg:p-6 text-start">
|
console.log("[PromptEnhancer] Starting enhancement...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
|
||||||
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
try {
|
||||||
{t.title}
|
const result = await modelAdapter.enhancePrompt(currentPrompt, selectedProvider, selectedModel);
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
console.log("[PromptEnhancer] Enhancement result:", result);
|
||||||
{t.description}
|
|
||||||
</CardDescription>
|
if (result.success && result.data) {
|
||||||
</CardHeader>
|
setEnhancedPrompt(result.data);
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
} else {
|
||||||
<div className="space-y-2 text-start">
|
console.error("[PromptEnhancer] Enhancement failed:", result.error);
|
||||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
setError(result.error || "Failed to enhance prompt");
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
}
|
||||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
} catch (err) {
|
||||||
<Button
|
console.error("[PromptEnhancer] Enhancement error:", err);
|
||||||
key={provider}
|
setError(err instanceof Error ? err.message : "An error occurred");
|
||||||
variant={selectedProvider === provider ? "default" : "outline"}
|
} finally {
|
||||||
size="sm"
|
setProcessing(false);
|
||||||
onClick={() => setSelectedProvider(provider)}
|
}
|
||||||
className={cn(
|
};
|
||||||
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
|
||||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
const handleCopy = async () => {
|
||||||
)}
|
if (enhancedPrompt) {
|
||||||
>
|
await navigator.clipboard.writeText(enhancedPrompt);
|
||||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
setCopied(true);
|
||||||
</Button>
|
setTimeout(() => setCopied(false), 2000);
|
||||||
))}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setCurrentPrompt("");
|
||||||
|
setEnhancedPrompt(null);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
|
<Card className="h-fit">
|
||||||
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
|
<Sparkles className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
|
{t.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{t.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
|
<div className="space-y-2 text-start">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
|
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||||
|
<Button
|
||||||
|
key={provider}
|
||||||
|
variant={selectedProvider === provider ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedProvider(provider)}
|
||||||
|
className={cn(
|
||||||
|
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
||||||
|
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-start">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<option key={model} value={model}>
|
<option key={model} value={model}>
|
||||||
{model}
|
{model}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-start">
|
|
||||||
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
|
||||||
<Textarea
|
|
||||||
placeholder={t.placeholder}
|
|
||||||
value={currentPrompt}
|
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
|
||||||
{error}
|
|
||||||
{!apiKeys[selectedProvider] && (
|
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
|
||||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="space-y-2 text-start">
|
||||||
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||||
{isProcessing ? (
|
<Textarea
|
||||||
<>
|
placeholder={t.placeholder}
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
value={currentPrompt}
|
||||||
{common.generating}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
</>
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
) : (
|
/>
|
||||||
<>
|
</div>
|
||||||
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
|
||||||
{t.title}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
|
||||||
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
|
||||||
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}>
|
{error && (
|
||||||
<CardHeader className="p-4 lg:p-6 text-start">
|
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
{error}
|
||||||
<span className="flex items-center gap-2">
|
{!apiKeys[selectedProvider] && (
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
{t.resultTitle}
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
{enhancedPrompt && (
|
</div>
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
)}
|
||||||
{copied ? (
|
</div>
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleEnhance} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
|
{common.generating}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<>
|
||||||
|
<Sparkles className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
{t.title}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||||
|
<RefreshCw className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className={cn("flex flex-col", !enhancedPrompt && "opacity-50")}>
|
||||||
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
|
{t.enhancedTitle}
|
||||||
|
</span>
|
||||||
|
{enhancedPrompt && (
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
|
{copied ? (
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{language === "ru" ? "Профессиональный промпт, готовый для кодинг-агентов" : language === "he" ? "פרומפט מקצועי מוכן לסוכני קידוד" : "Professional prompt ready for coding agents"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
|
{enhancedPrompt ? (
|
||||||
|
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{enhancedPrompt}</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
||||||
|
{language === "ru" ? "Улучшенный промпт появится здесь" : language === "he" ? "פרומפט משופר יופיע כאן" : "Enhanced prompt will appear here"}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardContent>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
</Card>
|
||||||
{language === "ru" ? "Профессиональный промпт, готовый для кодинг-агентов" : language === "he" ? "פרומפט מקצועי מוכן לסוכני קידוד" : "Professional prompt ready for coding agents"}
|
</div>
|
||||||
</CardDescription>
|
);
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
|
||||||
{enhancedPrompt ? (
|
|
||||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{enhancedPrompt}</pre>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-[150px] lg:h-[200px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground italic">
|
|
||||||
{language === "ru" ? "Улучшенный промпт появится здесь" : language === "he" ? "פרומפט משופר יופיע כאן" : "Enhanced prompt will appear here"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,16 @@ import { cn } from "@/lib/utils";
|
|||||||
import { translations } from "@/lib/i18n/translations";
|
import { translations } from "@/lib/i18n/translations";
|
||||||
|
|
||||||
export default function UXDesignerPrompt() {
|
export default function UXDesignerPrompt() {
|
||||||
language,
|
const {
|
||||||
|
language,
|
||||||
|
currentPrompt,
|
||||||
|
enhancedPrompt,
|
||||||
|
selectedProvider,
|
||||||
|
selectedModels,
|
||||||
|
availableModels,
|
||||||
|
apiKeys,
|
||||||
|
isProcessing,
|
||||||
|
error,
|
||||||
setSelectedProvider,
|
setSelectedProvider,
|
||||||
setCurrentPrompt,
|
setCurrentPrompt,
|
||||||
setEnhancedPrompt,
|
setEnhancedPrompt,
|
||||||
@@ -21,229 +30,229 @@ export default function UXDesignerPrompt() {
|
|||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
const t = translations[language].uxDesigner;
|
const t = translations[language].uxDesigner;
|
||||||
const common = translations[language].common;
|
const common = translations[language].common;
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
const [generatedPrompt, setGeneratedPrompt] = useState<string | null>(null);
|
||||||
|
|
||||||
const selectedModel = selectedModels[selectedProvider];
|
const selectedModel = selectedModels[selectedProvider];
|
||||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
loadAvailableModels();
|
loadAvailableModels();
|
||||||
const saved = localStorage.getItem("promptarch-api-keys");
|
const saved = localStorage.getItem("promptarch-api-keys");
|
||||||
if (saved) {
|
if (saved) {
|
||||||
try {
|
try {
|
||||||
const keys = JSON.parse(saved);
|
const keys = JSON.parse(saved);
|
||||||
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama);
|
||||||
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load API keys:", e);
|
console.error("Failed to load API keys:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [selectedProvider]);
|
||||||
}, [selectedProvider]);
|
|
||||||
|
|
||||||
const loadAvailableModels = async () => {
|
const loadAvailableModels = async () => {
|
||||||
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
const fallbackModels = modelAdapter.getAvailableModels(selectedProvider);
|
||||||
setAvailableModels(selectedProvider, fallbackModels);
|
setAvailableModels(selectedProvider, fallbackModels);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelAdapter.listModels(selectedProvider);
|
const result = await modelAdapter.listModels(selectedProvider);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load models:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error("Failed to load models:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!currentPrompt.trim()) {
|
if (!currentPrompt.trim()) {
|
||||||
setError("Please enter an app description");
|
setError("Please enter an app description");
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = apiKeys[selectedProvider];
|
|
||||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
|
||||||
|
|
||||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
|
||||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProcessing(true);
|
|
||||||
setError(null);
|
|
||||||
setGeneratedPrompt(null);
|
|
||||||
|
|
||||||
console.log("[UXDesignerPrompt] Starting generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel);
|
|
||||||
|
|
||||||
console.log("[UXDesignerPrompt] Generation result:", result);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setGeneratedPrompt(result.data);
|
|
||||||
setEnhancedPrompt(result.data);
|
|
||||||
} else {
|
|
||||||
console.error("[UXDesignerPrompt] Generation failed:", result.error);
|
|
||||||
setError(result.error || "Failed to generate UX designer prompt");
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("[UXDesignerPrompt] Generation error:", err);
|
|
||||||
setError(err instanceof Error ? err.message : "An error occurred");
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const apiKey = apiKeys[selectedProvider];
|
||||||
if (generatedPrompt) {
|
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||||
await navigator.clipboard.writeText(generatedPrompt);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClear = () => {
|
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||||
setCurrentPrompt("");
|
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||||
setGeneratedPrompt(null);
|
return;
|
||||||
setEnhancedPrompt(null);
|
}
|
||||||
setError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
setProcessing(true);
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
setError(null);
|
||||||
<Card className="h-fit">
|
setGeneratedPrompt(null);
|
||||||
<CardHeader className="p-4 lg:p-6 text-start">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
console.log("[UXDesignerPrompt] Starting generation...", { selectedProvider, selectedModel, hasQwenAuth: modelAdapter.hasQwenAuth() });
|
||||||
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
|
||||||
{t.title}
|
try {
|
||||||
</CardTitle>
|
const result = await modelAdapter.generateUXDesignerPrompt(currentPrompt, selectedProvider, selectedModel);
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
|
||||||
{t.description}
|
console.log("[UXDesignerPrompt] Generation result:", result);
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
if (result.success && result.data) {
|
||||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
setGeneratedPrompt(result.data);
|
||||||
<div className="space-y-2 text-start">
|
setEnhancedPrompt(result.data);
|
||||||
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
} else {
|
||||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
console.error("[UXDesignerPrompt] Generation failed:", result.error);
|
||||||
{(["ollama", "zai"] as const).map((provider) => (
|
setError(result.error || "Failed to generate UX designer prompt");
|
||||||
<Button
|
}
|
||||||
key={provider}
|
} catch (err) {
|
||||||
variant={selectedProvider === provider ? "default" : "outline"}
|
console.error("[UXDesignerPrompt] Generation error:", err);
|
||||||
size="sm"
|
setError(err instanceof Error ? err.message : "An error occurred");
|
||||||
onClick={() => setSelectedProvider(provider)}
|
} finally {
|
||||||
className={cn(
|
setProcessing(false);
|
||||||
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
}
|
||||||
selectedProvider === provider && "bg-primary text-primary-foreground"
|
};
|
||||||
)}
|
|
||||||
>
|
const handleCopy = async () => {
|
||||||
{provider === "ollama" ? "Ollama" : "Z.AI"}
|
if (generatedPrompt) {
|
||||||
</Button>
|
await navigator.clipboard.writeText(generatedPrompt);
|
||||||
))}
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setCurrentPrompt("");
|
||||||
|
setGeneratedPrompt(null);
|
||||||
|
setEnhancedPrompt(null);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 lg:grid-cols-2 text-start">
|
||||||
|
<Card className="h-fit">
|
||||||
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||||
|
<Palette className="h-4 w-4 lg:h-5 lg:w-5" />
|
||||||
|
{t.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{t.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
|
<div className="space-y-2 text-start">
|
||||||
|
<label className="text-xs lg:text-sm font-medium">{common.aiProvider}</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||||
|
{(["ollama", "zai"] as const).map((provider) => (
|
||||||
|
<Button
|
||||||
|
key={provider}
|
||||||
|
variant={selectedProvider === provider ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedProvider(provider)}
|
||||||
|
className={cn(
|
||||||
|
"capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3",
|
||||||
|
selectedProvider === provider && "bg-primary text-primary-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-start">
|
<div className="space-y-2 text-start">
|
||||||
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
<label className="text-xs lg:text-sm font-medium">{common.model}</label>
|
||||||
<select
|
<select
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<option key={model} value={model}>
|
<option key={model} value={model}>
|
||||||
{model}
|
{model}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-start">
|
|
||||||
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
|
||||||
<Textarea
|
|
||||||
placeholder={t.placeholder}
|
|
||||||
value={currentPrompt}
|
|
||||||
onChange={(e) => setCurrentPrompt(e.target.value)}
|
|
||||||
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
|
||||||
/>
|
|
||||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
|
||||||
{t.inputDesc}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
|
||||||
{error}
|
|
||||||
{!apiKeys[selectedProvider] && (
|
|
||||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
|
||||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
|
||||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="space-y-2 text-start">
|
||||||
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||||
{isProcessing ? (
|
<Textarea
|
||||||
<>
|
placeholder={t.placeholder}
|
||||||
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
value={currentPrompt}
|
||||||
{common.generating}
|
onChange={(e) => setCurrentPrompt(e.target.value)}
|
||||||
</>
|
className="min-h-[150px] lg:min-h-[200px] resize-y text-sm lg:text-base p-3 lg:p-4"
|
||||||
) : (
|
/>
|
||||||
<>
|
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||||
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
{t.inputDesc}
|
||||||
{language === "ru" ? "Создать UX Промпт" : language === "he" ? "חולל פרומפט UX" : "Generate UX Prompt"}
|
</p>
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
|
||||||
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
|
|
||||||
<span className="sm:hidden">×</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}>
|
{error && (
|
||||||
<CardHeader className="p-4 lg:p-6 text-start">
|
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
{error}
|
||||||
<span className="flex items-center gap-2">
|
{!apiKeys[selectedProvider] && (
|
||||||
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||||
<span className="hidden sm:inline">{t.resultTitle}</span>
|
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
<span className="sm:hidden">{language === "ru" ? "UX Промпт" : language === "he" ? "פרומפט UX" : "UX Prompt"}</span>
|
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||||
</span>
|
</div>
|
||||||
{generatedPrompt && (
|
)}
|
||||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
</div>
|
||||||
{copied ? (
|
)}
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleGenerate} disabled={isProcessing || !currentPrompt.trim()} className="flex-1 h-9 lg:h-10 text-xs lg:text-sm">
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4 animate-spin" />
|
||||||
|
{common.generating}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
<>
|
||||||
|
<Palette className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
{language === "ru" ? "Создать UX Промпт" : language === "he" ? "חולל פרומפט UX" : "Generate UX Prompt"}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleClear} disabled={isProcessing} className="h-9 lg:h-10 text-xs lg:text-sm px-3">
|
||||||
|
<span className="hidden sm:inline">{language === "ru" ? "Очистить" : language === "he" ? "נקה" : "Clear"}</span>
|
||||||
|
<span className="sm:hidden">×</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className={cn("flex flex-col", !generatedPrompt && "opacity-50")}>
|
||||||
|
<CardHeader className="p-4 lg:p-6 text-start">
|
||||||
|
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||||
|
<span className="hidden sm:inline">{t.resultTitle}</span>
|
||||||
|
<span className="sm:hidden">{language === "ru" ? "UX Промпт" : language === "he" ? "פרומפט UX" : "UX Prompt"}</span>
|
||||||
|
</span>
|
||||||
|
{generatedPrompt && (
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||||
|
{copied ? (
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs lg:text-sm">
|
||||||
|
{t.resultDesc}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||||
|
{generatedPrompt ? (
|
||||||
|
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
|
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{generatedPrompt}</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4 italic">
|
||||||
|
{t.emptyState}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardContent>
|
||||||
<CardDescription className="text-xs lg:text-sm">
|
</Card>
|
||||||
{t.resultDesc}
|
</div>
|
||||||
</CardDescription>
|
);
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
|
||||||
{generatedPrompt ? (
|
|
||||||
<div className="rounded-md border bg-muted/50 p-3 lg:p-4 max-h-[350px] lg:max-h-[400px] overflow-y-auto animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
||||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{generatedPrompt}</pre>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-[250px] lg:h-[400px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground px-4 italic">
|
|
||||||
{t.emptyState}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const translations = {
|
|||||||
title: "Prompt Enhancer",
|
title: "Prompt Enhancer",
|
||||||
description: "Transform your simple ideas into professional, high-quality prompts",
|
description: "Transform your simple ideas into professional, high-quality prompts",
|
||||||
placeholder: "Enter your prompt here...",
|
placeholder: "Enter your prompt here...",
|
||||||
|
inputLabel: "Your Prompt",
|
||||||
enhancedTitle: "Enhanced Prompt",
|
enhancedTitle: "Enhanced Prompt",
|
||||||
enhancedDesc: "Your prompt has been optimized for better AI performance",
|
enhancedDesc: "Your prompt has been optimized for better AI performance",
|
||||||
},
|
},
|
||||||
@@ -150,6 +151,7 @@ export const translations = {
|
|||||||
title: "Улучшение промптов",
|
title: "Улучшение промптов",
|
||||||
description: "Превратите ваши простые идеи в профессиональные, качественные промпты",
|
description: "Превратите ваши простые идеи в профессиональные, качественные промпты",
|
||||||
placeholder: "Введите ваш промпт здесь...",
|
placeholder: "Введите ваш промпт здесь...",
|
||||||
|
inputLabel: "Ваш промпт",
|
||||||
enhancedTitle: "Улучшенный промпт",
|
enhancedTitle: "Улучшенный промпт",
|
||||||
enhancedDesc: "Ваш промпт оптимизирован для лучшей работы ИИ",
|
enhancedDesc: "Ваш промпт оптимизирован для лучшей работы ИИ",
|
||||||
},
|
},
|
||||||
@@ -271,6 +273,7 @@ export const translations = {
|
|||||||
title: "משפר פרומפטים",
|
title: "משפר פרומפטים",
|
||||||
description: "הפוך רעיונות פשוטים לפרומפטים מקצועיים באיכות גבוהה",
|
description: "הפוך רעיונות פשוטים לפרומפטים מקצועיים באיכות גבוהה",
|
||||||
placeholder: "הזן את הפרומפט שלך כאן...",
|
placeholder: "הזן את הפרומפט שלך כאן...",
|
||||||
|
inputLabel: "הפרומפט שלך",
|
||||||
enhancedTitle: "פרומפט משופר",
|
enhancedTitle: "פרומפט משופר",
|
||||||
enhancedDesc: "הפרומפט שלך הותאם לביצועי בינה מלאכותית טובים יותר",
|
enhancedDesc: "הפרומפט שלך הותאם לביצועי בינה מלאכותית טובים יותר",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user