11"use client" ;
22
33import {
4- isTextUIPart ,
5- isToolUIPart ,
6- getToolName ,
7- type UIMessage ,
4+ type ChatStatus ,
85 type UIDataTypes ,
6+ type UIMessage ,
97 type UITools ,
10- type ChatStatus ,
8+ getToolName ,
9+ isTextUIPart ,
10+ isToolUIPart ,
1111} from "ai" ;
1212import { useEffect , useMemo , useRef } from "react" ;
1313import { ToolPartRenderer } from "./tool-part-renderer" ;
14+ import { SessionCodeBlock } from "./tool-parts/session-code-block" ;
1415import { StepFlow , type StepInfo } from "./tool-parts/step-flow" ;
1516
1617interface MessageListProps {
@@ -75,6 +76,9 @@ const VISIBLE_TOOLS = new Set([
7576 "manage_subgraphs" ,
7677 "scaffold_subgraph" ,
7778 "recall_sessions" ,
79+ "query_subgraph" ,
80+ "diagnose" ,
81+ "show_code" ,
7882] ) ;
7983
8084const TOOL_STEP_LABELS : Record < string , string > = {
@@ -91,6 +95,7 @@ const TOOL_STEP_LABELS: Record<string, string> = {
9195 query_subgraph : "Querying subgraph data" ,
9296 manage_keys : "Managing API keys" ,
9397 manage_subgraphs : "Managing subgraphs" ,
98+ show_code : "Generating code example" ,
9499} ;
95100
96101function MessageBubble ( {
@@ -135,26 +140,18 @@ function MessageBubble({
135140 { segments ?. map ( ( seg ) => {
136141 if ( seg . type === "text" ) {
137142 if ( ! seg . text . trim ( ) ) return null ;
138- return (
139- < div
140- key = { seg . key }
141- className = "msg-content"
142- dangerouslySetInnerHTML = { {
143- __html : formatMarkdown ( seg . text ) ,
144- } }
145- />
146- ) ;
143+ return < MessageTextContent key = { seg . key } text = { seg . text } /> ;
147144 }
148145 if ( seg . type === "step-flow" ) {
149- return (
150- < StepFlow key = { seg . key } steps = { seg . steps } />
151- ) ;
146+ return < StepFlow key = { seg . key } steps = { seg . steps } /> ;
152147 }
153148 if ( seg . type === "tool" ) {
154149 return (
155150 < ToolPartRenderer
156151 key = { seg . key }
157- part = { seg . part as Parameters < typeof ToolPartRenderer > [ 0 ] [ "part" ] }
152+ part = {
153+ seg . part as Parameters < typeof ToolPartRenderer > [ 0 ] [ "part" ]
154+ }
158155 addToolOutput = { addToolOutput }
159156 />
160157 ) ;
@@ -184,7 +181,9 @@ function groupPartsIntoSegments(
184181 if ( toolBuffer . length >= 2 ) {
185182 // Multiple tool calls → render as step flow
186183 const totalVisible = toolBuffer . filter ( ( t ) =>
187- VISIBLE_TOOLS . has ( getToolName ( t . part as Parameters < typeof getToolName > [ 0 ] ) ) ,
184+ VISIBLE_TOOLS . has (
185+ getToolName ( t . part as Parameters < typeof getToolName > [ 0 ] ) ,
186+ ) ,
188187 ) . length ;
189188
190189 if ( totalVisible >= 2 ) {
@@ -317,31 +316,105 @@ function InlineToolCard({ part }: { part: UIMessage["parts"][number] }) {
317316function MiniLogo ( ) {
318317 return (
319318 < svg viewBox = "4 7 40 28" width = "18" height = "12" fill = "none" >
320- < polygon
321- points = "8,25 28,17 42,25 22,33"
322- fill = "rgba(255,255,255,0.15)"
323- />
324- < polygon
325- points = "8,19 28,11 42,19 22,27"
326- fill = "rgba(255,255,255,0.4)"
327- />
319+ < polygon points = "8,25 28,17 42,25 22,33" fill = "rgba(255,255,255,0.15)" />
320+ < polygon points = "8,19 28,11 42,19 22,27" fill = "rgba(255,255,255,0.4)" />
328321 </ svg >
329322 ) ;
330323}
331324
325+ /** Split text into alternating prose and fenced code block chunks */
326+ function parseTextWithCodeBlocks (
327+ text : string ,
328+ ) : Array <
329+ | { type : "prose" ; content : string }
330+ | { type : "code" ; code : string ; lang : string }
331+ > {
332+ const parts : Array <
333+ | { type : "prose" ; content : string }
334+ | { type : "code" ; code : string ; lang : string }
335+ > = [ ] ;
336+ const fenceRegex = / ^ ` ` ` ( \w * ) \s * \n ( [ \s \S ] * ?) ^ ` ` ` \s * $ / gm;
337+ let lastIndex = 0 ;
338+ let match : RegExpExecArray | null ;
339+
340+ while ( ( match = fenceRegex . exec ( text ) ) !== null ) {
341+ // Prose before this code block
342+ if ( match . index > lastIndex ) {
343+ parts . push ( {
344+ type : "prose" ,
345+ content : text . slice ( lastIndex , match . index ) ,
346+ } ) ;
347+ }
348+ parts . push ( {
349+ type : "code" ,
350+ code : match [ 2 ] . trimEnd ( ) ,
351+ lang : match [ 1 ] || "text" ,
352+ } ) ;
353+ lastIndex = match . index + match [ 0 ] . length ;
354+ }
355+
356+ // Remaining prose after last code block
357+ if ( lastIndex < text . length ) {
358+ parts . push ( { type : "prose" , content : text . slice ( lastIndex ) } ) ;
359+ }
360+
361+ return parts ;
362+ }
363+
364+ /** Renders text with fenced code blocks as SessionCodeBlock components */
365+ function MessageTextContent ( { text } : { text : string } ) {
366+ const chunks = useMemo ( ( ) => parseTextWithCodeBlocks ( text ) , [ text ] ) ;
367+
368+ // No code blocks — fast path
369+ if ( chunks . length === 1 && chunks [ 0 ] . type === "prose" ) {
370+ return (
371+ < div
372+ className = "msg-content"
373+ dangerouslySetInnerHTML = { { __html : formatMarkdown ( chunks [ 0 ] . content ) } }
374+ />
375+ ) ;
376+ }
377+
378+ return (
379+ < >
380+ { chunks . map ( ( chunk , i ) => {
381+ if ( chunk . type === "code" ) {
382+ return (
383+ < SessionCodeBlock
384+ key = { `code-${ i } ` }
385+ code = { chunk . code }
386+ lang = { chunk . lang }
387+ />
388+ ) ;
389+ }
390+ if ( ! chunk . content . trim ( ) ) return null ;
391+ return (
392+ < div
393+ key = { `prose-${ i } ` }
394+ className = "msg-content"
395+ dangerouslySetInnerHTML = { { __html : formatMarkdown ( chunk . content ) } }
396+ />
397+ ) ;
398+ } ) }
399+ </ >
400+ ) ;
401+ }
402+
332403function formatMarkdown ( text : string ) : string {
333- return text
334- . replace ( / & / g, "&" )
335- . replace ( / < / g, "<" )
336- . replace ( / > / g, ">" )
337- // Headers
338- . replace ( / ^ # # # ( .+ ) $ / gm, '<h4 class="msg-h4">$1</h4>' )
339- . replace ( / ^ # # ( .+ ) $ / gm, '<h3 class="msg-h3">$1</h3>' )
340- // Bold + code
341- . replace ( / \* \* ( .+ ?) \* \* / g, "<strong>$1</strong>" )
342- . replace ( / ` ( [ ^ ` ] + ) ` / g, '<code class="inline-code">$1</code>' )
343- // List items
344- . replace ( / ^ - ( .+ ) $ / gm, '<div class="msg-li">$1</div>' )
345- // Line breaks (but not after block elements)
346- . replace ( / \n (? ! < ) / g, "<br>" ) ;
404+ return (
405+ text
406+ . replace ( / & / g, "&" )
407+ . replace ( / < / g, "<" )
408+ . replace ( / > / g, ">" )
409+ // Headers
410+ . replace ( / ^ # # # ( .+ ) $ / gm, '<h4 class="msg-h4">$1</h4>' )
411+ . replace ( / ^ # # ( .+ ) $ / gm, '<h3 class="msg-h3">$1</h3>' )
412+ // Bold + code
413+ . replace ( / \* \* ( .+ ?) \* \* / g, "<strong>$1</strong>" )
414+ . replace ( / ` ( [ ^ ` ] + ) ` / g, '<code class="inline-code">$1</code>' )
415+ // List items
416+ . replace ( / ^ - ( .+ ) $ / gm, '<div class="msg-li">$1</div>' )
417+ // Line breaks (but not after block elements)
418+ . replace ( / \n (? ! < ) / g, "<br>" )
419+ ) ;
347420}
0 commit comments