"use client"; import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { xApi, videoApi } from "@/lib/api"; import type { XPostHistoryEntry } from "@/lib/api/x"; import type { VideoPostHistoryEntry } from "@/lib/api/video"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { HelpTip } from "@/components/ui/help-tip"; import { AtSign, ChevronDown, Film, History, Rocket, Sparkles, } from "lucide-react"; const HISTORY_LIMIT = 50; const PLATFORM_LABELS: Record = { x: "X", tiktok: "TikTok" }; const VIDEO_HINT = "Rendered by the video pipeline — from a release, a feature spotlight, or an on-demand request"; type UnifiedRow = | { kind: "x"; entry: XPostHistoryEntry } | { kind: "video"; entry: VideoPostHistoryEntry }; function xKindMeta(source: XPostHistoryEntry["source"]) { if (source === "x_post") return { label: "X post", icon: Rocket, hint: "Drafted automatically when a release publishes", }; if (source === "x_feature") return { label: "Feature spotlight", icon: Sparkles, hint: "Drafted periodically by the Head of Marketing's feature-spotlight sweep", }; return { label: "X reply", icon: AtSign, hint: "Drafted automatically in reply to a meaningful mention on X", }; } // One unified row: X entries link the posted tweet / show the reject reason; // video entries list each platform's posted id (X id links out, TikTok's // inbox upload has no public URL so just the raw id is shown). function UnifiedHistoryRow({ row }: { row: UnifiedRow }) { const posted = row.entry.status === "completed"; const statusBadge = posted ? ( Posted ) : ( Rejected ); const timestamp = ( {new Date(row.entry.acted_at).toLocaleString()} ); if (row.kind === "x") { const meta = xKindMeta(row.entry.source); return (
{meta.label} {statusBadge} {timestamp}

{row.entry.body}

{posted && row.entry.tweet_id && ( View on X )} {!posted && row.entry.reject_reason && (

Reason: {row.entry.reject_reason}

)}
); } const platformIds = Object.entries(row.entry.posted); return (
Video {row.entry.occasion && ( {row.entry.occasion} )} {statusBadge} {timestamp}
{posted && platformIds.length > 0 && (
{platformIds.map(([platform, id]) => platform === "x" ? ( {PLATFORM_LABELS[platform] ?? platform}: {id} ) : ( {PLATFORM_LABELS[platform] ?? platform}: {id} ), )}
)} {!posted && row.entry.reject_reason && (

Reason: {row.entry.reject_reason}

)}
); } // Unified, collapsed-by-default history across X and video — merged client- // side, newest-acted first. Lazy-fetched only once expanded (both sources in // parallel); fixed 50-row-per-source limit (server default), no "show more". export function SocialHistorySection({ className }: { className?: string }) { const [open, setOpen] = useState(false); const { data: xHistory, isLoading: xLoading } = useQuery({ queryKey: ["x", "posts", "history"], queryFn: () => xApi.listHistory(HISTORY_LIMIT), enabled: open, }); const { data: videoHistory, isLoading: videoLoading } = useQuery({ queryKey: ["video", "posts", "history"], queryFn: () => videoApi.listHistory(HISTORY_LIMIT), enabled: open, }); const isLoading = open && (xLoading || videoLoading); const rows: UnifiedRow[] = [ ...(xHistory ?? []).map((entry): UnifiedRow => ({ kind: "x", entry })), ...(videoHistory ?? []).map( (entry): UnifiedRow => ({ kind: "video", entry, }), ), ].sort( (a, b) => new Date(b.entry.acted_at).getTime() - new Date(a.entry.acted_at).getTime(), ); return ( History Every posted or rejected draft across X and video — newest first. {isLoading && (

Loading...

)} {!isLoading && rows.length === 0 && (

No acted-on drafts yet.

)} {rows.map((row) => ( ))}
); }