@@ -12,7 +12,7 @@ import type { KeyboardEvent } from 'react'
1212import clsx from 'clsx'
1313import { Virtuoso , VirtuosoHandle } from 'react-virtuoso'
1414import { Area , Panel } from '../../../../types'
15- import { ArrowLeftIcon , ChevronLeftIcon } from '@heroicons/react/24/solid'
15+ import { ChevronLeftIcon } from '@heroicons/react/24/solid'
1616
1717export function Chat ( ) {
1818 const { frameId, scenes } = useValues ( frameLogic )
@@ -35,12 +35,14 @@ export function Chat() {
3535 chatMessagesLoading,
3636 isCreatingChat,
3737 contextSelectionSummary,
38+ logExpanded,
3839 } = useValues ( chatLogic ( { frameId, sceneId : selectedSceneId } ) )
3940 const {
4041 setInput,
4142 submitMessage,
4243 clearChat,
4344 toggleContextItemsExpanded,
45+ toggleLogExpanded,
4446 selectChat,
4547 backToList,
4648 createChat,
@@ -53,12 +55,6 @@ export function Chat() {
5355 const scrollerElementRef = useRef < HTMLElement | null > ( null )
5456 const shouldStickToBottomRef = useRef ( true )
5557 const lastMessage = messages [ messages . length - 1 ]
56- const pendingAssistantPlaceholder =
57- messages . length > 0 &&
58- messages [ messages . length - 1 ] . isPlaceholder &&
59- ! messages [ messages . length - 1 ] . content &&
60- ! messages [ messages . length - 1 ] . tool
61- const pendingThinkingIndex = pendingAssistantPlaceholder ? messages . length - 2 : null
6258 const isChatView = chatView === 'chat' && activeChatId
6359 const hasBackendApiKey = Boolean ( savedSettings ?. openAI ?. backendApiKey ?. trim ( ) )
6460 const missingBackendApiKey = ! hasBackendApiKey
@@ -193,10 +189,30 @@ export function Chat() {
193189 )
194190 }
195191
196- const renderLogLine = ( line : string ) => {
192+ const stripBracketSegments = ( value : string ) =>
193+ value
194+ . replace ( / \s * \[ [ ^ \] ] + \] \s * / g, ' ' )
195+ . replace ( / \s + / g, ' ' )
196+ . trim ( )
197+
198+ const stripReviewIssues = ( value : string ) => {
199+ const match = value . match ( / ^ ( W A R N I N G : S c e n e r e v i e w i s s u e s : ) \s * ( \[ .* \] ) $ / )
200+ return match ? match [ 1 ] : value
201+ }
202+
203+ const renderLogLine = (
204+ line : string ,
205+ options : { showStage ?: boolean ; showDetails ?: boolean } = { showStage : true , showDetails : true }
206+ ) => {
207+ const { showStage = true , showDetails = true } = options
208+ const lineWithReview = showDetails ? line : stripReviewIssues ( line )
209+
197210 const contextMatch = line . match ( / ^ ( .* S e l e c t e d \d + c o n t e x t i t e m s : ) ( .+ ) $ / )
198211 if ( contextMatch ) {
199212 const [ , label , items ] = contextMatch
213+ if ( ! showDetails ) {
214+ return < span className = "text-slate-100" > { label . trim ( ) } </ span >
215+ }
200216 const tokens = items
201217 . split ( ',' )
202218 . map ( ( item ) => item . trim ( ) )
@@ -240,11 +256,12 @@ export function Chat() {
240256 const [ , time , stage , message ] = structuredMatch
241257 const statusMatch = message . match ( / ^ ( S U C C E S S | E R R O R ) : \s + ( .* ) $ / )
242258 const statusMessage = statusMatch ? statusMatch [ 2 ] : message
259+ const filteredStatusMessage = showDetails ? statusMessage : stripReviewIssues ( statusMessage )
243260 const generatedSceneName = extractGeneratedSceneName ( statusMessage )
244261 return (
245262 < div className = "flex flex-wrap gap-x-2 gap-y-1" >
246263 < span className = "text-slate-500" > { time } </ span >
247- < span className = "text-sky-300" > { stage } </ span >
264+ { showStage ? < span className = "text-sky-300" > { stage } </ span > : null }
248265 { statusMatch ? (
249266 < span className = { clsx ( 'font-semibold' , statusMatch [ 1 ] === 'ERROR' ? 'text-red-400' : 'text-emerald-300' ) } >
250267 { statusMatch [ 1 ] } :
@@ -253,40 +270,82 @@ export function Chat() {
253270 { generatedSceneName ? (
254271 < span className = "text-slate-100" > { renderGeneratedSceneMessage ( generatedSceneName ) } </ span >
255272 ) : statusMatch ? (
256- < span className = "text-slate-100" > { statusMatch [ 2 ] } </ span >
273+ < span className = "text-slate-100" > { filteredStatusMessage } </ span >
257274 ) : (
258- < span className = "text-slate-100" > { message } </ span >
275+ < span className = "text-slate-100" >
276+ { showStage
277+ ? filteredStatusMessage
278+ : stripBracketSegments ( showDetails ? message : stripReviewIssues ( message ) ) }
279+ </ span >
259280 ) }
260281 </ div >
261282 )
262283 }
263284
264- const generatedSceneName = extractGeneratedSceneName ( line )
285+ const sanitizedLine = showStage ? lineWithReview : stripBracketSegments ( lineWithReview )
286+ const generatedSceneName = extractGeneratedSceneName ( lineWithReview )
265287 if ( generatedSceneName ) {
266288 return < span className = "text-slate-100" > { renderGeneratedSceneMessage ( generatedSceneName ) } </ span >
267289 }
268290
269- return < span className = "text-slate-100" > { line } </ span >
291+ return < span className = "text-slate-100" > { sanitizedLine } </ span >
270292 }
271293
272- const renderMessageBody = ( messageContent : string , isLog : boolean ) => {
273- if ( ! messageContent ) {
274- return null
275- }
294+ const renderLogMessage = ( messageContent : string , messageId : string , isStreaming ?: boolean ) => {
295+ const lines = messageContent ? messageContent . split ( '\n' ) : [ ]
296+ const displayLines = lines . length > 0 ? lines : [ 'Thinking…' ]
297+ const lastLine = displayLines [ displayLines . length - 1 ] ?? ''
298+ const isExpanded = logExpanded [ messageId ] ?? false
299+ const canExpand = displayLines . length > 1
276300
277- if ( isLog ) {
278- const lines = messageContent . split ( '\n' )
301+ if ( ! isExpanded ) {
279302 return (
280- < div className = "space-y-2 font-mono text-xs" >
281- { lines . map ( ( line , index ) => (
282- < div key = { `${ line } -${ index } ` } className = "whitespace-pre-wrap break-words" >
283- { renderLogLine ( line ) }
303+ < button
304+ type = "button"
305+ className = { clsx ( 'flex w-full items-start text-left' , canExpand ? 'cursor-pointer' : 'cursor-default' ) }
306+ onClick = { ( ) => activeChatId && canExpand && toggleLogExpanded ( activeChatId , messageId ) }
307+ disabled = { ! canExpand }
308+ >
309+ { isStreaming ? < Spinner className = "h-4 w-4 mr-2 text-slate-400" /> : < span className = "h-4 w-4" /> }
310+ < span className = { clsx ( 'flex-1 text-sm' , isStreaming ? 'opacity-70' : '' ) } >
311+ { renderLogLine ( lastLine , { showStage : false , showDetails : false } ) }
312+ </ span >
313+ { isStreaming ? (
314+ < div className = "w-2" >
315+ < span className = "ai-scene-ellipsis text-slate-400" />
284316 </ div >
285- ) ) }
286- </ div >
317+ ) : null }
318+ </ button >
287319 )
288320 }
289321
322+ return (
323+ < div className = "space-y-2 text-sm" >
324+ { displayLines . map ( ( line , index ) => (
325+ < div key = { `${ line } -${ index } ` } className = "whitespace-pre-wrap break-words" >
326+ { renderLogLine ( line ) }
327+ </ div >
328+ ) ) }
329+ < button
330+ type = "button"
331+ className = "text-xs text-slate-500 hover:text-slate-300 transition"
332+ onClick = { ( ) => activeChatId && toggleLogExpanded ( activeChatId , messageId ) }
333+ >
334+ Hide log steps
335+ </ button >
336+ </ div >
337+ )
338+ }
339+
340+ const renderMessageBody = ( messageContent : string , isLog : boolean , messageId : string , isStreaming ?: boolean ) => {
341+ if ( isLog ) {
342+ return renderLogMessage ( messageContent , messageId , isStreaming )
343+ }
344+
345+ if ( ! messageContent ) {
346+ return null
347+ }
348+
290349 return < div className = "whitespace-pre-wrap break-words" > { messageContent } </ div >
291350 }
292351
@@ -398,15 +457,7 @@ export function Chat() {
398457 < span className = "uppercase tracking-wide" > { message . role } </ span >
399458 { message . tool ? < span className = "text-slate-500" > tool: { message . tool } </ span > : null }
400459 </ div >
401- < div >
402- { renderMessageBody ( message . content , isLog ) }
403- { pendingThinkingIndex === index && isLog && message . isStreaming ? (
404- < div className = "inline-flex items-center gap-2 text-slate-300 pt-2" >
405- < span > Thinking…</ span >
406- </ div >
407- ) : null }
408- { message . isStreaming ? < span className = "ml-1 animate-pulse" > ▍</ span > : null }
409- </ div >
460+ < div > { renderMessageBody ( message . content , isLog , message . id , message . isStreaming ) } </ div >
410461 </ div >
411462 </ div >
412463 )
0 commit comments