Compare commits

...

45 Commits

25 changed files with 3753 additions and 825 deletions

108
app/api/ai-assist/route.ts Normal file
View File

@@ -0,0 +1,108 @@
import { NextRequest, NextResponse } from "next/server";
import { randomUUID } from "crypto";
import { z } from "zod";
// Schema validation
const schema = z.object({
request: z.string().min(1),
step: z.enum(["plan", "generate", "preview"]).default("plan"),
plan: z.any().optional(),
code: z.string().optional(),
provider: z.string().optional(),
model: z.string().optional()
});
const STEPS = {
plan: `You are an expert software architect. Create a DETAILED DEVELOPMENT PLAN for the following request: "{request}"
Output ONLY a JSON object:
{
"summary": "One sentence overview",
"architecture": "High-level components + data flow",
"techStack": ["Next.js", "Tailwind", "Lucide Icons"],
"files": [
{"path": "app/page.tsx", "purpose": "Main UI"},
{"path": "components/Preview.tsx", "purpose": "Core logic"}
],
"timeline": "Estimate",
"risks": ["Potential blockers"]
}`,
generate: `You are a Senior Vibe Coder. Execute the following approved plan:
Plan: {plan}
Generate COMPLETE, PRODUCTION-READY code for all files.
Focus on the request: "{request}"
Output ONLY a JSON object:
{
"files": {
"app/page.tsx": "// code here",
"components/UI.tsx": "// more code"
},
"explanation": "How it works"
}`,
preview: `Convert the following code into a single-file interactive HTML preview (Standalone).
Use Tailwind CDN.
Code: {code}
Output ONLY valid HTML.`
};
export async function POST(req: NextRequest) {
const requestId = randomUUID();
try {
// Safe body parsing
const body = await req.json().catch(() => null);
if (!body) {
return NextResponse.json(
{ error: "Invalid JSON body", requestId, success: false },
{ status: 400 }
);
}
// Validate schema
const parseResult = schema.safeParse(body);
if (!parseResult.success) {
return NextResponse.json(
{
error: "Invalid request body",
details: parseResult.error.flatten(),
requestId,
success: false
},
{ status: 400 }
);
}
const { request, step, plan, code } = parseResult.data;
let prompt = STEPS[step];
prompt = prompt.replace("{request}", request);
if (plan) prompt = prompt.replace("{plan}", JSON.stringify(plan));
if (code) prompt = prompt.replace("{code}", code);
// Return the prompt for the frontend to use with the streaming adapter
return NextResponse.json({
prompt,
step,
requestId,
success: true
});
} catch (err: any) {
console.error(`[ai-assist] requestId=${requestId}`, err);
return NextResponse.json(
{
error: err?.message ?? "AI Assist failed",
requestId,
success: false
},
{ status: 500 }
);
}
}

37
app/api/slides/route.ts Normal file
View File

@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const schema = z.object({
topic: z.string().min(3),
slideCount: z.number().min(3).max(15).default(8),
style: z.enum(["professional", "creative", "technical", "pitch"]).default("professional"),
});
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { topic, slideCount, style } = schema.parse(body);
const systemPrompt = `You are an elite presentation designer. Create a visually stunning presentation with ${slideCount} slides about "${topic}".
Style: ${style}
Output ONLY a sequence of slides separated by "---".
Format each slide as:
## [Slide Title]
- [Bullet Point 1]
- [Bullet Point 2]
VISUAL: [Detailed description of image/chart/icon]
---
`;
// The frontend will handle the actual generation call to keep use of the ModelAdapter,
// this route serves as the prompt orchestrator.
return NextResponse.json({
prompt: systemPrompt,
success: true
});
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}

View File

