"use client"; import { useState, useEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { ChevronDown, ChevronRight, Copy, Check, ExternalLink, Loader2, Phone, PenLine } from "lucide-react"; import type { BriefSchema, CitedPoint, Member } from "@/lib/types"; import { billsAPI, membersAPI } 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"; } function formatRepName(member: Member): string { // DB stores name as "Last, First" — convert to "First Last" for the letter if (member.name.includes(", ")) { const [last, first] = member.name.split(", "); return `${first} ${last}`; } return member.name; } 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); // Zip → rep lookup (debounced via React Query enabled flag) const zipTrimmed = zipCode.trim(); const isValidZip = /^\d{5}$/.test(zipTrimmed); const { data: zipReps, isFetching: zipFetching } = useQuery({ queryKey: ["members-by-zip", zipTrimmed], queryFn: () => membersAPI.byZip(zipTrimmed), enabled: isValidZip, staleTime: 24 * 60 * 60 * 1000, retry: false, }); // Filter reps to match the bill's chamber const relevantReps = zipReps?.filter((m) => recipient === "senate" ? m.chamber === "Senate" : m.chamber === "House of Representatives" ) ?? []; // Use first matched rep's name for the letter salutation const repName = relevantReps.length > 0 ? formatRepName(relevantReps[0]) : undefined; 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, rep_name: repName, }); 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" /> {zipFetching && }

optional · not stored

{/* Rep lookup results */} {isValidZip && !zipFetching && relevantReps.length > 0 && (

Your {recipient === "senate" ? "senators" : "representative"}

{relevantReps.map((rep) => (
{rep.photo_url && ( {rep.name} )}

{formatRepName(rep)}

{rep.party && (

{rep.party} · {rep.state}

)}
{rep.phone && ( {rep.phone} )} {rep.official_url && ( Contact )}
))} {repName && (

Letter will be addressed to{" "} {recipient === "senate" ? "Senator" : "Representative"} {repName}.

)}
)} {isValidZip && !zipFetching && relevantReps.length === 0 && zipReps !== undefined && (

Could not find your {recipient === "senate" ? "senators" : "representative"} for that ZIP. The letter will use a generic salutation.

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

{error}

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

Edit before sending