import type { StatusCardRefreshPolicy } from "@paperclipai/shared"; import { ChevronDown } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { estimateStatusCardCost } from "./format"; export interface StatusCardSettingsValue { refreshPolicy: StatusCardRefreshPolicy; } export function defaultSettingsValue(): StatusCardSettingsValue { return { refreshPolicy: { mode: "manual", triggers: { statusTransitions: true, membershipChanges: true, humanComments: true, assigneeChanges: true, anyUpdate: false, }, }, }; } const INTERVAL_OPTIONS = [5, 15, 30, 60]; const DEBOUNCE_OPTIONS = [30, 60, 120, 300]; type TriggerKey = keyof StatusCardRefreshPolicy["triggers"]; const TRIGGER_ROWS: { key: TriggerKey; label: string; noisy?: boolean }[] = [ { key: "statusTransitions", label: "Became blocked / needs review / done / cancelled" }, { key: "membershipChanges", label: "New issue matches the query · issue leaves the query" }, { key: "humanComments", label: "Human comments" }, { key: "assigneeChanges", label: "Assignee changes" }, { key: "anyUpdate", label: "Any update at all (noisy — includes in-progress churn)", noisy: true }, ]; function RadioRow({ selected, title, badge, onSelect, children, }: { selected: boolean; title: string; badge?: React.ReactNode; onSelect: () => void; children?: React.ReactNode; }) { return (
{selected && children ?
{children}
: null}
); } export function StatusCardSettingsForm({ value, onChange, }: { value: StatusCardSettingsValue; onChange: (next: StatusCardSettingsValue) => void; }) { const { refreshPolicy: policy } = value; // Change triggers, active-hours, and the daily token cap only govern // *automatic* updates. In Manual mode none of them apply, so the whole // "Advanced" group is hidden rather than shown-but-dimmed. const autoUpdating = policy.mode !== "manual"; const costEstimate = estimateStatusCardCost(policy); const setPolicy = (patch: Partial) => onChange({ ...value, refreshPolicy: { ...policy, ...patch } }); const setMode = (mode: StatusCardRefreshPolicy["mode"]) => { const patch: Partial = { mode }; if (mode === "interval") patch.intervalMinutes = policy.intervalMinutes ?? 15; if (mode === "reactive") { patch.debounceSeconds = policy.debounceSeconds ?? 60; patch.maxUpdatesPerHour = policy.maxUpdatesPerHour ?? 6; } setPolicy(patch); }; const toggleTrigger = (key: TriggerKey) => setPolicy({ triggers: { ...policy.triggers, [key]: !policy.triggers[key] } }); const activeHours = policy.activeHours; const setActiveHoursEnabled = (enabled: boolean) => setPolicy({ activeHours: enabled ? { start: activeHours?.start ?? "08:00", end: activeHours?.end ?? "19:00", timezone: activeHours?.timezone ?? "UTC" } : undefined, }); return (

Auto-update policy

Default } onSelect={() => setMode("manual")} /> setMode("interval")} >
Check every
setMode("reactive")} >
Wait after the last change · max setPolicy({ maxUpdatesPerHour: Math.max(1, Number(event.target.value) || 1) })} className="h-8 w-16 text-sm" aria-label="Max updates per hour" /> updates/hour
{/* Change triggers, active hours, and the daily token cap only apply to automatic updates, so they are hidden entirely in Manual mode and tucked under a collapsed "Advanced" disclosure otherwise. */} {autoUpdating ? ( Advanced

Count as a change

{TRIGGER_ROWS.map((row) => ( ))}

Guardrails

{activeHours ? (
setPolicy({ activeHours: { ...activeHours, start: event.target.value } })} className="h-8 w-32" aria-label="Active hours start" /> – setPolicy({ activeHours: { ...activeHours, end: event.target.value } })} className="h-8 w-32" aria-label="Active hours end" /> setPolicy({ activeHours: { ...activeHours, timezone: event.target.value } })} className="h-8 w-40" placeholder="Timezone" aria-label="Active hours timezone" />
) : null}
Daily token cap { const parsed = Number(event.target.value); setPolicy({ dailyTokenCap: event.target.value === "" || parsed <= 0 ? undefined : parsed }); }} className="h-8 w-36" placeholder="no cap" aria-label="Daily token cap" />
) : null}
Estimated cost = {costEstimate.cost}

{costEstimate.primary}

{costEstimate.note ?

{costEstimate.note}

: null}

Rough estimate from typical update sizes; actual cost is tracked per update.

); }