11"use client"
22
3+ import { useMemo } from "react"
34import {
45 ChainOfThought ,
56 ChainOfThoughtHeader ,
@@ -8,33 +9,48 @@ import {
89 ChainOfThoughtSearchResults ,
910 ChainOfThoughtSearchResult ,
1011} from "@/src/components/ai-elements/chain-of-thought"
11- import { SearchIcon , BrainIcon , CheckIcon , LoaderIcon } from "lucide-react"
12+ import { Badge } from "@/ui/badge"
13+ import { SearchIcon , BrainIcon , CheckIcon , LoaderIcon , ClockIcon } from "lucide-react"
1214
1315export interface ReasoningStep {
1416 id : string
1517 label : string
1618 description ?: string
1719 status : "complete" | "active" | "pending"
1820 searchResults ?: string [ ]
21+ duration ?: number
1922}
2023
2124interface AgentChainOfThoughtProps {
2225 steps : ReasoningStep [ ]
2326 isStreaming ?: boolean
2427 defaultOpen ?: boolean
28+ className ?: string
2529}
2630
2731export function AgentChainOfThought ( {
2832 steps,
2933 isStreaming = false ,
3034 defaultOpen = true ,
35+ className,
3136} : AgentChainOfThoughtProps ) {
3237 if ( ! steps || steps . length === 0 ) { return null }
3338
39+ const completedCount = useMemo ( ( ) => steps . filter ( ( s ) => s . status === "complete" ) . length , [ steps ] )
40+ const activeStep = useMemo ( ( ) => steps . find ( ( s ) => s . status === "active" ) , [ steps ] )
41+
3442 return (
35- < ChainOfThought defaultOpen = { defaultOpen } >
36- < ChainOfThoughtHeader >
37- { isStreaming ? "Thinking..." : "Chain of Thought" }
43+ < ChainOfThought defaultOpen = { defaultOpen } className = { className } >
44+ < ChainOfThoughtHeader className = "flex items-center gap-2" >
45+ < span className = "flex-1" >
46+ { isStreaming
47+ ? activeStep ?. label ?? "Thinking..."
48+ : "Chain of Thought"
49+ }
50+ </ span >
51+ < Badge variant = "secondary" className = "text-xs font-normal" >
52+ { completedCount } /{ steps . length }
53+ </ Badge >
3854 </ ChainOfThoughtHeader >
3955 < ChainOfThoughtContent >
4056 { steps . map ( ( step ) => (
@@ -51,6 +67,12 @@ export function AgentChainOfThought({
5167 description = { step . description }
5268 status = { step . status }
5369 >
70+ { step . duration && step . status === "complete" && (
71+ < span className = "flex items-center gap-1 text-xs text-muted-foreground" >
72+ < ClockIcon className = "size-3" />
73+ { step . duration } s
74+ </ span >
75+ ) }
5476 { step . searchResults && step . searchResults . length > 0 && (
5577 < ChainOfThoughtSearchResults >
5678 { step . searchResults . map ( ( result , i ) => (
@@ -68,29 +90,61 @@ export function AgentChainOfThought({
6890 )
6991}
7092
93+ type StepType = "step" | "search" | "analysis" | "decision"
94+
95+ function categorizeStep ( text : string ) : StepType {
96+ const lower = text . toLowerCase ( )
97+ if ( lower . includes ( "search" ) || lower . includes ( "looking for" ) || lower . includes ( "finding" ) ) {
98+ return "search"
99+ }
100+ if ( lower . includes ( "analyzing" ) || lower . includes ( "examining" ) || lower . includes ( "reviewing" ) ) {
101+ return "analysis"
102+ }
103+ if ( lower . includes ( "decided" ) || lower . includes ( "conclusion" ) || lower . includes ( "therefore" ) ) {
104+ return "decision"
105+ }
106+ return "step"
107+ }
108+
71109export function parseReasoningToSteps ( reasoning : string ) : ReasoningStep [ ] {
72110 if ( ! reasoning ) { return [ ] }
73111
74112 const lines = reasoning . split ( "\n" ) . filter ( ( line ) => line . trim ( ) )
75113 const steps : ReasoningStep [ ] = [ ]
114+ let currentSearchTerms : string [ ] = [ ]
76115
77116 lines . forEach ( ( line , index ) => {
78117 const trimmed = line . trim ( )
79- if ( trimmed . startsWith ( "-" ) || trimmed . startsWith ( "•" ) || ( / ^ \d + \. / . exec ( trimmed ) ) ) {
80- steps . push ( {
81- id : `step-${ index } ` ,
82- label : trimmed . replace ( / ^ [ - • \d . ] + \s * / , "" ) ,
83- status : "complete" ,
84- } )
85- } else if ( trimmed . length > 10 ) {
118+
119+ // Skip very short lines
120+ if ( trimmed . length < 5 ) { return }
121+
122+ // Check for bullet points or numbered lists
123+ const isBullet = trimmed . startsWith ( "-" ) || trimmed . startsWith ( "•" ) || / ^ \d + \. / . test ( trimmed )
124+ const content = isBullet
125+ ? trimmed . replace ( / ^ [ - • \d . ] + \s * / , "" )
126+ : trimmed
127+
128+ // Extract search terms if mentioned
129+ const searchMatch = / (?: s e a r c h | l o o k i n g f o r | f i n d i n g ) [: \s] + [ " ' ] ? ( [ ^ " ' \n ] + ) [ " ' ] ? / i. exec ( content )
130+ if ( searchMatch ) {
131+ currentSearchTerms . push ( searchMatch [ 1 ] . trim ( ) )
132+ }
133+
134+ if ( content . length > 10 ) {
135+ const stepType = categorizeStep ( content )
86136 steps . push ( {
87137 id : `step-${ index } ` ,
88- label : trimmed . slice ( 0 , 80 ) + ( trimmed . length > 80 ? "..." : "" ) ,
89- description : trimmed . length > 80 ? trimmed : undefined ,
138+ label : content . length > 80 ? content . slice ( 0 , 77 ) + "..." : content ,
139+ description : content . length > 80 ? content : undefined ,
90140 status : "complete" ,
141+ searchResults : stepType === "search" ? [ ...currentSearchTerms ] : undefined ,
91142 } )
143+
144+ // Reset after each step to prevent accumulation
145+ currentSearchTerms = [ ]
92146 }
93147 } )
94148
95- return steps . slice ( 0 , 10 )
149+ return steps . slice ( 0 , 15 )
96150}
0 commit comments