import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { PostMetrics, postTypeLabel } from "../../components/studio/PostMetrics"; import { ReplyComposer, type ReplySelection } from "../../components/studio/ReplyComposer"; import { Badge, Button, Card, EmptyState, Textarea } from "../../components/ui"; import { useData, useRepos } from "../../data/DataContext"; import type { OwnPost, OwnPostReply, Persona, ThreadsAccount } from "../../domain/types"; import { useI18n } from "../../i18n/I18nContext"; import type { AttachedImage } from "../../lib/attachImage"; import { isPersonaReady } from "../../lib/personaPrompt"; import { formatLocalDateTime } from "../../lib/time"; type Props = { /** 頂部預設帳號(貼文列表視角) */ accountId: string; /** 頂部預設人設 */ personaId: string; accounts: ThreadsAccount[]; personas: Persona[]; }; type ReplyFilter = "pending" | "replied" | "all"; function defaultSelection(accountId: string, personaId: string): ReplySelection { return { accountId, personaId }; } /** 第一層來訊(別人留在貼文下,不是子留言) */ function isTopLevelIncoming(r: OwnPostReply, myUsernames: Set): boolean { if (r.parent_reply_id) return false; if (r.is_mine) return false; return !myUsernames.has(r.username); } function isPendingReply(r: OwnPostReply): boolean { return (r.reply_status || "pending") === "pending"; } function childrenOf(all: OwnPostReply[], parentId: string): OwnPostReply[] { return all .filter((r) => r.parent_reply_id === parentId) .slice() .sort((a, b) => a.created_at - b.created_at); } export function OwnPostsPanel({ accountId, personaId, accounts, personas }: Props) { const repos = useRepos(); const { refresh, tick } = useData(); const { t } = useI18n(); const navigate = useNavigate(); const [posts, setPosts] = useState([]); const [syncedAt, setSyncedAt] = useState(null); const [expanded, setExpanded] = useState(""); /** 每筆回覆草稿 key = postId 或 postId:replyId */ const [draftByKey, setDraftByKey] = useState>({}); /** 每筆回覆附圖 */ const [imagesByKey, setImagesByKey] = useState>({}); /** 每筆回覆的帳號/人設(預設吃頂部) */ const [selByKey, setSelByKey] = useState>({}); /** 哪些貼文已手動按過「分析結構」(才顯示 AI 分析結果) */ const [analyzedIds, setAnalyzedIds] = useState>({}); /** 哪些 key 已打開回覆編輯區 */ const [composeOpen, setComposeOpen] = useState>({}); /** 留言篩選:預設未回覆 */ const [replyFilter, setReplyFilter] = useState("pending"); const [busy, setBusy] = useState(""); const [message, setMessage] = useState(""); const myUsernames = new Set(accounts.map((a) => a.username)); useEffect(() => { void (async () => { if (!accountId) { setPosts([]); return; } setPosts(await repos.ownPosts.list(accountId)); setSyncedAt(await repos.ownPosts.lastSyncedAt()); })(); }, [accountId, repos.ownPosts, tick]); function getSel(key: string): ReplySelection { return selByKey[key] || defaultSelection(accountId, personaId); } function setSel(key: string, next: ReplySelection) { setSelByKey((m) => ({ ...m, [key]: next })); } function openCompose(key: string) { setComposeOpen((m) => ({ ...m, [key]: true })); setSelByKey((m) => ({ ...m, [key]: m[key] || defaultSelection(accountId, personaId), })); if (!draftByKey[key]) { setDraftByKey((m) => ({ ...m, [key]: m[key] || "" })); } } async function sync() { if (!accountId) return; setBusy("sync"); setMessage(""); try { const list = await repos.ownPosts.sync(accountId); setPosts(list); setSyncedAt(await repos.ownPosts.lastSyncedAt()); setMessage(t("posts.syncDone")); refresh(); } finally { setBusy(""); } } async function genReply(key: string, post: OwnPost, replyId?: string) { const sel = getSel(key); const persona = personas.find((p) => p.id === sel.personaId); if (!isPersonaReady(persona)) { setMessage(t("posts.needPersona")); return; } setBusy(key); setMessage(""); try { const text = await repos.ownPosts.generateReply({ postId: post.id, replyId, personaId: sel.personaId, }); setDraftByKey((m) => ({ ...m, [key]: text })); setComposeOpen((m) => ({ ...m, [key]: true })); } catch (e) { setMessage(e instanceof Error ? e.message : t("posts.genFail")); } finally { setBusy(""); } } async function send(key: string, post: OwnPost, replyId?: string) { const text = draftByKey[key]; const sel = getSel(key); const imgs = imagesByKey[key] || []; if (!text?.trim()) { setMessage(t("posts.needText")); return; } setBusy(`send:${key}`); try { const next = await repos.ownPosts.sendReply({ postId: post.id, replyId, text, accountId: sel.accountId, imageUrls: imgs.map((i) => i.url), }); setPosts((list) => list.map((p) => (p.id === next.id ? next : p))); const n = imgs.length; const user = accounts.find((a) => a.id === sel.accountId)?.username || t("posts.accountFallback"); setMessage(t("posts.sent", { user }) + (n ? t("posts.sentImages", { n }) : "")); setComposeOpen((m) => ({ ...m, [key]: false })); setDraftByKey((m) => ({ ...m, [key]: "" })); setImagesByKey((m) => ({ ...m, [key]: [] })); // 送出後展開留言串,方便看到剛掛上的子留言 setExpanded(post.id); refresh(); } catch (e) { setMessage(e instanceof Error ? e.message : t("posts.sendFail")); } finally { setBusy(""); } } async function analyze(post: OwnPost) { setBusy(`an:${post.id}`); setMessage(""); try { const next = await repos.ownPosts.analyzePost(post.id); setPosts((list) => list.map((p) => (p.id === next.id ? next : p))); setAnalyzedIds((m) => ({ ...m, [post.id]: true })); setExpanded(post.id); setMessage(t("posts.analyzeDone")); refresh(); } catch (e) { setMessage(e instanceof Error ? e.message : t("posts.analyzeFail")); } finally { setBusy(""); } } function openComposeMimic(post: OwnPost) { navigate(`/app/studio?tab=compose&mimic=${encodeURIComponent(post.text)}`); } return (

{syncedAt ? t("posts.syncedAt", { time: formatLocalDateTime(syncedAt) }) : t("posts.notSynced")}

{message ? (

{message}

) : null} {posts.length === 0 ? ( ) : ( posts.map((post) => { const open = expanded === post.id; const rootKey = post.id; const showAnalysis = Boolean(analyzedIds[post.id]); return (
{post.thumbnail_url || post.media_url ? ( ) : null}
{post.topic_tag ? {post.topic_tag} : null} {postTypeLabel(post, t)} {post.is_quote_post ? quote : null} {post.insights_status && post.insights_status !== "ok" ? ( {post.insights_status} ) : null}

{post.text}

{formatLocalDateTime(post.published_at)} {post.shortcode ? ` · ${post.shortcode}` : ""} {post.permalink ? ( <> {" · "} {t("posts.openThreads")} ) : null}

{open ? : null} {/* 成效數字本來就有;AI 結構分析僅手動後顯示 */} {showAnalysis && post.insight ? (

{t("posts.insight", { text: post.insight })}

) : null} {showAnalysis && post.formula_summary ? (

{t("posts.review", { text: post.formula_summary })}

) : null} {showAnalysis && post.formula_detail ? (