11/* eslint-disable no-case-declarations */
22/* eslint-disable react/no-array-index-key */
33import { cn , getTheme , Message } from "@/lib/utils" ;
4- import { useContext , useEffect , useState } from "react" ;
4+ import { useContext , useEffect , useRef , useState , useCallback } from "react" ;
55import { useTheme } from "next-themes" ;
66import Image from "next/image" ;
77import { ChevronDown , ChevronRight , Share2 , Copy , Loader2 , Play , Search , X , Send , MessagesSquare } from "lucide-react" ;
@@ -39,8 +39,8 @@ interface Props {
3939}
4040
4141export default function Chat ( { onClose } : Props ) {
42- const { theme } = useTheme ( ) ;
43- const { currentTheme } = getTheme ( theme ) ;
42+ const { resolvedTheme } = useTheme ( ) ;
43+ const { currentTheme } = getTheme ( resolvedTheme ) ;
4444 const { setIndicator } = useContext ( IndicatorContext ) ;
4545 const { graphName, runQuery } = useContext ( GraphContext ) ;
4646 const { isQueryLoading } = useContext ( QueryLoadingContext ) ;
@@ -51,12 +51,60 @@ export default function Chat({ onClose }: Props) {
5151 const { toast } = useToast ( ) ;
5252 const route = useRouter ( ) ;
5353
54+ const [ mounted , setMounted ] = useState ( false ) ;
5455 const [ messages , setMessages ] = useState < Message [ ] > ( [ ] ) ;
5556 const [ messagesList , setMessagesList ] = useState < ( Message | [ Message [ ] , boolean ] ) [ ] > ( [ ] ) ;
5657 const [ newMessage , setNewMessage ] = useState ( "" ) ;
5758 const [ isLoading , setIsLoading ] = useState ( false ) ;
5859 const [ queryCollapse , setQueryCollapse ] = useState < { [ key : string ] : boolean } > ( { } ) ;
5960 const [ collapseEligible , setCollapseEligible ] = useState < { [ key : number ] : boolean } > ( { } ) ;
61+ const textRefs = useRef < Map < number , HTMLElement > > ( new Map ( ) ) ;
62+ const observerRef = useRef < ResizeObserver | null > ( null ) ;
63+ const queryCollapseRef = useRef ( queryCollapse ) ;
64+ queryCollapseRef . current = queryCollapse ;
65+
66+ useEffect ( ( ) => {
67+ setMounted ( true ) ;
68+ } , [ ] ) ;
69+
70+ // Create a single ResizeObserver that recomputes collapse eligibility on resize
71+ useEffect ( ( ) => {
72+ observerRef . current = new ResizeObserver ( ( entries ) => {
73+ for ( const entry of entries ) {
74+ const el = entry . target as HTMLElement ;
75+ textRefs . current . forEach ( ( ref , i ) => {
76+ if ( ref !== el ) return ;
77+ const shouldCollapse = el . scrollHeight > 64 ;
78+ // Only upgrade to eligible, never downgrade — collapsed items have small height
79+ if ( ! shouldCollapse ) return ;
80+ setCollapseEligible ( prev => {
81+ if ( prev [ i ] ) return prev ;
82+ return { ...prev , [ i ] : true } ;
83+ } ) ;
84+ } ) ;
85+ }
86+ } ) ;
87+
88+ return ( ) => observerRef . current ?. disconnect ( ) ;
89+ } , [ ] ) ;
90+
91+ const setTextRef = useCallback ( ( i : number ) => ( r : HTMLElement | null ) => {
92+ if ( r ) {
93+ textRefs . current . set ( i , r ) ;
94+ // Measure on mount — only set eligible if content is tall enough
95+ if ( r . scrollHeight > 64 ) {
96+ setCollapseEligible ( prev => {
97+ if ( prev [ i ] ) return prev ;
98+ return { ...prev , [ i ] : true } ;
99+ } ) ;
100+ }
101+ observerRef . current ?. observe ( r ) ;
102+ } else {
103+ const prev = textRefs . current . get ( i ) ;
104+ if ( prev ) observerRef . current ?. unobserve ( prev ) ;
105+ textRefs . current . delete ( i ) ;
106+ }
107+ } , [ ] ) ;
60108
61109 // Load messages and cypher only preference for current graph on mount
62110 useEffect ( ( ) => {
@@ -325,14 +373,7 @@ export default function Chat({ onClose }: Props) {
325373 const i = messages . findIndex ( m => m === message ) ;
326374
327375 return (
328- < div ref = { r => {
329- if ( ! r ) return ;
330- const shouldCollapse = r . scrollHeight > 64 ;
331- setCollapseEligible ( prev => {
332- if ( prev [ i ] === shouldCollapse ) return prev ;
333- return { ...prev , [ i ] : shouldCollapse } ;
334- } ) ;
335- } } className = "flex gap-2 items-start" >
376+ < div className = "flex gap-2 items-start" >
336377 {
337378 collapseEligible [ i ] &&
338379 < Button
@@ -344,7 +385,7 @@ export default function Chat({ onClose }: Props) {
344385 { queryCollapse [ i ] ? < ChevronRight size = { 25 } /> : < ChevronDown size = { 25 } /> }
345386 </ Button >
346387 }
347- < div className = "overflow-hidden SofiaSans" >
388+ < div ref = { setTextRef ( i ) } className = "overflow-hidden SofiaSans" >
348389 {
349390 queryCollapse [ i ] ? (
350391 < ShadTooltip >
@@ -460,8 +501,8 @@ export default function Chat({ onClose }: Props) {
460501 ? < div className = "h-8 w-8 rounded-full flex items-center justify-center bg-primary" >
461502 < p className = "text-foreground text-sm truncate text-center" > { message . role . charAt ( 0 ) . toUpperCase ( ) } </ p >
462503 </ div >
463- : < div className = "h-8 w-8" >
464- < Image className = "rounded-full" src = { `/icons/F-${ currentTheme } .svg` } style = { { height : "100%" , width : "100%" } } alt = "Assistant" width = { 0 } height = { 0 } />
504+ : < div className = "h-8 w-8 relative " >
505+ { mounted && currentTheme && < Image className = "rounded-full" src = { `/icons/F-${ currentTheme } .svg` } alt = "Assistant" fill /> }
465506 </ div > ;
466507 return (
467508 < li
0 commit comments