1- import { useMemo , useState } from 'react' ;
1+ import React , { useMemo , useState } from 'react' ;
22import { useAppContext } from '../utils/app.context' ;
33import { Message , PendingMessage } from '../utils/types' ;
44import { classNames } from '../utils/misc' ;
55import MarkdownDisplay , { CopyButton } from './MarkdownDisplay' ;
66import { ChevronLeftIcon , ChevronRightIcon } from '@heroicons/react/24/outline' ;
7+ import { ToolCallArgsDisplay } from './tool_calling/ToolCallArgsDisplay' ;
8+ import { ToolCallResultDisplay } from './tool_calling/ToolCallResultDisplay' ;
79
810interface SplitMessage {
911 content : PendingMessage [ 'content' ] ;
@@ -13,6 +15,7 @@ interface SplitMessage {
1315
1416export default function ChatMessage ( {
1517 msg,
18+ chainedParts,
1619 siblingLeafNodeIds,
1720 siblingCurrIdx,
1821 id,
@@ -22,6 +25,7 @@ export default function ChatMessage({
2225 isPending,
2326} : {
2427 msg : Message | PendingMessage ;
28+ chainedParts ?: ( Message | PendingMessage ) [ ] ;
2529 siblingLeafNodeIds : Message [ 'id' ] [ ] ;
2630 siblingCurrIdx : number ;
2731 id ?: string ;
@@ -48,8 +52,11 @@ export default function ChatMessage({
4852 const nextSibling = siblingLeafNodeIds [ siblingCurrIdx + 1 ] ;
4953 const prevSibling = siblingLeafNodeIds [ siblingCurrIdx - 1 ] ;
5054
51- // for reasoning model, we split the message into content, thought, and tool output
52- const { content, thought, isThinking } : SplitMessage = useMemo ( ( ) => {
55+ const {
56+ content : mainDisplayableContent ,
57+ thought,
58+ isThinking,
59+ } : SplitMessage = useMemo ( ( ) => {
5360 if (
5461 msg . content === null ||
5562 ( msg . role !== 'assistant' && msg . role !== 'tool' )
@@ -65,12 +72,10 @@ export default function ChatMessage({
6572 actualContent += thinkSplit [ 0 ] ;
6673
6774 while ( thinkSplit [ 1 ] !== undefined ) {
68- // <think> tag found
6975 thinkSplit = thinkSplit [ 1 ] . split ( '</think>' , 2 ) ;
7076 thought += thinkSplit [ 0 ] ;
7177 isThinking = true ;
7278 if ( thinkSplit [ 1 ] !== undefined ) {
73- // </think> closing tag found
7479 isThinking = false ;
7580 thinkSplit = thinkSplit [ 1 ] . split ( '<think>' , 2 ) ;
7681 actualContent += thinkSplit [ 0 ] ;
@@ -79,10 +84,19 @@ export default function ChatMessage({
7984
8085 return { content : actualContent , thought, isThinking } ;
8186 } , [ msg ] ) ;
87+
8288 if ( ! viewingChat ) return null ;
8389
8490 const toolCalls = msg . tool_calls ?? null ;
8591
92+ const hasContentInMainMsg =
93+ mainDisplayableContent && mainDisplayableContent . trim ( ) !== '' ;
94+ const hasContentInChainedParts = chainedParts ?. some (
95+ ( part ) => part . content && part . content . trim ( ) !== ''
96+ ) ;
97+ const entireTurnHasSomeDisplayableContent =
98+ hasContentInMainMsg || hasContentInChainedParts ;
99+
86100 return (
87101 < div className = "group" id = { id } >
88102 < div
@@ -98,7 +112,6 @@ export default function ChatMessage({
98112 'chat-bubble-base-300' : msg . role !== 'user' ,
99113 } ) }
100114 >
101- { /* textarea for editing message */ }
102115 { editingContent !== null && (
103116 < >
104117 < textarea
@@ -127,21 +140,16 @@ export default function ChatMessage({
127140 </ button >
128141 </ >
129142 ) }
130- { /* not editing content, render message */ }
131143 { editingContent === null && (
132144 < >
133- { content === null ? (
145+ { mainDisplayableContent === null &&
146+ ! toolCalls &&
147+ ! chainedParts ?. length ? (
134148 < >
135- { toolCalls ? null : (
136- < >
137- { /* show loading dots for pending message */ }
138- < span className = "loading loading-dots loading-md" > </ span >
139- </ >
140- ) }
149+ < span className = "loading loading-dots loading-md" > </ span >
141150 </ >
142151 ) : (
143152 < >
144- { /* render message as markdown */ }
145153 < div dir = "auto" >
146154 { thought && (
147155 < details
@@ -152,7 +160,6 @@ export default function ChatMessage({
152160 { isPending && isThinking ? (
153161 < span >
154162 < span
155- v-if = "isGenerating"
156163 className = "loading loading-spinner loading-md mr-2"
157164 style = { { verticalAlign : 'middle' } }
158165 > </ span >
@@ -182,71 +189,69 @@ export default function ChatMessage({
182189 Extra content
183190 </ summary >
184191 < div className = "collapse-content" >
185- { msg . extra . map (
186- ( extra , i ) =>
187- extra . type === 'textFile' ? (
188- < div key = { extra . name } >
189- < b > { extra . name } </ b >
190- < pre > { extra . content } </ pre >
191- </ div >
192- ) : extra . type === 'context' ? (
193- < div key = { i } >
194- < pre > { extra . content } </ pre >
195- </ div >
196- ) : null // TODO: support other extra types
192+ { msg . extra . map ( ( extra , i ) =>
193+ extra . type === 'textFile' ? (
194+ < div key = { extra . name } >
195+ < b > { extra . name } </ b >
196+ < pre > { extra . content } </ pre >
197+ </ div >
198+ ) : extra . type === 'context' ? (
199+ < div key = { i } >
200+ < pre > { extra . content } </ pre >
201+ </ div >
202+ ) : null
197203 ) }
198204 </ div >
199205 </ details >
200206 ) }
201207
202- { msg . role === 'tool' ? (
203- < details
204- className = "collapse bg-base-200 collapse-arrow mb-4"
205- open = { true }
206- >
207- < summary className = "collapse-title" >
208- < b > Tool call result</ b >
209- </ summary >
210- < div className = "collapse-content" >
211- < MarkdownDisplay
212- content = { content }
213- isGenerating = { false } // Tool results are not "generating"
214- />
215- </ div >
216- </ details >
208+ { msg . role === 'tool' && mainDisplayableContent ? (
209+ < ToolCallResultDisplay content = { mainDisplayableContent } />
217210 ) : (
218- < MarkdownDisplay
219- content = { content }
220- isGenerating = { isPending }
221- />
211+ mainDisplayableContent &&
212+ mainDisplayableContent . trim ( ) !== '' && (
213+ < MarkdownDisplay
214+ content = { mainDisplayableContent }
215+ isGenerating = { isPending }
216+ />
217+ )
222218 ) }
223219 </ div >
224220 </ >
225221 ) }
226222 { toolCalls &&
227- toolCalls . map ( ( toolCall , i ) => (
228- < details
229- key = { i }
230- className = "collapse bg-base-200 collapse-arrow mb-4"
231- open = { false } // todo: make this configurable like showThoughtInProgress
232- >
233- < summary className = "collapse-title" >
234- < b > Tool call:</ b > { toolCall . function . name }
235- </ summary >
223+ toolCalls . map ( ( toolCall ) => (
224+ < ToolCallArgsDisplay key = { toolCall . id } toolCall = { toolCall } />
225+ ) ) }
236226
237- < div className = "collapse-content" >
238- < div className = "font-bold mb-1" > Arguments:</ div >
239- < pre className = "whitespace-pre-wrap bg-base-300 p-2 rounded" >
240- { JSON . stringify (
241- JSON . parse ( toolCall . function . arguments ) ,
242- null ,
243- 2
244- ) }
245- </ pre >
227+ { chainedParts ?. map ( ( part ) => (
228+ < React . Fragment key = { part . id } >
229+ { part . role === 'tool' && part . content && (
230+ < ToolCallResultDisplay
231+ content = { part . content }
232+ baseClassName = "collapse bg-base-200 collapse-arrow mb-4 mt-2"
233+ />
234+ ) }
235+
236+ { part . role === 'assistant' && part . content && (
237+ < div dir = "auto" className = "mt-2" >
238+ < MarkdownDisplay
239+ content = { part . content }
240+ isGenerating = { ! ! isPending }
241+ />
246242 </ div >
247- </ details >
248- ) ) }
249- { /* render timings if enabled */ }
243+ ) }
244+
245+ { part . tool_calls &&
246+ part . tool_calls . map ( ( toolCall ) => (
247+ < ToolCallArgsDisplay
248+ key = { toolCall . id }
249+ toolCall = { toolCall }
250+ baseClassName = "collapse bg-base-200 collapse-arrow mb-4 mt-2"
251+ />
252+ ) ) }
253+ </ React . Fragment >
254+ ) ) }
250255 { timings && config . showTokensPerSecond && (
251256 < div className = "dropdown dropdown-hover dropdown-top mt-2" >
252257 < div
@@ -275,8 +280,7 @@ export default function ChatMessage({
275280 </ div >
276281 </ div >
277282
278- { /* actions for each message */ }
279- { msg . content !== null && (
283+ { ( entireTurnHasSomeDisplayableContent || msg . role === 'user' ) && (
280284 < div
281285 className = { classNames ( {
282286 'flex items-center gap-2 mx-4 mt-2 mb-2' : true ,
@@ -308,7 +312,6 @@ export default function ChatMessage({
308312 </ button >
309313 </ div >
310314 ) }
311- { /* user message */ }
312315 { msg . role === 'user' && (
313316 < button
314317 className = "badge btn-mini show-on-hover"
@@ -318,28 +321,34 @@ export default function ChatMessage({
318321 ✍️ Edit
319322 </ button >
320323 ) }
321- { /* assistant message */ }
322324 { msg . role === 'assistant' && (
323325 < >
324326 { ! isPending && (
325327 < button
326328 className = "badge btn-mini show-on-hover mr-2"
327329 onClick = { ( ) => {
328- if ( msg . content !== null ) {
330+ if ( entireTurnHasSomeDisplayableContent ) {
329331 onRegenerateMessage ( msg as Message ) ;
330332 }
331333 } }
332- disabled = { msg . content === null }
334+ disabled = { ! entireTurnHasSomeDisplayableContent }
333335 >
334336 🔄 Regenerate
335337 </ button >
336338 ) }
337339 </ >
338340 ) }
339- < CopyButton
340- className = "badge btn-mini show-on-hover mr-2"
341- content = { msg . content }
342- />
341+ { entireTurnHasSomeDisplayableContent && (
342+ < CopyButton
343+ className = "badge btn-mini show-on-hover mr-2"
344+ content = {
345+ msg . content ??
346+ chainedParts ?. find ( ( p ) => p . role === 'assistant' && p . content )
347+ ?. content ??
348+ ''
349+ }
350+ />
351+ ) }
343352 </ div >
344353 ) }
345354 </ div >
0 commit comments