| 1 | // Adapted from DeepSeek Harness c291e7961a (MIT). |
| 2 | /** Assistant reasoning disclosure, independent of Tool-call presentation. */ |
| 3 | import { useState, type ReactNode } from 'react' |
| 4 | import { Atom as IconThinkOutline14 } from 'lucide-react' |
| 5 | import { DisclosureRow } from './DisclosureRow' |
| 6 | import type { useT } from '../../lib/i18n' |
| 7 | const a11yCss = { visuallyHidden: 'sr-only' } |
| 8 | import css from './ReasoningRow.styles' |
| 9 | |
| 10 | function firstLine(text: string): string { |
| 11 | const newline = text.indexOf('\n') |
| 12 | return newline === -1 ? text : text.slice(0, newline) |
| 13 | } |
| 14 | |
| 15 | function latestLine(text: string): string { |
| 16 | const visible = text.trimEnd() |
| 17 | const newline = visible.lastIndexOf('\n') |
| 18 | return newline === -1 ? visible : visible.slice(newline + 1) |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * Render one assistant reasoning block as the Think disclosure row. The |
| 23 | * collapsed summary omits double-asterisk markers; expanded content preserves |
| 24 | * the complete text. |
| 25 | * @param props.text - complete or streaming reasoning text. |
| 26 | * @param props.running - whether this block is the streaming tail. |
| 27 | * @param props.t - conversation locale seat for the running status. |
| 28 | * @returns the reasoning disclosure. |
| 29 | */ |
| 30 | export function ReasoningRow({ text, running, t, children, beforeToggle, duration }: { text: string; running: boolean; t: ReturnType<typeof useT>; children?: ReactNode; beforeToggle?: () => void; duration?: string }) { |
| 31 | const [expanded, setExpanded] = useState(false) |
| 32 | const summary = (running ? latestLine(text) : firstLine(text.trimStart())).replaceAll('**', '') |
| 33 | |
| 34 | return ( |
| 35 | <div |
| 36 | className={css.root} |
| 37 | data-variant="think" |
| 38 | data-state={running ? 'running' : 'ok'} |
| 39 | data-expanded={expanded || undefined} |
| 40 | > |
| 41 | {running && <span className={a11yCss.visuallyHidden}>{t('chat.running')}</span>} |
| 42 | <DisclosureRow |
| 43 | rowClassName={css.row} |
| 44 | leadingClassName={css.leading} |
| 45 | titleClassName={css.title} |
| 46 | chevronClassName={css.chevron} |
| 47 | icon={<IconThinkOutline14 size={14} />} |
| 48 | title={t('chat.reasoning')} |
| 49 | open={expanded} |
| 50 | expandable |
| 51 | expandOnRowClick |
| 52 | onToggle={() => { beforeToggle?.(); setExpanded(value => !value) }} |
| 53 | collapsedContent={( |
| 54 | <> |
| 55 | <span className={css.separator} aria-hidden /> |
| 56 | <span className={css.summary} data-follow-end={running || undefined}> |
| 57 | <span className={css.summaryText}>{summary}</span> |
| 58 | </span> |
| 59 | {duration && !running && <span className="dsh-row-duration">{duration}</span>} |
| 60 | </> |
| 61 | )} |
| 62 | > |
| 63 | <div className={css.thinkBody}>{children ?? text}</div> |
| 64 | </DisclosureRow> |
| 65 | </div> |
| 66 | ) |
| 67 | } |
| 68 |