"use client"; import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { xApi } from "@/lib/api"; import type { XPost, XPostExecuteResult } from "@/lib/api/x"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { HelpTip } from "@/components/ui/help-tip"; import { AtSign, CheckCircle2, Rocket, Sparkles, XCircle } from "lucide-react"; import { toast } from "sonner"; const MAX_TWEET_CHARS = 280; const _MIN_REASON_CHARS = 4; function sourceMeta(source: XPost["source"]) { if (source === "x_post") return { label: "Release 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: "Mention reply", icon: AtSign, hint: "Drafted automatically in reply to a meaningful mention on X", }; } // Explains what Approve does, or why it's disabled — surfaced on the button // itself so the CEO doesn't have to guess between "already posting", "over // the limit", or "empty". Always returns a non-empty string (never null): // HelpTip renders a bare child vs. a Tooltip-wrapped one depending on // truthiness, and toggling that branch on a live-changing condition (like // `approving`) unmounts/remounts the child, losing any DOM reference a // caller captured before the state flip. function approveHint( approving: boolean, overLimit: boolean, bodyEmpty: boolean, ): string { if (approving) return "Already posting this draft"; if (overLimit) return `Over X's ${MAX_TWEET_CHARS}-character limit — trim the draft to enable`; if (bodyEmpty) return "Draft body is empty"; return "Post this draft to X"; } function describeExecuteResult(result: XPostExecuteResult): string { if (result.status === "posted") return "Posted to X."; if (result.status === "already_posted") return "Already posted — no-op."; if (result.status === "already_in_progress") return "A post is already in progress for this draft."; if (result.status === "no_credentials") return "No X credentials configured — set them below first."; return `${result.status}: ${result.detail}`; } // One row of the queue: an editable draft body + char counter + approve/reject. function XPostRow({ post, onApprove, onReject, approving, }: { post: XPost; onApprove: (taskId: string, body: string) => void; onReject: (post: XPost) => void; approving: boolean; }) { // `edited` holds the user's in-progress textarea input; null means "show // the server value". Deriving the displayed body avoids syncing query // state into local state with an effect (mirrors TranscriptRetentionCard). const [edited, setEdited] = useState(null); const body = edited ?? post.body; const meta = sourceMeta(post.source); const overLimit = body.length > MAX_TWEET_CHARS; return (
{meta.label} {post.release_version && ( v{post.release_version} )} {post.mention && ( re: {post.mention.text} )} {post.feature && ( feature: {post.feature.title} )}