"use client"; import { useState, useEffect, useRef } from "react"; import { ChevronDown, ChevronRight, Copy, Check, Loader2, PenLine } from "lucide-react"; import type { BriefSchema, CitedPoint } from "@/lib/types"; import { billsAPI } from "@/lib/api"; import { useIsFollowing } from "@/lib/hooks/useFollows"; interface DraftLetterPanelProps { billId: string; brief: BriefSchema; chamber?: string; } type Stance = "yes" | "no" | null; type Tone = "short" | "polite" | "firm"; function pointText(p: string | CitedPoint): string { return typeof p === "string" ? p : p.text; } function pointKey(p: string | CitedPoint, i: number): string { return `${i}-${typeof p === "string" ? p.slice(0, 40) : p.text.slice(0, 40)}`; } function chamberToRecipient(chamber?: string): "house" | "senate" { return chamber?.toLowerCase() === "senate" ? "senate" : "house"; } export function DraftLetterPanel({ billId, brief, chamber }: DraftLetterPanelProps) { const [open, setOpen] = useState(false); const existing = useIsFollowing("bill", billId); const [stance, setStance] = useState(null); const prevModeRef = useRef(undefined); // Keep stance in sync with follow mode changes (including unfollow → null) useEffect(() => { const newMode = existing?.follow_mode; if (newMode === prevModeRef.current) return; prevModeRef.current = newMode; if (newMode === "pocket_boost") setStance("yes"); else if (newMode === "pocket_veto") setStance("no"); else setStance(null); }, [existing?.follow_mode]); const recipient = chamberToRecipient(chamber); const [tone, setTone] = useState("polite"); const [selected, setSelected] = useState>(new Set()); const [includeCitations, setIncludeCitations] = useState(true); const [zipCode, setZipCode] = useState(""); const [loading, setLoading] = useState(false); const [draft, setDraft] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); const keyPoints = brief.key_points ?? []; const risks = brief.risks ?? []; const allPoints = [ ...keyPoints.map((p, i) => ({ group: "key" as const, index: i, text: pointText(p), raw: p })), ...risks.map((p, i) => ({ group: "risk" as const, index: keyPoints.length + i, text: pointText(p), raw: p })), ]; function togglePoint(globalIndex: number) { setSelected((prev) => { const next = new Set(prev); if (next.has(globalIndex)) { next.delete(globalIndex); } else if (next.size < 3) { next.add(globalIndex); } return next; }); } async function handleGenerate() { if (selected.size === 0 || stance === null) return; const selectedPoints = allPoints .filter((p) => selected.has(p.index)) .map((p) => { if (includeCitations && typeof p.raw !== "string" && p.raw.citation) { return `${p.text} (${p.raw.citation})`; } return p.text; }); setLoading(true); setError(null); setDraft(null); try { const result = await billsAPI.generateDraft(billId, { stance, recipient, tone, selected_points: selectedPoints, include_citations: includeCitations, zip_code: zipCode.trim() || undefined, }); setDraft(result.draft); } catch (err: unknown) { const detail = err && typeof err === "object" && "response" in err && err.response && typeof err.response === "object" && "data" in err.response && err.response.data && typeof err.response.data === "object" && "detail" in err.response.data ? String((err.response.data as { detail: string }).detail) : "Failed to generate letter. Please try again."; setError(detail); } finally { setLoading(false); } } async function handleCopy() { if (!draft) return; await navigator.clipboard.writeText(draft); setCopied(true); setTimeout(() => setCopied(false), 2000); } return (
{open && (
{/* Stance + Tone */}

Stance

{(["yes", "no"] as ("yes" | "no")[]).map((s) => ( ))}
{stance === null && (

Select a position to continue

)}

Tone

{/* Point selector */}

Select up to 3 points to include {selected.size > 0 && ( ({selected.size}/3) )}

{keyPoints.length > 0 && ( <>

Key Points

{keyPoints.map((p, i) => { const globalIndex = i; const isChecked = selected.has(globalIndex); const isDisabled = !isChecked && selected.size >= 3; return ( ); })} )} {risks.length > 0 && ( <>

Concerns

{risks.map((p, i) => { const globalIndex = keyPoints.length + i; const isChecked = selected.has(globalIndex); const isDisabled = !isChecked && selected.size >= 3; return ( ); })} )}
{/* Options row */}
setZipCode(e.target.value)} placeholder="ZIP code" maxLength={10} className="text-xs bg-background border border-border rounded px-2 py-1.5 text-foreground w-28 placeholder:text-muted-foreground" />

optional · not stored

{/* Generate button */} {error && (

{error}

)} {/* Draft output */} {draft && (

Edit before sending