{"version":3,"file":"CopilotChatReasoningMessage.mjs","names":[],"sources":["../../../src/components/chat/CopilotChatReasoningMessage.tsx"],"sourcesContent":["import { ReasoningMessage, Message } from \"@ag-ui/core\";\nimport { useState, useEffect, useRef } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { twMerge } from \"tailwind-merge\";\nimport { Streamdown } from \"streamdown\";\nimport { WithSlots, renderSlot } from \"@/lib/slots\";\n\nexport type CopilotChatReasoningMessageProps = WithSlots<\n  {\n    header: typeof CopilotChatReasoningMessage.Header;\n    contentView: typeof CopilotChatReasoningMessage.Content;\n    toggle: typeof CopilotChatReasoningMessage.Toggle;\n  },\n  {\n    message: ReasoningMessage;\n    messages?: Message[];\n    isRunning?: boolean;\n  } & React.HTMLAttributes<HTMLDivElement>\n>;\n\n/**\n * Formats an elapsed duration (in seconds) to a human-readable string.\n */\nfunction formatDuration(seconds: number): string {\n  if (seconds < 1) return \"a few seconds\";\n  if (seconds < 60) return `${Math.round(seconds)} seconds`;\n  const mins = Math.floor(seconds / 60);\n  const secs = Math.round(seconds % 60);\n  if (secs === 0) return `${mins} minute${mins > 1 ? \"s\" : \"\"}`;\n  return `${mins}m ${secs}s`;\n}\n\nexport function CopilotChatReasoningMessage({\n  message,\n  messages,\n  isRunning,\n  header,\n  contentView,\n  toggle,\n  children,\n  className,\n  ...props\n}: CopilotChatReasoningMessageProps) {\n  const isLatest = messages?.[messages.length - 1]?.id === message.id;\n  const isStreaming = !!(isRunning && isLatest);\n  const hasContent = !!(message.content && message.content.length > 0);\n\n  // Track elapsed time while streaming\n  const startTimeRef = useRef<number | null>(null);\n  const [elapsed, setElapsed] = useState(0);\n\n  useEffect(() => {\n    if (isStreaming && startTimeRef.current === null) {\n      startTimeRef.current = Date.now();\n    }\n\n    if (!isStreaming && startTimeRef.current !== null) {\n      // Final snapshot of elapsed time\n      setElapsed((Date.now() - startTimeRef.current) / 1000);\n      return;\n    }\n\n    if (!isStreaming) return;\n\n    // Tick every second while streaming\n    const timer = setInterval(() => {\n      if (startTimeRef.current !== null) {\n        setElapsed((Date.now() - startTimeRef.current) / 1000);\n      }\n    }, 1000);\n    return () => clearInterval(timer);\n  }, [isStreaming]);\n\n  // Default to open while streaming, auto-collapse when streaming ends\n  const [isOpen, setIsOpen] = useState(isStreaming);\n\n  useEffect(() => {\n    if (isStreaming) {\n      setIsOpen(true);\n    } else {\n      // Auto-collapse when reasoning finishes\n      setIsOpen(false);\n    }\n  }, [isStreaming]);\n\n  const label = isStreaming\n    ? \"Thinking…\"\n    : `Thought for ${formatDuration(elapsed)}`;\n\n  const boundHeader = renderSlot(header, CopilotChatReasoningMessage.Header, {\n    isOpen,\n    label,\n    hasContent,\n    isStreaming,\n    onClick: hasContent ? () => setIsOpen((prev) => !prev) : undefined,\n  });\n\n  const boundContent = renderSlot(\n    contentView,\n    CopilotChatReasoningMessage.Content,\n    {\n      isStreaming,\n      hasContent,\n      children: message.content,\n    },\n  );\n\n  const boundToggle = renderSlot(toggle, CopilotChatReasoningMessage.Toggle, {\n    isOpen,\n    children: boundContent,\n  });\n\n  if (children) {\n    return (\n      <div data-copilotkit style={{ display: \"contents\" }}>\n        {children({\n          header: boundHeader,\n          contentView: boundContent,\n          toggle: boundToggle,\n          message,\n          messages,\n          isRunning,\n        })}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={twMerge(\"cpk:my-1\", className)}\n      data-message-id={message.id}\n      {...props}\n    >\n      {boundHeader}\n      {boundToggle}\n    </div>\n  );\n}\n\nexport namespace CopilotChatReasoningMessage {\n  export const Header: React.FC<\n    React.ButtonHTMLAttributes<HTMLButtonElement> & {\n      isOpen?: boolean;\n      label?: string;\n      hasContent?: boolean;\n      isStreaming?: boolean;\n    }\n  > = ({\n    isOpen,\n    label = \"Thoughts\",\n    hasContent,\n    isStreaming,\n    className,\n    children: headerChildren,\n    ...headerProps\n  }) => {\n    const isExpandable = !!hasContent;\n\n    return (\n      <button\n        type=\"button\"\n        className={twMerge(\n          \"cpk:inline-flex cpk:items-center cpk:gap-1 cpk:py-1 cpk:text-sm cpk:text-muted-foreground cpk:transition-colors cpk:select-none\",\n          isExpandable\n            ? \"cpk:hover:text-foreground cpk:cursor-pointer\"\n            : \"cpk:cursor-default\",\n          className,\n        )}\n        aria-expanded={isExpandable ? isOpen : undefined}\n        {...headerProps}\n      >\n        <span className=\"cpk:font-medium\">{label}</span>\n        {isStreaming && !hasContent && (\n          <span className=\"cpk:inline-flex cpk:items-center cpk:ml-1\">\n            <span className=\"cpk:w-1.5 cpk:h-1.5 cpk:rounded-full cpk:bg-muted-foreground cpk:animate-pulse\" />\n          </span>\n        )}\n        {headerChildren}\n        {isExpandable && (\n          <ChevronRight\n            className={twMerge(\n              \"cpk:size-3.5 cpk:shrink-0 cpk:transition-transform cpk:duration-200\",\n              isOpen && \"cpk:rotate-90\",\n            )}\n          />\n        )}\n      </button>\n    );\n  };\n\n  export const Content: React.FC<\n    React.HTMLAttributes<HTMLDivElement> & {\n      isStreaming?: boolean;\n      hasContent?: boolean;\n    }\n  > = ({\n    isStreaming,\n    hasContent,\n    className,\n    children: contentChildren,\n    ...contentProps\n  }) => {\n    // Don't render the content area at all when there's nothing to show\n    if (!hasContent && !isStreaming) return null;\n\n    return (\n      <div\n        className={twMerge(\"cpk:pb-2 cpk:pt-1\", className)}\n        {...contentProps}\n      >\n        <div className=\"cpk:text-sm cpk:text-muted-foreground\">\n          <Streamdown>\n            {typeof contentChildren === \"string\" ? contentChildren : \"\"}\n          </Streamdown>\n          {isStreaming && hasContent && (\n            <span className=\"cpk:inline-flex cpk:items-center cpk:ml-1 cpk:align-middle\">\n              <span className=\"cpk:w-2 cpk:h-2 cpk:rounded-full cpk:bg-muted-foreground cpk:animate-pulse-cursor\" />\n            </span>\n          )}\n        </div>\n      </div>\n    );\n  };\n\n  export const Toggle: React.FC<\n    React.HTMLAttributes<HTMLDivElement> & {\n      isOpen?: boolean;\n    }\n  > = ({ isOpen, className, children: toggleChildren, ...toggleProps }) => {\n    return (\n      <div\n        className={twMerge(\n          \"cpk:grid cpk:transition-[grid-template-rows] cpk:duration-200 cpk:ease-in-out\",\n          className,\n        )}\n        style={{ gridTemplateRows: isOpen ? \"1fr\" : \"0fr\" }}\n        {...toggleProps}\n      >\n        <div className=\"cpk:overflow-hidden\">{toggleChildren}</div>\n      </div>\n    );\n  };\n}\n\nCopilotChatReasoningMessage.Header.displayName =\n  \"CopilotChatReasoningMessage.Header\";\nCopilotChatReasoningMessage.Content.displayName =\n  \"CopilotChatReasoningMessage.Content\";\nCopilotChatReasoningMessage.Toggle.displayName =\n  \"CopilotChatReasoningMessage.Toggle\";\n\nexport default CopilotChatReasoningMessage;\n"],"mappings":";;;;;;;;;;;AAuBA,SAAS,eAAe,SAAyB;AAC/C,KAAI,UAAU,EAAG,QAAO;AACxB,KAAI,UAAU,GAAI,QAAO,GAAG,KAAK,MAAM,QAAQ,CAAC;CAChD,MAAM,OAAO,KAAK,MAAM,UAAU,GAAG;CACrC,MAAM,OAAO,KAAK,MAAM,UAAU,GAAG;AACrC,KAAI,SAAS,EAAG,QAAO,GAAG,KAAK,SAAS,OAAO,IAAI,MAAM;AACzD,QAAO,GAAG,KAAK,IAAI,KAAK;;AAG1B,SAAgB,4BAA4B,EAC1C,SACA,UACA,WACA,QACA,aACA,QACA,UACA,WACA,GAAG,SACgC;CACnC,MAAM,WAAW,WAAW,SAAS,SAAS,IAAI,OAAO,QAAQ;CACjE,MAAM,cAAc,CAAC,EAAE,aAAa;CACpC,MAAM,aAAa,CAAC,EAAE,QAAQ,WAAW,QAAQ,QAAQ,SAAS;CAGlE,MAAM,eAAe,OAAsB,KAAK;CAChD,MAAM,CAAC,SAAS,cAAc,SAAS,EAAE;AAEzC,iBAAgB;AACd,MAAI,eAAe,aAAa,YAAY,KAC1C,cAAa,UAAU,KAAK,KAAK;AAGnC,MAAI,CAAC,eAAe,aAAa,YAAY,MAAM;AAEjD,eAAY,KAAK,KAAK,GAAG,aAAa,WAAW,IAAK;AACtD;;AAGF,MAAI,CAAC,YAAa;EAGlB,MAAM,QAAQ,kBAAkB;AAC9B,OAAI,aAAa,YAAY,KAC3B,aAAY,KAAK,KAAK,GAAG,aAAa,WAAW,IAAK;KAEvD,IAAK;AACR,eAAa,cAAc,MAAM;IAChC,CAAC,YAAY,CAAC;CAGjB,MAAM,CAAC,QAAQ,aAAa,SAAS,YAAY;AAEjD,iBAAgB;AACd,MAAI,YACF,WAAU,KAAK;MAGf,WAAU,MAAM;IAEjB,CAAC,YAAY,CAAC;CAEjB,MAAM,QAAQ,cACV,cACA,eAAe,eAAe,QAAQ;CAE1C,MAAM,cAAc,WAAW,QAAQ,4BAA4B,QAAQ;EACzE;EACA;EACA;EACA;EACA,SAAS,mBAAmB,WAAW,SAAS,CAAC,KAAK,GAAG;EAC1D,CAAC;CAEF,MAAM,eAAe,WACnB,aACA,4BAA4B,SAC5B;EACE;EACA;EACA,UAAU,QAAQ;EACnB,CACF;CAED,MAAM,cAAc,WAAW,QAAQ,4BAA4B,QAAQ;EACzE;EACA,UAAU;EACX,CAAC;AAEF,KAAI,SACF,QACE,oBAAC;EAAI;EAAgB,OAAO,EAAE,SAAS,YAAY;YAChD,SAAS;GACR,QAAQ;GACR,aAAa;GACb,QAAQ;GACR;GACA;GACA;GACD,CAAC;GACE;AAIV,QACE,qBAAC;EACC,WAAW,QAAQ,YAAY,UAAU;EACzC,mBAAiB,QAAQ;EACzB,GAAI;aAEH,aACA;GACG;;;wCAYH,EACH,QACA,QAAQ,YACR,YACA,aACA,WACA,UAAU,gBACV,GAAG,kBACC;EACJ,MAAM,eAAe,CAAC,CAAC;AAEvB,SACE,qBAAC;GACC,MAAK;GACL,WAAW,QACT,mIACA,eACI,iDACA,sBACJ,UACD;GACD,iBAAe,eAAe,SAAS;GACvC,GAAI;;IAEJ,oBAAC;KAAK,WAAU;eAAmB;MAAa;IAC/C,eAAe,CAAC,cACf,oBAAC;KAAK,WAAU;eACd,oBAAC,UAAK,WAAU,mFAAmF;MAC9F;IAER;IACA,gBACC,oBAAC,gBACC,WAAW,QACT,uEACA,UAAU,gBACX,GACD;;IAEG;;yCASR,EACH,aACA,YACA,WACA,UAAU,iBACV,GAAG,mBACC;AAEJ,MAAI,CAAC,cAAc,CAAC,YAAa,QAAO;AAExC,SACE,oBAAC;GACC,WAAW,QAAQ,qBAAqB,UAAU;GAClD,GAAI;aAEJ,qBAAC;IAAI,WAAU;eACb,oBAAC,wBACE,OAAO,oBAAoB,WAAW,kBAAkB,KAC9C,EACZ,eAAe,cACd,oBAAC;KAAK,WAAU;eACd,oBAAC,UAAK,WAAU,sFAAsF;MACjG;KAEL;IACF;;wCAQL,EAAE,QAAQ,WAAW,UAAU,gBAAgB,GAAG,kBAAkB;AACvE,SACE,oBAAC;GACC,WAAW,QACT,iFACA,UACD;GACD,OAAO,EAAE,kBAAkB,SAAS,QAAQ,OAAO;GACnD,GAAI;aAEJ,oBAAC;IAAI,WAAU;cAAuB;KAAqB;IACvD;;;AAKZ,4BAA4B,OAAO,cACjC;AACF,4BAA4B,QAAQ,cAClC;AACF,4BAA4B,OAAO,cACjC;AAEF,0CAAe"}