import { memo, useMemo, useCallback, Fragment } from 'react' import type { ReactNode } from 'react' import { motion } from 'framer-motion' import { cn } from '../../lib/utils' import { useReasoningStepContext } from './context' import type { ReasoningStepHeaderProps, ReasoningStepDetailsProps, ReasoningStepDetailRowProps, ReasoningStepDetail, } from './types' // ===== 样式常量 ===== const iconSizeStyle = { width: 'var(--font-size-xs)', height: 'var(--font-size-xs)' } const FILE_ICON_COLOR_MAP: Record = { ts: 'text-info', tsx: 'text-info', js: 'text-info', jsx: 'text-info', py: 'text-teal', md: 'text-text-secondary', mdx: 'text-text-secondary', } function getFileIconColorClass(fileType?: string, filename?: string): string { const ext = (fileType ?? filename?.split('.').pop() ?? '').toLowerCase() return FILE_ICON_COLOR_MAP[ext] ?? 'text-text-tertiary' } // ===== 辅助组件 ===== const FileIconPlaceholder = memo(function FileIconPlaceholder({ className }: { className?: string }) { return ( ) }) const InlineRow = memo(function InlineRow({ prefix, primary, secondary, }: { prefix?: ReactNode primary: string secondary?: string }) { return (
{prefix ?
{prefix}
: null}
{primary} {secondary ? ( {secondary} ) : null}
) }) // ===== ReasoningStep.Header ===== export function ReasoningStepHeader({ className }: ReasoningStepHeaderProps) { const ctx = useReasoningStepContext() const dynamicSpread = useMemo(() => { if (typeof ctx.text === 'string') return ctx.text.length * ctx.spread return 10 * ctx.spread }, [ctx.text, ctx.spread]) const getIcon = () => (ctx.icon ? ctx.icon : ctx.eyeIcon) const renderPrimaryText = () => { if (typeof ctx.text !== 'string') return ctx.text if (ctx.status === 'in-progress') { return ( {ctx.text} ) } return ( {ctx.text} ) } const renderDescription = () => { if (!ctx.description) return null return ( {ctx.description} ) } return (
{ctx.effectiveShowIcon && (
{getIcon()}
)}
{renderPrimaryText()} {ctx.description ? renderDescription() : null} {ctx.hasExpandableContent && (
{ctx.arrowRightIcon}
)}
) } ReasoningStepHeader.displayName = 'ReasoningStepHeader' // ===== ReasoningStep.DetailRow(内部递归组件)===== interface DetailRowInternalProps { detail: ReasoningStepDetail depth: number pathKey: string detailIndent: string disableExpandAnimation: boolean expandedNestedKeys: Set onToggleNested: (pathKey: string) => void arrowRightIcon: ReactNode } const DetailRowInternal = memo(function DetailRowInternal({ detail, depth, pathKey, detailIndent, expandedNestedKeys, onToggleNested, arrowRightIcon, }: DetailRowInternalProps) { const hasNested = detail.details != null && detail.details.length > 0 const isExpandedNested = expandedNestedKeys.has(pathKey) const showIconOnRow = !hasNested const primary = detail.type === 'action' ? detail.action : detail.filename const secondary = detail.type === 'action' ? detail.desc : [detail.lines, detail.path].filter(Boolean).join(' ') const prefix = showIconOnRow && detail.type === 'file' ? ( ) : undefined const handleClick = useCallback(() => { if (hasNested) onToggleNested(pathKey) }, [hasNested, onToggleNested, pathKey]) const rowContent = (
{hasNested && (
{arrowRightIcon}
)}
) const rowWithOptionalLine = (
{rowContent}
) if (!hasNested) return {rowWithOptionalLine} const isThirdLevelContainer = depth === 1 const nestedWrapperClass = isThirdLevelContainer ? 'rounded-lg border border-border-tertiary bg-bg-base p-2 flex flex-col gap-0' : 'flex flex-col' return (
{rowWithOptionalLine}
{detail.details?.map((d, i) => ( ))}
) }) // ===== ReasoningStep.DetailRow(公开 API)===== export function ReasoningStepDetailRow({ detail, depth = 0, pathKey = '0', }: ReasoningStepDetailRowProps) { const ctx = useReasoningStepContext() return ( ) } ReasoningStepDetailRow.displayName = 'ReasoningStepDetailRow' // ===== ReasoningStep.Details ===== export function ReasoningStepDetails({ className, children }: ReasoningStepDetailsProps) { const ctx = useReasoningStepContext() const hasDetails = ctx.details != null && ctx.details.length > 0 return (
{hasDetails ? (
{ctx.details?.map((d, i) => ( ))}
) : ( children ?? ctx.detailsChildren )}
) } ReasoningStepDetails.displayName = 'ReasoningStepDetails' // ===== ReasoningStep.DefaultLayout ===== export function ReasoningStepDefaultLayout() { const ctx = useReasoningStepContext() return (
ctx.setIsHovered(true)} onMouseLeave={() => ctx.setIsHovered(false)} > {ctx.hasExpandableContent && }
) } ReasoningStepDefaultLayout.displayName = 'ReasoningStepDefaultLayout'