"use client"; import { useState, useCallback } from "react"; import { useSession } from "next-auth/react"; import { useApi } from "@/lib/hooks/use-api"; import { competitorApi, type Competitor, type CompetitorRecommendation, type CompetitorGapSummary, type CompetitorInsight } from "@/lib/api/competitor"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { LoadingState, ErrorState, EmptyState } from "@/components/ui/api-states"; import { CompetitorRadarChart } from "@/components/charts/CompetitorRadarChart"; import { Users, Plus, Trash2, Loader2, Lightbulb, BarChart3, Play, AlertTriangle, } from "lucide-react"; const ANALYSIS_TYPES = [ { value: "visibility", label: "可见度分析" }, { value: "sentiment", label: "情感分析" }, { value: "citation", label: "引用对比" }, { value: "content_gap", label: "内容差距" }, ]; const RADAR_COLORS = [ "hsl(346.8 77.2% 49.8%)", "hsl(24.6 95% 53.1%)", "hsl(142.1 76.2% 36.3%)", "hsl(262.1 83.3% 57.8%)", "hsl(45 93.5% 47.6%)", ]; export default function CompetitorsPage() { const { data: session } = useSession(); const token = session?.accessToken ?? ""; const { data: brandsData, isLoading: brandsLoading } = useApi<{ items: { id: string; name: string }[] }>("/api/v1/brands/?limit=1"); const brandId = brandsData?.items?.[0]?.id ?? null; const brandName = brandsData?.items?.[0]?.name ?? ""; const competitorsUrl = brandId ? `/api/v1/brands/${brandId}/competitors/` : null; const { data: competitorsData, isLoading: competitorsLoading, error: competitorsError, refresh: refreshCompetitors } = useApi<{ items: Competitor[]; total: number }>(competitorsUrl); const recommendationsUrl = brandId ? `/api/v1/brands/${brandId}/competitors/recommendations/` : null; const { data: recommendationsData, isLoading: recommendationsLoading, error: recommendationsError, refresh: refreshRecommendations } = useApi(recommendationsUrl); const gapSummaryUrl = brandId ? `/api/v1/competitor/brand/${brandId}/gap-summary` : null; const { data: gapSummaryData, isLoading: gapSummaryLoading, error: gapSummaryError } = useApi(gapSummaryUrl); const insightsUrl = brandId ? `/api/v1/competitor/brand/${brandId}` : null; const { data: insightsData, isLoading: insightsLoading, refresh: refreshInsights } = useApi<{ items: CompetitorInsight[]; total: number }>(insightsUrl); const competitors = competitorsData?.items ?? []; const recommendations = recommendationsData ?? []; const gapSummaries = gapSummaryData ?? []; const insights = insightsData?.items ?? []; const [addDialogOpen, setAddDialogOpen] = useState(false); const [manualName, setManualName] = useState(""); const [adding, setAdding] = useState(false); const [deletingId, setDeletingId] = useState(null); const [analysisCompetitorId, setAnalysisCompetitorId] = useState(""); const [analysisType, setAnalysisType] = useState(""); const [analyzing, setAnalyzing] = useState(false); const [analysisError, setAnalysisError] = useState(null); const handleAddFromRecommendation = useCallback(async (name: string) => { if (!token || !brandId) return; setAdding(true); try { await competitorApi.add(token, brandId, { name }); refreshCompetitors(); } catch { } finally { setAdding(false); } }, [token, brandId, refreshCompetitors]); const handleAddManual = useCallback(async () => { const trimmed = manualName.trim(); if (!trimmed || !token || !brandId) return; setAdding(true); try { await competitorApi.add(token, brandId, { name: trimmed }); setManualName(""); refreshCompetitors(); setAddDialogOpen(false); } catch { } finally { setAdding(false); } }, [manualName, token, brandId, refreshCompetitors]); const handleDelete = useCallback(async (competitorId: string) => { if (!token || !brandId) return; setDeletingId(competitorId); try { await competitorApi.delete(token, brandId, competitorId); refreshCompetitors(); } catch { } finally { setDeletingId(null); } }, [token, brandId, refreshCompetitors]); const handleAnalyze = useCallback(async () => { if (!token || !brandId || !analysisCompetitorId || !analysisType) return; setAnalyzing(true); setAnalysisError(null); try { await competitorApi.analyze(token, { brand_id: brandId, competitor_id: analysisCompetitorId, analysis_type: analysisType, }); refreshInsights(); } catch (err) { setAnalysisError(err instanceof Error ? err.message : "分析失败"); } finally { setAnalyzing(false); } }, [token, brandId, analysisCompetitorId, analysisType, refreshInsights]); const radarData = (() => { if (!gapSummaries.length) return []; const dimensions = Object.keys(gapSummaries[0]?.dimensions || {}); return dimensions.map((dim) => { const item: { label: string; [key: string]: string | number | undefined } = { label: dim, [brandName]: 50 }; gapSummaries.forEach((gs) => { const val = gs.dimensions[dim]; item[gs.competitor_name] = typeof val === "number" ? val : 0; }); return item; }); })(); const radarCompetitors = gapSummaries.map((gs, i) => ({ name: gs.competitor_name, color: RADAR_COLORS[i % RADAR_COLORS.length], })); if (brandsLoading) { return (

竞品分析

分析竞品表现,发现差距与机会

); } return (

竞品分析

分析竞品表现,发现差距与机会

添加竞品 从推荐选择 手动输入 {recommendationsLoading ? (
加载推荐中...
) : recommendationsError ? ( ) : recommendations.length === 0 ? ( ) : (
{recommendations.map((rec) => { const alreadyAdded = competitors.some((c) => c.name === rec.name); return (

{rec.name}

{rec.reason}

); })}
)}
setManualName(e.target.value)} placeholder="输入竞品名称" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); handleAddManual(); } }} />
竞品列表 {competitors.length}/5 {competitorsLoading ? ( ) : competitorsError ? ( ) : competitors.length === 0 ? ( } /> ) : (
{competitors.map((comp) => (

{comp.name}

添加于 {new Date(comp.created_at).toLocaleDateString("zh-CN")}

))}
)}
竞品分析
{analysisError && (
{analysisError}
)} {insightsLoading ? ( ) : insights.length > 0 ? (
{insights.map((insight) => (
{insight.insight_type} {insight.competitor_name && ( {insight.competitor_name} )} {new Date(insight.created_at).toLocaleString("zh-CN")}
{insight.recommendations && insight.recommendations.length > 0 && (
{insight.recommendations.map((rec, i) => (
{rec}
))}
)}
))}
) : null}
竞品雷达图 {gapSummaryLoading ? ( ) : gapSummaryError ? ( ) : radarData.length > 0 ? ( ) : ( } /> )} 差距评分 {gapSummaryLoading ? ( ) : gapSummaryError ? ( ) : gapSummaries.length === 0 ? ( } /> ) : (
{gapSummaries.map((gs) => (
{gs.competitor_name} 50 ? "destructive" : gs.gap_score > 20 ? "secondary" : "default"} > 差距 {gs.gap_score.toFixed(1)}
{Object.entries(gs.dimensions).map(([key, val]) => (
{key}: {typeof val === "number" ? val.toFixed(1) : String(val)}
))}
))}
)}
); }