haixunMaster/components/viral-analysis-panel.tsx

199 lines
7.1 KiB
TypeScript
Raw Permalink Normal View History

2026-06-21 12:50:31 +00:00
"use client";
import { useEffect, useState } from "react";
import { Copy, Flame, ImageIcon, Loader2, MessageCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { notify } from "@/lib/notifications/store";
import type { ViralAnalysis } from "@/lib/types/viral";
interface ViralAnalysisPanelProps {
scanItemId: string;
mediaUrls?: string[];
viralAnalysis?: ViralAnalysis | null;
onUpdate: () => void;
}
export function ViralAnalysisPanel({
scanItemId,
mediaUrls = [],
viralAnalysis,
onUpdate,
}: ViralAnalysisPanelProps) {
const [analyzing, setAnalyzing] = useState(false);
const [replicating, setReplicating] = useState(false);
const [analysis, setAnalysis] = useState<ViralAnalysis | null>(viralAnalysis ?? null);
useEffect(() => {
setAnalysis(viralAnalysis ?? null);
}, [viralAnalysis, scanItemId]);
async function handleAnalyze() {
setAnalyzing(true);
try {
const res = await fetch("/api/analyze-viral", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ scanItemId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
notify({ type: "error", title: "爆款分析失敗", message: data.error });
return;
}
setAnalysis(data.analysis);
onUpdate();
} catch {
notify({ type: "error", title: "爆款分析失敗", message: "網路連線異常,請稍後再試" });
} finally {
setAnalyzing(false);
}
}
async function handleReplicate() {
setReplicating(true);
try {
const res = await fetch("/api/replicate-viral", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ scanItemId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
notify({ type: "error", title: "複製爆款失敗", message: data.error });
return;
}
notify({ type: "success", title: "複製版草稿已生成", href: "/" });
onUpdate();
} catch {
notify({ type: "error", title: "複製爆款失敗", message: "網路連線異常,請稍後再試" });
} finally {
setReplicating(false);
}
}
return (
<div className="mt-3 space-y-3 border-t border-border pt-3">
{mediaUrls.length > 0 && (
<div className="flex gap-2 overflow-x-auto pb-1">
{mediaUrls.slice(0, 4).map((url) => (
<div
key={url}
className="relative h-20 w-20 shrink-0 overflow-hidden rounded-lg border border-border bg-muted"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={url} alt="貼文附圖" className="h-full w-full object-cover" />
</div>
))}
</div>
)}
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" onClick={handleAnalyze} disabled={analyzing}>
{analyzing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Flame className="h-3.5 w-3.5" />
)}
{analyzing ? "分析中…" : analysis ? "重新分析" : "爆款分析"}
</Button>
{analysis && (
<Button size="sm" onClick={handleReplicate} disabled={replicating}>
{replicating ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
{replicating ? "生成中…" : "複製爆款"}
</Button>
)}
</div>
{analysis && (
<div className="space-y-3 rounded-lg border border-border bg-background p-3.5 text-[13px]">
<div>
<p className="mb-1.5 flex items-center gap-1.5 font-semibold">
<Flame className="h-3.5 w-3.5" />
</p>
<ul className="list-inside list-disc space-y-0.5 text-muted-foreground">
{analysis.whyViral.map((reason) => (
<li key={reason}>{reason}</li>
))}
</ul>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div>
<p className="font-medium">Hook </p>
<p className="text-muted-foreground">{analysis.hookPattern}</p>
</div>
<div>
<p className="font-medium"></p>
<p className="text-muted-foreground">{analysis.structurePattern}</p>
</div>
<div>
<p className="font-medium"></p>
<p className="text-muted-foreground">{analysis.emotionalTrigger}</p>
</div>
<div>
<p className="font-medium"></p>
<p className="text-muted-foreground">{analysis.timingAngle}</p>
</div>
</div>
<div>
<p className="mb-1.5 flex items-center gap-1.5 font-semibold">
<MessageCircle className="h-3.5 w-3.5" />
</p>
<p className="text-muted-foreground">{analysis.commentInsights.whyPeopleEngage}</p>
<div className="mt-2 flex flex-wrap gap-1.5">
{analysis.commentInsights.themes.map((t) => (
<Badge key={t} variant="outline" className="text-[11px]">
{t}
</Badge>
))}
</div>
{analysis.commentInsights.audienceQuestions.length > 0 && (
<p className="mt-2 text-[12px] text-muted-foreground">
{analysis.commentInsights.audienceQuestions.join("、")}
</p>
)}
</div>
{analysis.visualAnalysis.hasImages && (
<div>
<p className="mb-1.5 flex items-center gap-1.5 font-semibold">
<ImageIcon className="h-3.5 w-3.5" />
</p>
<p className="text-muted-foreground">
{analysis.visualAnalysis.layout} · {analysis.visualAnalysis.colorMood}
</p>
<p className="mt-1 text-muted-foreground"> hook{analysis.visualAnalysis.visualHook}</p>
<ul className="mt-2 list-inside list-disc text-muted-foreground">
{analysis.visualAnalysis.replicationTips.map((tip) => (
<li key={tip}>{tip}</li>
))}
</ul>
{analysis.visualAnalysis.imageGenPrompt && (
<div className="mt-2 rounded-md bg-muted p-2.5">
<p className="text-[11px] font-medium">AI Prompt</p>
<p className="mt-1 font-mono text-[11px] leading-relaxed text-muted-foreground">
{analysis.visualAnalysis.imageGenPrompt}
</p>
</div>
)}
</div>
)}
<div>
<p className="font-medium"></p>
<p className="text-muted-foreground">{analysis.replicationStrategy}</p>
</div>
</div>
)}
</div>
);
}