feat: complete translations for russian and hebrew across all components
This commit is contained in:
@@ -51,13 +51,13 @@ class CanvasErrorBoundary extends React.Component<{ children: React.ReactNode },
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center bg-[#0b1414] p-8 text-center rounded-b-2xl">
|
||||
<StopCircle className="h-10 w-10 text-red-500/40 mb-4" />
|
||||
<h4 className="text-xs font-black uppercase tracking-widest text-red-400 mb-2">Canvas Crashed</h4>
|
||||
<h4 className="text-xs font-black uppercase tracking-widest text-red-400 mb-2">{t.canvasCrashed}</h4>
|
||||
<p className="text-[10px] font-mono text-slate-500 max-w-xs">{this.state.error}</p>
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: null })}
|
||||
className="mt-4 px-3 py-1.5 text-[9px] font-black uppercase tracking-widest text-blue-400 border border-blue-500/30 rounded-xl hover:bg-blue-500/10 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
{t.tryAgain}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -67,13 +67,7 @@ class CanvasErrorBoundary extends React.Component<{ children: React.ReactNode },
|
||||
}
|
||||
const BuildingArtifact = ({ type }: { type: string }) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const steps = [
|
||||
"Initializing neural links...",
|
||||
"Scaffolding architecture...",
|
||||
"Writing logic blocks...",
|
||||
"Injecting dynamic modules...",
|
||||
"Finalizing interactive layers..."
|
||||
];
|
||||
const steps = t.thinkingSteps;
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,7 +87,7 @@ const BuildingArtifact = ({ type }: { type: string }) => {
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-black uppercase tracking-[0.3em] mb-4 text-white drop-shadow-lg">
|
||||
Building <span className="text-blue-500">{type}</span>
|
||||
{t.building} <span className="text-blue-500">{type}</span>
|
||||
</h3>
|
||||
|
||||
<div className="w-full max-w-sm h-1.5 bg-slate-800/50 rounded-full overflow-hidden mb-10 backdrop-blur-sm border border-white/5">
|
||||
@@ -267,7 +261,7 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri
|
||||
{renderError ? (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center p-12 text-center animate-in zoom-in-95 duration-300">
|
||||
<StopCircle className="h-10 w-10 text-red-500/40 mb-5" />
|
||||
<h4 className="text-xs font-black uppercase tracking-[0.2em] text-red-400 mb-3">Runtime Execution Error</h4>
|
||||
<h4 className="text-xs font-black uppercase tracking-[0.2em] text-red-400 mb-3">{t.runtimeError}</h4>
|
||||
<p className="text-[9px] font-mono text-slate-500 max-w-sm border border-red-500/10 bg-red-500/5 p-4 rounded-xl leading-relaxed">
|
||||
{renderError}
|
||||
</p>
|
||||
@@ -277,7 +271,7 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri
|
||||
className="mt-6 text-[9px] font-black uppercase tracking-widest text-slate-400 hover:text-white"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Try Refreshing Page
|
||||
{t.tryRefreshing}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -307,7 +301,7 @@ const ThinkingIndicator = () => (
|
||||
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.15s]" />
|
||||
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-blue-700/60 dark:text-blue-200/60 uppercase tracking-widest ml-2">Neural Link Thinking...</span>
|
||||
<span className="text-[10px] font-black text-blue-700/60 dark:text-blue-200/60 uppercase tracking-widest ml-2">{t.neuralLinkThinking}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -345,7 +339,7 @@ function parseStreamingContent(text: string, currentAgent: string) {
|
||||
};
|
||||
if (preview.isStreaming) {
|
||||
const isUpdate = text.toLowerCase().includes("update") || text.toLowerCase().includes("fix") || text.toLowerCase().includes("change");
|
||||
status = isUpdate ? `Applying surgical edits to ${preview.type}...` : `Generating ${preview.type} artifact...`;
|
||||
status = isUpdate ? t.applyingEdits(preview.type) : t.generatingArtifact(preview.type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +406,7 @@ function parseStreamingContent(text: string, currentAgent: string) {
|
||||
}
|
||||
|
||||
if (!chatDisplay && preview && preview.isStreaming) {
|
||||
chatDisplay = `Rendering live artifact...`;
|
||||
chatDisplay = t.renderingLive;
|
||||
}
|
||||
|
||||
return { chatDisplay, preview, agent, status };
|
||||
@@ -436,9 +430,9 @@ export default function AIAssist() {
|
||||
} = useStore();
|
||||
const t = translations[language].aiAssist;
|
||||
|
||||
const activeTab = aiAssistTabs?.find(t => t.id === activeTabId) || aiAssistTabs?.[0] || {
|
||||
const activeTab = aiAssistTabs?.find(tab => tab.id === activeTabId) || aiAssistTabs?.[0] || {
|
||||
id: 'default',
|
||||
title: 'New Chat',
|
||||
title: t.newChat,
|
||||
history: [],
|
||||
currentAgent: 'general',
|
||||
previewData: null,
|
||||
@@ -704,7 +698,7 @@ export default function AIAssist() {
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-slate-900 dark:text-blue-50 tracking-tight">{t.title}</h2>
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.25em] text-blue-700/70 dark:text-blue-200/70">
|
||||
Agent {currentAgent}
|
||||
{t.agentLabel} {t.agents[currentAgent as keyof typeof t.agents] || currentAgent}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -751,7 +745,13 @@ export default function AIAssist() {
|
||||
)}
|
||||
>
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
<span className="text-[10px] font-black uppercase tracking-wider">{tab.title}</span>
|
||||
<span className="text-[10px] font-black uppercase tracking-wider">
|
||||
{tab.title === "New Chat"
|
||||
? t.chatTitle
|
||||
: tab.title.startsWith("Chat ")
|
||||
? `${t.chatPrefix} ${tab.title.split(" ")[1]}`
|
||||
: tab.title}
|
||||
</span>
|
||||
{aiAssistTabs.length > 1 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -768,7 +768,7 @@ export default function AIAssist() {
|
||||
<button
|
||||
onClick={() => addAIAssistTab()}
|
||||
className="p-1.5 rounded-xl hover:bg-white/10 text-slate-400 hover:text-white transition-all ml-1"
|
||||
title="New Chat"
|
||||
title={t.newChat}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -778,12 +778,12 @@ export default function AIAssist() {
|
||||
<div className="px-6 pt-6">
|
||||
<div className="flex flex-wrap gap-2 pb-4">
|
||||
{[
|
||||
{ label: "General", agent: "general", icon: <Orbit className="h-3.5 w-3.5" /> },
|
||||
{ label: "Code", agent: "code", icon: <Code2 className="h-3.5 w-3.5" /> },
|
||||
{ label: "Design", agent: "design", icon: <Palette className="h-3.5 w-3.5" /> },
|
||||
{ label: "SEO", agent: "seo", icon: <Search className="h-3.5 w-3.5" /> },
|
||||
{ label: "Web", agent: "web", icon: <LayoutPanelLeft className="h-3.5 w-3.5" /> },
|
||||
{ label: "App", agent: "app", icon: <Play className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.general, agent: "general", icon: <Orbit className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.code, agent: "code", icon: <Code2 className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.design, agent: "design", icon: <Palette className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.seo, agent: "seo", icon: <Search className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.web, agent: "web", icon: <LayoutPanelLeft className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.app, agent: "app", icon: <Play className="h-3.5 w-3.5" /> },
|
||||
].map(({ label, agent, icon }) => (
|
||||
<button
|
||||
key={agent}
|
||||
@@ -811,16 +811,12 @@ export default function AIAssist() {
|
||||
<Ghost className="h-20 w-20 text-blue-400/40 animate-bounce duration-[3s]" />
|
||||
<div className="absolute inset-0 bg-blue-500/10 blur-3xl rounded-full" />
|
||||
</div>
|
||||
<h3 className="text-3xl font-black text-slate-900 dark:text-blue-50 mb-3 tracking-tighter">Studio-grade AI Assist</h3>
|
||||
<h3 className="text-3xl font-black text-slate-900 dark:text-blue-50 mb-3 tracking-tighter">{t.studioTitle}</h3>
|
||||
<p className="max-w-xs text-sm font-medium text-slate-600 dark:text-blue-100/70 leading-relaxed">
|
||||
Switch agents, stream answers, and light up the canvas with live artifacts.
|
||||
{t.studioDesc}
|
||||
</p>
|
||||
<div className="mt-10 flex flex-wrap justify-center gap-3">
|
||||
{[
|
||||
{ label: "Build a landing UI", agent: "web" },
|
||||
{ label: "SEO diagnostic", agent: "seo" },
|
||||
{ label: "Mobile onboarding", agent: "app" },
|
||||
].map((chip) => (
|
||||
{t.suggestions.map((chip: any) => (
|
||||
<Badge
|
||||
key={chip.label}
|
||||
variant="secondary"
|
||||
@@ -864,25 +860,25 @@ export default function AIAssist() {
|
||||
{msg.role === "assistant" && aiPlan && i === aiAssistHistory.length - 1 && assistStep === "plan" && (
|
||||
<div className="mt-6 p-6 rounded-2xl bg-blue-500/5 border border-blue-500/20 backdrop-blur-sm animate-in zoom-in-95 duration-300">
|
||||
<h3 className="text-sm font-black text-blue-400 uppercase tracking-widest mb-4 flex items-center gap-2">
|
||||
<LayoutPanelLeft className="h-4 w-4" /> Proposed Solution Plan
|
||||
<LayoutPanelLeft className="h-4 w-4" /> {t.proposedPlan}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Architecture</p>
|
||||
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">{t.architecture}</p>
|
||||
<p className="text-xs text-slate-400">{aiPlan.architecture}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Tech Stack</p>
|
||||
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">{t.techStack}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{aiPlan.techStack?.map((t: string) => (
|
||||
<Badge key={t} variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t}</Badge>
|
||||
{aiPlan.techStack?.map((t_stack: string) => (
|
||||
<Badge key={t_stack} variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t_stack}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Files</p>
|
||||
<p className="text-[10px] text-slate-400">{aiPlan.files?.length} modules planned</p>
|
||||
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">{t.files}</p>
|
||||
<p className="text-[10px] text-slate-400">{t.filesPlanned(aiPlan.files?.length || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
@@ -890,7 +886,7 @@ export default function AIAssist() {
|
||||
disabled={isProcessing}
|
||||
className="w-full mt-4 bg-blue-600 hover:bg-blue-500 text-white font-black uppercase text-[10px] tracking-widest py-5 rounded-xl shadow-lg shadow-blue-500/20"
|
||||
>
|
||||
{isProcessing ? "Starting Engine..." : "Approve & Generate Development"}
|
||||
{isProcessing ? t.startingEngine : t.approveGenerate}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -908,7 +904,7 @@ export default function AIAssist() {
|
||||
setShowCanvas(true);
|
||||
}}
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5 mr-2" /> Activate Artifact
|
||||
<Zap className="h-3.5 w-3.5 mr-2" /> {t.activateArtifact}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -927,7 +923,7 @@ export default function AIAssist() {
|
||||
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">
|
||||
{msg.role === "assistant" ? `Agent ${msg.agent || 'core'}` : 'Explorer'}
|
||||
{msg.role === "assistant" ? `${t.agentLabel} ${t.agents[msg.agent as keyof typeof t.agents] || msg.agent || t.coreAgent}` : t.userLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -975,11 +971,11 @@ export default function AIAssist() {
|
||||
<div className="flex items-center justify-between mt-4 text-[11px] font-semibold text-blue-700/70 dark:text-blue-100/70">
|
||||
<span className="flex items-center gap-2">
|
||||
<Wand2 className="h-3.5 w-3.5" />
|
||||
Ask for a design, code, or research artifact.
|
||||
{t.askArtifact}
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<LayoutPanelLeft className="h-3.5 w-3.5" />
|
||||
Canvas {previewData ? "ready" : "idle"}
|
||||
{t.canvasLabel} {previewData ? t.canvasReady : t.canvasIdle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -997,19 +993,19 @@ export default function AIAssist() {
|
||||
{viewMode === "preview" ? <Monitor className="h-5 w-5 text-blue-400" /> : <Code2 className="h-5 w-5 text-amber-300" />}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xs font-black text-blue-50 uppercase tracking-[0.2em]">{currentPreviewData?.type || "Live"} Canvas</h3>
|
||||
<h3 className="text-xs font-black text-blue-50 uppercase tracking-[0.2em]">{t.canvasTitle(currentPreviewData?.type || t.live)}</h3>
|
||||
<div className="flex bg-blue-900/60 rounded-xl p-1 mt-2">
|
||||
<button
|
||||
onClick={() => setViewMode("preview")}
|
||||
className={cn("px-4 py-1.5 text-[10px] uppercase font-black rounded-lg transition-all", viewMode === "preview" ? "bg-blue-500 text-white shadow-lg" : "text-blue-300/60 hover:text-blue-100")}
|
||||
>
|
||||
Live Render
|
||||
{t.liveRender}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("code")}
|
||||
className={cn("px-4 py-1.5 text-[10px] uppercase font-black rounded-lg transition-all", viewMode === "code" ? "bg-blue-500 text-white shadow-lg" : "text-blue-300/60 hover:text-blue-100")}
|
||||
>
|
||||
Inspect Code
|
||||
{t.inspectCode}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1059,7 +1055,7 @@ export default function AIAssist() {
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn("w-2 h-2 rounded-full", currentPreviewData?.isStreaming ? "bg-amber-500 animate-pulse" : "bg-blue-500")} />
|
||||
<span className="text-[10px] text-blue-200/60 font-bold uppercase tracking-widest leading-none">
|
||||
{currentPreviewData?.isStreaming ? "Neural Link Active" : "Sync Complete"}
|
||||
{currentPreviewData?.isStreaming ? t.neuralLinkActive : t.syncComplete}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[9px] border-blue-900 text-blue-200/50 font-black">
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function ActionPlanGenerator() {
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter PRD or project requirements");
|
||||
setError(t.enterPrdError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function ActionPlanGenerator() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,11 +112,11 @@ export default function ActionPlanGenerator() {
|
||||
setActionPlan(newPlan);
|
||||
} else {
|
||||
console.error("[ActionPlanGenerator] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate action plan");
|
||||
setError(result.error || t.errorGenerate);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[ActionPlanGenerator] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -176,7 +176,7 @@ export default function ActionPlanGenerator() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{language === "ru" ? "PRD / Требования" : language === "he" ? "PRD / דרישות" : "PRD / Requirements"}</label>
|
||||
<label className="text-xs lg:text-sm font-medium">{t.inputLabel}</label>
|
||||
<Textarea
|
||||
placeholder={t.placeholder}
|
||||
value={currentPrompt}
|
||||
@@ -206,7 +206,7 @@ export default function ActionPlanGenerator() {
|
||||
) : (
|
||||
<>
|
||||
<ListTodo className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
{language === "ru" ? "Создать план действий" : language === "he" ? "חולל תוכנית פעולה" : "Generate Action Plan"}
|
||||
{t.generateButton}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -231,7 +231,7 @@ export default function ActionPlanGenerator() {
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{language === "ru" ? "Разбивка задач, фреймворки и рекомендации по архитектуре" : language === "he" ? "פירוט משימות, פרימוורקים והמלצות ארכיטקטורה" : "Task breakdown, frameworks, and architecture recommendations"}
|
||||
{t.generatedDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
@@ -240,7 +240,7 @@ export default function ActionPlanGenerator() {
|
||||
<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"}
|
||||
{t.roadmap}
|
||||
</h4>
|
||||
<pre className="whitespace-pre-wrap text-xs lg:text-sm leading-relaxed">{actionPlan.rawContent}</pre>
|
||||
</div>
|
||||
@@ -248,13 +248,12 @@ export default function ActionPlanGenerator() {
|
||||
<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"}
|
||||
{t.quickNotes}
|
||||
</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>
|
||||
{t.notes.map((note: string, i: number) => (
|
||||
<li key={i}>{note}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,37 +56,7 @@ export default function GoogleAdsGenerator() {
|
||||
const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider);
|
||||
|
||||
// Fun progress messages
|
||||
const progressMessages = language === "ru" ? [
|
||||
"🔍 Изучаю ваш сайт...",
|
||||
"🧠 Анализирую конкурентов...",
|
||||
"💡 Генерирую гениальные идеи...",
|
||||
"📊 Исследую рыночные тренды...",
|
||||
"🎯 Определяю целевую аудиторию...",
|
||||
"✨ Создаю магию рекламы...",
|
||||
"🚀 Почти готово, потерпите...",
|
||||
"📝 Пишу убедительные тексты...",
|
||||
"🔥 Оптимизирую для конверсий..."
|
||||
] : language === "he" ? [
|
||||
"🔍 בודק את האתר שלך...",
|
||||
"🧠 מנתח מתחרים...",
|
||||
"💡 מייצר רעיונות גאוניים...",
|
||||
"📊 חוקר מגמות שוק...",
|
||||
"🎯 מזהה קהל יעד...",
|
||||
"✨ יוצר קסם פרסום...",
|
||||
"🚀 כמעט שם, רק רגע...",
|
||||
"📝 כותב טקסטים משכנעים...",
|
||||
"🔥 מייעל להמרות..."
|
||||
] : [
|
||||
"🔍 Studying your website...",
|
||||
"🧠 Analyzing competitors...",
|
||||
"💡 Generating brilliant ideas...",
|
||||
"📊 Researching market trends...",
|
||||
"🎯 Identifying target audience...",
|
||||
"✨ Creating advertising magic...",
|
||||
"🚀 Almost there, hang tight...",
|
||||
"📝 Writing persuasive copy...",
|
||||
"🔥 Optimizing for conversions..."
|
||||
];
|
||||
const progressMessages = t.progressMessages;
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSections((prev) =>
|
||||
@@ -159,12 +129,12 @@ export default function GoogleAdsGenerator() {
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!websiteUrl.trim()) {
|
||||
setError("Please enter a website URL");
|
||||
setError(t.errorWebsite);
|
||||
return;
|
||||
}
|
||||
const filteredProducts = products.filter(p => p.name.trim() !== "");
|
||||
if (filteredProducts.length === 0) {
|
||||
setError(language === "ru" ? "Добавьте хотя бы один продукт или услугу" : language === "he" ? "הוסף לפחות מוצר או שירות אחד" : "Please add at least one product or service");
|
||||
setError(t.errorProducts);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -172,7 +142,7 @@ export default function GoogleAdsGenerator() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -240,15 +210,15 @@ export default function GoogleAdsGenerator() {
|
||||
setExpandedSections(["keywords"]);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ads data:", e);
|
||||
setError("Failed to parse the generated ads content. Please try again.");
|
||||
setError(t.errorParse || "Failed to parse the generated ads content. Please try again.");
|
||||
}
|
||||
} else {
|
||||
console.error("[GoogleAdsGenerator] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate Google Ads campaign");
|
||||
setError(result.error || t.errorGenerate);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[GoogleAdsGenerator] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.error);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -256,12 +226,12 @@ export default function GoogleAdsGenerator() {
|
||||
|
||||
const handleMagicWand = async () => {
|
||||
if (!websiteUrl.trim()) {
|
||||
setError("Please enter a website URL");
|
||||
setError(t.errorWebsite);
|
||||
return;
|
||||
}
|
||||
const firstProduct = products.find(p => p.name.trim() !== "");
|
||||
if (!firstProduct) {
|
||||
setError(language === "ru" ? "Добавьте хотя бы один продукт" : language === "he" ? "הוסף לפחות מוצר אחד" : "Please add at least one product to promote");
|
||||
setError(t.errorProducts);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -269,7 +239,7 @@ export default function GoogleAdsGenerator() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -316,10 +286,10 @@ export default function GoogleAdsGenerator() {
|
||||
});
|
||||
setExpandedSections(["market", "strategies"]);
|
||||
} else {
|
||||
setError(result.error || "Magic Wand failed to research the market");
|
||||
setError(result.error || t.errorMagicWand);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "An error occurred during Magic Wand research");
|
||||
setError(err instanceof Error ? err.message : t.errorMagicWandGeneral);
|
||||
} finally {
|
||||
setIsMagicThinking(false);
|
||||
}
|
||||
@@ -335,10 +305,10 @@ export default function GoogleAdsGenerator() {
|
||||
};
|
||||
|
||||
const sections = [
|
||||
{ id: "keywords", title: language === "ru" ? "Исследование ключевых слов" : language === "he" ? "מחקר מילות מפתח" : "Keywords Research" },
|
||||
{ id: "adcopies", title: language === "ru" ? "Варианты объявлений" : language === "he" ? "גרסאות עותקי מודעות" : "Ad Copy Variations" },
|
||||
{ id: "campaigns", title: language === "ru" ? "Структура кампании" : language === "he" ? "מבנה קמפיין" : "Campaign Structure" },
|
||||
{ id: "implementation", title: language === "ru" ? "Руководство по внедрению" : language === "he" ? "מדריך יישום" : "Implementation Guide" },
|
||||
{ id: "keywords", title: t.keywordsResearch },
|
||||
{ id: "adcopies", title: t.adCopyVariations },
|
||||
{ id: "campaigns", title: t.campaignStructure },
|
||||
{ id: "implementation", title: t.implementationGuide },
|
||||
];
|
||||
|
||||
const renderSectionContent = (sectionId: string) => {
|
||||
@@ -351,7 +321,7 @@ export default function GoogleAdsGenerator() {
|
||||
{googleAdsResult.keywords?.primary?.length > 0 && (
|
||||
<div className="p-4 rounded-xl bg-indigo-50/30 border border-indigo-100/50 shadow-sm">
|
||||
<h4 className="text-[10px] font-black tracking-widest text-indigo-600 uppercase mb-3 flex items-center gap-2">
|
||||
<Target className="h-3 w-3" /> Primary High-Intent Keywords
|
||||
<Target className="h-3 w-3" /> {t.labels.primaryKeywords}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{googleAdsResult.keywords.primary.map((k, i) => (
|
||||
@@ -370,7 +340,7 @@ export default function GoogleAdsGenerator() {
|
||||
{googleAdsResult.keywords?.longTail?.length > 0 && (
|
||||
<div className="p-4 rounded-xl bg-emerald-50/30 border border-emerald-100/50 shadow-sm">
|
||||
<h4 className="text-[10px] font-black tracking-widest text-emerald-600 uppercase mb-3 flex items-center gap-2">
|
||||
<TrendingUp className="h-3 w-3" /> Long-Tail Opportunities
|
||||
<TrendingUp className="h-3 w-3" /> {t.labels.longTail}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{googleAdsResult.keywords.longTail.map((k, i) => (
|
||||
@@ -384,7 +354,7 @@ export default function GoogleAdsGenerator() {
|
||||
{googleAdsResult.keywords?.negative?.length > 0 && (
|
||||
<div className="p-4 rounded-xl bg-rose-50/30 border border-rose-100/50 shadow-sm">
|
||||
<h4 className="text-[10px] font-black tracking-widest text-rose-600 uppercase mb-3 flex items-center gap-2">
|
||||
<ShieldAlert className="h-3 w-3" /> Negative Keywords (Exclude)
|
||||
<ShieldAlert className="h-3 w-3" /> {t.labels.negative}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{googleAdsResult.keywords.negative.map((k, i) => (
|
||||
@@ -404,7 +374,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div key={i} className="relative group p-5 rounded-2xl border bg-white shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 h-full w-1.5 bg-indigo-500 rounded-l-2xl" />
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="text-[10px] font-black uppercase tracking-tighter text-indigo-500">Google Search Preview • Ad Variation {i + 1}</div>
|
||||
<div className="text-[10px] font-black uppercase tracking-tighter text-indigo-500">{t.labels.preview} • {t.labels.variation} {i + 1}</div>
|
||||
<div className="flex gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-slate-100 shadow-inner" />
|
||||
<span className="h-2 w-2 rounded-full bg-slate-100 shadow-inner" />
|
||||
@@ -439,20 +409,20 @@ export default function GoogleAdsGenerator() {
|
||||
</div>
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<div className="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-1">{camp.type} Strategy</div>
|
||||
<div className="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-1">{camp.type} {t.labels.strategy}</div>
|
||||
<h4 className="text-lg font-black tracking-tight">{camp.name}</h4>
|
||||
</div>
|
||||
{camp.budget && (
|
||||
<div className="text-right p-2 rounded-xl bg-white/10 backdrop-blur-md border border-white/10">
|
||||
<div className="text-base lg:text-lg font-black text-indigo-400">${camp.budget.monthly}</div>
|
||||
<div className="text-[9px] font-black text-white/40 uppercase tracking-tighter">Budget / Month</div>
|
||||
<div className="text-[9px] font-black text-white/40 uppercase tracking-tighter">{t.adGuide.budgetMonth}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{camp.adGroups?.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="text-[10px] uppercase font-black text-white/30 tracking-widest flex items-center gap-2">
|
||||
<div className="h-px flex-1 bg-white/10" /> Target Ad Groups <div className="h-px flex-1 bg-white/10" />
|
||||
<div className="h-px flex-1 bg-white/10" /> {t.adGuide.targetGroups} <div className="h-px flex-1 bg-white/10" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{camp.adGroups.map((g, j) => (
|
||||
@@ -474,7 +444,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="p-5 rounded-2xl border bg-indigo-50/20">
|
||||
<h4 className="text-[10px] font-black tracking-widest text-indigo-600 uppercase mb-4 flex items-center gap-2">
|
||||
<div className="h-6 w-6 rounded-full bg-indigo-600 text-white flex items-center justify-center font-black text-[10px]">1</div>
|
||||
Step-by-Step Configuration
|
||||
{t.labels.config}
|
||||
</h4>
|
||||
<ol className="space-y-3">
|
||||
{googleAdsResult.implementation.setupSteps.map((step, i) => (
|
||||
@@ -492,7 +462,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="p-5 rounded-2xl border bg-emerald-50/20">
|
||||
<h4 className="text-[10px] font-black tracking-widest text-emerald-600 uppercase mb-4 flex items-center gap-2">
|
||||
<div className="h-6 w-6 rounded-full bg-emerald-600 text-white flex items-center justify-center font-black text-[10px]">2</div>
|
||||
Quality Score Optimization
|
||||
{t.labels.quality}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{googleAdsResult.implementation.qualityScoreTips.map((tip, i) => (
|
||||
@@ -524,7 +494,7 @@ export default function GoogleAdsGenerator() {
|
||||
<BarChart3 className="h-12 w-12" />
|
||||
</div>
|
||||
<div className="text-[10px] uppercase font-black text-indigo-600/70 mb-1.5 flex items-center gap-1.5 tracking-wider">
|
||||
<BarChart3 className="h-3 w-3" /> Industry Size
|
||||
<BarChart3 className="h-3 w-3" /> {t.metrics.industrySize}
|
||||
</div>
|
||||
<div className="text-base lg:text-lg font-black text-slate-800 tracking-tight leading-none">{magicWandResult.marketAnalysis.industrySize}</div>
|
||||
</div>
|
||||
@@ -533,7 +503,7 @@ export default function GoogleAdsGenerator() {
|
||||
<TrendingUp className="h-12 w-12" />
|
||||
</div>
|
||||
<div className="text-[10px] uppercase font-black text-emerald-600/70 mb-1.5 flex items-center gap-1.5 tracking-wider">
|
||||
<TrendingUp className="h-3 w-3" /> Growth Rate
|
||||
<TrendingUp className="h-3 w-3" /> {t.metrics.growthRate}
|
||||
</div>
|
||||
<div className="text-base lg:text-lg font-black text-slate-800 tracking-tight leading-none">{magicWandResult.marketAnalysis.growthRate}</div>
|
||||
</div>
|
||||
@@ -541,7 +511,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="space-y-5">
|
||||
<div className="p-4 rounded-xl border bg-slate-50/30">
|
||||
<h4 className="text-xs font-black text-slate-500 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<Users className="h-4 w-4 text-indigo-500" /> Market Leaders
|
||||
<Users className="h-4 w-4 text-indigo-500" /> {t.metrics.marketLeaders}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{magicWandResult.marketAnalysis.topCompetitors.map((c, i) => (
|
||||
@@ -553,7 +523,7 @@ export default function GoogleAdsGenerator() {
|
||||
</div>
|
||||
<div className="p-4 rounded-xl border bg-slate-50/30">
|
||||
<h4 className="text-xs font-black text-slate-500 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<Rocket className="h-4 w-4 text-purple-500" /> Emerging Trends
|
||||
<Rocket className="h-4 w-4 text-purple-500" /> {t.metrics.emergingTrends}
|
||||
</h4>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{magicWandResult.marketAnalysis.marketTrends.map((t, i) => (
|
||||
@@ -581,7 +551,7 @@ export default function GoogleAdsGenerator() {
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-black text-slate-900 tracking-tight leading-none">{comp.competitor}</h4>
|
||||
<span className="text-[10px] font-bold text-indigo-500 uppercase tracking-widest">Competitor Intel</span>
|
||||
<span className="text-[10px] font-bold text-indigo-500 uppercase tracking-widest">{t.metrics.competitorIntel}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ShieldAlert className="h-5 w-5 text-amber-500 group-hover:rotate-12 transition-transform" />
|
||||
@@ -589,7 +559,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-600 flex items-center gap-1.5 mb-2">
|
||||
<span className="h-1 w-4 bg-emerald-500 rounded-full" /> Strengths
|
||||
<span className="h-1 w-4 bg-emerald-500 rounded-full" /> {t.metrics.strengths}
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{comp.strengths.map((s, j) => (
|
||||
@@ -604,7 +574,7 @@ export default function GoogleAdsGenerator() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-[10px] font-black uppercase tracking-widest text-rose-600 flex items-center gap-1.5 mb-2">
|
||||
<span className="h-1 w-4 bg-rose-500 rounded-full" /> Weaknesses
|
||||
<span className="h-1 w-4 bg-rose-500 rounded-full" /> {t.metrics.weaknesses}
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{comp.weaknesses.map((w, j) => (
|
||||
@@ -620,9 +590,9 @@ export default function GoogleAdsGenerator() {
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-slate-100">
|
||||
<div className="flex items-start gap-2.5 p-3 rounded-xl bg-slate-50 border border-slate-200/50">
|
||||
<div className="h-7 w-7 rounded-lg bg-indigo-500 text-white flex items-center justify-center shrink-0 font-black text-[10px]">SPY</div>
|
||||
<div className="h-7 w-7 rounded-lg bg-indigo-500 text-white flex items-center justify-center shrink-0 font-black text-[10px]">{t.spy}</div>
|
||||
<p className="text-[11px] font-medium text-slate-600 italic leading-relaxed">
|
||||
<span className="font-black text-indigo-600 not-italic mr-1.5 uppercase leading-none">Intelligence:</span>
|
||||
<span className="font-black text-indigo-600 not-italic mr-1.5 uppercase leading-none">{t.metrics.intelligence}:</span>
|
||||
"{comp.adStrategy}"
|
||||
</p>
|
||||
</div>
|
||||
@@ -648,22 +618,22 @@ export default function GoogleAdsGenerator() {
|
||||
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
|
||||
{t.metrics.risk(strat.riskLevel)}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 mt-1 font-bold italic">{strat.timeToResults} to results</span>
|
||||
<span className="text-[10px] text-slate-400 mt-1 font-bold italic">{strat.timeToResults}</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>
|
||||
<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>
|
||||
<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) => (
|
||||
@@ -676,10 +646,10 @@ export default function GoogleAdsGenerator() {
|
||||
{strat.adCopyGuide && (
|
||||
<div className="mt-4 p-4 rounded-xl bg-gradient-to-br from-slate-900 to-slate-800 text-white shadow-lg space-y-4 border border-slate-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-[10px] font-black uppercase tracking-widest text-indigo-400">Google Ads Setup Guide</div>
|
||||
<div className="text-[10px] font-black uppercase tracking-widest text-indigo-400">{t.adGuide.title}</div>
|
||||
<div className="flex gap-1">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span className="text-[9px] text-emerald-400 font-bold uppercase">Ready to Paste</span>
|
||||
<span className="text-[9px] text-emerald-400 font-bold uppercase">{t.adGuide.ready}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -687,7 +657,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="p-3 rounded-lg bg-white/5 border border-white/10 space-y-2">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase flex items-center gap-2">
|
||||
<span className="h-4 w-4 rounded bg-indigo-500 text-white flex items-center justify-center font-black">1</span>
|
||||
Paste into Headlines (max 30 symbols)
|
||||
{t.adGuide.headlines}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{strat.adCopyGuide.headlines.map((h, j) => (
|
||||
@@ -702,7 +672,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="p-3 rounded-lg bg-white/5 border border-white/10 space-y-2">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase flex items-center gap-2">
|
||||
<span className="h-4 w-4 rounded bg-indigo-500 text-white flex items-center justify-center font-black">2</span>
|
||||
Paste into Descriptions (max 90 symbols)
|
||||
{t.adGuide.descriptions}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{strat.adCopyGuide.descriptions.map((d, j) => (
|
||||
@@ -717,7 +687,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="p-3 rounded-lg bg-white/5 border border-white/10 space-y-2">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase flex items-center gap-2">
|
||||
<span className="h-4 w-4 rounded bg-indigo-500 text-white flex items-center justify-center font-black">3</span>
|
||||
Paste into Keywords Section
|
||||
{t.adGuide.keywords}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{strat.adCopyGuide.keywords.map((k, j) => (
|
||||
@@ -730,7 +700,7 @@ export default function GoogleAdsGenerator() {
|
||||
|
||||
<div className="p-3 rounded-lg bg-indigo-500/10 border border-indigo-500/20">
|
||||
<div className="text-[9px] font-black text-indigo-300 uppercase mb-1 flex items-center gap-1.5">
|
||||
<Rocket className="h-3 w-3" /> Quick Implementation Tip
|
||||
<Rocket className="h-3 w-3" /> {t.adGuide.tip}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-300 font-medium leading-relaxed italic">
|
||||
"{strat.adCopyGuide.setupGuide}"
|
||||
@@ -742,7 +712,7 @@ export default function GoogleAdsGenerator() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 items-center">
|
||||
<div className="space-y-1">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase">Channel Mix</div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase">{t.adGuide.channelMix}</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>
|
||||
@@ -750,7 +720,7 @@ export default function GoogleAdsGenerator() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase text-right">Expected ROI</div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase text-right">{t.metrics.roi}</div>
|
||||
<div className="text-lg font-black text-emerald-600 tracking-tighter">{strat.expectedROI}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -813,7 +783,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.websiteUrl}</label>
|
||||
<Input
|
||||
placeholder="e.g., www.your-business.com"
|
||||
placeholder={t.websitePlaceholder}
|
||||
value={websiteUrl}
|
||||
onChange={(e) => setWebsiteUrl(e.target.value)}
|
||||
className="text-sm"
|
||||
@@ -827,7 +797,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div key={index} className="space-y-1.5 p-2.5 rounded-lg border bg-muted/20">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={`${language === "ru" ? "Название продукта" : language === "he" ? "שם המוצר" : "Product name"} ${index + 1}`}
|
||||
placeholder={`${t.productName} ${index + 1}`}
|
||||
value={product.name}
|
||||
onChange={(e) => updateProduct(index, "name", e.target.value)}
|
||||
className="text-sm"
|
||||
@@ -844,7 +814,7 @@ export default function GoogleAdsGenerator() {
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
placeholder={language === "ru" ? "URL страницы продукта (необязательно)" : language === "he" ? "כתובת URL של עמוד המוצר (אופציונלי)" : "Product page URL (optional)"}
|
||||
placeholder={t.productUrlPlaceholder}
|
||||
value={product.url}
|
||||
onChange={(e) => updateProduct(index, "url", e.target.value)}
|
||||
className="text-xs text-muted-foreground"
|
||||
@@ -853,7 +823,7 @@ export default function GoogleAdsGenerator() {
|
||||
))}
|
||||
<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"}
|
||||
{t.addProduct}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -893,7 +863,7 @@ export default function GoogleAdsGenerator() {
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
|
||||
<Textarea
|
||||
placeholder="e.g., Small business owners in USA looking for productivity tools"
|
||||
placeholder={t.audiencePlaceholder}
|
||||
value={targetAudience}
|
||||
onChange={(e) => setTargetAudience(e.target.value)}
|
||||
className="min-h-[80px] lg:min-h-[100px] resize-y text-sm"
|
||||
@@ -1015,8 +985,8 @@ export default function GoogleAdsGenerator() {
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{magicWandResult
|
||||
? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes")
|
||||
: (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready")
|
||||
? t.strategicDirections
|
||||
: t.generatedCampaign
|
||||
}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@@ -1056,7 +1026,7 @@ export default function GoogleAdsGenerator() {
|
||||
</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"}
|
||||
{common.emptyState}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -16,8 +16,7 @@ export default function HistoryPanel() {
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
const message = language === "ru" ? "Вы уверены, что хотите очистить всю историю?" : language === "he" ? "האם אתה בטוח שברצונך למחוק את כל ההיסטוריה?" : "Are you sure you want to clear all history?";
|
||||
if (confirm(message)) {
|
||||
if (confirm(t.confirmClear)) {
|
||||
clearHistory();
|
||||
}
|
||||
};
|
||||
@@ -30,7 +29,7 @@ export default function HistoryPanel() {
|
||||
<Clock className="mx-auto h-10 w-10 lg:h-12 lg:w-12 text-muted-foreground/50" />
|
||||
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-muted-foreground font-medium">{t.empty}</p>
|
||||
<p className="mt-1.5 lg:mt-2 text-xs lg:text-sm text-muted-foreground">
|
||||
{language === "ru" ? "Начните использовать инструменты, чтобы увидеть историю здесь" : language === "he" ? "התחל להשתמש בכלים כדי לראות אותם כאן" : "Start using tools to see them here"}
|
||||
{t.emptyDesc}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -44,7 +43,7 @@ export default function HistoryPanel() {
|
||||
<div>
|
||||
<CardTitle className="text-base lg:text-lg">{t.title}</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{history.length} {language === "ru" ? "элем." : language === "he" ? "פריטים" : "items"}
|
||||
{history.length} {t.items}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleClear} className="h-8 w-8 lg:h-9 lg:w-9" title={t.clear}>
|
||||
|
||||
@@ -51,13 +51,13 @@ const MarketResearcher = () => {
|
||||
|
||||
const validateUrls = () => {
|
||||
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
|
||||
if (!websiteUrl || !urlRegex.test(websiteUrl)) return "Invalid primary website URL";
|
||||
if (!websiteUrl || !urlRegex.test(websiteUrl)) return t.invalidPrimaryUrl;
|
||||
|
||||
const validCompetitors = competitorUrls.filter(url => url.trim().length > 0);
|
||||
if (validCompetitors.length < 2) return "At least 2 competitor websites are required";
|
||||
if (validCompetitors.length < 2) return t.minCompetitors;
|
||||
|
||||
for (const url of validCompetitors) {
|
||||
if (!urlRegex.test(url)) return `Invalid competitor URL: ${url}`;
|
||||
if (!urlRegex.test(url)) return `${t.invalidCompetitorUrl}: ${url}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -99,7 +99,7 @@ const MarketResearcher = () => {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,19 +130,19 @@ const MarketResearcher = () => {
|
||||
websiteUrl,
|
||||
additionalUrls: filteredAddUrls,
|
||||
competitors: filteredCompetitors,
|
||||
productMapping: [{ productName: productMapping || "Main Product", features: [] }],
|
||||
productMapping: [{ productName: productMapping || t.mainProduct, features: [] }],
|
||||
generatedAt: new Date(),
|
||||
rawContent: result.data
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to parse market research JSON:", e);
|
||||
setError("Failed to parse the AI response. Please try again.");
|
||||
setError(t.parseError);
|
||||
}
|
||||
} else {
|
||||
setError(result.error || "Research failed");
|
||||
setError(result.error || t.researchFailed);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "An unexpected error occurred");
|
||||
setError(err instanceof Error ? err.message : t.unexpectedError);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
@@ -155,8 +155,8 @@ const MarketResearcher = () => {
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b bg-slate-50/50">
|
||||
<th className="px-4 py-3 font-black text-slate-900 uppercase tracking-wider text-[10px]">Product</th>
|
||||
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">Your Price</th>
|
||||
<th className="px-4 py-3 font-black text-slate-900 uppercase tracking-wider text-[10px]">{t.product}</th>
|
||||
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">{t.yourPrice}</th>
|
||||
{marketResearchResult.competitors.map((comp, i) => (
|
||||
<th key={i} className="px-4 py-3 font-black text-slate-500 uppercase tracking-wider text-[10px]">{comp}</th>
|
||||
))}
|
||||
@@ -172,7 +172,7 @@ const MarketResearcher = () => {
|
||||
return (
|
||||
<td key={comp} className="px-4 py-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-slate-600">{compPrice ? compPrice.price : "N/A"}</span>
|
||||
<span className="font-medium text-slate-600">{compPrice ? compPrice.price : t.notAvailable}</span>
|
||||
{compPrice?.url && (
|
||||
<a
|
||||
href={compPrice.url.startsWith('http') ? compPrice.url : `https://${compPrice.url}`}
|
||||
@@ -181,7 +181,7 @@ const MarketResearcher = () => {
|
||||
className="inline-flex items-center gap-1 text-[10px] text-indigo-500 hover:text-indigo-700 font-bold transition-colors group/link"
|
||||
>
|
||||
<ExternalLink className="h-2.5 w-2.5" />
|
||||
View Product
|
||||
{t.viewProduct}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -203,8 +203,8 @@ const MarketResearcher = () => {
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b bg-slate-50/50">
|
||||
<th className="px-4 py-3 font-black text-slate-900 uppercase tracking-wider text-[10px]">Feature</th>
|
||||
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">You</th>
|
||||
<th className="px-4 py-3 font-black text-slate-900 uppercase tracking-wider text-[10px]">{t.feature}</th>
|
||||
<th className="px-4 py-3 font-black text-indigo-600 uppercase tracking-wider text-[10px]">{t.you}</th>
|
||||
{marketResearchResult.competitors.map((comp, i) => (
|
||||
<th key={i} className="px-4 py-3 font-black text-slate-500 uppercase tracking-wider text-[10px]">{comp}</th>
|
||||
))}
|
||||
@@ -227,7 +227,7 @@ const MarketResearcher = () => {
|
||||
typeof compStatus.status === 'boolean' ? (
|
||||
compStatus.status ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <X className="h-4 w-4 text-slate-300" />
|
||||
) : <span className="text-xs font-medium text-slate-600">{compStatus.status}</span>
|
||||
) : "N/A"}
|
||||
) : t.notAvailable}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
@@ -258,7 +258,7 @@ const MarketResearcher = () => {
|
||||
<Card className="border-slate-200/60 shadow-xl shadow-slate-200/40 overflow-hidden bg-white/80 backdrop-blur-md">
|
||||
<CardHeader className="bg-slate-50/50 border-b p-5">
|
||||
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2">
|
||||
<Globe className="h-4 w-4" /> Company Profile
|
||||
<Globe className="h-4 w-4" /> {t.companyProfile}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
@@ -276,14 +276,14 @@ const MarketResearcher = () => {
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600 flex justify-between items-center">
|
||||
{t.additionalUrls}
|
||||
<Button variant="ghost" size="sm" onClick={handleAddUrl} className="h-6 px-2 hover:bg-slate-100 text-[10px] font-black uppercase">
|
||||
<Plus className="h-3 w-3 mr-1" /> Add URL
|
||||
<Plus className="h-3 w-3 mr-1" /> {t.addUrl}
|
||||
</Button>
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{additionalUrls.map((url, i) => (
|
||||
<div key={i} className="flex gap-2 group">
|
||||
<Input
|
||||
placeholder="Sub-page URL (e.g., pricing, features)"
|
||||
placeholder={t.urlPlaceholder}
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
const newUrls = [...additionalUrls];
|
||||
@@ -305,7 +305,7 @@ const MarketResearcher = () => {
|
||||
<Card className="border-slate-200/60 shadow-xl shadow-slate-200/40 overflow-hidden bg-white/80 backdrop-blur-md">
|
||||
<CardHeader className="bg-slate-50/50 border-b p-5">
|
||||
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2">
|
||||
<ShieldAlert className="h-4 w-4" /> Competitive Intel
|
||||
<ShieldAlert className="h-4 w-4" /> {t.competitiveIntel}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
@@ -313,7 +313,7 @@ const MarketResearcher = () => {
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600 flex justify-between items-center">
|
||||
{t.competitors}
|
||||
<Button variant="ghost" size="sm" onClick={handleAddCompetitor} disabled={competitorUrls.length >= 10} className="h-6 px-2 hover:bg-slate-100 text-[10px] font-black uppercase">
|
||||
<Plus className="h-3 w-3 mr-1" /> Add Competitor
|
||||
<Plus className="h-3 w-3 mr-1" /> {t.addCompetitor}
|
||||
</Button>
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
@@ -347,13 +347,13 @@ const MarketResearcher = () => {
|
||||
onChange={(e) => setProductMapping(e.target.value)}
|
||||
className="min-h-[80px] bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-sm"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-400 font-medium italic">Describe which products/features to compare across all sites.</p>
|
||||
<p className="text-[10px] text-slate-400 font-medium italic">{t.mappingDesc}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600">Research Parameters</label>
|
||||
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.parameters}</label>
|
||||
<Textarea
|
||||
placeholder="Any specific depth or focus? (e.g., 'Focus on enterprise features', 'Analyze pricing tiers')"
|
||||
placeholder={t.parametersPlaceholder}
|
||||
value={specialInstructions}
|
||||
onChange={(e) => setSpecialInstructions(e.target.value)}
|
||||
className="min-h-[80px] bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-sm"
|
||||
@@ -365,7 +365,7 @@ const MarketResearcher = () => {
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-widest">
|
||||
<span className="text-indigo-600 flex items-center gap-1.5">
|
||||
<Loader2 className="h-3 w-3 animate-spin" /> {language === "ru" ? "Идет анализ" : language === "he" ? "מנתח..." : "Analysis in progress"}
|
||||
<Loader2 className="h-3 w-3 animate-spin" /> {t.analysisInProgress}
|
||||
</span>
|
||||
<span className="text-slate-400">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
@@ -382,7 +382,7 @@ const MarketResearcher = () => {
|
||||
<Rocket className="h-4 w-4 text-indigo-400 group-hover:block hidden" />
|
||||
</div>
|
||||
<h4 className="text-[9px] font-black uppercase tracking-[0.2em] text-indigo-400 mb-2 flex items-center gap-1.5">
|
||||
<span className="h-1 w-1 bg-indigo-400 rounded-full animate-pulse" /> AI Thoughts & Actions
|
||||
<span className="h-1 w-1 bg-indigo-400 rounded-full animate-pulse" /> {t.aiThoughts}
|
||||
</h4>
|
||||
<p className="text-xs font-bold leading-relaxed italic animate-in fade-in slide-in-from-left-2 duration-700">
|
||||
"{t.thoughts?.[thoughtIndex] || t.researching}"
|
||||
@@ -427,9 +427,9 @@ const MarketResearcher = () => {
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/20 rounded-full blur-3xl -mr-32 -mt-32" />
|
||||
<div className="relative z-10 flex justify-between items-start">
|
||||
<div>
|
||||
<Badge variant="outline" className="mb-2 border-indigo-400/50 text-indigo-300 font-black uppercase tracking-widest text-[10px]">Market Intel Report</Badge>
|
||||
<Badge variant="outline" className="mb-2 border-indigo-400/50 text-indigo-300 font-black uppercase tracking-widest text-[10px]">{t.marketIntelReport}</Badge>
|
||||
<CardTitle className="text-2xl font-black tracking-tight">{marketResearchResult.websiteUrl}</CardTitle>
|
||||
<CardDescription className="text-indigo-200 font-medium">Generated on {marketResearchResult.generatedAt.toLocaleDateString()}</CardDescription>
|
||||
<CardDescription className="text-indigo-200 font-medium">{t.generatedOn} {marketResearchResult.generatedAt.toLocaleDateString()}</CardDescription>
|
||||
</div>
|
||||
<div className="p-3 rounded-2xl bg-white/10 backdrop-blur-md border border-white/20">
|
||||
<BarChart3 className="h-6 w-6 text-indigo-300" />
|
||||
@@ -439,10 +439,10 @@ const MarketResearcher = () => {
|
||||
<CardContent className="p-0">
|
||||
<Tabs defaultValue="summary" className="w-full">
|
||||
<TabsList className="w-full h-14 bg-slate-50 border-b rounded-none px-6 justify-start gap-4">
|
||||
<TabsTrigger value="summary" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">Summary</TabsTrigger>
|
||||
<TabsTrigger value="pricing" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">Price Matrix</TabsTrigger>
|
||||
<TabsTrigger value="features" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">Feature Table</TabsTrigger>
|
||||
<TabsTrigger value="positioning" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">Positioning</TabsTrigger>
|
||||
<TabsTrigger value="summary" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.summary}</TabsTrigger>
|
||||
<TabsTrigger value="pricing" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.pricing}</TabsTrigger>
|
||||
<TabsTrigger value="features" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.features}</TabsTrigger>
|
||||
<TabsTrigger value="positioning" className="data-[state=active]:bg-transparent data-[state=active]:text-indigo-600 data-[state=active]:border-b-2 data-[state=active]:border-indigo-600 rounded-none h-full font-black uppercase tracking-widest text-[10px] px-0">{t.positioning}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="p-6">
|
||||
@@ -450,7 +450,7 @@ const MarketResearcher = () => {
|
||||
<div className="space-y-6">
|
||||
<div className="p-5 rounded-2xl bg-indigo-50 border border-indigo-100">
|
||||
<h3 className="text-sm font-black text-indigo-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" /> Executive Summary
|
||||
<TrendingUp className="h-4 w-4" /> {t.executiveSummary}
|
||||
</h3>
|
||||
<p className="text-sm text-indigo-900/80 leading-relaxed font-medium">
|
||||
{marketResearchResult.executiveSummary}
|
||||
@@ -460,7 +460,7 @@ const MarketResearcher = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-5 rounded-2xl border bg-emerald-50/30 border-emerald-100">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-emerald-600 mb-3 flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4" /> Strategic Advantages
|
||||
<CheckCircle2 className="h-4 w-4" /> {t.strategicAdvantages}
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{marketResearchResult.competitiveAnalysis.advantages.map((adv, i) => (
|
||||
@@ -472,7 +472,7 @@ const MarketResearcher = () => {
|
||||
</div>
|
||||
<div className="p-5 rounded-2xl border bg-rose-50/30 border-rose-100">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-rose-600 mb-3 flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" /> Identified Gaps
|
||||
<AlertCircle className="h-4 w-4" /> {t.identifiedGaps}
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{marketResearchResult.competitiveAnalysis.disadvantages.map((dis, i) => (
|
||||
@@ -486,7 +486,7 @@ const MarketResearcher = () => {
|
||||
|
||||
<div className="p-5 rounded-2xl border bg-amber-50/30 border-amber-100">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-amber-600 mb-3 flex items-center gap-2">
|
||||
<Lightbulb className="h-4 w-4" /> Key Recommendations
|
||||
<Lightbulb className="h-4 w-4" /> {t.recommendations}
|
||||
</h4>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{marketResearchResult.recommendations.map((rec, i) => (
|
||||
@@ -503,8 +503,8 @@ const MarketResearcher = () => {
|
||||
<TabsContent value="pricing" className="m-0 focus-visible:ring-0">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight">Price Comparison Matrix</h3>
|
||||
<Badge className="bg-slate-900 text-[10px] font-black uppercase">Live Market Data</Badge>
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight">{t.priceMatrix}</h3>
|
||||
<Badge className="bg-slate-900 text-[10px] font-black uppercase">{t.liveMarketData}</Badge>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 overflow-hidden">
|
||||
{renderPriceMatrix()}
|
||||
@@ -515,8 +515,8 @@ const MarketResearcher = () => {
|
||||
<TabsContent value="features" className="m-0 focus-visible:ring-0">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight">Feature Benchmarking</h3>
|
||||
<Badge className="bg-indigo-600 text-[10px] font-black uppercase">Functional Audit</Badge>
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight">{t.featureBenchmarking}</h3>
|
||||
<Badge className="bg-indigo-600 text-[10px] font-black uppercase">{t.functionalAudit}</Badge>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 overflow-hidden">
|
||||
{renderFeatureTable()}
|
||||
@@ -529,7 +529,7 @@ const MarketResearcher = () => {
|
||||
<div className="space-y-4">
|
||||
<div className="p-5 rounded-2xl bg-slate-900 text-white shadow-xl">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-indigo-400 mb-3 flex items-center gap-2">
|
||||
<Target className="h-4 w-4" /> Market Landscape
|
||||
<Target className="h-4 w-4" /> {t.marketLandscape}
|
||||
</h4>
|
||||
<p className="text-xs font-medium leading-relaxed opacity-90">
|
||||
{marketResearchResult.marketPositioning.landscape}
|
||||
@@ -539,7 +539,7 @@ const MarketResearcher = () => {
|
||||
<div className="space-y-4">
|
||||
<div className="p-5 rounded-2xl bg-indigo-600 text-white shadow-xl">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-indigo-200 mb-3 flex items-center gap-2">
|
||||
<Rocket className="h-4 w-4" /> Segmentation Strategy
|
||||
<Rocket className="h-4 w-4" /> {t.segmentationStrategy}
|
||||
</h4>
|
||||
<p className="text-xs font-medium leading-relaxed font-bold">
|
||||
{marketResearchResult.marketPositioning.segmentation}
|
||||
@@ -547,7 +547,7 @@ const MarketResearcher = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2 p-5 rounded-2xl border bg-slate-50 italic">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-2">Research Methodology</h4>
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-2">{t.methodology}</h4>
|
||||
<p className="text-[10px] font-medium text-slate-400">
|
||||
{marketResearchResult.methodology}
|
||||
</p>
|
||||
@@ -563,7 +563,7 @@ const MarketResearcher = () => {
|
||||
<div className="h-20 w-20 rounded-3xl bg-white border border-slate-100 flex items-center justify-center mb-6 shadow-sm group-hover:scale-110 group-hover:rotate-3 transition-all duration-500">
|
||||
<BarChart3 className="h-10 w-10 text-slate-300 group-hover:text-indigo-500 transition-colors" />
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-400 tracking-tight group-hover:text-slate-600 transition-colors">Awaiting Analysis Parameters</h3>
|
||||
<h3 className="text-xl font-black text-slate-400 tracking-tight group-hover:text-slate-600 transition-colors">{t.awaitingParameters}</h3>
|
||||
<p className="text-sm text-slate-400 font-medium max-w-[280px] mt-2 group-hover:text-slate-500 transition-colors">
|
||||
{t.emptyState}
|
||||
</p>
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function PRDGenerator() {
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter an idea to generate PRD");
|
||||
setError(t.enterIdeaError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function PRDGenerator() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,11 +117,11 @@ export default function PRDGenerator() {
|
||||
setPRD(newPRD);
|
||||
} else {
|
||||
console.error("[PRDGenerator] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate PRD");
|
||||
setError(result.error || t.errorGenerate);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[PRDGenerator] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -136,12 +136,12 @@ export default function PRDGenerator() {
|
||||
};
|
||||
|
||||
const sections = [
|
||||
{ id: "overview", title: language === "ru" ? "Обзор продукта" : language === "he" ? "סקירת מוצר" : "Product Overview" },
|
||||
{ id: "personas", title: language === "ru" ? "Персоны пользователей" : language === "he" ? "פרסונות משתמשים" : "User Personas & Use Cases" },
|
||||
{ id: "functional", title: language === "ru" ? "Функциональные требования" : language === "he" ? "דרישות פונקציונליות" : "Functional Requirements" },
|
||||
{ id: "nonfunctional", title: language === "ru" ? "Нефункциональные требования" : language === "he" ? "דרישות לא פונקציונליות" : "Non-functional Requirements" },
|
||||
{ id: "architecture", title: language === "ru" ? "Техническая архитектура" : language === "he" ? "ארכיטקטורה טכנית" : "Technical Architecture" },
|
||||
{ id: "metrics", title: language === "ru" ? "Успешность метрик" : language === "he" ? "מדדי הצלחה" : "Success Metrics" },
|
||||
{ id: "overview", title: t.sections.overview },
|
||||
{ id: "personas", title: t.sections.personas },
|
||||
{ id: "functional", title: t.sections.functional },
|
||||
{ id: "nonfunctional", title: t.sections.nonfunctional },
|
||||
{ id: "architecture", title: t.sections.architecture },
|
||||
{ id: "metrics", title: t.sections.metrics },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -244,7 +244,7 @@ export default function PRDGenerator() {
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{language === "ru" ? "Структурированный документ требований готов к разработке" : language === "he" ? "מסמך דרישות מובנה מוכן לפיתוח" : "Structured requirements document ready for development"}
|
||||
{t.generatedDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
@@ -273,7 +273,7 @@ export default function PRDGenerator() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[200px] lg:h-[300px] items-center justify-center text-center text-xs lg:text-sm text-muted-foreground">
|
||||
{language === "ru" ? "Здесь появится созданный PRD" : language === "he" ? "PRD שחולל יופיע כאן" : "Generated PRD will appear here"}
|
||||
{t.emptyState}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function PromptEnhancer() {
|
||||
|
||||
const handleEnhance = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter a prompt to enhance");
|
||||
setError(t.enterPromptError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function PromptEnhancer() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,11 +97,11 @@ export default function PromptEnhancer() {
|
||||
setEnhancedPrompt(result.data);
|
||||
} else {
|
||||
console.error("[PromptEnhancer] Enhancement failed:", result.error);
|
||||
setError(result.error || "Failed to enhance prompt");
|
||||
setError(result.error || t.errorEnhance);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[PromptEnhancer] Enhancement error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.errorEnhance);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -207,7 +207,7 @@ export default function PromptEnhancer() {
|
||||
</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>
|
||||
<span className="hidden sm:inline">{t.clear}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -231,7 +231,7 @@ export default function PromptEnhancer() {
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{language === "ru" ? "Профессиональный промпт, готовый для кодинг-агентов" : language === "he" ? "פרומפט מקצועי מוכן לסוכני קידוד" : "Professional prompt ready for coding agents"}
|
||||
{t.enhancedDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
@@ -241,7 +241,7 @@ export default function PromptEnhancer() {
|
||||
</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"}
|
||||
{t.emptyState}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function SettingsPanel() {
|
||||
} catch (error) {
|
||||
console.error("Qwen OAuth failed", error);
|
||||
window.alert(
|
||||
error instanceof Error ? error.message : t.qwenAuth + " failed"
|
||||
error instanceof Error ? error.message : t.qwenAuthFailed
|
||||
);
|
||||
} finally {
|
||||
setIsAuthLoading(false);
|
||||
@@ -104,7 +104,7 @@ export default function SettingsPanel() {
|
||||
{t.apiKeys}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{language === "ru" ? "Настройте ключи API для различных провайдеров ИИ" : language === "he" ? "הגדר מפתחות API עבור ספקי בינה מלאכותית שונים" : "Configure API keys for different AI providers"}
|
||||
{t.apiKeysDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 lg:space-y-6 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
@@ -155,7 +155,7 @@ export default function SettingsPanel() {
|
||||
disabled={isAuthLoading}
|
||||
>
|
||||
{isAuthLoading
|
||||
? (language === "ru" ? "Вход..." : language === "he" ? "מתחבר..." : "Signing in...")
|
||||
? t.signingIn
|
||||
: qwenTokens
|
||||
? t.logoutQwen
|
||||
: t.loginQwen}
|
||||
@@ -297,7 +297,7 @@ export default function SettingsPanel() {
|
||||
<CardHeader className="p-4 lg:p-6 text-start">
|
||||
<CardTitle className="text-base lg:text-lg">{t.dataPrivacy}</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{language === "ru" ? "Ваши настройки обработки данных" : language === "he" ? "העדפות הטיפול בנתונים שלך" : "Your data handling preferences"}
|
||||
{t.dataPrivacyTitleDesc}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
|
||||
@@ -43,22 +43,22 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
<div className="border-b p-4 lg:p-6">
|
||||
<a href="https://www.rommark.dev" className="mb-4 flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
<Menu className="h-3 w-3" />
|
||||
<span>{language === "en" ? "Back to rommark.dev" : language === "ru" ? "Вернуться на rommark.dev" : "חזרה ל-rommark.dev"}</span>
|
||||
<span>{t.backToRommark}</span>
|
||||
</a>
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="block">
|
||||
<h1 className="flex items-center gap-2 text-lg lg:text-xl font-bold hover:opacity-80 transition-opacity">
|
||||
<div className="flex h-7 w-7 lg:h-8 lg:w-8 items-center justify-center rounded-lg bg-[#4285F4] text-primary-foreground text-sm lg:text-base">
|
||||
PA
|
||||
</div>
|
||||
PromptArch
|
||||
{t.title}
|
||||
</h1>
|
||||
</a>
|
||||
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="mt-2 lg:mt-3 flex items-center gap-1.5 rounded-md px-2 lg:px-3 py-1 lg:py-1.5 text-xs text-primary hover:bg-primary/10 transition-colors">
|
||||
<Github className="h-3 w-3 lg:h-3.5 lg:w-3.5" />
|
||||
<span>View on GitHub</span>
|
||||
<span>{t.viewOnGithub}</span>
|
||||
</a>
|
||||
<p className="mt-1 lg:mt-2 text-[10px] lg:text-xs text-muted-foreground">
|
||||
Forked from <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
|
||||
{t.forkedFrom} <a href="https://github.com/ClavixDev/Clavix" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Clavix</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
|
||||
<div className="mt-4 p-2 lg:p-3 border-t border-border/50">
|
||||
<div className="flex items-center gap-2 mb-2 text-[10px] lg:text-xs font-semibold text-muted-foreground uppercase">
|
||||
<Languages className="h-3 w-3" /> {language === "en" ? "Language" : language === "ru" ? "Язык" : "שפה"}
|
||||
<Languages className="h-3 w-3" /> {t.language}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(["en", "ru", "he"] as const).map((lang) => (
|
||||
@@ -105,11 +105,11 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
|
||||
<div className="border-t p-3 lg:p-4 hidden lg:block">
|
||||
<div className="rounded-md bg-muted/50 p-2 lg:p-3 text-[10px] lg:text-xs text-muted-foreground">
|
||||
<p className="font-medium text-foreground">Quick Tips</p>
|
||||
<p className="font-medium text-foreground">{t.quickTips}</p>
|
||||
<ul className="mt-1.5 lg:mt-2 space-y-0.5 lg:space-y-1">
|
||||
<li>• Use different providers for best results</li>
|
||||
<li>• Copy enhanced prompts to your AI agent</li>
|
||||
<li>• PRDs generate better action plans</li>
|
||||
<li>• {t.tip1}</li>
|
||||
<li>• {t.tip2}</li>
|
||||
<li>• {t.tip3}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
|
||||
PA
|
||||
</div>
|
||||
<span className="font-bold text-lg">PromptArch</span>
|
||||
<span className="font-bold text-lg">{t.title}</span>
|
||||
</a>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -482,7 +482,7 @@ export default function SlidesGenerator() {
|
||||
max-width: 90%;
|
||||
animation: fadeIn 1s ease-out 0.3s both;
|
||||
">
|
||||
${slide.content || "Content goes here..."}
|
||||
${slide.content || t.slideContentPlaceholder}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -519,7 +519,7 @@ export default function SlidesGenerator() {
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!topic.trim()) {
|
||||
setError("Please enter a topic for your presentation");
|
||||
setError(t.enterTopicError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@ export default function SlidesGenerator() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -588,7 +588,7 @@ export default function SlidesGenerator() {
|
||||
language: languageName,
|
||||
slides: [{
|
||||
id: "slide-1",
|
||||
title: "Generated Content",
|
||||
title: t.generatedContent,
|
||||
content: result.data,
|
||||
htmlContent: `
|
||||
<div style="padding: 2rem; font-family: system-ui; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); min-height: 100%; color: #f8fafc;">
|
||||
@@ -927,7 +927,7 @@ export default function SlidesGenerator() {
|
||||
|
||||
{/* Topic Input */}
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="text-xs lg:text-sm font-medium">{uiLanguage === "ru" ? "Тема презентации" : uiLanguage === "he" ? "נושא המצגת" : "Presentation Topic"}</label>
|
||||
<label className="text-xs lg:text-sm font-medium">{t.topic}</label>
|
||||
<Textarea
|
||||
placeholder={t.placeholder}
|
||||
value={topic}
|
||||
@@ -941,7 +941,7 @@ export default function SlidesGenerator() {
|
||||
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
||||
<Upload className="h-3.5 w-3.5 text-blue-500" />
|
||||
{t.attachFiles}
|
||||
<span className="text-[10px] text-muted-foreground font-normal">({uiLanguage === "ru" ? "необязательно" : uiLanguage === "he" ? "אופציונלי" : "Optional"})</span>
|
||||
<span className="text-[10px] text-muted-foreground font-normal">({t.optional})</span>
|
||||
</label>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -969,10 +969,10 @@ export default function SlidesGenerator() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium">
|
||||
{isDragOver ? "Drop files here" : "Drag & drop or click to upload"}
|
||||
{isDragOver ? t.dropFiles : t.uploadFiles}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
PowerPoint, PDFs, Docs, Images, Color Palettes
|
||||
{t.fileTypes}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1006,7 +1006,7 @@ export default function SlidesGenerator() {
|
||||
{formatFileSize(file.size)}
|
||||
{file.colors && file.colors.length > 0 && (
|
||||
<span className="ml-2">
|
||||
• {file.colors.length} colors extracted
|
||||
• {file.colors.length} {t.colorsExtracted}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
@@ -1049,7 +1049,7 @@ export default function SlidesGenerator() {
|
||||
>
|
||||
{LANGUAGES.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.nativeName} ({lang.name})
|
||||
{lang.nativeName} ({(t.languages as any)[lang.code] || lang.name})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -1058,16 +1058,16 @@ export default function SlidesGenerator() {
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
||||
<Palette className="h-3.5 w-3.5 text-purple-500" />
|
||||
Theme
|
||||
{t.theme}
|
||||
</label>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(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"
|
||||
>
|
||||
{THEMES.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.icon} {t.name}
|
||||
{THEMES.map((themeItem) => (
|
||||
<option key={themeItem.id} value={themeItem.id}>
|
||||
{themeItem.icon} {(t.themes as any)[themeItem.id.split('-')[0]] || themeItem.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -1092,8 +1092,8 @@ export default function SlidesGenerator() {
|
||||
: "border-muted hover:border-violet-300"
|
||||
)}
|
||||
>
|
||||
<p className="text-xs font-medium">{style.name}</p>
|
||||
<p className="text-[10px] text-muted-foreground">{style.description}</p>
|
||||
<p className="text-xs font-medium">{((t.animStyles as any)[style.id])?.name || style.name}</p>
|
||||
<p className="text-[10px] text-muted-foreground">{((t.animStyles as any)[style.id])?.desc || style.description}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1105,7 +1105,7 @@ export default function SlidesGenerator() {
|
||||
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
{showAdvanced ? (uiLanguage === "ru" ? "Скрыть" : uiLanguage === "he" ? "הסתר" : "Hide") : (uiLanguage === "ru" ? "Показать" : uiLanguage === "he" ? "הצג" : "Show")} {uiLanguage === "ru" ? "Раширенные настройки" : uiLanguage === "he" ? "אפשרויות מתקדמות" : "Advanced Options"}
|
||||
{showAdvanced ? t.hide : t.show} {t.advanced}
|
||||
</button>
|
||||
|
||||
{/* Advanced Options */}
|
||||
@@ -1124,7 +1124,7 @@ export default function SlidesGenerator() {
|
||||
>
|
||||
{AUDIENCES.map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.icon} {a.name}
|
||||
{a.icon} {(t.audiences as any)[a.id] || a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -1133,7 +1133,7 @@ export default function SlidesGenerator() {
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium flex items-center gap-1.5">
|
||||
<Hash className="h-3.5 w-3.5 text-orange-500" />
|
||||
Number of Slides
|
||||
{t.numSlides}
|
||||
</label>
|
||||
<select
|
||||
value={slideCount}
|
||||
@@ -1142,7 +1142,7 @@ export default function SlidesGenerator() {
|
||||
>
|
||||
<option value={0}>{t.sameAsSource}</option>
|
||||
{[5, 8, 10, 12, 15, 20, 25, 30].map((n) => (
|
||||
<option key={n} value={n}>{n} slides</option>
|
||||
<option key={n} value={n}>{n} {t.slides}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -1151,11 +1151,11 @@ export default function SlidesGenerator() {
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium flex items-center gap-1.5">
|
||||
<Building2 className="h-3.5 w-3.5 text-cyan-500" />
|
||||
Organization Name (Optional)
|
||||
{t.organization} ({t.optional})
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g., Acme Corporation"
|
||||
placeholder={t.organizationPlaceholder}
|
||||
value={organization}
|
||||
onChange={(e) => setOrganization(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
@@ -1184,7 +1184,7 @@ export default function SlidesGenerator() {
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
<span className="text-[10px] lg:text-xs">{common.configApiKey}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1217,7 +1217,7 @@ export default function SlidesGenerator() {
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className={cn("h-4 w-4 lg:h-5 lg:w-5", slidesPresentation ? "text-green-500" : "text-muted-foreground")} />
|
||||
{uiLanguage === "ru" ? "Предпросмотр слайдов" : uiLanguage === "he" ? "תצוגה מקדימה של השקופיות" : "Slide Preview"}
|
||||
{t.slidePreview}
|
||||
</span>
|
||||
{slidesPresentation && (
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -1241,7 +1241,7 @@ export default function SlidesGenerator() {
|
||||
</CardTitle>
|
||||
{slidesPresentation && (
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{slidesPresentation.title} • {slidesPresentation.slides.length} slides • {slidesPresentation.language}
|
||||
{slidesPresentation.title} • {slidesPresentation.slides.length} {t.slides} • {slidesPresentation.language}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
@@ -1321,7 +1321,7 @@ export default function SlidesGenerator() {
|
||||
</p>
|
||||
{slidesPresentation.slides[currentSlide]?.notes && (
|
||||
<p className="text-xs text-blue-500 mt-2 italic">
|
||||
Notes: {slidesPresentation.slides[currentSlide]?.notes}
|
||||
{t.notesLabel}: {slidesPresentation.slides[currentSlide]?.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -1335,7 +1335,7 @@ export default function SlidesGenerator() {
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t.emptyState}</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
{uiLanguage === "ru" ? "Введите тему и создайте анимированные слайды" : uiLanguage === "he" ? "הזן נושא וחולל שקופיות אקטיביות" : "Enter a topic and generate your animated slides"}
|
||||
{t.enterTopic}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function UXDesignerPrompt() {
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) {
|
||||
setError("Please enter an app description");
|
||||
setError(t.enterDescriptionError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function UXDesignerPrompt() {
|
||||
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
|
||||
|
||||
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
|
||||
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
|
||||
setError(`${common.error}: ${common.configApiKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,11 +99,11 @@ export default function UXDesignerPrompt() {
|
||||
setEnhancedPrompt(result.data);
|
||||
} else {
|
||||
console.error("[UXDesignerPrompt] Generation failed:", result.error);
|
||||
setError(result.error || "Failed to generate UX designer prompt");
|
||||
setError(result.error || t.errorGenerate);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[UXDesignerPrompt] Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "An error occurred");
|
||||
setError(err instanceof Error ? err.message : t.errorGenerate);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -207,12 +207,12 @@ export default function UXDesignerPrompt() {
|
||||
) : (
|
||||
<>
|
||||
<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"}
|
||||
{t.generateButton}
|
||||
</>
|
||||
)}
|
||||
</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="hidden sm:inline">{translations[language].promptEnhancer.clear}</span>
|
||||
<span className="sm:hidden">×</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -225,7 +225,7 @@ export default function UXDesignerPrompt() {
|
||||
<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 className="sm:hidden">{t.uxPromptMobile}</span>
|
||||
</span>
|
||||
{generatedPrompt && (
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user