199 lines
7.1 KiB
TypeScript
199 lines
7.1 KiB
TypeScript
"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>
|
||
);
|
||
} |