feat: complete translations for russian and hebrew across all components

This commit is contained in:
Gemini AI
2025-12-29 11:44:55 +04:00
Unverified
parent a7f1ea1dc0
commit 0589742879
12 changed files with 1080 additions and 300 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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