"use client"; import { useCallback, useEffect, useState } from "react"; import { ExternalLink, Loader2, MessageSquare, RefreshCw, Send, Sparkles, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { EmptyState } from "@/components/layout/empty-state"; import { PageHeader } from "@/components/layout/page-header"; import { InlineAlert } from "@/components/ui/inline-alert"; import { Textarea } from "@/components/ui/textarea"; import { notify } from "@/lib/notifications/store"; import { useCapabilities } from "@/lib/capabilities/context"; import { useActionFeedback } from "@/lib/use-action-feedback"; import { THREADS_MAX_CHARS } from "@/lib/utils"; interface ReplyDraft { id: string; text: string; rationale?: string | null; status: string; publishedAt?: string | null; createdAt: string; } interface InboundReply { id: string; text: string; authorName?: string | null; permalink?: string | null; postedAt?: string | null; likeCount?: number | null; sentiment?: string | null; intent?: string | null; status: string; createdAt: string; published: { id: string; text: string; permalink?: string | null; publishedAt?: string | null; } | null; replyDrafts: ReplyDraft[]; } const statusLabels: Record = { NEW: { label: "待處理", variant: "warning" }, DRAFTED: { label: "已生成草稿", variant: "secondary" }, REPLIED: { label: "已回覆", variant: "success" }, }; const sentimentLabels: Record = { positive: "正面", neutral: "中性", negative: "負面", question: "提問", lead: "潛在客戶", }; export default function EngagementPage() { const [replies, setReplies] = useState([]); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(null); const [draftTexts, setDraftTexts] = useState>({}); const { feedback, clearFeedback, showError, showSuccess } = useActionFeedback(); const { isReady } = useCapabilities(); const threadsApiReady = isReady("threadsApi"); const aiReady = isReady("ai"); const load = useCallback(async (silent = false) => { if (!silent) setLoading(true); try { const res = await fetch("/api/engagement/replies"); const data = await res.json().catch(() => ({})); if (!res.ok) { showError(data.error ?? "無法載入留言", "載入失敗"); setReplies([]); return; } setReplies(data.replies ?? []); setDraftTexts( Object.fromEntries( (data.replies ?? []).flatMap((reply: InboundReply) => reply.replyDrafts.map((draft) => [draft.id, draft.text]) ) ) ); } catch { showError("網路連線異常,請稍後再試", "載入失敗"); setReplies([]); } finally { if (!silent) setLoading(false); } }, [showError]); useEffect(() => { load(); }, [load]); async function syncReplies() { setBusy("sync"); try { const res = await fetch("/api/engagement/replies", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sync: true }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { showError(data.error ?? "同步失敗", "同步留言失敗"); return; } showSuccess("已從 Threads 同步留言"); load(true); } catch { showError("網路連線異常,請稍後再試", "同步留言失敗"); } finally { setBusy(null); } } async function generateDraft(replyId: string) { setBusy(`gen-${replyId}`); try { const res = await fetch(`/api/engagement/replies/${replyId}/generate`, { method: "POST", }); const data = await res.json().catch(() => ({})); if (!res.ok) { notify({ type: "error", title: "生成回覆失敗", message: data.error }); return; } notify({ type: "success", title: "已生成回覆草稿" }); load(true); } catch { notify({ type: "error", title: "生成回覆失敗", message: "網路連線異常,請稍後再試" }); } finally { setBusy(null); } } async function saveDraft(draftId: string) { setBusy(`save-${draftId}`); try { const res = await fetch(`/api/engagement/reply-drafts/${draftId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: draftTexts[draftId], status: "EDITED" }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { showError(data.error ?? "無法儲存草稿", "儲存失敗"); return; } showSuccess("草稿已儲存"); load(true); } catch { showError("網路連線異常,請稍後再試", "儲存失敗"); } finally { setBusy(null); } } async function publishDraft(draftId: string) { setBusy(`publish-${draftId}`); try { const res = await fetch(`/api/engagement/reply-drafts/${draftId}/publish`, { method: "POST", }); const data = await res.json().catch(() => ({})); if (!res.ok) { notify({ type: "error", title: "發布回覆失敗", message: data.error }); return; } notify({ type: "success", title: "回覆已發布到 Threads" }); load(true); } catch { notify({ type: "error", title: "發布回覆失敗", message: "網路連線異常,請稍後再試" }); } finally { setBusy(null); } } async function copyText(text: string) { try { await navigator.clipboard.writeText(text); showSuccess("已複製回覆"); } catch { showError("無法複製,請手動選取文字", "複製失敗"); } } const pendingCount = replies.filter((r) => r.status === "NEW").length; return (
{busy === "sync" ? : } 同步留言 } /> {feedback && ( )} {!threadsApiReady && ( )} {loading ? (
{[0, 1, 2].map((i) => (
))}
) : replies.length === 0 ? ( {busy === "sync" ? : } 立即同步 ) : ( ) } /> ) : (
{pendingCount > 0 && (
{pendingCount} 則待處理
)} {replies.map((reply) => { const statusInfo = statusLabels[reply.status] ?? { label: reply.status, variant: "secondary" as const, }; return (
@{reply.authorName ?? "匿名"} {statusInfo.label} {reply.sentiment && ( {sentimentLabels[reply.sentiment] ?? reply.sentiment} )} {reply.postedAt ? new Date(reply.postedAt).toLocaleDateString("zh-TW", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }) : ""} {reply.likeCount != null && ` · ${reply.likeCount} 讚`}
{reply.status === "NEW" && ( )} {reply.permalink && ( )}

{reply.text}

{reply.intent && (

意圖分析:{reply.intent}

)}
{reply.published && (

你的貼文

{reply.published.text}

{reply.published.permalink && ( 查看貼文 )}
)} {reply.replyDrafts.map((draft) => { const value = draftTexts[draft.id] ?? draft.text; const isPublished = draft.status === "PUBLISHED"; return (
{isPublished ? "已發布" : "待發布"}
{value.length}/{THREADS_MAX_CHARS}