Ctrl K
ComunidadGuías…

components/reasoning-section.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use client'

import { useEffect, useState } from 'react'

import type { ReasoningPart } from '@ai-sdk/provider-utils'

import { cn } from '@/lib/utils'

import { useArtifact } from '@/components/artifact/artifact-context'

import { CollapsibleMessage } from './collapsible-message'
import { DefaultSkeleton } from './default-skeleton'
import { MarkdownMessage } from './message'
import ProcessHeader from './process-header'

interface ReasoningContent {
  reasoning: string
  isDone: boolean
}

export interface ReasoningSectionProps {
  content: ReasoningContent
  isOpen: boolean
  onOpenChange: (open: boolean) => void
  showIcon?: boolean
  variant?: 'default' | 'minimal' | 'process' | 'process-sub'
  isSingle?: boolean // Whether this is a single item or part of a group
  isFirst?: boolean
  isLast?: boolean
}

export function ReasoningSection({
  content,
  isOpen,
  onOpenChange,
  showIcon = false,
  variant = 'default',
  isSingle = true,
  isFirst = false,
  isLast = false
}: ReasoningSectionProps) {
  const { open } = useArtifact()
  // Show a short preview when collapsed; switch to a generic label when expanded
  const HEADER_PREVIEW_CHARS = 120
  const SANITIZE_MARKDOWN_PREVIEW = true
  const [preview, setPreview] = useState<string | null>(null)

  const toPreview = (text: string) => {
    const firstLine = (text || '').split(/\r?\n/)[0] || ''
    if (!SANITIZE_MARKDOWN_PREVIEW) return firstLine
    return firstLine
      .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // links [text](url)
      .replace(/`([^`]+)`/g, '$1') // inline code
      .replace(/\*\*([^*]+)\*\*/g, '$1') // bold **text**
      .replace(/__([^_]+)__/g, '$1') // bold __text__
      .replace(/^#{1,6}\s*/, '') // heading markers at start
  }

  // Lock a preview during streaming to avoid frequent churn; refresh once when done
  useEffect(() => {
    const text = content?.reasoning || ''
    if (!text) return
    const prepared = toPreview(text)
    if (!content.isDone) {
      // Set once during streaming
      if (!preview) setPreview(prepared.slice(0, HEADER_PREVIEW_CHARS))
    } else {
      // On completion, ensure preview reflects the final string (single update)
      const finalPreview = prepared.slice(0, HEADER_PREVIEW_CHARS)
      if (preview !== finalPreview) setPreview(finalPreview)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [content.reasoning, content.isDone])

  const headerLabel = isOpen
    ? 'Thoughts'
    : preview && preview.length > 0
      ? preview
      : !content.isDone
        ? 'Thinking...'
        : 'Thoughts'

  const reasoningHeader = (
    <ProcessHeader
      label={
        <div className="flex items-center gap-2 min-w-0">
          <div className="size-4 shrink-0 flex items-center justify-center relative">
            <div className="size-1.5 rounded-full bg-muted-foreground" />
          </div>
          <span className="truncate block min-w-0 max-w-full">
            {headerLabel}
          </span>
        </div>
      }
      onInspect={() =>
        open({ type: 'reasoning', text: content.reasoning } as ReasoningPart)
      }
      isLoading={!content.isDone}
      ariaExpanded={isOpen}
    />
  )

  if (!content) return <DefaultSkeleton />

  // Return null if done and reasoning text is empty
  if (content.isDone && !content.reasoning?.trim()) return null

  return (
    <div className="relative">
      {/* Rails for header - show based on position */}
      {!isFirst && (
        <div className="absolute left-[19.5px] w-px bg-border h-2 top-0" />
      )}
      {!isLast && (
        <div className="absolute left-[19.5px] w-px bg-border h-2 bottom-0" />
      )}
      <CollapsibleMessage
        role="assistant"
        isCollapsible={true}
        header={reasoningHeader}
        isOpen={isOpen}
        onOpenChange={onOpenChange}
        showBorder={isSingle}
        showIcon={showIcon}
        variant={variant}
        showSeparator={false}
        headerClickBehavior="split"
      >
        <div className="flex">
          {/* Rail space */}
          <div className="w-[16px] shrink-0 flex justify-center">
            <div
              className={cn(
                'w-px bg-border/50 transition-opacity duration-200',
                isOpen ? 'opacity-100' : 'opacity-0'
              )}
              style={{
                marginTop: isFirst ? '0' : '-1rem',
                marginBottom: isLast ? '0' : '-1rem'
              }}
            />
          </div>
          <div className="w-2 shrink-0" />
          <div className="[&_p]:text-xs [&_p]:text-muted-foreground/80 flex-1">
            <MarkdownMessage message={content.reasoning} />
          </div>
        </div>
      </CollapsibleMessage>
    </div>
  )
}