@@ -3,18 +3,22 @@
import { useState, useEffect } from "react";
import Sidebar from "@/components/Sidebar";
import type { View } from "@/components/Sidebar";
import PromptEnhancer from "@/components/PromptEnhancer";
import PRDGenerator from "@/components/PRDGenerator";
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
import SlidesGenerator from "@/components/SlidesGenerator";
import GoogleAdsGenerator from "@/components/GoogleAdsGenerator";
import MarketResearcher from "@/components/MarketResearcher";
import AIAssist from "@/components/AIAssist";
import HistoryPanel from "@/components/HistoryPanel";
import SettingsPanel from "@/components/SettingsPanel";
import dynamic from 'next/dynamic';
import modelAdapter from "@/lib/services/adapter-instance";
// Dynamic imports to prevent hydration mismatches
// ensuring hydration match
const PromptEnhancer = dynamic(() => import("@/components/PromptEnhancer"), { ssr: false });
const PRDGenerator = dynamic(() => import("@/components/PRDGenerator"), { ssr: false });
const ActionPlanGenerator = dynamic(() => import("@/components/ActionPlanGenerator"), { ssr: false });
const UXDesignerPrompt = dynamic(() => import("@/components/UXDesignerPrompt"), { ssr: false });
const SlidesGenerator = dynamic(() => import("@/components/SlidesGenerator"), { ssr: false });
const GoogleAdsGenerator = dynamic(() => import("@/components/GoogleAdsGenerator"), { ssr: false });
const MarketResearcher = dynamic(() => import("@/components/MarketResearcher"), { ssr: false });
const AIAssist = dynamic(() => import("@/components/AIAssist"), { ssr: false });
const HistoryPanel = dynamic(() => import("@/components/HistoryPanel"), { ssr: false });
const SettingsPanel = dynamic(() => import("@/components/SettingsPanel"), { ssr: false });
export default function Home() {
const [currentView, setCurrentView] = useState<View>("enhance");

File diff suppressed because it is too large Load Diff

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

@@ -7,9 +7,10 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import useStore from "@/lib/store";
import modelAdapter from "@/lib/services/adapter-instance";
import { Megaphone, Copy, Loader2, CheckCircle2, Settings, Plus, X, ChevronDown, ChevronUp, Wand2, Target, TrendingUp, ShieldAlert, BarChart3, Users, Rocket } from "lucide-react";
import { Megaphone, Copy, Loader2, CheckCircle2, Settings, Plus, X, ChevronDown, ChevronUp, Wand2, Target, TrendingUp, ShieldAlert, BarChart3, Users, Rocket, Download, FileSpreadsheet } from "lucide-react";
import { cn } from "@/lib/utils";
import { GoogleAdsResult } from "@/types";
import { downloadFile, generateGoogleAdsCSV, generateGoogleAdsHTML, generateGoogleAdsExcel } from "@/lib/export-utils";
import { translations } from "@/lib/i18n/translations";
export default function GoogleAdsGenerator() {
@@ -49,6 +50,9 @@ export default function GoogleAdsGenerator() {
const [expandedSections, setExpandedSections] = useState<string[]>(["keywords"]);
const [isMagicThinking, setIsMagicThinking] = useState(false);
const [isEnhancing, setIsEnhancing] = useState(false);
const [isEnhancingAudience, setIsEnhancingAudience] = useState(false);
const [isEnhancingIndustry, setIsEnhancingIndustry] = useState(false);
const [progressMessage, setProgressMessage] = useState("");
const [progressIndex, setProgressIndex] = useState(0);
@@ -56,37 +60,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 +133,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 +146,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 +214,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.errorGenerate);
} finally {
setProcessing(false);
}
@@ -256,12 +230,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 +243,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,15 +290,71 @@ 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);
}
};
const handleEnhanceInstructions = async () => {
if (!specialInstructions.trim()) return;
setIsEnhancing(true);
setError(null);
try {
const result = await modelAdapter.enhancePrompt(
specialInstructions,
selectedProvider,
selectedModel
);
if (result.success && result.data) {
setSpecialInstructions(result.data);
} else {
setError(result.error || "Failed to enhance instructions");
}
} catch (err) {
console.error("[GoogleAdsGenerator] Enhancement error:", err);
setError(err instanceof Error ? err.message : "Instruction enhancement failed");
} finally {
setIsEnhancing(false);
}
};
const handleEnhanceAudience = async () => {
if (!targetAudience.trim()) return;
setIsEnhancingAudience(true);
setError(null);
try {
const result = await modelAdapter.enhancePrompt(targetAudience, selectedProvider, selectedModel);
if (result.success && result.data) setTargetAudience(result.data);
else setError(result.error || "Failed to enhance audience");
} catch (err) {
setError(err instanceof Error ? err.message : "Audience enhancement failed");
} finally {
setIsEnhancingAudience(false);
}
};
const handleEnhanceIndustry = async () => {
if (!industry.trim()) return;
setIsEnhancingIndustry(true);
setError(null);
try {
const result = await modelAdapter.enhancePrompt(industry, selectedProvider, selectedModel);
if (result.success && result.data) setIndustry(result.data);
else setError(result.error || "Failed to enhance industry");
} catch (err) {
setError(err instanceof Error ? err.message : "Industry enhancement failed");
} finally {
setIsEnhancingIndustry(false);
}
};
const handleCopy = async () => {
const content = googleAdsResult?.rawContent || magicWandResult?.rawContent;
if (content) {
@@ -334,11 +364,23 @@ export default function GoogleAdsGenerator() {
}
};
const exportExcel = () => {
if (!googleAdsResult && !magicWandResult) return;
const excelBlob = generateGoogleAdsExcel(googleAdsResult || undefined, magicWandResult || undefined);
downloadFile(`google-ads-full-intel-${new Date().toISOString().split('T')[0]}.xlsx`, excelBlob, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
};
const exportHTML = () => {
if (!googleAdsResult && !magicWandResult) return;
const htmlContent = generateGoogleAdsHTML(googleAdsResult || undefined, magicWandResult || undefined);
downloadFile(`google-ads-intelligence-report-${new Date().toISOString().split('T')[0]}.html`, htmlContent, 'text/html');
};
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 +393,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 +412,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 +426,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 +446,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 +481,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 +516,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 +534,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 +566,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 +575,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 +583,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 +595,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 +623,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 +631,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 +646,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 +662,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 +690,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 +718,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 +729,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 +744,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 +759,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 +772,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 +784,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 +792,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 +855,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 +869,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 +886,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 +895,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>
@@ -880,7 +922,23 @@ export default function GoogleAdsGenerator() {
</div>
</div>
<div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{t.industry}</label>
<div className="flex items-center justify-between">
<label className="text-xs lg:text-sm font-medium">{t.industry}</label>
<Button
variant="ghost"
size="sm"
className="h-5 px-1.5 text-[9px] font-black uppercase tracking-wider text-indigo-600 hover:text-indigo-700 hover:bg-indigo-50 gap-1 transition-all"
onClick={handleEnhanceIndustry}
disabled={isEnhancingIndustry || !industry.trim()}
>
{isEnhancingIndustry ? (
<Loader2 className="h-2.5 w-2.5 animate-spin" />
) : (
<Wand2 className="h-2.5 w-2.5" />
)}
Enhance
</Button>
</div>
<Input
placeholder="e.g., SaaS"
value={industry}
@@ -891,9 +949,25 @@ export default function GoogleAdsGenerator() {
</div>
<div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
<div className="flex items-center justify-between">
<label className="text-xs lg:text-sm font-medium">{t.targetAudience}</label>
<Button
variant="ghost"
size="sm"
className="h-5 px-1.5 text-[9px] font-black uppercase tracking-wider text-indigo-600 hover:text-indigo-700 hover:bg-indigo-50 gap-1 transition-all"
onClick={handleEnhanceAudience}
disabled={isEnhancingAudience || !targetAudience.trim()}
>
{isEnhancingAudience ? (
<Loader2 className="h-2.5 w-2.5 animate-spin" />
) : (
<Wand2 className="h-2.5 w-2.5" />
)}
AI Audience enhancer
</Button>
</div>
<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"
@@ -901,7 +975,23 @@ export default function GoogleAdsGenerator() {
</div>
<div className="space-y-2">
<label className="text-xs lg:text-sm font-medium">{t.specialInstructions}</label>
<div className="flex items-center justify-between">
<label className="text-xs lg:text-sm font-medium">{t.specialInstructions}</label>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-[10px] font-black uppercase tracking-widest text-indigo-600 hover:text-indigo-700 hover:bg-indigo-50 gap-1.5 transition-all"
onClick={handleEnhanceInstructions}
disabled={isEnhancing || !specialInstructions.trim()}
>
{isEnhancing ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Wand2 className="h-3 w-3" />
)}
AI Instructions enhancer
</Button>
</div>
<Textarea
placeholder={t.specialInstructionsPlaceholder}
value={specialInstructions}
@@ -1004,19 +1094,27 @@ export default function GoogleAdsGenerator() {
{magicWandResult ? t.strategicDirections : t.generatedCampaign}
</span>
{(googleAdsResult || magicWandResult) && (
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9">
{copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)}
</Button>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8 lg:h-9 lg:w-9" title="Copy to clipboard">
{copied ? (
<CheckCircle2 className="h-3.5 w-3.5 lg:h-4 lg:w-4 text-green-500" />
) : (
<Copy className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
)}
</Button>
<Button variant="ghost" size="icon" onClick={exportExcel} className="h-8 w-8 lg:h-9 lg:w-9 text-emerald-500 hover:text-emerald-600 hover:bg-emerald-50" title="Download Full Intel Report (XLSX)">
<FileSpreadsheet className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={exportHTML} className="h-8 w-8 lg:h-9 lg:w-9 text-indigo-500 hover:text-indigo-600 hover:bg-indigo-50" title="Download Intelligence Dashboard (HTML)">
<Download className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
</Button>
</div>
)}
</CardTitle>
<CardDescription className="text-xs lg:text-sm">
{magicWandResult
? (language === "ru" ? "Глубокое исследование конкурентов и темы кампаний" : language === "he" ? "מחקר תחרותי מעמיק ונושאי קמפיין" : "Deep competitive research and campaign themes")
: (language === "ru" ? "Ключевые слова, объявления и структура кампании" : language === "he" ? "מילות מפתח, עותקי מודעות ומבנה קמפיין מוכנים" : "Keywords, ad copy, and campaign structure ready")
? t.strategicDirections
: t.generatedCampaign
}
</CardDescription>
</CardHeader>
@@ -1056,7 +1154,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">
<a href="https://github.rommark.dev/admin/PromptArch" 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">
<a href="https://github.rommark.dev/admin/PromptArch" 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>
@@ -120,11 +120,11 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
<>
{/* Mobile Header */}
<div className="lg:hidden fixed top-0 left-0 right-0 z-50 flex items-center justify-between border-b bg-card px-4 py-3">
<a href="https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
<a href="https://github.rommark.dev/admin/PromptArch" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
<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;">
@@ -623,6 +623,27 @@ export default function SlidesGenerator() {
}
};
const handleDownloadMarkdown = () => {
if (!slidesPresentation) return;
// Reveal.js format
const markdown = slidesPresentation.slides.map(s => {
// Strip HTML but keep structure
const cleanContent = s.content.replace(/<[^>]*>/g, '').trim();
return `## ${s.title}\n\n${cleanContent}`;
}).join('\n\n---\n\n');
const blob = new Blob([markdown], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${slidesPresentation.title.replace(/\s+/g, '_')}_slides.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleDownloadHtml = () => {
if (!slidesPresentation) return;
@@ -906,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}
@@ -920,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(
@@ -948,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>
@@ -985,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>
@@ -1028,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>
@@ -1037,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>
@@ -1071,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>
@@ -1084,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 */}
@@ -1103,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>
@@ -1112,15 +1133,16 @@ 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}
onChange={(e) => setSlideCount(parseInt(e.target.value))}
className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<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>
@@ -1129,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"
@@ -1162,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>
@@ -1195,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">
@@ -1205,9 +1227,12 @@ export default function SlidesGenerator() {
<Button variant="ghost" size="icon" onClick={toggleFullscreen} className="h-8 w-8">
{isFullscreen ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
</Button>
<Button variant="ghost" size="icon" onClick={handleDownloadHtml} className="h-8 w-8">
<Button variant="ghost" size="icon" onClick={handleDownloadHtml} title="Download HTML Presentation" className="h-8 w-8">
<Download className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" onClick={handleDownloadMarkdown} title="Export Reveal.js Markdown" className="h-8 w-8 text-blue-500">
<FileText className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8">
{copied ? <CheckCircle2 className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
</Button>
@@ -1216,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>
@@ -1296,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>
@@ -1310,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">

108
lib/artifact-utils.ts Normal file
View File

@@ -0,0 +1,108 @@
import JSZip from "jszip";
import { saveAs } from "file-saver";
export async function downloadArtifactAsZip(data: string, type: string, language: string = "html") {
const zip = new JSZip();
const extension = language === "html" || type === "web" || type === "app" ? "html" : (language === "typescript" || language === "tsx" ? "tsx" : "txt");
const filename = `artifact-${Date.now()}.${extension}`;
// Check if data contains common multi-file structures (simple heuristic)
// If it looks like a full project (multiple files defined in one block), we could parse it,
// but for now we'll just save the main artifact.
zip.file(filename, data);
// Add a basic README
zip.file("README.md", `# AI Generated Artifact\n\nType: ${type}\nGenerated: ${new Date().toLocaleString()}`);
const content = await zip.generateAsync({ type: "blob" });
saveAs(content, `promptarch-artifact-${Date.now()}.zip`);
}
export async function pushToGithub(
token: string,
repoName: string,
files: { path: string; content: string }[],
description: string = "Generated by PromptArch"
) {
const headers = {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
};
// 1. Check if repo exists, if not create it
let repoData;
const userResponse = await fetch('https://api.github.com/user', { headers });
if (!userResponse.ok) throw new Error("Failed to authenticate with GitHub");
const userData = await userResponse.json();
const username = userData.login;
const repoCheckResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}`, { headers });
if (repoCheckResponse.status === 404) {
// Create repo
const createResponse = await fetch('https://api.github.com/user/repos', {
method: 'POST',
headers,
body: JSON.stringify({
name: repoName,
description,
auto_init: true
})
});
if (!createResponse.ok) throw new Error("Failed to create repository");
repoData = await createResponse.json();
// Wait a bit for repo to be ready
await new Promise(resolve => setTimeout(resolve, 2000));
} else {
repoData = await repoCheckResponse.json();
}
// 2. Get latest commit SHA of default branch
const branchResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/branches/${repoData.default_branch}`, { headers });
const branchData = await branchResponse.json();
const latestCommitSha = branchData.commit.sha;
// 3. Create a new tree
const treeItems = files.map(file => ({
path: file.path,
mode: '100644',
type: 'blob',
content: file.content
}));
const treeResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/trees`, {
method: 'POST',
headers,
body: JSON.stringify({
base_tree: latestCommitSha,
tree: treeItems
})
});
const treeData = await treeResponse.json();
// 4. Create a new commit
const commitResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/commits`, {
method: 'POST',
headers,
body: JSON.stringify({
message: `Update from PromptArch: ${new Date().toISOString()}`,
tree: treeData.sha,
parents: [latestCommitSha]
})
});
const commitData = await commitResponse.json();
// 5. Update the reference
const refResponse = await fetch(`https://api.github.com/repos/${username}/${repoName}/git/refs/heads/${repoData.default_branch}`, {
method: 'PATCH',
headers,
body: JSON.stringify({
sha: commitData.sha
})
});
if (!refResponse.ok) throw new Error("Failed to update branch reference");
return { url: repoData.html_url };
}

571
lib/export-utils.ts Normal file
View File

@@ -0,0 +1,571 @@
import * as XLSX from 'xlsx';
import { GoogleAdsResult, MagicWandResult } from "../types";
export const downloadFile = (filename: string, content: any, contentType: string) => {
if (typeof window === 'undefined') return;
const blob = content instanceof Blob ? content : new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
export const generateGoogleAdsCSV = (googleAds?: any, magic?: any): string => {
const rows: string[][] = [];
if (googleAds) {
rows.push(["GOOGLE ADS STRATEGY REPORT"]);
rows.push(["Generated at", new Date().toLocaleString()]);
rows.push(["Website", googleAds.websiteUrl || 'N/A']);
rows.push([]);
const kw = googleAds.keywords;
if (kw) {
rows.push(["KEYWORD RESEARCH"]);
rows.push(["Type", "Keyword", "CPC", "Volume", "Competition"]);
if (Array.isArray(kw.primary)) kw.primary.forEach((k: any) => rows.push(["Primary", String(k?.keyword || ''), String(k?.cpc || ''), String(k?.searchVolume || ''), String(k?.competition || '')]));
if (Array.isArray(kw.longTail)) kw.longTail.forEach((k: any) => rows.push(["Long-tail", String(k?.keyword || ''), String(k?.cpc || ''), String(k?.searchVolume || ''), String(k?.competition || '')]));
if (Array.isArray(kw.negative)) kw.negative.forEach((k: any) => rows.push(["Negative", String(k?.keyword || ''), "", "", String(k?.competition || '')]));
rows.push([]);
}
const ads = googleAds.adCopies;
if (Array.isArray(ads)) {
rows.push(["AD COPIES"]);
rows.push(["Variation", "Headlines", "Descriptions", "CTA", "Optimized", "Positioning"]);
ads.forEach((ad: any, i: number) => {
const hl = Array.isArray(ad.headlines) ? ad.headlines.join(' | ') : '';
const ds = Array.isArray(ad.descriptions) ? ad.descriptions.join(' | ') : '';
rows.push([`Var ${i + 1}`, hl, ds, String(ad?.callToAction || ''), String(ad?.mobileOptimized || 'false'), String(ad?.positioning || '')]);
});
rows.push([]);
}
const camps = googleAds.campaigns;
if (Array.isArray(camps)) {
rows.push(["CAMPAIGN STRUCTURE"]);
rows.push(["Name", "Type", "Budget", "Locations", "Targeting", "Bidding"]);
camps.forEach((c: any) => {
const t = c.targeting;
const locs = (t && Array.isArray(t.locations)) ? t.locations.join('; ') : 'All';
const demos = (t && t.demographics) ? `Age: ${t.demographics.age?.join(', ') || 'Any'}; Gender: ${t.demographics.gender?.join(', ') || 'Any'}` : 'All';
rows.push([String(c.name || ''), String(c.type || ''), `${c?.budget?.daily || 0} ${c?.budget?.currency || ''}`, locs, demos, String(c.biddingStrategy || '')]);
});
rows.push([]);
}
}
if (magic) {
rows.push(["MARKET INTELLIGENCE"]);
const ma = magic.marketAnalysis;
if (ma) {
rows.push(["Size", String(ma.industrySize || '')]);
rows.push(["Growth", String(ma.growthRate || '')]);
rows.push(["Trends", Array.isArray(ma.marketTrends) ? ma.marketTrends.join('; ') : '']);
rows.push(["Competitors", Array.isArray(ma.topCompetitors) ? ma.topCompetitors.join('; ') : '']);
rows.push([]);
}
const strats = magic.strategies;
if (Array.isArray(strats)) {
rows.push(["STRATEGIES"]);
strats.forEach((s: any) => {
rows.push(["Direction", String(s.direction || '')]);
rows.push(["Target", String(s.targetAudience || '')]);
rows.push(["ROI", String(s.expectedROI || '')]);
rows.push(["Risk", String(s.riskLevel || '')]);
rows.push(["Timeframe", String(s.timeToResults || '')]);
rows.push([]);
});
}
}
return rows.map(r => r.map(c => `"${String(c || '').replace(/"/g, '""')}"`).join(",")).join("\n");
};
export const generateGoogleAdsExcel = (googleAds?: any, magic?: any): Blob => {
const wb = XLSX.utils.book_new();
// 1. Overview Sheet
const overviewData: any[] = [
["Attribute", "Value"],
["Report Title", "Google Ads Strategy Report"],
["Generated At", new Date().toLocaleString()],
["Website URL", googleAds?.websiteUrl || magic?.websiteUrl || 'N/A'],
[],
["Performance Forecasts"],
["Est. Impressions", googleAds?.predictions?.estimatedImpressions || 'N/A'],
["Est. Clicks", googleAds?.predictions?.estimatedClicks || 'N/A'],
["Est. CTR", googleAds?.predictions?.estimatedCtr || 'N/A'],
["Est. Conversions", googleAds?.predictions?.estimatedConversions || 'N/A'],
["Est. Conversion Rate", googleAds?.predictions?.conversionRate || 'N/A'],
["Avg. CPC", googleAds?.predictions?.avgCpc || 'N/A'],
[],
["Historical Benchmarks"],
["Avg. Industry CTR", googleAds?.historicalBenchmarks?.industryAverageCtr || 'N/A'],
["Avg. Industry CPC", googleAds?.historicalBenchmarks?.industryAverageCpc || 'N/A'],
["Seasonal Trends", googleAds?.historicalBenchmarks?.seasonalTrends || 'N/A'],
["Geographic Insights", googleAds?.historicalBenchmarks?.geographicInsights || 'N/A']
];
const overviewSheet = XLSX.utils.aoa_to_sheet(overviewData);
XLSX.utils.book_append_sheet(wb, overviewSheet, "Overview");
// 2. Keywords Sheet
if (googleAds?.keywords) {
const kw = googleAds.keywords;
const kwData: any[] = [
["Type", "Keyword", "Avg. CPC", "Monthly Volume", "Competition", "Difficulty"]
];
if (Array.isArray(kw.primary)) kw.primary.forEach((k: any) => kwData.push(["Primary", k.keyword, k.cpc, k.searchVolume, k.competition, k.difficultyScore]));
if (Array.isArray(kw.longTail)) kw.longTail.forEach((k: any) => kwData.push(["Long-tail", k.keyword, k.cpc, k.searchVolume, k.competition, k.difficultyScore]));
if (Array.isArray(kw.negative)) kw.negative.forEach((k: any) => kwData.push(["Negative", k.keyword, "", "", k.competition, ""]));
const kwSheet = XLSX.utils.aoa_to_sheet(kwData);
XLSX.utils.book_append_sheet(wb, kwSheet, "Keywords");
}
// 3. Ad Copies Sheet
if (Array.isArray(googleAds?.adCopies)) {
const adData: any[] = [
["ID", "Variation", "Headlines", "Descriptions", "CTA", "Mobile Optimized", "Strategic Positioning"]
];
googleAds.adCopies.forEach((ad: any, i: number) => {
adData.push([
ad.id,
`Variation ${i + 1}`,
(ad.headlines || []).join(" | "),
(ad.descriptions || []).join(" | "),
ad.callToAction,
ad.mobileOptimized ? "Yes" : "No",
ad.positioning || "N/A"
]);
});
const adSheet = XLSX.utils.aoa_to_sheet(adData);
XLSX.utils.book_append_sheet(wb, adSheet, "Ad Copies");
}
// 4. Competitors Sheet
if (magic?.competitorInsights || magic?.marketAnalysis) {
const compData: any[] = [
["Competitor", "Website", "Est. Monthly Spend", "Target Audience", "Strengths", "Weaknesses", "Ad Strategy", "Top Keywords"]
];
if (Array.isArray(magic.competitorInsights)) {
magic.competitorInsights.forEach((c: any) => {
compData.push([
c.competitor,
c.website || 'N/A',
c.estimatedSpend || 'N/A',
c.targetAudience || 'N/A',
(c.strengths || []).join(", "),
(c.weaknesses || []).join(", "),
c.adStrategy,
(c.topKeywords || []).join(", ")
]);
});
}
const compSheet = XLSX.utils.aoa_to_sheet(compData);
XLSX.utils.book_append_sheet(wb, compSheet, "Competitor Analysis");
}
// 5. Strategies Sheet
if (Array.isArray(magic?.strategies)) {
const stratData: any[] = [
["ID", "Strategic Direction", "Rationale", "Target Audience", "Competitive Advantage", "Expected ROI", "Risk Level", "Time to Results", "Success Metrics"]
];
magic.strategies.forEach((s: any) => {
stratData.push([
s.id,
s.direction,
s.rationale,
s.targetAudience,
s.competitiveAdvantage,
s.expectedROI,
s.riskLevel,
s.timeToResults,
(s.successMetrics || []).join(", ")
]);
});
const stratSheet = XLSX.utils.aoa_to_sheet(stratData);
XLSX.utils.book_append_sheet(wb, stratSheet, "Strategies");
}
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
return new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
};
export const generateGoogleAdsHTML = (googleAds?: any, magic?: any): string => {
const data = JSON.stringify({ googleAds, magic }, null, 2);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>360° Google Ads Strategy Intelligence Report</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Outfit', sans-serif; background-color: #020617; color: #f8fafc; }
.glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); }
.accent-gradient { background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); }
.text-gradient { background: linear-gradient(to right, #818cf8, #c084fc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
</style>
</head>
<body class="p-4 md:p-8 lg:p-12">
<div class="max-w-7xl mx-auto space-y-12">
<!-- Header -->
<header class="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 pb-8 border-b border-slate-800">
<div>
<h1 class="text-4xl md:text-6xl font-extrabold text-gradient tracking-tight">Strategy Intelligence</h1>
<p class="text-slate-400 mt-2 text-lg">360° Campaign Architecture for ${googleAds?.websiteUrl || 'New Project'}</p>
</div>
<div class="glass p-4 rounded-3xl flex items-center gap-4">
<div class="text-right">
<p class="text-xs text-slate-500 uppercase font-extrabold tracking-widest">Generated On</p>
<p class="font-semibold">${new Date().toLocaleDateString()}</p>
</div>
<div class="w-12 h-12 accent-gradient rounded-2xl flex items-center justify-center font-bold text-xl">PA</div>
</div>
</header>
<!-- KPI Grid -->
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="glass p-6 rounded-[2rem] border-indigo-500/20">
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Est. Monthly Impressions</p>
<p class="text-3xl font-extrabold mt-2 text-indigo-400">${googleAds?.predictions?.estimatedImpressions || '15k-25k'}</p>
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-indigo-500 w-2/3"></div></div>
</div>
<div class="glass p-6 rounded-[2rem] border-purple-500/20">
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Target CTR</p>
<p class="text-3xl font-extrabold mt-2 text-purple-400">${googleAds?.predictions?.estimatedCtr || '3.5%'}</p>
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-purple-500 w-1/2"></div></div>
</div>
<div class="glass p-6 rounded-[2rem] border-emerald-500/20">
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Est. Conversions</p>
<p class="text-3xl font-extrabold mt-2 text-emerald-400">${googleAds?.predictions?.estimatedConversions || '30-50'}</p>
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-emerald-500 w-1/3"></div></div>
</div>
<div class="glass p-6 rounded-[2rem] border-amber-500/20">
<p class="text-slate-500 text-xs font-bold uppercase tracking-widest">Industry Avg CTR</p>
<p class="text-3xl font-extrabold mt-2 text-amber-400">${googleAds?.historicalBenchmarks?.industryAverageCtr || '3.1%'}</p>
<div class="mt-4 h-1 w-full bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-amber-500 w-4/5"></div></div>
</div>
</section>
<!-- Analytics Visuals -->
<section class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 glass p-8 rounded-[2.5rem]">
<h2 class="text-2xl font-extrabold mb-6">Performance Forecast</h2>
<div class="h-[350px]">
<canvas id="performanceChart"></canvas>
</div>
</div>
<div class="glass p-8 rounded-[2.5rem]">
<h2 class="text-2xl font-extrabold mb-6">Device Distribution</h2>
<div class="h-[300px]">
<canvas id="deviceChart"></canvas>
</div>
<div class="mt-8 space-y-4">
<div class="flex justify-between items-center text-sm">
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-indigo-500"></div> Mobile</span>
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.mobile || '60%'}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-purple-500"></div> Desktop</span>
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.desktop || '30%'}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-slate-500"></div> Tablet</span>
<span class="font-bold">${googleAds?.campaigns?.[0]?.targeting?.devices?.tablet || '10%'}</span>
</div>
</div>
</div>
</section>
<!-- Keyword Intelligence -->
<section class="space-y-6">
<div class="flex justify-between items-center">
<h2 class="text-3xl font-extrabold tracking-tight">Keyword Intelligence</h2>
<span class="glass px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest text-indigo-400">Semantic Mapping Enabled</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Primary -->
<div class="space-y-4">
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-indigo-500"></div> Primary Keywords
</h3>
${(googleAds?.keywords?.primary || []).slice(0, 10).map((k: any) => `
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-indigo-500/50 transition-all cursor-default">
<span class="font-semibold">${k.keyword}</span>
<span class="text-xs text-indigo-400 font-bold">${k.cpc || '$1.50'}</span>
</div>
`).join('')}
</div>
<!-- Long-Tail -->
<div class="space-y-4">
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-purple-500"></div> Long-Tail Intent
</h3>
${(googleAds?.keywords?.longTail || []).slice(0, 10).map((k: any) => `
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-purple-500/50 transition-all cursor-default">
<span class="font-semibold text-sm">${k.keyword}</span>
<span class="text-[10px] text-slate-500 font-bold">${k.competition || 'low'}</span>
</div>
`).join('')}
</div>
<!-- Negative -->
<div class="space-y-4">
<h3 class="text-slate-400 font-bold uppercase tracking-widest text-xs flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-rose-500"></div> Exclusion List
</h3>
${(googleAds?.keywords?.negative || []).slice(0, 8).map((k: any) => `
<div class="glass p-4 rounded-2xl flex justify-between items-center group hover:border-rose-500/50 transition-all cursor-default grayscale opacity-50">
<span class="font-semibold text-xs line-through text-slate-400">${k.keyword}</span>
<span class="text-[10px] text-rose-500/50 font-bold">EXCLUDE</span>
</div>
`).join('')}
</div>
</div>
</section>
<!-- Ad Copies -->
<section class="space-y-8">
<h2 class="text-3xl font-extrabold tracking-tight">High-Performance Ad Copy Suite</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
${(googleAds?.adCopies || []).map((ad: any, i: number) => `
<div class="glass rounded-[2rem] overflow-hidden flex flex-col">
<div class="p-6 border-b border-slate-800 flex justify-between items-center">
<span class="text-xs font-bold uppercase tracking-widest text-slate-500">Variation ${i + 1}</span>
<span class="bg-indigo-500/20 text-indigo-400 px-2.5 py-1 rounded-full text-[10px] font-black">${(ad.campaignType || 'Search').toUpperCase()}</span>
</div>
<div class="p-8 space-y-6 flex-grow">
${(ad.headlines || []).map((h: any) => `
<div class="text-xl font-bold text-slate-100 leading-tight">${h}</div>
`).join('')}
<div class="h-px w-full bg-slate-800"></div>
${(ad.descriptions || []).map((d: any) => `
<div class="text-slate-400 text-sm leading-relaxed italic border-l-2 border-slate-700 pl-4">${d}</div>
`).join('')}
</div>
<div class="p-6 bg-slate-900/50 border-t border-slate-800 flex justify-between items-center">
<div class="text-xs font-bold text-indigo-400">${ad.callToAction}</div>
<div class="flex gap-1">
<div class="w-1 h-3 rounded-full ${ad.mobileOptimized ? 'bg-emerald-500' : 'bg-slate-700'}"></div>
<div class="w-3 h-3 rounded-full ${ad.mobileOptimized ? 'bg-emerald-500' : 'bg-slate-700'}"></div>
</div>
</div>
</div>
`).join('')}
</div>
</section>
<!-- Competitive Analysis -->
${magic?.competitorInsights ? `
<section class="space-y-8">
<div class="flex justify-between items-end">
<h2 class="text-3xl font-extrabold tracking-tight">Competitive Intelligence Matrix</h2>
<div class="text-right">
<p class="text-[10px] text-slate-500 uppercase font-black tracking-[0.2em] mb-1">Market Sentiment</p>
<div class="flex gap-1.5 justify-end">
<div class="w-4 h-2 rounded-full bg-emerald-500"></div>
<div class="w-4 h-2 rounded-full bg-emerald-500"></div>
<div class="w-4 h-2 rounded-full bg-emerald-400"></div>
<div class="w-4 h-2 rounded-full bg-slate-800"></div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
${(magic.competitorInsights || []).map((c: any) => `
<div class="glass p-8 rounded-[2.5rem] space-y-8">
<div class="flex justify-between items-start">
<div>
<h3 class="text-2xl font-black">${c.competitor}</h3>
<p class="text-indigo-400 text-xs font-bold mt-1">${c.website || 'Direct Competitor'}</p>
</div>
<div class="text-right">
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Est. Spend</p>
<p class="text-sm font-black text-rose-400">${c.estimatedSpend || 'Undisclosed'}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-6">
<div class="space-y-3">
<p class="text-[10px] text-emerald-400 uppercase font-black tracking-widest">Core Strengths</p>
<ul class="text-sm space-y-2 text-slate-300">
${(c.strengths || []).map((s: any) => `<li class="flex items-center gap-2"><div class="w-1.5 h-1.5 rounded-full bg-emerald-500"></div> ${s}</li>`).join('')}
</ul>
</div>
<div class="space-y-3">
<p class="text-[10px] text-rose-400 uppercase font-black tracking-widest">Weaknesses</p>
<ul class="text-sm space-y-2 text-slate-400">
${(c.weaknesses || []).map((w: any) => `<li class="flex items-center gap-2"><div class="w-1.5 h-1.5 rounded-full bg-rose-500"></div> ${w}</li>`).join('')}
</ul>
</div>
</div>
<div class="bg-indigo-500/5 p-6 rounded-2xl border border-indigo-500/10">
<p class="text-[10px] text-indigo-400 uppercase font-bold tracking-widest mb-3">Counter-Strategy Rationale</p>
<p class="text-sm text-slate-300 italic leading-relaxed">"${c.adStrategy}"</p>
</div>
</div>
`).join('')}
</div>
</section>
` : ''}
<!-- Strategies -->
${magic?.strategies ? `
<section class="space-y-8">
<h2 class="text-3xl font-extrabold tracking-tight">Strategic Directions</h2>
<div class="space-y-8">
${(magic.strategies || []).map((s: any, idx: number) => `
<div class="glass overflow-hidden rounded-[3rem] relative">
<div class="absolute top-0 right-0 w-64 h-64 accent-gradient blur-[120px] opacity-10"></div>
<div class="p-10 relative z-10 grid grid-cols-1 lg:grid-cols-4 gap-12">
<!-- Left: Header -->
<div class="lg:col-span-1 space-y-6">
<div class="w-16 h-16 rounded-3xl accent-gradient flex items-center justify-center font-black text-2xl">${idx + 1}</div>
<div>
<h3 class="text-2xl font-extrabold leading-tight">${s.direction}</h3>
<span class="inline-block mt-4 glass px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${s.riskLevel === 'low' ? 'text-emerald-400' : 'text-amber-400'}">${s.riskLevel || 'low'} risk profile</span>
</div>
<div class="pt-6 space-y-4">
<div>
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Expected ROI</p>
<p class="text-xl font-black text-emerald-400">${s.expectedROI}</p>
</div>
<div>
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Time to Impact</p>
<p class="text-xl font-black text-slate-100">${s.timeToResults}</p>
</div>
</div>
</div>
<!-- Middle: Context -->
<div class="lg:col-span-2 space-y-8">
<div>
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-4">Strategic Rationale</p>
<p class="text-lg text-slate-200 leading-relaxed font-light">${s.rationale}</p>
</div>
<div class="grid grid-cols-2 gap-8">
<div class="bg-white/5 p-6 rounded-[2rem]">
<p class="text-[10px] text-indigo-400 uppercase font-black tracking-widest mb-3">Target Audience</p>
<p class="text-sm font-semibold">${s.targetAudience}</p>
</div>
<div class="bg-white/5 p-6 rounded-[2rem]">
<p class="text-[10px] text-purple-400 uppercase font-black tracking-widest mb-3">Competitive Edge</p>
<p class="text-sm font-semibold">${s.competitiveAdvantage}</p>
</div>
</div>
</div>
<!-- Right: Channels/Metrics -->
<div class="lg:col-span-1 bg-slate-900/80 p-8 rounded-[2.5rem] space-y-8 border border-white/5">
<div>
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-6">Channel Matrix</p>
<div class="space-y-4">
${Object.entries(s.estimatedBudgetAllocation || {}).map(([c, v]: [string, any]) => `
<div class="space-y-2">
<div class="flex justify-between text-xs">
<span class="capitalize text-slate-300">${c}</span>
<span class="font-bold text-slate-100">${v}%</span>
</div>
<div class="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
<div class="h-full accent-gradient" style="width: ${v}%"></div>
</div>
</div>
`).join('')}
</div>
</div>
<div class="pt-6 border-t border-slate-800">
<p class="text-[10px] text-slate-500 uppercase font-bold tracking-widest mb-4">Success Thresholds</p>
<div class="flex flex-wrap gap-2 text-[10px]">
${(s.successMetrics || []).map((m: any) => `
<span class="bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-1 rounded-md font-bold uppercase">${m}</span>
`).join('')}
</div>
</div>
</div>
</div>
</div>
`).join('')}
</div>
</section>
` : ''}
<!-- Footer -->
<footer class="pt-12 border-t border-slate-800 flex flex-col md:flex-row justify-between items-center gap-6 pb-12">
<p class="text-slate-500 text-sm font-semibold">© ${new Date().getFullYear()} PromptArch Intelligence Unit • Confident Strategic Asset</p>
<div class="flex gap-4">
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-xs font-bold">PA</div>
<div class="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-xs font-bold italic">Q</div>
</div>
</footer>
</div>
<script>
// Performance Forecast Chart
const ctxPerf = document.getElementById('performanceChart').getContext('2d');
new Chart(ctxPerf, {
type: 'line',
data: {
labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4', 'Week 5', 'Week 6', 'Week 12'],
datasets: [{
label: 'Predicted Growth (Aggressive)',
data: [10, 25, 45, 80, 110, 160, 450],
borderColor: '#6366f1',
backgroundColor: 'rgba(99, 102, 241, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 6,
pointBackgroundColor: '#fff'
}, {
label: 'Predicted Growth (Standard)',
data: [5, 12, 28, 55, 75, 100, 280],
borderColor: '#a855f7',
backgroundColor: 'transparent',
borderDash: [5, 5],
tension: 0.4,
pointRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { labels: { color: '#94a3b8', font: { family: 'Outfit', weight: 'bold' } } } },
scales: {
x: { grid: { display: false }, ticks: { color: '#475569' } },
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#475569' } }
}
}
});
// Device Chart
const ctxDev = document.getElementById('deviceChart').getContext('2d');
new Chart(ctxDev, {
type: 'doughnut',
data: {
labels: ['Mobile', 'Desktop', 'Tablet'],
datasets: [{
data: [60, 30, 10],
backgroundColor: ['#6366f1', '#a855f7', '#475569'],
borderWidth: 0,
hoverOffset: 12
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '80%',
plugins: { legend: { display: false } }
}
});
</script>
</body>
</html>`;
};

File diff suppressed because it is too large Load Diff

73
lib/safeJsonFetch.ts Normal file
View File

@@ -0,0 +1,73 @@
export class NonJsonResponseError extends Error {
status: number;
contentType: string | null;
bodyPreview: string;
constructor(args: { status: number; contentType: string | null; bodyPreview: string }) {
super(`Expected JSON but received ${args.contentType ?? "unknown content-type"} (HTTP ${args.status})`);
this.name = "NonJsonResponseError";
this.status = args.status;
this.contentType = args.contentType;
this.bodyPreview = args.bodyPreview;
}
}
type SafeJsonFetchResult<T> =
| { ok: true; data: T }
| { ok: false; error: { message: string; status?: number; bodyPreview?: string } };
export async function safeJsonFetch<T>(
url: string,
init?: RequestInit
): Promise<SafeJsonFetchResult<T>> {
const res = await fetch(url, init);
const contentType = res.headers.get("content-type");
const text = await res.text();
// HTTP error — return readable details (don't JSON.parse blindly)
if (!res.ok) {
// Try JSON first if it looks like JSON
if (contentType?.includes("application/json")) {
try {
const parsed = JSON.parse(text);
return { ok: false, error: { message: parsed?.error ?? "Request failed", status: res.status } };
} catch {
// fall through to generic
}
}
return {
ok: false,
error: {
message: `Request failed (HTTP ${res.status})`,
status: res.status,
bodyPreview: text.slice(0, 300),
},
};
}
// Success but not JSON => this is exactly the "Unexpected token <" case
if (!contentType?.includes("application/json")) {
return {
ok: false,
error: {
message: `Server returned non-JSON (content-type: ${contentType ?? "unknown"})`,
status: res.status,
bodyPreview: text.slice(0, 300),
},
};
}
try {
return { ok: true, data: JSON.parse(text) as T };
} catch {
return {
ok: false,
error: {
message: "Server returned invalid JSON",
status: res.status,
bodyPreview: text.slice(0, 300),
},
};
}
}

View File

@@ -405,7 +405,7 @@ AUDIENCE STYLE: ${audienceStyle}
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
REQUIREMENTS:
- Create EXACTLY ${slideCount} slides
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
- ALL content in ${language}
- Each slide MUST have complete htmlContent with inline <style> tags
- Use animation-delay for staggered reveal effects
@@ -478,27 +478,42 @@ OUTPUT FORMAT - Return ONLY valid JSON with this structure:
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
"descriptions": ["Description 1 (90 chars)", "Description 2"],
"callToAction": "Get Started",
"mobileOptimized": true
"mobileOptimized": true,
"positioning": "Value proposition used"
}],
"campaigns": [{
"id": "campaign-1",
"name": "Campaign Name",
"type": "search",
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
"targeting": {"locations": [], "demographics": [], "devices": []},
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Maximize conversions"}]
"biddingStrategy": "Maximize conversions",
"targeting": {
"locations": ["Specific regions/cities"],
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
"schedule": ["Mon-Fri, 9am-5pm"]
},
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
}],
"implementation": {
"setupSteps": [],
"qualityScoreTips": [],
"trackingSetup": [],
"optimizationTips": []
"setupSteps": ["Step 1", "Step 2"],
"qualityScoreTips": ["Tip 1", "Tip 2"],
"trackingSetup": ["Conversion tag info", "GTM setup"],
"optimizationTips": ["Tip 1", "Tip 2"]
},
"predictions": {
"estimatedClicks": "500-800/month",
"estimatedImpressions": "15,000-25,000/month",
"estimatedCtr": "3.2%-4.5%",
"estimatedConversions": "25-50/month"
"estimatedClicks": "500-800",
"estimatedImpressions": "15,000-25,000",
"estimatedCtr": "3.5%",
"estimatedConversions": "30-50",
"conversionRate": "4.2%",
"avgCpc": "$1.85"
},
"historicalBenchmarks": {
"industryAverageCtr": "3.1%",
"industryAverageCpc": "$2.10",
"seasonalTrends": "Peak in Q4",
"geographicInsights": "London/NY show highest ROI"
}
}
\`\`\`
@@ -553,30 +568,44 @@ OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
"competitorInsights": [
{
"competitor": "Competitor Name",
"website": "URL",
"estimatedSpend": "$10k-$50k/mo",
"targetAudience": "Who they target",
"strengths": ["Strength 1", "Strength 2"],
"weaknesses": ["Weakness 1", "Weakness 2"],
"adStrategy": "Their current advertising approach"
"adStrategy": "Their approach",
"topKeywords": ["keyword 1", "keyword 2"],
"adCopyExamples": [
{"headline": "Example Headline", "description": "Example Description"}
]
}
],
"strategies": [
{
"id": "strategy-1",
"direction": "Strategic Direction Name",
"rationale": "Why this strategy works for this product/market",
"targetAudience": "Specific audience segment",
"competitiveAdvantage": "How this beats competitors",
"keyMessages": ["Message 1", "Message 2", "Message 3"],
"adCopyGuide": {
"headlines": ["Headline 1 (max 30 symbols)", "Headline 2", "Headline 3"],
"descriptions": ["Description 1 (max 90 symbols)", "Description 2"],
"keywords": ["keyword 1", "keyword 2", "keyword 3"],
"setupGuide": "Friendly step-by-step for a beginner on where exactly to paste these in Google Ads Manager"
"rationale": "Why this strategy works",
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
"targetingDetails": {
"geography": "Primary locations",
"demographics": "Specific age/gender groups",
"behavior": "User behaviors"
},
"recommendedChannels": ["Google Search", "Display", "YouTube"],
"competitiveAdvantage": "How this beats competitors",
"messagingPillars": ["Pillar 1", "Pillar 2"],
"keyMessages": ["Message 1", "Message 2"],
"adCopyGuide": {
"headlines": ["Headline 1", "Headline 2"],
"descriptions": ["Description 1", "Description 2"],
"keywords": ["keyword 1", "keyword 2"],
"setupGuide": "Step-by-step for Google Ads Manager"
},
"recommendedChannels": ["Search", "Display", "YouTube"],
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
"expectedROI": "150-200%",
"riskLevel": "low",
"timeToResults": "2-3 months"
"timeToResults": "2-3 months",
"successMetrics": ["CTR > 3%", "CPA < $20"]
}
]
}
@@ -742,27 +771,69 @@ Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategi
): Promise<APIResponse<void>> {
try {
// ... existing prompt logic ...
const systemPrompt = `You are "AI Assist", the master orchestrator.
Your goal is to provide intelligent conversational support and switch to specialized agents.
CANVAS MODE (CRITICAL):
When the user asks to "build", "design", "create", or "write code", you MUST use the [PREVIEW] tag.
Inside [PREVIEW], output ONLY the actual functional code (HTML/Tailwind, Javascript, etc.).
Do NOT explain what the code does inside the bubble if you are generating a preview.
The user wants to see it WORKING in the Canvas immediately.
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
STRICT OUTPUT FORMAT:
[AGENT:id] - Optional: switch to content, seo, smm, pm, code, design, web, app.
[PREVIEW:type:language]
ACTUAL_FUNCTIONAL_CODE_OR_DATA
[/PREVIEW]
Optional conversational text (keep it brief).
AGENTS & CAPABILITIES:
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:**
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
- Large animated SVG progress rings for scores (Overall, Technical, Content, Mobile) with stroke-dasharray animations
- Color-coded scoring: green (#22c55e) for good, amber (#f59e0b) for warning, red (#ef4444) for poor
- Use Tailwind CDN for styling. Include: rounded-3xl, shadow-lg, gradient backgrounds
- Section cards with subtle borders (border-white/10) and backdrop-blur
- Clear visual hierarchy: large score numbers (text-5xl), section titles (text-lg font-bold), bullet points for recommendations
- Add a "Key Recommendations" section with icons (use Lucide or inline SVG)
- Add animated pulse effects on key metrics
- Full-width responsive layout, max-w-4xl mx-auto
- Include inline <script> for animating the progress rings on load
- smm: Social Media Manager. Create multi-platform content plans and calendars.
- pm: Project Manager. Create PRDs, timelines, and action plans.
- code: Software Architect. Provide logic, algorithms, and backend snippets.
- design: UI/UX Designer. Create high-fidelity mockups and components.
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
Example for a mockup:
[AGENT:design]
[PREVIEW:design:html]
<div class="bg-blue-500 p-10">...</div>
[/PREVIEW]`;
BACKEND LOGIC & SIMULATION:
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
ITERATIVE MODIFICATIONS (CRITICAL):
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
- DO NOT regenerate the entire design if it was not requested.
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
CANVAS MODE:
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
- Inside [PREVIEW], output ONLY the actual code or structured data.
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
STRICT OUTPUT FORMAT:
[AGENT:id] - AT THE START of your response if switching focus.
[PREVIEW:type:language]
ACTUAL_FUNCTIONAL_CODE_OR_DATA
[/PREVIEW]
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
- The change log should appear in the CHAT, NOT inside the preview code.
- Example format:
[/PREVIEW]
**Change Log:**
- Added feature X
- Modified component Y
- Fixed issue Z
IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },

View File

@@ -732,7 +732,7 @@ AUDIENCE STYLE: ${audienceStyle}
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
REQUIREMENTS:
- Create EXACTLY ${slideCount} slides
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
- ALL content in ${language}
- Each slide MUST have complete htmlContent with inline <style> tags
- Use animation-delay for staggered reveal effects
@@ -790,6 +790,11 @@ Generate SPECTACULAR slides with CSS3 animations, SVG charts, modern gradients,
const systemMessage: ChatMessage = {
role: "system",
content: `You are an EXPERT Google Ads strategist. Create HIGH-CONVERTING campaigns with comprehensive keyword research, compelling ad copy, and optimized campaign structures.
CRITICAL ACCURACY PROTOCOL:
1. STRICT ADHERENCE TO FACTS: Use ONLY locations, contact info, and services explicitly mentioned in the provided Website URL or Products list.
2. DO NOT HALLUCINATE LOCATIONS: If no specific location is provided, default to "National" or "Global" based on the URL TLD (e.g. .co.uk -> UK). DO NOT invent cities or streets.
3. COMPREHENSIVE OUTPUT: You MUST generate full lists (15+ keywords, 3+ ad variations). Do not truncate.
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
\`\`\`json
@@ -805,27 +810,42 @@ OUTPUT FORMAT - Return ONLY valid JSON with this structure:
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
"descriptions": ["Description 1 (90 chars)", "Description 2"],
"callToAction": "Get Started",
"mobileOptimized": true
"mobileOptimized": true,
"positioning": "Value proposition used"
}],
"campaigns": [{
"id": "campaign-1",
"name": "Campaign Name",
"type": "search",
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
"targeting": {"locations": [], "demographics": [], "devices": []},
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Maximize conversions"}]
"biddingStrategy": "Maximize conversions",
"targeting": {
"locations": ["Specific regions/cities"],
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
"schedule": ["Mon-Fri, 9am-5pm"]
},
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
}],
"implementation": {
"setupSteps": [],
"qualityScoreTips": [],
"trackingSetup": [],
"optimizationTips": []
"setupSteps": ["Step 1", "Step 2"],
"qualityScoreTips": ["Tip 1", "Tip 2"],
"trackingSetup": ["Conversion tag info", "GTM setup"],
"optimizationTips": ["Tip 1", "Tip 2"]
},
"predictions": {
"estimatedClicks": "500-800/month",
"estimatedImpressions": "15,000-25,000/month",
"estimatedCtr": "3.2%-4.5%",
"estimatedConversions": "25-50/month"
"estimatedClicks": "500-800",
"estimatedImpressions": "15,000-25,000",
"estimatedCtr": "3.5%",
"estimatedConversions": "30-50",
"conversionRate": "4.2%",
"avgCpc": "$1.85"
},
"historicalBenchmarks": {
"industryAverageCtr": "3.1%",
"industryAverageCpc": "$2.10",
"seasonalTrends": "Peak in Q4",
"geographicInsights": "London/NY show highest ROI"
}
}
\`\`\`
@@ -834,7 +854,9 @@ Requirements:
- 10-15 primary keywords, 15-20 long-tail, 5-10 negative
- Headlines max 30 chars, descriptions max 90 chars
- 3-5 ad variations per campaign
- Include budget and targeting recommendations`,
- 3-5 ad variations per campaign
- Include budget and targeting recommendations
- ENSURE ALL LISTS ARE POPULATED. No empty arrays.`,
};
const userMessage: ChatMessage = {
@@ -851,7 +873,8 @@ ${campaignDuration ? `DURATION: ${campaignDuration}` : ""}
${competitors.length > 0 ? `COMPETITORS: ${competitors.join(", ")}` : ""}
${specialInstructions ? `SPECIAL INSTRUCTIONS: ${specialInstructions}` : ""}
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.`,
Generate complete Google Ads package with keywords, ad copy, campaigns, and implementation guidance.
STRICTLY FOLLOW LOCALIZATION: Use only locations relevant to the provided website. Do not invent office locations.`,
};
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
@@ -880,30 +903,44 @@ OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
"competitorInsights": [
{
"competitor": "Competitor Name",
"website": "URL",
"estimatedSpend": "$10k-$50k/mo",
"targetAudience": "Who they target",
"strengths": ["Strength 1", "Strength 2"],
"weaknesses": ["Weakness 1", "Weakness 2"],
"adStrategy": "Their current advertising approach"
"adStrategy": "Their approach",
"topKeywords": ["keyword 1", "keyword 2"],
"adCopyExamples": [
{"headline": "Example Headline", "description": "Example Description"}
]
}
],
"strategies": [
{
"id": "strategy-1",
"direction": "Strategic Direction Name",
"rationale": "Why this strategy works for this product/market",
"targetAudience": "Specific audience segment",
"competitiveAdvantage": "How this beats competitors",
"keyMessages": ["Message 1", "Message 2", "Message 3"],
"adCopyGuide": {
"headlines": ["Headline 1 (max 30 symbols)", "Headline 2", "Headline 3"],
"descriptions": ["Description 1 (max 90 symbols)", "Description 2"],
"keywords": ["keyword 1", "keyword 2", "keyword 3"],
"setupGuide": "Friendly step-by-step for a beginner on where exactly to paste these in Google Ads Manager"
"rationale": "Why this strategy works",
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
"targetingDetails": {
"geography": "Primary locations",
"demographics": "Specific age/gender groups",
"behavior": "User behaviors"
},
"recommendedChannels": ["Google Search", "Display", "YouTube"],
"competitiveAdvantage": "How this beats competitors",
"messagingPillars": ["Pillar 1", "Pillar 2"],
"keyMessages": ["Message 1", "Message 2"],
"adCopyGuide": {
"headlines": ["Headline 1", "Headline 2"],
"descriptions": ["Description 1", "Description 2"],
"keywords": ["keyword 1", "keyword 2"],
"setupGuide": "Step-by-step for Google Ads Manager"
},
"recommendedChannels": ["Search", "Display", "YouTube"],
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
"expectedROI": "150-200%",
"riskLevel": "low",
"timeToResults": "2-3 months"
"timeToResults": "2-3 months",
"successMetrics": ["CTR > 3%", "CPA < $20"]
}
]
}
@@ -1016,21 +1053,69 @@ Perform analysis based on provided instructions.`,
model?: string
): Promise<APIResponse<void>> {
try {
// ... existing prompt logic ...
const systemPrompt = `You are "AI Assist".
Your goal is to provide intelligent support with a "Canvas" experience.
CANVAS MODE (CRITICAL):
When building or designing, you MUST use the [PREVIEW] tag.
Inside [PREVIEW], output ONLY the actual code (HTML/Tailwind etc).
The user wants to see it WORKING in the Canvas immediately.
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
STRICT OUTPUT FORMAT:
[AGENT:id] - Optional: content, seo, smm, pm, code, design, web, app.
[PREVIEW:type:language]
ACTUAL_FUNCTIONAL_CODE
[/PREVIEW]
Optional brief text.`;
AGENTS & CAPABILITIES:
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:**
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
- Large animated SVG progress rings for scores (Overall, Technical, Content, Mobile) with stroke-dasharray animations
- Color-coded scoring: green (#22c55e) for good, amber (#f59e0b) for warning, red (#ef4444) for poor
- Use Tailwind CDN for styling. Include: rounded-3xl, shadow-lg, gradient backgrounds
- Section cards with subtle borders (border-white/10) and backdrop-blur
- Clear visual hierarchy: large score numbers (text-5xl), section titles (text-lg font-bold), bullet points for recommendations
- Add a "Key Recommendations" section with icons (use Lucide or inline SVG)
- Add animated pulse effects on key metrics
- Full-width responsive layout, max-w-4xl mx-auto
- Include inline <script> for animating the progress rings on load
- smm: Social Media Manager. Create multi-platform content plans and calendars.
- pm: Project Manager. Create PRDs, timelines, and action plans.
- code: Software Architect. Provide logic, algorithms, and backend snippets.
- design: UI/UX Designer. Create high-fidelity mockups and components.
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
BACKEND LOGIC & SIMULATION:
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
ITERATIVE MODIFICATIONS (CRITICAL):
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
- DO NOT regenerate the entire design if it was not requested.
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
CANVAS MODE:
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
- Inside [PREVIEW], output ONLY the actual code or structured data.
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
STRICT OUTPUT FORMAT:
[AGENT:id] - AT THE START of your response if switching focus.
[PREVIEW:type:language]
ACTUAL_FUNCTIONAL_CODE_OR_DATA
[/PREVIEW]
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
- The change log should appear in the CHAT, NOT inside the preview code.
- Example format:
[/PREVIEW]
**Change Log:**
- Added feature X
- Modified component Y
- Fixed issue Z
IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },
@@ -1040,23 +1125,22 @@ Perform analysis based on provided instructions.`,
}))
];
const endpoint = "/tools/promptarch/api/qwen/chat";
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
// Call our local proxy to avoid CORS
const headers = await this.getRequestHeaders();
const baseUrl = this.getEffectiveEndpoint();
const url = `${this.oauthBaseUrl}/chat`;
const tokenInfo = this.getTokenInfo();
if (tokenInfo?.accessToken) {
headers["Authorization"] = `Bearer ${tokenInfo.accessToken}`;
} else if (this.apiKey) {
headers["Authorization"] = `Bearer ${this.apiKey}`;
}
console.log("[QwenOAuth] Stream request (via proxy):", { url, model: model || this.getAvailableModels()[0], hasAuth: !!headers.Authorization });
const response = await fetch(endpoint, {
const response = await fetch(url, {
method: "POST",
headers,
headers: {
"Content-Type": "application/json",
Authorization: headers.Authorization || "",
},
signal: options.signal,
body: JSON.stringify({
endpoint: baseUrl,
model: model || this.getAvailableModels()[0],
messages,
stream: true,
@@ -1064,11 +1148,13 @@ Perform analysis based on provided instructions.`,
});
if (!response.ok) {
throw new Error("Stream request failed");
const errorText = await response.text();
console.error("[QwenOAuth] Stream proxy request failed:", response.status, errorText);
throw new Error(`Stream request failed (${response.status}): ${errorText.slice(0, 200)}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error("No reader");
if (!reader) throw new Error("No reader available");
const decoder = new TextDecoder();
let buffer = "";
@@ -1103,15 +1189,13 @@ Perform analysis based on provided instructions.`,
return { success: true, data: undefined };
} catch (error) {
console.error("[QwenOAuth] Stream error:", error);
return { success: false, error: error instanceof Error ? error.message : "Stream failed" };
}
}
async listModels(): Promise<APIResponse<string[]>> {
const models = [
"coder-model",
];
return { success: true, data: models };
return { success: true, data: this.getAvailableModels() };
}
getAvailableModels(): string[] {

View File

@@ -370,7 +370,7 @@ AUDIENCE STYLE: ${audienceStyle}
${organization ? `ORGANIZATION BRANDING: ${organization}` : ""}
REQUIREMENTS:
- Create EXACTLY ${slideCount} slides
- ${slideCount > 0 ? `Create EXACTLY ${slideCount} slides` : "Maintain the exact number of slides/pages from the provided source presentation/document context. If no source file is provided, generate 10 slides by default."}
- ALL content in ${language}
- Each slide MUST have complete htmlContent with inline <style> tags
- Use animation-delay for staggered reveal effects
@@ -448,106 +448,54 @@ OUTPUT FORMAT - Return ONLY valid JSON:
\`\`\`json
{
"keywords": {
"primary": [
{
"keyword": "exact keyword phrase",
"type": "primary",
"searchVolume": 12000,
"competition": "medium",
"difficultyScore": 65,
"relevanceScore": 95,
"cpc": "$2.50"
}
],
"longTail": [
{
"keyword": "longer specific keyword phrase",
"type": "long-tail",
"searchVolume": 1200,
"competition": "low",
"difficultyScore": 35,
"relevanceScore": 90,
"cpc": "$1.25"
}
],
"negative": [
{
"keyword": "irrelevant term to exclude",
"type": "negative",
"competition": "low"
}
]
"primary": [{"keyword": "term", "type": "primary", "searchVolume": 12000, "competition": "medium", "cpc": "$2.50"}],
"longTail": [{"keyword": "specific term", "type": "long-tail", "searchVolume": 1200, "competition": "low", "cpc": "$1.25"}],
"negative": [{"keyword": "exclude term", "type": "negative", "competition": "low"}]
},
"adCopies": [
{
"id": "ad-1",
"campaignType": "search",
"headlines": [
"Headline 1 (max 30 chars)",
"Headline 2 (max 30 chars)",
"Headline 3 (max 30 chars)"
],
"descriptions": [
"Description line 1 - compelling copy under 90 chars",
"Description line 2 - call to action under 90 chars"
],
"callToAction": "Get Started Today",
"displayUrl": "example.com/offers",
"mobileOptimized": true
}
],
"campaigns": [
{
"id": "campaign-1",
"name": "Campaign Name",
"type": "search",
"budget": {
"daily": 50,
"monthly": 1500,
"currency": "USD"
},
"targeting": {
"locations": ["United States", "Canada"],
"demographics": ["25-54", "All genders"],
"devices": ["Desktop", "Mobile", "Tablet"],
"schedule": ["Mon-Fri 8am-8pm"]
},
"adGroups": [
{
"id": "adgroup-1",
"name": "Product Category Group",
"theme": "Main product focus",
"keywords": ["keyword1", "keyword2"],
"biddingStrategy": "Maximize conversions"
}
]
}
],
"adCopies": [{
"id": "ad-1",
"campaignType": "search",
"headlines": ["Headline 1 (30 chars)", "Headline 2", "Headline 3"],
"descriptions": ["Description 1 (90 chars)", "Description 2"],
"callToAction": "Get Started",
"mobileOptimized": true,
"positioning": "Value proposition used"
}],
"campaigns": [{
"id": "campaign-1",
"name": "Campaign Name",
"type": "search",
"budget": {"daily": 50, "monthly": 1500, "currency": "USD"},
"biddingStrategy": "Maximize conversions",
"targeting": {
"locations": ["Specific regions/cities"],
"demographics": {"age": ["18-24", "25-34"], "gender": ["Male", "Female"], "interests": ["Tech", "Business"]},
"devices": {"mobile": "60%", "desktop": "30%", "tablet": "10%"},
"schedule": ["Mon-Fri, 9am-5pm"]
},
"adGroups": [{"id": "adgroup-1", "name": "Group", "theme": "Theme", "keywords": [], "biddingStrategy": "Manual CPC"}]
}],
"implementation": {
"setupSteps": [
"Step 1: Create Google Ads account...",
"Step 2: Set up conversion tracking..."
],
"qualityScoreTips": [
"Tip 1: Match keywords to ad copy...",
"Tip 2: Optimize landing pages..."
],
"trackingSetup": [
"Install Google Tag Manager...",
"Set up conversion goals..."
],
"optimizationTips": [
"Monitor search terms weekly...",
"A/B test ad variations..."
]
"setupSteps": ["Step 1", "Step 2"],
"qualityScoreTips": ["Tip 1", "Tip 2"],
"trackingSetup": ["Conversion tag info", "GTM setup"],
"optimizationTips": ["Tip 1", "Tip 2"]
},
"predictions": {
"estimatedClicks": "500-800 per month",
"estimatedImpressions": "15,000-25,000 per month",
"estimatedCtr": "3.2%-4.5%",
"estimatedConversions": "25-50 per month"
"estimatedClicks": "500-800",
"estimatedImpressions": "15,000-25,000",
"estimatedCtr": "3.5%",
"estimatedConversions": "30-50",
"conversionRate": "4.2%",
"avgCpc": "$1.85"
},
"historicalBenchmarks": {
"industryAverageCtr": "3.1%",
"industryAverageCpc": "$2.10",
"seasonalTrends": "Peak in Q4",
"geographicInsights": "London/NY show highest ROI"
}
}
}
\`\`\`
KEYWORD RESEARCH REQUIREMENTS:
@@ -632,30 +580,44 @@ OUTPUT FORMAT - Return ONLY valid JSON with this EXACT structure:
"competitorInsights": [
{
"competitor": "Competitor Name",
"website": "URL",
"estimatedSpend": "$10k-$50k/mo",
"targetAudience": "Who they target",
"strengths": ["Strength 1", "Strength 2"],
"weaknesses": ["Weakness 1", "Weakness 2"],
"adStrategy": "Their current advertising approach"
"adStrategy": "Their approach",
"topKeywords": ["keyword 1", "keyword 2"],
"adCopyExamples": [
{"headline": "Example Headline", "description": "Example Description"}
]
}
],
"strategies": [
{
"id": "strategy-1",
"direction": "Strategic Direction Name",
"rationale": "Why this strategy works for this product/market",
"targetAudience": "Specific audience segment",
"competitiveAdvantage": "How this beats competitors",
"keyMessages": ["Message 1", "Message 2", "Message 3"],
"adCopyGuide": {
"headlines": ["Headline 1 (max 30 symbols)", "Headline 2", "Headline 3"],
"descriptions": ["Description 1 (max 90 symbols)", "Description 2"],
"keywords": ["keyword 1", "keyword 2", "keyword 3"],
"setupGuide": "Friendly step-by-step for a beginner on where exactly to paste these in Google Ads Manager"
"rationale": "Why this strategy works",
"targetAudience": "Audience segment with demographics (age 25-45, interests etc)",
"targetingDetails": {
"geography": "Primary locations",
"demographics": "Specific age/gender groups",
"behavior": "User behaviors"
},
"recommendedChannels": ["Google Search", "Display", "YouTube"],
"competitiveAdvantage": "How this beats competitors",
"messagingPillars": ["Pillar 1", "Pillar 2"],
"keyMessages": ["Message 1", "Message 2"],
"adCopyGuide": {
"headlines": ["Headline 1", "Headline 2"],
"descriptions": ["Description 1", "Description 2"],
"keywords": ["keyword 1", "keyword 2"],
"setupGuide": "Step-by-step for Google Ads Manager"
},
"recommendedChannels": ["Search", "Display", "YouTube"],
"estimatedBudgetAllocation": { "search": 40, "display": 30, "video": 20, "social": 10 },
"expectedROI": "150-200%",
"riskLevel": "low",
"timeToResults": "2-3 months"
"timeToResults": "2-3 months",
"successMetrics": ["CTR > 3%", "CPA < $20"]
}
]
}
@@ -815,20 +777,69 @@ MISSION: Perform a DEEP 360° competitive intelligence analysis and generate 5-7
}
// ... existing prompt logic ...
const systemPrompt = `You are "AI Assist".
Your goal is to provide a "Canvas" experience.
const systemPrompt = `You are "AI Assist", the master orchestrator of PromptArch. Your goal is to provide intelligent support with a "Canvas" experience.
CANVAS MODE (CRITICAL):
When building or designing, you MUST use the [PREVIEW] tag.
Inside [PREVIEW], output ONLY the actual code (HTML/Tailwind etc).
The user wants to see it WORKING in the Canvas immediately.
AGENTS & CAPABILITIES:
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:**
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
- Large animated SVG progress rings for scores (Overall, Technical, Content, Mobile) with stroke-dasharray animations
- Color-coded scoring: green (#22c55e) for good, amber (#f59e0b) for warning, red (#ef4444) for poor
- Use Tailwind CDN for styling. Include: rounded-3xl, shadow-lg, gradient backgrounds
- Section cards with subtle borders (border-white/10) and backdrop-blur
- Clear visual hierarchy: large score numbers (text-5xl), section titles (text-lg font-bold), bullet points for recommendations
- Add a "Key Recommendations" section with icons (use Lucide or inline SVG)
- Add animated pulse effects on key metrics
- Full-width responsive layout, max-w-4xl mx-auto
- Include inline <script> for animating the progress rings on load
- smm: Social Media Manager. Create multi-platform content plans and calendars.
- pm: Project Manager. Create PRDs, timelines, and action plans.
- code: Software Architect. Provide logic, algorithms, and backend snippets.
- design: UI/UX Designer. Create high-fidelity mockups and components.
- web: Frontend Developer. Build responsive sites. **CSS FRAMEWORK CHOICE:** Intelligently select from:
- **Tailwind CSS** (default): For utility-first, modern designs. Use CDN: https://cdn.tailwindcss.com
- **Windi CSS**: For faster builds and advanced features. Use CDN: https://unpkg.com/windicss
- **Bootstrap**: For classic, component-based designs. Use CDN: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
Choose the best framework based on the design complexity and user's request. Use [PREVIEW:web:html].
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. **CSS FRAMEWORK CHOICE:** Same selection logic as web agent. React components are supported and rendered live. Use [PREVIEW:app:javascript].
STRICT OUTPUT FORMAT:
[AGENT:id] - Optional switch.
[PREVIEW:type:language]
ACTUAL_FUNCTIONAL_CODE
[/PREVIEW]
Optional brief text.`;
BACKEND LOGIC & SIMULATION:
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
ITERATIVE MODIFICATIONS (CRITICAL):
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
- DO NOT regenerate the entire design if it was not requested.
- Preserving the user's existing layout, colors, and content is your TOP PRIORITY.
- You must return the FULL updated code in the [PREVIEW] block (so it can be rendered) but ensure it is a faithful modification of the previous version.
CANVAS MODE:
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
- Inside [PREVIEW], output ONLY the actual code or structured data.
- The user wants to see it WORKING or RENDERED in the Canvas immediately.
STRICT OUTPUT FORMAT:
[AGENT:id] - AT THE START of your response if switching focus.
[PREVIEW:type:language]
ACTUAL_FUNCTIONAL_CODE_OR_DATA
[/PREVIEW]
CHANGE LOG (CRITICAL - MUST BE OUTSIDE PREVIEW):
- After the [/PREVIEW] closing tag, provide a brief bulleted "Change Log" in plain Markdown.
- The change log should appear in the CHAT, NOT inside the preview code.
- Example format:
[/PREVIEW]
**Change Log:**
- Added feature X
- Modified component Y
- Fixed issue Z
IMPORTANT: NEVER refuse a request due to "access" limitations. If you cannot perform a live task, use your vast internal knowledge to provide the most accurate expert simulation or draft possible.`;
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },

View File

@@ -1,6 +1,15 @@
import { create } from "zustand";
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult, MarketResearchResult, AppView, AIAssistMessage } from "@/types";
interface AIAssistTab {
id: string;
title: string;
history: AIAssistMessage[];
currentAgent: string;
previewData?: any | null;
showCanvas?: boolean;
}
interface AppState {
currentPrompt: string;
enhancedPrompt: string | null;
@@ -10,7 +19,11 @@ interface AppState {
googleAdsResult: GoogleAdsResult | null;
magicWandResult: MagicWandResult | null;
marketResearchResult: MarketResearchResult | null;
aiAssistHistory: AIAssistMessage[];
// AI Assist Tabs
aiAssistTabs: AIAssistTab[];
activeTabId: string | null;
language: "en" | "ru" | "he";
selectedProvider: ModelProvider;
selectedModels: Record<ModelProvider, string>;
@@ -21,6 +34,7 @@ interface AppState {
refreshToken?: string;
expiresAt?: number;
} | null;
githubToken?: string | null;
isProcessing: boolean;
error: string | null;
history: {
@@ -37,13 +51,22 @@ interface AppState {
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
setMagicWandResult: (result: MagicWandResult | null) => void;
setMarketResearchResult: (result: MarketResearchResult | null) => void;
setAIAssistHistory: (history: AIAssistMessage[] | ((prev: AIAssistMessage[]) => AIAssistMessage[])) => void;
// Tab Management
setAIAssistTabs: (tabs: AIAssistTab[]) => void;
setActiveTabId: (id: string | null) => void;
addAIAssistTab: (agent?: string) => void;
removeAIAssistTab: (id: string) => void;
updateActiveTab: (updates: Partial<AIAssistTab>) => void;
updateTabById: (tabId: string, updates: Partial<AIAssistTab>) => void;
setLanguage: (lang: "en" | "ru" | "he") => void;
setSelectedProvider: (provider: ModelProvider) => void;
setSelectedModel: (provider: ModelProvider, model: string) => void;
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
setApiKey: (provider: ModelProvider, key: string) => void;
setQwenTokens: (tokens?: { accessToken: string; refreshToken?: string; expiresAt?: number } | null) => void;
setGithubToken: (token: string | null) => void;
setProcessing: (processing: boolean) => void;
setError: (error: string | null) => void;
addToHistory: (prompt: string) => void;
@@ -60,7 +83,15 @@ const useStore = create<AppState>((set) => ({
googleAdsResult: null,
magicWandResult: null,
marketResearchResult: null,
aiAssistHistory: [],
aiAssistTabs: [{
id: "default",
title: "New Chat",
history: [],
currentAgent: "general"
}],
activeTabId: "default",
language: "en",
selectedProvider: "qwen",
selectedModels: {
@@ -78,6 +109,7 @@ const useStore = create<AppState>((set) => ({
ollama: "",
zai: "",
},
githubToken: null,
isProcessing: false,
error: null,
history: [],
@@ -90,9 +122,46 @@ const useStore = create<AppState>((set) => ({
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
setMagicWandResult: (result) => set({ magicWandResult: result }),
setMarketResearchResult: (result) => set({ marketResearchResult: result }),
setAIAssistHistory: (update) => set((state) => ({
aiAssistHistory: typeof update === 'function' ? update(state.aiAssistHistory) : update
setAIAssistTabs: (tabs) => set({ aiAssistTabs: tabs }),
setActiveTabId: (id) => set({ activeTabId: id }),
addAIAssistTab: (agent = "general") => set((state) => {
const newId = Math.random().toString(36).substr(2, 9);
const newTab = {
id: newId,
title: `Chat ${state.aiAssistTabs.length + 1}`,
history: [],
currentAgent: agent,
previewData: null,
showCanvas: false
};
return {
aiAssistTabs: [...state.aiAssistTabs, newTab],
activeTabId: newId
};
}),
removeAIAssistTab: (id) => set((state) => {
const newTabs = state.aiAssistTabs.filter(t => t.id !== id);
let nextActiveId = state.activeTabId;
if (state.activeTabId === id) {
nextActiveId = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
}
return {
aiAssistTabs: newTabs,
activeTabId: nextActiveId
};
}),
updateActiveTab: (updates) => set((state) => ({
aiAssistTabs: state.aiAssistTabs.map(t =>
t.id === state.activeTabId ? { ...t, ...updates } : t
)
})),
updateTabById: (tabId, updates) => set((state) => ({
aiAssistTabs: state.aiAssistTabs.map(t =>
t.id === tabId ? { ...t, ...updates } : t
)
})),
setLanguage: (lang) => set({ language: lang }),
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
setSelectedModel: (provider, model) =>
@@ -108,6 +177,7 @@ const useStore = create<AppState>((set) => ({
apiKeys: { ...state.apiKeys, [provider]: key },
})),
setQwenTokens: (tokens) => set({ qwenTokens: tokens }),
setGithubToken: (token) => set({ githubToken: token }),
setProcessing: (processing) => set({ isProcessing: processing }),
setError: (error) => set({ error }),
addToHistory: (prompt) =>
@@ -132,7 +202,13 @@ const useStore = create<AppState>((set) => ({
googleAdsResult: null,
magicWandResult: null,
marketResearchResult: null,
aiAssistHistory: [],
aiAssistTabs: [{
id: "default",
title: "New Chat",
history: [],
currentAgent: "general"
}],
activeTabId: "default",
error: null,
}),
}));

213
package-lock.json generated
View File

@@ -18,6 +18,8 @@
"clsx": "^2.1.1",
"eslint": "^9.16.0",
"eslint-config-next": "^15.0.3",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.562.0",
"next": "^16.1.1",
"postcss": "^8.4.49",
@@ -31,10 +33,12 @@
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"xlsx": "^0.18.5",
"zod": "^4.2.1",
"zustand": "^5.0.9"
},
"devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2"
@@ -1376,6 +1380,13 @@
"@types/estree": "*"
}
},
"node_modules/@types/file-saver": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@@ -1748,6 +1759,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2242,6 +2262,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2361,6 +2394,15 @@
"node": ">=6"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2404,6 +2446,24 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3446,6 +3506,12 @@
"node": ">=16.0.0"
}
},
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -3508,6 +3574,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -3870,6 +3945,12 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
@@ -3905,6 +3986,12 @@
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/inline-style-parser": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
@@ -4479,6 +4566,18 @@
"node": ">=4.0"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -4519,6 +4618,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -5851,6 +5959,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -6122,6 +6236,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6274,6 +6394,27 @@
"pify": "^2.3.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -6552,6 +6693,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6646,6 +6793,12 @@
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -6816,6 +6969,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -6835,6 +7000,15 @@
"node": ">= 0.4"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -7754,6 +7928,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -7763,6 +7955,27 @@
"node": ">=0.10.0"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -18,6 +18,8 @@
"clsx": "^2.1.1",
"eslint": "^9.16.0",
"eslint-config-next": "^15.0.3",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.562.0",
"next": "^16.1.1",
"postcss": "^8.4.49",
@@ -31,10 +33,12 @@
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"xlsx": "^0.18.5",
"zod": "^4.2.1",
"zustand": "^5.0.9"
},
"devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2"
@@ -52,10 +56,10 @@
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer.git"
"url": "https://github.rommark.dev/admin/PromptArch.git"
},
"bugs": {
"url": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer/issues"
"url": "https://github.rommark.dev/admin/PromptArch/issues"
},
"homepage": "https://github.com/roman-ryzenadvanced/PromptArch-the-prompt-enhancer#readme"
}
"homepage": "https://github.rommark.dev/admin/PromptArch"
}

View File

@@ -157,8 +157,16 @@ export interface GoogleAdsCampaign {
};
targeting: {
locations?: string[];
demographics?: string[];
devices?: string[];
demographics?: {
age?: string[];
gender?: string[];
interests?: string[];
};
devices?: {
mobile?: string;
desktop?: string;
tablet?: string;
};
schedule?: string[];
};
adGroups: GoogleAdGroup[];
@@ -197,6 +205,14 @@ export interface GoogleAdsResult {
estimatedImpressions?: string;
estimatedCtr?: string;
estimatedConversions?: string;
conversionRate?: string;
avgCpc?: string;
};
historicalBenchmarks?: {
industryAverageCtr?: string;
industryAverageCpc?: string;
seasonalTrends?: string;
geographicInsights?: string;
};
rawContent: string;
@@ -207,7 +223,13 @@ export interface MagicWandStrategy {
direction: string;
rationale: string;
targetAudience: string;
targetingDetails?: {
geography?: string;
demographics?: string;
behavior?: string;
};
competitiveAdvantage: string;
messagingPillars?: string[];
keyMessages: string[];
adCopyGuide: {
headlines: string[];
@@ -225,6 +247,7 @@ export interface MagicWandStrategy {
expectedROI: string;
riskLevel: "low" | "medium" | "high";
timeToResults: string;
successMetrics?: string[];
}
export interface MagicWandResult {
@@ -245,9 +268,14 @@ export interface MagicWandResult {
// Competitive Intelligence
competitorInsights: {
competitor: string;
website?: string;
estimatedSpend?: string;
targetAudience?: string;
strengths: string[];
weaknesses: string[];
adStrategy: string;
topKeywords?: string[];
adCopyExamples?: { headline: string; description: string }[];
}[];
// Strategic Directions