"use client"; import { CheckIcon, LightbulbIcon, RotateCcwIcon, SparklesIcon, Undo2Icon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export interface NudgeOption { id: string; label: string; title: string; detail?: string; confidence: number; reasons?: string[]; result?: string; } export interface NudgeProps { options: NudgeOption[]; undoMs?: number; onAccept?: (option: NudgeOption) => void; onUndo?: (option: NudgeOption) => void; className?: string; } const spring = { type: "spring", stiffness: 400, damping: 32 } as const; const press = "transition-[scale,background-color,box-shadow] duration-150 ease-out active:scale-[0.97]"; function level(c: number) { if (c >= 0.8) return { label: "High confidence", bar: "bg-success", text: "text-success-foreground" }; if (c >= 0.55) return { label: "Fair confidence", bar: "bg-warning", text: "text-warning-foreground" }; return { label: "Low confidence", bar: "bg-muted-foreground", text: "text-muted-foreground" }; } function RollingPercent({ value }: { value: number }) { const reduce = useReducedMotion(); const [shown, setShown] = useState(value); const from = useRef(value); useEffect(() => { if (reduce) { setShown(value); return; } const start = from.current; const t0 = performance.now(); let raf = 0; const tick = (t: number) => { const p = Math.min(1, (t - t0) / 500); const e = 1 - (1 - p) ** 3; setShown(Math.round(start + (value - start) * e)); if (p < 1) raf = requestAnimationFrame(tick); else from.current = value; }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); from.current = value; }; }, [value, reduce]); return {shown}%; } function ConfidenceMeter({ value }: { value: number }) { const reduce = useReducedMotion(); const l = level(value); const bars = 5; const filled = Math.max(1, Math.round(value * bars)); return (
{current.detail}
} {current.reasons && current.reasons.length > 0 && (Options
{phase === "dismissed" ? "Suggestion dismissed" : current.result ?? "Done"}
{phase === "dismissed" ? "We'll suggest something else later." : current.title}