diff --git a/app/page.tsx b/app/page.tsx index ab537fa..ae740c5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ; case "googleads": return ; + case "market-research": + return ; case "history": return ; case "settings": diff --git a/components/MarketResearcher.tsx b/components/MarketResearcher.tsx new file mode 100644 index 0000000..5cde212 --- /dev/null +++ b/components/MarketResearcher.tsx @@ -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([""]); + const [competitorUrls, setCompetitorUrls] = useState(["", "", ""]); + const [productMapping, setProductMapping] = useState(""); + const [specialInstructions, setSpecialInstructions] = useState(""); + + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(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 ( +
+ + + + + + {marketResearchResult.competitors.map((comp, i) => ( + + ))} + + + + {marketResearchResult.priceComparisonMatrix.map((item, i) => ( + + + + {marketResearchResult.competitors.map((comp) => { + const compPrice = item.competitorPrices.find(cp => cp.competitor === comp || comp.includes(cp.competitor)); + return ( + + ); + })} + + ))} + +
ProductYour Price{comp}
{item.product}{item.userPrice} + {compPrice ? compPrice.price : "N/A"} +
+
+ ); + }; + + const renderFeatureTable = () => { + if (!marketResearchResult?.featureComparisonTable) return null; + return ( +
+ + + + + + {marketResearchResult.competitors.map((comp, i) => ( + + ))} + + + + {marketResearchResult.featureComparisonTable.map((item, i) => ( + + + + {marketResearchResult.competitors.map((comp) => { + const compStatus = item.competitorStatus.find(cs => cs.competitor === comp || comp.includes(cs.competitor)); + return ( + + ); + })} + + ))} + +
FeatureYou{comp}
{item.feature} + {typeof item.userStatus === 'boolean' ? ( + item.userStatus ? : + ) : {item.userStatus}} + + {compStatus ? ( + typeof compStatus.status === 'boolean' ? ( + compStatus.status ? : + ) : {compStatus.status} + ) : "N/A"} +
+
+ ); + }; + + return ( +
+ {/* Header Section */} +
+
+
+ +
+

{t.title}

+
+

{t.description}

+
+ +
+ {/* Configuration Panel */} +
+ + + + Company Profile + + + +
+ + setWebsiteUrl(e.target.value)} + className="bg-slate-50 border-slate-200 focus:bg-white transition-all font-medium" + /> +
+ +
+ +
+ {additionalUrls.map((url, i) => ( +
+ { + 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" + /> + +
+ ))} +
+
+
+
+ + + + + Competitive Intel + + + +
+ +
+ {competitorUrls.map((url, i) => ( +
+ { + 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 && ( + + )} +
+ ))} +
+
+ +
+ +