33import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle' ;
44import { ResizablePanel , ResizablePanelGroup } from '@/components/ui/resizable' ;
55import { Skeleton } from '@/components/ui/skeleton' ;
6- import { Button } from '@/components/ui/button' ;
7- import { Tooltip , TooltipContent , TooltipTrigger } from '@/components/ui/tooltip' ;
8- import { CheckCircle , Loader2 , PanelLeftClose , PanelLeft } from 'lucide-react' ;
6+ import { CheckCircle , Loader2 } from 'lucide-react' ;
97import { CSSProperties , forwardRef , memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
108import scrollIntoView from 'scroll-into-view-if-needed' ;
119import { Reference , referenceSchema , SBChatMessage , Source } from "../../types" ;
@@ -18,9 +16,6 @@ import { ReferencedSourcesListView } from './referencedSourcesListView';
1816import isEqual from "fast-deep-equal/react" ;
1917import { ANSWER_TAG } from '../../constants' ;
2018
21- // Minimum width in pixels for the horizontal layout to work well
22- const MIN_HORIZONTAL_LAYOUT_WIDTH = 768 ;
23-
2419interface ChatThreadListItemProps {
2520 userMessage : SBChatMessage ;
2621 assistantMessage ?: SBChatMessage ;
@@ -38,7 +33,6 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
3833 chatId,
3934 index,
4035} , ref ) => {
41- const containerRef = useRef < HTMLDivElement > ( null ) ;
4236 const leftPanelRef = useRef < HTMLDivElement > ( null ) ;
4337 const [ leftPanelHeight , setLeftPanelHeight ] = useState < number | null > ( null ) ;
4438 const answerRef = useRef < HTMLDivElement > ( null ) ;
@@ -49,32 +43,6 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
4943 const hasAutoCollapsed = useRef ( false ) ;
5044 const userHasManuallyExpanded = useRef ( false ) ;
5145
52- // Layout state: track container width and user preference
53- const [ containerWidth , setContainerWidth ] = useState < number | null > ( null ) ;
54- const [ isReferencePanelCollapsed , setIsReferencePanelCollapsed ] = useState ( false ) ;
55-
56- // Determine if we should use vertical layout based on container width
57- const shouldUseVerticalLayout = containerWidth !== null && containerWidth < MIN_HORIZONTAL_LAYOUT_WIDTH ;
58-
59- // Monitor container width for responsive layout
60- useEffect ( ( ) => {
61- if ( ! containerRef . current ) {
62- return ;
63- }
64-
65- const resizeObserver = new ResizeObserver ( ( entries ) => {
66- for ( const entry of entries ) {
67- setContainerWidth ( entry . contentRect . width ) ;
68- }
69- } ) ;
70-
71- resizeObserver . observe ( containerRef . current ) ;
72-
73- return ( ) => {
74- resizeObserver . disconnect ( ) ;
75- } ;
76- } , [ ] ) ;
77-
7846 const userQuestion = useMemo ( ( ) => {
7947 return userMessage . parts . length > 0 && userMessage . parts [ 0 ] . type === 'text' ? userMessage . parts [ 0 ] . text : '' ;
8048 } , [ userMessage ] ) ;
@@ -337,162 +305,10 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
337305 } , [ references , sources ] ) ;
338306
339307
340- // Content for the answer/question panel
341- const answerPanelContent = (
342- < div
343- ref = { leftPanelRef }
344- className = "py-4 h-full"
345- >
346- < div className = "flex flex-row gap-2 mb-4" >
347- { isStreaming ? (
348- < Loader2 className = "w-4 h-4 animate-spin flex-shrink-0 mt-1.5" />
349- ) : (
350- < CheckCircle className = "w-4 h-4 text-green-700 flex-shrink-0 mt-1.5" />
351- ) }
352- < MarkdownRenderer
353- content = { userQuestion . trim ( ) }
354- className = "prose-p:m-0"
355- escapeHtml = { true }
356- />
357- </ div >
358-
359- { isThinking && (
360- < div className = "space-y-4 mb-4" >
361- < Skeleton className = "h-4 max-w-32" />
362- < div className = "space-y-2" >
363- < Skeleton className = "h-3 max-w-72" />
364- < Skeleton className = "h-3 max-w-64" />
365- < Skeleton className = "h-3 max-w-56" />
366- </ div >
367- </ div >
368- ) }
369-
370- < DetailsCard
371- chatId = { chatId }
372- isExpanded = { isDetailsPanelExpanded }
373- onExpandedChanged = { onExpandDetailsPanel }
374- isThinking = { isThinking }
375- isStreaming = { isStreaming }
376- thinkingSteps = { uiVisibleThinkingSteps }
377- metadata = { assistantMessage ?. metadata }
378- />
379-
380- { ( answerPart && assistantMessage ) ? (
381- < AnswerCard
382- ref = { answerRef }
383- answerText = { answerPart . text }
384- chatId = { chatId }
385- messageId = { assistantMessage . id }
386- traceId = { assistantMessage . metadata ?. traceId }
387- />
388- ) : ! isStreaming && (
389- < p className = "text-destructive" > Error: No answer response was provided</ p >
390- ) }
391- </ div >
392- ) ;
393-
394- // Content for the references panel
395- const referencesPanelContent = (
396- < >
397- { referencedFileSources . length > 0 ? (
398- < ReferencedSourcesListView
399- index = { index }
400- references = { references }
401- sources = { referencedFileSources }
402- hoveredReference = { hoveredReference }
403- selectedReference = { selectedReference }
404- onSelectedReferenceChanged = { setSelectedReference }
405- onHoveredReferenceChanged = { setHoveredReference }
406- style = { shouldUseVerticalLayout || isReferencePanelCollapsed ? { } : rightPanelStyle }
407- />
408- ) : isStreaming ? (
409- < div className = "space-y-4" >
410- { Array . from ( { length : 3 } ) . map ( ( _ , index ) => (
411- < Skeleton key = { index } className = "w-full h-48" />
412- ) ) }
413- </ div >
414- ) : (
415- < div className = "p-4 text-center text-muted-foreground text-sm" >
416- No file references found
417- </ div >
418- ) }
419- </ >
420- ) ;
421-
422- // Layout toggle button for narrow containers
423- const layoutToggleButton = referencedFileSources . length > 0 && (
424- < Tooltip >
425- < TooltipTrigger asChild >
426- < Button
427- variant = "ghost"
428- size = "sm"
429- className = "h-7 w-7 p-0"
430- onClick = { ( ) => setIsReferencePanelCollapsed ( ! isReferencePanelCollapsed ) }
431- >
432- { isReferencePanelCollapsed ? (
433- < PanelLeft className = "h-4 w-4" />
434- ) : (
435- < PanelLeftClose className = "h-4 w-4" />
436- ) }
437- </ Button >
438- </ TooltipTrigger >
439- < TooltipContent side = "bottom" >
440- { isReferencePanelCollapsed ? 'Show code references' : 'Hide code references' }
441- </ TooltipContent >
442- </ Tooltip >
443- ) ;
444-
445- // Vertical layout for narrow containers
446- if ( shouldUseVerticalLayout ) {
447- return (
448- < div
449- className = "flex flex-col relative"
450- ref = { ( node ) => {
451- // Handle both refs
452- containerRef . current = node ;
453- if ( typeof ref === 'function' ) {
454- ref ( node ) ;
455- } else if ( ref ) {
456- ref . current = node ;
457- }
458- } }
459- >
460- { /* Answer panel with toggle button */ }
461- < div className = "relative" >
462- { referencedFileSources . length > 0 && (
463- < div className = "absolute top-4 right-0 z-10" >
464- { layoutToggleButton }
465- </ div >
466- ) }
467- { answerPanelContent }
468- </ div >
469-
470- { /* References panel - collapsible in vertical layout */ }
471- { ! isReferencePanelCollapsed && referencedFileSources . length > 0 && (
472- < div className = "border-t pt-4 mt-4" >
473- < div className = "text-sm font-medium text-muted-foreground mb-3" > Code References</ div >
474- < div className = "max-h-[50vh] overflow-y-auto" >
475- { referencesPanelContent }
476- </ div >
477- </ div >
478- ) }
479- </ div >
480- ) ;
481- }
482-
483- // Horizontal layout for wider containers
484308 return (
485309 < div
486- className = "flex flex-col relative min-h-[calc(100vh-250px)]"
487- ref = { ( node ) => {
488- // Handle both refs
489- containerRef . current = node ;
490- if ( typeof ref === 'function' ) {
491- ref ( node ) ;
492- } else if ( ref ) {
493- ref . current = node ;
494- }
495- } }
310+ className = "flex flex-col md:flex-row relative min-h-[calc(100vh-250px)]"
311+ ref = { ref }
496312 >
497313 < ResizablePanelGroup
498314 direction = "horizontal"
@@ -503,42 +319,103 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
503319 >
504320 < ResizablePanel
505321 order = { 1 }
506- minSize = { isReferencePanelCollapsed ? 100 : 30 }
507- maxSize = { isReferencePanelCollapsed ? 100 : 70 }
508- defaultSize = { isReferencePanelCollapsed ? 100 : 50 }
322+ minSize = { 20 }
323+ maxSize = { 80 }
324+ defaultSize = { 50 }
509325 style = { {
510326 overflow : 'visible' ,
511327 } }
512328 >
513- < div className = "relative" >
514- { referencedFileSources . length > 0 && (
515- < div className = "absolute top-4 right-0 z-10" >
516- { layoutToggleButton }
329+ < div
330+ ref = { leftPanelRef }
331+ className = "py-4 h-full"
332+ >
333+ < div className = "flex flex-row gap-2 mb-4" >
334+ { isStreaming ? (
335+ < Loader2 className = "w-4 h-4 animate-spin flex-shrink-0 mt-1.5" />
336+ ) : (
337+ < CheckCircle className = "w-4 h-4 text-green-700 flex-shrink-0 mt-1.5" />
338+ ) }
339+ < MarkdownRenderer
340+ content = { userQuestion . trim ( ) }
341+ className = "prose-p:m-0"
342+ escapeHtml = { true }
343+ />
344+ </ div >
345+
346+ { isThinking && (
347+ < div className = "space-y-4 mb-4" >
348+ < Skeleton className = "h-4 max-w-32" />
349+ < div className = "space-y-2" >
350+ < Skeleton className = "h-3 max-w-72" />
351+ < Skeleton className = "h-3 max-w-64" />
352+ < Skeleton className = "h-3 max-w-56" />
353+ </ div >
517354 </ div >
518355 ) }
519- { answerPanelContent }
356+
357+ < DetailsCard
358+ chatId = { chatId }
359+ isExpanded = { isDetailsPanelExpanded }
360+ onExpandedChanged = { onExpandDetailsPanel }
361+ isThinking = { isThinking }
362+ isStreaming = { isStreaming }
363+ thinkingSteps = { uiVisibleThinkingSteps }
364+ metadata = { assistantMessage ?. metadata }
365+ />
366+
367+ { ( answerPart && assistantMessage ) ? (
368+ < AnswerCard
369+ ref = { answerRef }
370+ answerText = { answerPart . text }
371+ chatId = { chatId }
372+ messageId = { assistantMessage . id }
373+ traceId = { assistantMessage . metadata ?. traceId }
374+ />
375+ ) : ! isStreaming && (
376+ < p className = "text-destructive" > Error: No answer response was provided</ p >
377+ ) }
520378 </ div >
521379 </ ResizablePanel >
522- { ! isReferencePanelCollapsed && (
523- < >
524- < AnimatedResizableHandle className = 'mx-4' />
525- < ResizablePanel
526- order = { 2 }
527- minSize = { 30 }
528- maxSize = { 70 }
529- defaultSize = { 50 }
530- style = { {
531- overflow : 'clip' ,
532- maxHeight : '100%' ,
533- minWidth : 0 ,
534- } }
535- >
536- < div className = "sticky top-0" >
537- { referencesPanelContent }
380+ < AnimatedResizableHandle className = 'mx-4' />
381+ < ResizablePanel
382+ order = { 2 }
383+ minSize = { 20 }
384+ maxSize = { 80 }
385+ defaultSize = { 50 }
386+ style = { {
387+ overflow : 'clip' ,
388+ maxHeight : '100%' ,
389+ minWidth : 0 ,
390+ } }
391+ >
392+ < div
393+ className = "sticky top-0"
394+ >
395+ { referencedFileSources . length > 0 ? (
396+ < ReferencedSourcesListView
397+ index = { index }
398+ references = { references }
399+ sources = { referencedFileSources }
400+ hoveredReference = { hoveredReference }
401+ selectedReference = { selectedReference }
402+ onSelectedReferenceChanged = { setSelectedReference }
403+ onHoveredReferenceChanged = { setHoveredReference }
404+ style = { rightPanelStyle }
405+ />
406+ ) : isStreaming ? (
407+ < div className = "space-y-4" >
408+ { Array . from ( { length : 3 } ) . map ( ( _ , index ) => (
409+ < Skeleton key = { index } className = "w-full h-48" />
410+ ) ) }
538411 </ div >
539- </ ResizablePanel >
540- </ >
541- ) }
412+ ) : (
413+ < div className = "p-4 text-center text-muted-foreground text-sm" >
414+ No file references found
415+ </ div >
416+ ) }
417+ </ div >
418+ </ ResizablePanel >
542419 </ ResizablePanelGroup >
543420 </ div >
544421 )
@@ -588,4 +465,4 @@ const getNearestReferenceElement = (referenceElements: Element[]) => {
588465
589466 return currentDistance < nearestDistance ? current : nearest ;
590467 } ) ;
591- }
468+ }
0 commit comments