33import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle' ;
44import { ResizablePanel , ResizablePanelGroup } from '@/components/ui/resizable' ;
55import { Skeleton } from '@/components/ui/skeleton' ;
6- import { CheckCircle , Loader2 } from 'lucide-react' ;
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' ;
79import { CSSProperties , forwardRef , memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
810import scrollIntoView from 'scroll-into-view-if-needed' ;
911import { Reference , referenceSchema , SBChatMessage , Source } from "../../types" ;
@@ -16,6 +18,9 @@ import { ReferencedSourcesListView } from './referencedSourcesListView';
1618import isEqual from "fast-deep-equal/react" ;
1719import { ANSWER_TAG } from '../../constants' ;
1820
21+ // Minimum width in pixels for the horizontal layout to work well
22+ const MIN_HORIZONTAL_LAYOUT_WIDTH = 768 ;
23+
1924interface ChatThreadListItemProps {
2025 userMessage : SBChatMessage ;
2126 assistantMessage ?: SBChatMessage ;
@@ -33,6 +38,7 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
3338 chatId,
3439 index,
3540} , ref ) => {
41+ const containerRef = useRef < HTMLDivElement > ( null ) ;
3642 const leftPanelRef = useRef < HTMLDivElement > ( null ) ;
3743 const [ leftPanelHeight , setLeftPanelHeight ] = useState < number | null > ( null ) ;
3844 const answerRef = useRef < HTMLDivElement > ( null ) ;
@@ -43,6 +49,32 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
4349 const hasAutoCollapsed = useRef ( false ) ;
4450 const userHasManuallyExpanded = useRef ( false ) ;
4551
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+
4678 const userQuestion = useMemo ( ( ) => {
4779 return userMessage . parts . length > 0 && userMessage . parts [ 0 ] . type === 'text' ? userMessage . parts [ 0 ] . text : '' ;
4880 } , [ userMessage ] ) ;
@@ -305,10 +337,162 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
305337 } , [ references , sources ] ) ;
306338
307339
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
308484 return (
309485 < div
310- className = "flex flex-col md:flex-row relative min-h-[calc(100vh-250px)]"
311- ref = { ref }
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+ } }
312496 >
313497 < ResizablePanelGroup
314498 direction = "horizontal"
@@ -319,103 +503,42 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
319503 >
320504 < ResizablePanel
321505 order = { 1 }
322- minSize = { 30 }
323- maxSize = { 70 }
324- defaultSize = { 50 }
506+ minSize = { isReferencePanelCollapsed ? 100 : 30 }
507+ maxSize = { isReferencePanelCollapsed ? 100 : 70 }
508+ defaultSize = { isReferencePanelCollapsed ? 100 : 50 }
325509 style = { {
326510 overflow : 'visible' ,
327511 } }
328512 >
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 >
513+ < div className = "relative" >
514+ { referencedFileSources . length > 0 && (
515+ < div className = "absolute top-4 right-0 z-10" >
516+ { layoutToggleButton }
354517 </ div >
355518 ) }
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- ) }
519+ { answerPanelContent }
378520 </ div >
379521 </ ResizablePanel >
380- < AnimatedResizableHandle className = 'mx-4' />
381- < ResizablePanel
382- order = { 2 }
383- minSize = { 30 }
384- maxSize = { 70 }
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- ) ) }
411- </ div >
412- ) : (
413- < div className = "p-4 text-center text-muted-foreground text-sm" >
414- No file references found
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 }
415538 </ div >
416- ) }
417- </ div >
418- </ ResizablePanel >
539+ </ ResizablePanel >
540+ </ >
541+ ) }
419542 </ ResizablePanelGroup >
420543 </ div >
421544 )
0 commit comments