"use client"; import { Check, ChevronDown, Copy, RotateCcw, ThumbsDown, ThumbsUp, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type ReactNode, useCallback, useEffect, useId, useRef, useState, } from "react"; import { type CitationItem, CitationList, CitationStack, } from "@/components/agents/citations"; import { AgentDisclosure } from "@/components/agents/agent-disclosure"; import { EASE_OUT, SPRING_PRESS, SPRING_SWAP } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type StreamingResponseStatus = "streaming" | "complete" | "error"; export type StreamingResponseFeedback = "up" | "down" | null; export interface StreamingResponseProps { /** Rendered response content. Pass plain text or the output of a Markdown renderer. */ children: ReactNode; status?: StreamingResponseStatus; /** Plain-text value copied by the built-in copy action. */ copyText?: string; /** Overrides the built-in clipboard action. */ onCopy?: () => void | Promise; onRetry?: () => void; /** Optional sources shown as a compact footer disclosure after streaming. */ sources?: CitationItem[]; sourcesOpen?: boolean; defaultSourcesOpen?: boolean; onSourcesOpenChange?: (open: boolean) => void; sourceIdPrefix?: string; feedback?: StreamingResponseFeedback; defaultFeedback?: StreamingResponseFeedback; onFeedbackChange?: (feedback: StreamingResponseFeedback) => void; /** Set false when a surrounding conversation log announces streamed text. */ announce?: boolean; /** Hides the built-in completion actions without changing response status. */ showActions?: boolean; className?: string; contentClassName?: string; actionsClassName?: string; } function ResponseAction({ label, active = false, onClick, children, }: { label: string; active?: boolean; onClick: () => void; children: ReactNode; }) { const reduce = useReducedMotion() ?? false; return ( {children} ); } export function StreamingResponse({ children, status = "streaming", copyText, onCopy, onRetry, sources = [], sourcesOpen, defaultSourcesOpen = false, onSourcesOpenChange, sourceIdPrefix, feedback, defaultFeedback = null, onFeedbackChange, announce = true, showActions = true, className, contentClassName, actionsClassName, }: StreamingResponseProps) { const reduce = useReducedMotion() ?? false; const baseId = useId(); const [copied, setCopied] = useState(false); const [internalFeedback, setInternalFeedback] = useState(defaultFeedback); const [internalSourcesOpen, setInternalSourcesOpen] = useState(defaultSourcesOpen); const copyTimer = useRef(undefined); const currentFeedback = feedback ?? internalFeedback; const currentSourcesOpen = sourcesOpen ?? internalSourcesOpen; const streaming = status === "streaming"; const complete = status === "complete"; const canCopy = Boolean(copyText || onCopy); const hasSources = sources.length > 0; const shouldShowActions = showActions && !streaming && (canCopy || onRetry || complete || hasSources); const sourcesContentId = `${baseId}-sources`; const resolvedSourcePrefix = sourceIdPrefix ?? `response-source-${baseId.replace(/:/g, "")}`; useEffect( () => () => { if (copyTimer.current) window.clearTimeout(copyTimer.current); }, [], ); const handleCopy = useCallback(async () => { if (onCopy) await onCopy(); else if (copyText) await navigator.clipboard?.writeText(copyText); setCopied(true); if (copyTimer.current) window.clearTimeout(copyTimer.current); copyTimer.current = window.setTimeout(() => setCopied(false), 1600); }, [copyText, onCopy]); const setFeedback = (next: Exclude) => { const value = currentFeedback === next ? null : next; if (feedback === undefined) setInternalFeedback(value); onFeedbackChange?.(value); }; const setSourcesOpen = useCallback( (next: boolean) => { if (sourcesOpen === undefined) setInternalSourcesOpen(next); onSourcesOpenChange?.(next); }, [onSourcesOpenChange, sourcesOpen], ); return (
{children}
{shouldShowActions ? (
{canCopy ? ( {copied ? ( ) : ( )} ) : null} {onRetry ? ( ) : null} {complete ? ( <> setFeedback("up")} > setFeedback("down")} > ) : null} {hasSources ? ( ) : null}
{hasSources ? ( ) : null}
) : null}
); }