feat: Add AI Market Research module - Introduced Automated Market Analysis and Competitive Intelligence feature - Added 'Market Research' view to Sidebar and main navigation - Implemented MarketResearcher component with price comparison matrix and feature tables - Updated Ollama, Qwen, and Z.AI services to support market research generation - Added localized translations in English, Russian, and Hebrew

This commit is contained in:
Gemini AI
2025-12-28 03:02:00 +04:00
Unverified
parent 2c7b233bca
commit 792685ca6b
10 changed files with 810 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ 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 HistoryPanel from "@/components/HistoryPanel";
import SettingsPanel from "@/components/SettingsPanel";
import modelAdapter from "@/lib/services/adapter-instance";
@@ -35,6 +36,8 @@ export default function Home() {
return <SlidesGenerator />;
case "googleads":
return <GoogleAdsGenerator />;
case "market-research":
return <MarketResearcher />;
case "history":
return <HistoryPanel />;
case "settings":

View File

@@ -0,0 +1,506 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import useStore from "@/lib/store";
import { translations } from "@/lib/i18n/translations";
import modelAdapter from "@/lib/services/adapter-instance";
import { Search, Globe, Plus, Trash2, ShieldAlert, BarChart3, TrendingUp, Target, Rocket, Lightbulb, CheckCircle2, AlertCircle, Loader2, X } from "lucide-react";
import { cn } from "@/lib/utils";
const MarketResearcher = () => {
const { language, selectedProvider, selectedModels, apiKeys, setMarketResearchResult, marketResearchResult } = useStore();
const t = translations[language].marketResearch;
const common = translations[language].common;
const [websiteUrl, setWebsiteUrl] = useState("");
const [additionalUrls, setAdditionalUrls] = useState<string[]>([""]);
const [competitorUrls, setCompetitorUrls] = useState<string[]>(["", "", ""]);
const [productMapping, setProductMapping] = useState("");
const [specialInstructions, setSpecialInstructions] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const selectedModel = selectedModels[selectedProvider];
const handleAddUrl = () => setAdditionalUrls([...additionalUrls, ""]);
const handleRemoveUrl = (index: number) => {
const newUrls = [...additionalUrls];
newUrls.splice(index, 1);
setAdditionalUrls(newUrls);
};
const handleAddCompetitor = () => {
if (competitorUrls.length < 10) {
setCompetitorUrls([...competitorUrls, ""]);
}
};
const handleRemoveCompetitor = (index: number) => {
const newUrls = [...competitorUrls];
newUrls.splice(index, 1);
setCompetitorUrls(newUrls);
};
const validateUrls = () => {
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
if (!websiteUrl || !urlRegex.test(websiteUrl)) return "Invalid primary website URL";
const validCompetitors = competitorUrls.filter(url => url.trim().length > 0);
if (validCompetitors.length < 2) return "At least 2 competitor websites are required";
for (const url of validCompetitors) {
if (!urlRegex.test(url)) return `Invalid competitor URL: ${url}`;
}
return null;
};
const handleStartResearch = async () => {
const validationError = validateUrls();
if (validationError) {
setError(validationError);
return;
}
const apiKey = apiKeys[selectedProvider];
const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth();
if (!isQwenOAuth && (!apiKey || !apiKey.trim())) {
setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`);
return;
}
setIsProcessing(true);
setError(null);
setMarketResearchResult(null);
try {
const filteredCompetitors = competitorUrls.filter(u => u.trim() !== "");
const filteredAddUrls = additionalUrls.filter(u => u.trim() !== "");
const result = await modelAdapter.generateMarketResearch({
websiteUrl,
additionalUrls: filteredAddUrls,
competitors: filteredCompetitors,
productMapping,
specialInstructions
}, selectedProvider, selectedModel);
if (result.success && result.data) {
try {
const cleanJson = result.data.replace(/```json\s*([\s\S]*?)\s*```/i, '$1').trim();
const parsed = JSON.parse(cleanJson);
setMarketResearchResult({
...parsed,
id: Math.random().toString(36).substr(2, 9),
websiteUrl,
additionalUrls: filteredAddUrls,
competitors: filteredCompetitors,
productMapping: [{ productName: productMapping || "Main Product", 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.");
}
} else {
setError(result.error || "Research failed");
}
} catch (err) {
setError(err instanceof Error ? err.message : "An unexpected error occurred");
} finally {
setIsProcessing(false);
}
};
const renderPriceMatrix = () => {
if (!marketResearchResult?.priceComparisonMatrix) return null;
return (
<div className="overflow-x-auto">
<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>
{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>
))}
</tr>
</thead>
<tbody className="divide-y">
{marketResearchResult.priceComparisonMatrix.map((item, i) => (
<tr key={i} className="hover:bg-slate-50/30 transition-colors">
<td className="px-4 py-4 font-bold text-slate-900">{item.product}</td>
<td className="px-4 py-4 font-black text-indigo-600">{item.userPrice}</td>
{marketResearchResult.competitors.map((comp) => {
const compPrice = item.competitorPrices.find(cp => cp.competitor === comp || comp.includes(cp.competitor));
return (
<td key={comp} className="px-4 py-4 font-medium text-slate-600">
{compPrice ? compPrice.price : "N/A"}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
};
const renderFeatureTable = () => {
if (!marketResearchResult?.featureComparisonTable) return null;
return (
<div className="overflow-x-auto">
<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>
{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>
))}
</tr>
</thead>
<tbody className="divide-y">
{marketResearchResult.featureComparisonTable.map((item, i) => (
<tr key={i} className="hover:bg-slate-50/30 transition-colors">
<td className="px-4 py-4 font-bold text-slate-900">{item.feature}</td>
<td className="px-4 py-4">
{typeof item.userStatus === 'boolean' ? (
item.userStatus ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> : <X className="h-4 w-4 text-slate-300" />
) : <span className="text-xs font-semibold">{item.userStatus}</span>}
</td>
{marketResearchResult.competitors.map((comp) => {
const compStatus = item.competitorStatus.find(cs => cs.competitor === comp || comp.includes(cs.competitor));
return (
<td key={comp} className="px-4 py-4">
{compStatus ? (
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"}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
};
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Header Section */}
<div className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-200">
<Search className="h-6 w-6" />
</div>
<h2 className="text-3xl font-black tracking-tight text-slate-900">{t.title}</h2>
</div>
<p className="text-slate-500 font-medium ml-1.5">{t.description}</p>
</div>
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8 items-start">
{/* Configuration Panel */}
<div className="xl:col-span-5 space-y-6">
<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
</CardTitle>
</CardHeader>
<CardContent className="p-6 space-y-6">
<div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.websiteUrl}</label>
<Input
placeholder={t.websitePlaceholder}
value={websiteUrl}
onChange={(e) => setWebsiteUrl(e.target.value)}
className="bg-slate-50 border-slate-200 focus:bg-white transition-all font-medium"
/>
</div>
<div className="space-y-3">
<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
</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)"
value={url}
onChange={(e) => {
const newUrls = [...additionalUrls];
newUrls[i] = e.target.value;
setAdditionalUrls(newUrls);
}}
className="bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-xs"
/>
<Button variant="ghost" size="icon" onClick={() => handleRemoveUrl(i)} className="h-9 w-9 shrink-0 text-slate-400 hover:text-rose-500">
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
</CardContent>
</Card>
<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
</CardTitle>
</CardHeader>
<CardContent className="p-6 space-y-6">
<div className="space-y-3">
<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
</Button>
</label>
<div className="space-y-2">
{competitorUrls.map((url, i) => (
<div key={i} className="flex gap-2">
<Input
placeholder={t.competitorPlaceholder}
value={url}
onChange={(e) => {
const newUrls = [...competitorUrls];
newUrls[i] = e.target.value;
setCompetitorUrls(newUrls);
}}
className="bg-slate-50/50 border-slate-200 focus:bg-white transition-all text-xs"
/>
{competitorUrls.length > 2 && (
<Button variant="ghost" size="icon" onClick={() => handleRemoveCompetitor(i)} className="h-9 w-9 shrink-0 text-slate-400 hover:text-rose-500">
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
))}
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-slate-600">{t.productMapping}</label>
<Textarea
placeholder={t.mappingPlaceholder}
value={productMapping}
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>
</div>
<div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-slate-600">Research Parameters</label>
<Textarea
placeholder="Any specific depth or focus? (e.g., 'Focus on enterprise features', 'Analyze pricing tiers')"
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"
/>
</div>
{error && (
<div className="p-3 rounded-xl bg-rose-50 border border-rose-100 flex items-start gap-3 animate-in fade-in slide-in-from-top-2">
<AlertCircle className="h-4 w-4 text-rose-500 shrink-0 mt-0.5" />
<p className="text-xs font-bold text-rose-600">{error}</p>
</div>
)}
<Button
onClick={handleStartResearch}
disabled={isProcessing}
className="w-full h-12 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white font-black uppercase tracking-widest shadow-lg shadow-indigo-100 transition-all active:scale-95 disabled:opacity-70"
>
{isProcessing ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
{t.researching}
</>
) : (
<>
<Search className="mr-2 h-5 w-5" />
{t.generate}
</>
)}
</Button>
</CardContent>
</Card>
</div>
{/* Results Panel */}
<div className="xl:col-span-7">
{marketResearchResult ? (
<Card className="border-slate-200/60 shadow-2xl shadow-slate-200/50 overflow-hidden bg-white group min-h-[600px]">
<CardHeader className="bg-slate-900 text-white p-6 relative overflow-hidden">
<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>
<CardTitle className="text-2xl font-black tracking-tight">{marketResearchResult.websiteUrl}</CardTitle>
<CardDescription className="text-indigo-200 font-medium">Generated on {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" />
</div>
</div>
</CardHeader>
<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>
</TabsList>
<div className="p-6">
<TabsContent value="summary" className="m-0 focus-visible:ring-0">
<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
</h3>
<p className="text-sm text-indigo-900/80 leading-relaxed font-medium">
{marketResearchResult.executiveSummary}
</p>
</div>
<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
</h4>
<ul className="space-y-2">
{marketResearchResult.competitiveAnalysis.advantages.map((adv, i) => (
<li key={i} className="text-xs font-bold text-slate-700 flex gap-2">
<span className="text-emerald-500"></span> {adv}
</li>
))}
</ul>
</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
</h4>
<ul className="space-y-2">
{marketResearchResult.competitiveAnalysis.disadvantages.map((dis, i) => (
<li key={i} className="text-xs font-bold text-slate-700 flex gap-2">
<span className="text-rose-500"></span> {dis}
</li>
))}
</ul>
</div>
</div>
<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
</h4>
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
{marketResearchResult.recommendations.map((rec, i) => (
<li key={i} className="text-xs font-bold text-slate-700 p-3 bg-white border border-amber-100 rounded-xl shadow-sm flex items-center gap-3">
<span className="h-6 w-6 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center text-[10px] shrink-0">{i + 1}</span>
{rec}
</li>
))}
</ul>
</div>
</div>
</TabsContent>
<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>
</div>
<div className="rounded-xl border border-slate-200 overflow-hidden">
{renderPriceMatrix()}
</div>
</div>
</TabsContent>
<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>
</div>
<div className="rounded-xl border border-slate-200 overflow-hidden">
{renderFeatureTable()}
</div>
</div>
</TabsContent>
<TabsContent value="positioning" className="m-0 focus-visible:ring-0">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<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
</h4>
<p className="text-xs font-medium leading-relaxed opacity-90">
{marketResearchResult.marketPositioning.landscape}
</p>
</div>
</div>
<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
</h4>
<p className="text-xs font-medium leading-relaxed font-bold">
{marketResearchResult.marketPositioning.segmentation}
</p>
</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>
<p className="text-[10px] font-medium text-slate-400">
{marketResearchResult.methodology}
</p>
</div>
</div>
</TabsContent>
</div>
</Tabs>
</CardContent>
</Card>
) : (
<Card className="border-dashed border-2 border-slate-200 bg-slate-50/50 flex flex-col items-center justify-center p-12 min-h-[600px] text-center group">
<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>
<p className="text-sm text-slate-400 font-medium max-w-[280px] mt-2 group-hover:text-slate-500 transition-colors">
{t.emptyState}
</p>
</Card>
)}
</div>
</div>
</div>
);
};
export default MarketResearcher;

View File

@@ -3,11 +3,11 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import useStore from "@/lib/store";
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone, Languages } from "lucide-react";
import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X, Megaphone, Languages, Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { translations } from "@/lib/i18n/translations";
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "history" | "settings";
export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "googleads" | "market-research" | "history" | "settings";
interface SidebarProps {
currentView: View;
@@ -27,6 +27,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
{ id: "uxdesigner" as View, label: t.uxDesigner, icon: Palette },
{ id: "slides" as View, label: t.slidesGen, icon: Presentation },
{ id: "googleads" as View, label: t.googleAds, icon: Megaphone },
{ id: "market-research" as View, label: t.marketResearch, icon: Search },
{ id: "history" as View, label: t.history, icon: History, count: history.length },
{ id: "settings" as View, label: t.settings, icon: Settings },
];

View File

@@ -11,6 +11,7 @@ export const translations = {
slidesGen: "Slides Gen",
googleAds: "Google Ads",
uxDesigner: "UX Designer",
marketResearch: "Market Research",
settings: "Settings",
history: "History",
},
@@ -123,6 +124,20 @@ export const translations = {
generating: "Crafting your story...",
emptyState: "Your presentation will appear here",
attachFiles: "Attach files for context",
},
marketResearch: {
title: "AI Market Research",
description: "Automated competitive intelligence and market analysis",
websiteUrl: "Company Website",
websitePlaceholder: "Primary company website (e.g., mysite.com)",
additionalUrls: "Additional Page URLs (optional)",
competitors: "Competitor Websites (3-10 recommended)",
competitorPlaceholder: "Competitor URL (e.g., competitor.com)",
productMapping: "Product/Service Comparison",
mappingPlaceholder: "Product Name/Category",
generate: "Start Research",
researching: "Performing Deep Analysis...",
emptyState: "Your comprehensive market research report will appear here",
}
},
ru: {
@@ -135,6 +150,7 @@ export const translations = {
slidesGen: "Генератор слайдов",
googleAds: "Google Реклама",
uxDesigner: "UX Дизайнер",
marketResearch: "Анализ рынка",
settings: "Настройки",
history: "История",
},
@@ -247,6 +263,20 @@ export const translations = {
generating: "Создаем вашу историю...",
emptyState: "Ваша презентация появится здесь",
attachFiles: "Прикрепить файлы для контекста",
},
marketResearch: {
title: "ИИ Анализ рынка",
description: "Автоматизированная конкурентная разведка и анализ рынка",
websiteUrl: "Сайт вашей компании",
websitePlaceholder: "Основной сайт компании (напр., mysite.ru)",
additionalUrls: "Дополнительные URL (необязательно)",
competitors: "Сайты конкурентов (рекомендуется 3-10)",
competitorPlaceholder: "URL конкурента (напр., competitor.ru)",
productMapping: "Сравнение продуктов/услуг",
mappingPlaceholder: "Название/Категория продукта",
generate: "Начать исследование",
researching: "Выполнение глубокого анализа...",
emptyState: "Ваш подробный отчет об исследовании рынка появится здесь",
}
},
he: {
@@ -259,6 +289,7 @@ export const translations = {
slidesGen: "מחולל מצגות",
googleAds: "Google Ads",
uxDesigner: "מעצב UX",
marketResearch: "מחקר שוק",
settings: "הגדרות",
history: "היסטוריה",
},
@@ -371,6 +402,20 @@ export const translations = {
generating: "יוצר את הסיפור שלך...",
emptyState: "המצגת שלך תופיע כאן",
attachFiles: "צרף קבצים להקשר",
},
marketResearch: {
title: "מחקר שוק AI",
description: "מודיעין תחרותי וניתוח שוק אוטומטי",
websiteUrl: "אתר החברה",
websitePlaceholder: "אתר החברה הראשי (למשל: mysite.co.il)",
additionalUrls: "כתובות URL נוספות (אופציונלי)",
competitors: "אתרי מתחרים (מומלץ 3-10)",
competitorPlaceholder: "URL של מתחרה (למשל: competitor.com)",
productMapping: "השוואת מוצרים/שירותים",
mappingPlaceholder: "שם המוצר/קטגוריה",
generate: "התחל מחקר",
researching: "מבצע ניתוח מעמיק...",
emptyState: "דו\"ח מחקר השוק המקיף שלך יופיע כאן",
}
}
};

View File

@@ -240,6 +240,22 @@ export class ModelAdapter {
return this.callWithFallback((service) => service.generateMagicWand(websiteUrl, product, budget, specialInstructions, model), providers);
}
async generateMarketResearch(
options: {
websiteUrl: string;
additionalUrls?: string[];
competitors: string[];
productMapping: string;
specialInstructions?: string;
},
provider?: ModelProvider,
model?: string
): Promise<APIResponse<string>> {
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
const providers: ModelProvider[] = provider ? [provider] : fallback;
return this.callWithFallback((service) => service.generateMarketResearch(options, model), providers);
}
async chatCompletion(
messages: ChatMessage[],

View File

@@ -606,6 +606,80 @@ Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategi
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
}
async generateMarketResearch(
options: {
websiteUrl: string;
additionalUrls?: string[];
competitors: string[];
productMapping: string;
specialInstructions?: string;
},
model?: string
): Promise<APIResponse<string>> {
const { websiteUrl, additionalUrls = [], competitors = [], productMapping, specialInstructions = "" } = options;
const systemMessage: ChatMessage = {
role: "system",
content: `You are a WORLD-CLASS Market Research Analyst and Competitive Intelligence Expert. Your task is to perform a deep-dive automated market analysis and competitive intelligence gathering.
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
\`\`\`json
{
"executiveSummary": "High-level overview of findings",
"priceComparisonMatrix": [
{
"product": "Product Name",
"userPrice": "$XX.XX",
"competitorPrices": [
{ "competitor": "Competitor Name", "price": "$XX.XX" }
]
}
],
"featureComparisonTable": [
{
"feature": "Feature Name",
"userStatus": "Yes/No or description",
"competitorStatus": [
{ "competitor": "Competitor Name", "status": "Yes/No or description" }
]
}
],
"marketPositioning": {
"landscape": "Current market landscape description",
"segmentation": "Target market segments"
},
"competitiveAnalysis": {
"advantages": ["Point 1", "Point 2"],
"disadvantages": ["Point 1", "Point 2"]
},
"recommendations": ["Rec 1", "Rec 2"],
"methodology": "How the research was conducted"
}
\`\`\`
REQUIREMENTS:
- Analysis must be based on the provided website and competitor URLs.
- Price comparison should be as realistic as possible based on industry knowledge.
- Feature table should focus on core technical and business value.
- Competitive analysis must highlight USP (Unique Selling Proposition).`,
};
const userMessage: ChatMessage = {
role: "user",
content: `🔬 MARKET RESEARCH REQUEST 🔬
PRIMARY WEBSITE: ${websiteUrl}
ADDITIONAL PAGES: ${additionalUrls.join(", ")}
COMPETITORS: ${competitors.join(", ")}
PRODUCT COMPARISON MAPPING: ${productMapping}
${specialInstructions ? `CUSTOM PARAMETERS: ${specialInstructions}` : ""}
Please conduct a comprehensive competitive analysis and market research report.`,
};
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
}
}
export default OllamaCloudService;

View File

@@ -934,6 +934,55 @@ Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategi
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
}
async generateMarketResearch(
options: {
websiteUrl: string;
additionalUrls?: string[];
competitors: string[];
productMapping: string;
specialInstructions?: string;
},
model?: string
): Promise<APIResponse<string>> {
const { websiteUrl, additionalUrls = [], competitors = [], productMapping, specialInstructions = "" } = options;
const systemMessage: ChatMessage = {
role: "system",
content: `You are a WORLD-CLASS Market Research Analyst. Perform a deep-dive automated market analysis.
OUTPUT FORMAT - JSON:
{
"executiveSummary": "findings",
"priceComparisonMatrix": [
{ "product": "P", "userPrice": "$", "competitorPrices": [{ "competitor": "C", "price": "$" }] }
],
"featureComparisonTable": [
{ "feature": "F", "userStatus": "status", "competitorStatus": [{ "competitor": "C", "status": "status" }] }
],
"marketPositioning": { "landscape": "LS", "segmentation": "SG" },
"competitiveAnalysis": { "advantages": [], "disadvantages": [] },
"recommendations": [],
"methodology": "method"
}
REQUIREMENTS: Use provided URLs. Be realistic.`,
};
const userMessage: ChatMessage = {
role: "user",
content: `🔬 MARKET RESEARCH REQUEST 🔬
WEBSITE: ${websiteUrl}
PAGES: ${additionalUrls.join(", ")}
COMPETITORS: ${competitors.join(", ")}
MAPPING: ${productMapping}
${specialInstructions ? `CUSTOM: ${specialInstructions}` : ""}
Perform analysis based on provided instructions.`,
};
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
}
async listModels(): Promise<APIResponse<string[]>> {
const models = [
"coder-model",

View File

@@ -687,6 +687,78 @@ MISSION: Perform a DEEP 360° competitive intelligence analysis and generate 5-7
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
}
async generateMarketResearch(
options: {
websiteUrl: string;
additionalUrls?: string[];
competitors: string[];
productMapping: string;
specialInstructions?: string;
},
model?: string
): Promise<APIResponse<string>> {
const { websiteUrl, additionalUrls = [], competitors = [], productMapping, specialInstructions = "" } = options;
const systemMessage: ChatMessage = {
role: "system",
content: `You are a WORLD-CLASS Market Research Analyst and Competitive Intelligence Expert. Perform a deep-dive automated market analysis.
OUTPUT FORMAT - Return ONLY valid JSON with this structure:
\`\`\`json
{
"executiveSummary": "High-level overview of findings",
"priceComparisonMatrix": [
{
"product": "Product Name",
"userPrice": "$XX.XX",
"competitorPrices": [
{ "competitor": "Competitor Name", "price": "$XX.XX" }
]
}
],
"featureComparisonTable": [
{
"feature": "Feature Name",
"userStatus": "Yes/No or description",
"competitorStatus": [
{ "competitor": "Competitor Name", "status": "Yes/No or description" }
]
}
],
"marketPositioning": {
"landscape": "Current market landscape description",
"segmentation": "Target market segments"
},
"competitiveAnalysis": {
"advantages": ["Point 1", "Point 2"],
"disadvantages": ["Point 1", "Point 2"]
},
"recommendations": ["Rec 1", "Rec 2"],
"methodology": "How the research was conducted"
}
\`\`\`
REQUIREMENTS:
- Focus on accuracy and actionable intelligence.
- Be realistic with price and feature estimates based on the provided URLs.`,
};
const userMessage: ChatMessage = {
role: "user",
content: `🔬 MARKET RESEARCH REQUEST 🔬
PRIMARY WEBSITE: ${websiteUrl}
ADDITIONAL PAGES: ${additionalUrls.join(", ")}
COMPETITORS: ${competitors.join(", ")}
PRODUCT COMPARISON MAPPING: ${productMapping}
${specialInstructions ? `CUSTOM PARAMETERS: ${specialInstructions}` : ""}
Perform a COMPREHENSIVE competitive intelligence analysis.`,
};
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
}
}
export default ZaiPlanService;

View File

@@ -1,5 +1,5 @@
import { create } from "zustand";
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult } from "@/types";
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult, MarketResearchResult, AppView } from "@/types";
interface AppState {
currentPrompt: string;
@@ -9,6 +9,7 @@ interface AppState {
slidesPresentation: SlidesPresentation | null;
googleAdsResult: GoogleAdsResult | null;
magicWandResult: MagicWandResult | null;
marketResearchResult: MarketResearchResult | null;
language: "en" | "ru" | "he";
selectedProvider: ModelProvider;
selectedModels: Record<ModelProvider, string>;
@@ -34,6 +35,7 @@ interface AppState {
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
setMagicWandResult: (result: MagicWandResult | null) => void;
setMarketResearchResult: (result: MarketResearchResult | null) => void;
setLanguage: (lang: "en" | "ru" | "he") => void;
setSelectedProvider: (provider: ModelProvider) => void;
setSelectedModel: (provider: ModelProvider, model: string) => void;
@@ -55,6 +57,7 @@ const useStore = create<AppState>((set) => ({
slidesPresentation: null,
googleAdsResult: null,
magicWandResult: null,
marketResearchResult: null,
language: "en",
selectedProvider: "qwen",
selectedModels: {
@@ -83,6 +86,7 @@ const useStore = create<AppState>((set) => ({
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
setMagicWandResult: (result) => set({ magicWandResult: result }),
setMarketResearchResult: (result) => set({ marketResearchResult: result }),
setLanguage: (lang) => set({ language: lang }),
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
setSelectedModel: (provider, model) =>
@@ -121,6 +125,7 @@ const useStore = create<AppState>((set) => ({
slidesPresentation: null,
googleAdsResult: null,
magicWandResult: null,
marketResearchResult: null,
error: null,
}),
}));

View File

@@ -256,4 +256,40 @@ export interface MagicWandResult {
rawContent: string;
}
export interface MarketResearchResult {
id: string;
websiteUrl: string;
additionalUrls: string[];
competitors: string[];
productMapping: {
productName: string;
features: string[];
pricePoint?: string;
}[];
generatedAt: Date;
executiveSummary: string;
priceComparisonMatrix: {
product: string;
userPrice: string;
competitorPrices: { competitor: string; price: string }[];
}[];
featureComparisonTable: {
feature: string;
userStatus: boolean | string;
competitorStatus: { competitor: string; status: boolean | string }[];
}[];
marketPositioning: {
landscape: string;
segmentation: string;
};
competitiveAnalysis: {
advantages: string[];
disadvantages: string[];
};
recommendations: string[];
methodology: string;
rawContent: string;
}
export type AppView = "prompt-enhancer" | "prd-generator" | "action-plan" | "slides-gen" | "google-ads" | "ux-designer" | "market-research" | "settings" | "history";