11import { toast } from "@/components/ui/use-toast" ;
2- import { Dispatch , FormEvent , MutableRefObject , SetStateAction , useEffect , useRef , useState } from "react" ;
2+ import { Dispatch , FormEvent , SetStateAction , useEffect , useRef , useState } from "react" ;
33import Image from "next/image" ;
44import { AlignLeft , ArrowRight , ChevronDown , Lightbulb , Undo2 } from "lucide-react" ;
5- import { Message , MessageTypes , Path , PathData } from "@/lib/utils" ;
5+ import { Message , MessageTypes , Path , PathData , PATH_COLOR } from "@/lib/utils" ;
66import Input from "./Input" ;
77import { Graph , GraphData , Node } from "./model" ;
88import { cn , GraphRef } from "@/lib/utils" ;
@@ -32,7 +32,6 @@ interface Props {
3232 setPaths : Dispatch < SetStateAction < PathData [ ] > >
3333}
3434
35- const PATH_COLOR = "#ffde21" ;
3635const SUGGESTIONS = [
3736 "List a few recursive functions" ,
3837 "What is the name of the most used method?" ,
@@ -41,15 +40,31 @@ const SUGGESTIONS = [
4140 "Show a calling path between the drop_edge_range_index function and _query, only return function(s) names" ,
4241]
4342
44- const RemoveLastPath = ( messages : Message [ ] ) => {
45- const index = messages . findIndex ( ( m ) => m . type === MessageTypes . Path )
43+ type RemoveLastPathResult = {
44+ messages : Message [ ]
45+ insertIndex : number
46+ }
47+
48+ const RemoveLastPath = ( messages : Message [ ] ) : RemoveLastPathResult => {
49+ // Find the last Path message so we know where the user was in the conversation
50+ const index = messages . findLastIndex ( ( m ) => m . type === MessageTypes . Path )
4651
47- if ( index !== - 1 ) {
48- messages = [ ...messages . slice ( 0 , index - 2 ) , ...messages . slice ( index + 1 ) ] ;
49- messages = RemoveLastPath ( messages )
52+ if ( index === - 1 ) {
53+ return { messages, insertIndex : messages . length }
5054 }
5155
52- return messages
56+ const groupStart = Math . max ( 0 , index - 2 )
57+ const hasMessagesAfter = index < messages . length - 1
58+ const cleaned = [ ...messages . slice ( 0 , groupStart ) , ...messages . slice ( index + 1 ) ]
59+
60+ // Recurse to strip any remaining Path groups
61+ const { messages : finalMessages } = RemoveLastPath ( cleaned )
62+
63+ // If there were messages after the Path group, inject the answer there;
64+ // otherwise just append at the end
65+ const insertIndex = hasMessagesAfter ? groupStart : finalMessages . length
66+
67+ return { messages : finalMessages , insertIndex }
5368}
5469
5570export function Chat ( { messages, setMessages, query, setQuery, selectedPath, setSelectedPath, setChatOpen, repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setCooldownTicks, canvasRef, paths, setPaths } : Props ) {
@@ -95,74 +110,113 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
95110 const canvas = canvasRef . current
96111
97112 if ( ! canvas ) return
98- setSelectedPath ( prev => {
99- if ( prev ) {
100- if ( isPathResponse && paths . some ( ( path ) => [ ...path . nodes , ...path . links ] . every ( ( e : any ) => [ ...prev . nodes , ...prev . links ] . some ( ( el : any ) => el . id === e . id ) ) ) ) {
101- graph . getElements ( ) . forEach ( link => {
102- const { id } = link
103-
104- if ( prev . links . some ( e => e . id === id ) && ! p . links . some ( e => e . id === id ) ) {
105- link . isPathSelected = false
106- }
107- } )
108- } else {
109- const elements = graph . getElements ( ) . filter ( e => [ ...prev . links , ...prev . nodes ] . some ( el => el . id === e . id && ! [ ...p . nodes , ...p . links ] . some ( ele => ele . id === el . id ) ) )
110- if ( isPathResponse || isPathResponse === undefined ) {
111- elements . forEach ( e => {
112- e . isPath = false
113- e . isPathSelected = false
114- } )
113+
114+ // Sets for the new path
115+ const pNodeIds = new Set < number > ( p . nodes . map ( ( n : Node ) => n . id ) )
116+ const pLinkIds = new Set < number > ( p . links . map ( ( l : any ) => l . id ) )
117+ const pIds = new Set < number > ( [ ...pNodeIds , ...pLinkIds ] )
118+ const firstNodeId = p . nodes [ 0 ] . id
119+ const lastNodeId = p . nodes [ p . nodes . length - 1 ] . id
120+
121+ // Sets for the previous path (selectedPath is the current value from props)
122+ const prevNodeIds = new Set < number > ( ( selectedPath ?. nodes ?? [ ] ) . map ( ( n : any ) => n . id ) )
123+ const prevLinkIds = new Set < number > ( ( selectedPath ?. links ?? [ ] ) . map ( ( e : any ) => e . id ) )
124+ const prevIds = new Set < number > ( [ ...prevNodeIds , ...prevLinkIds ] )
125+
126+ // --- Unset previous path on graph elements ---
127+ if ( selectedPath ) {
128+ const pathAlreadyInPrev = isPathResponse && paths . some ( path =>
129+ [ ...path . nodes , ...path . links ] . every ( ( e : any ) => prevIds . has ( e . id ) )
130+ )
131+
132+ if ( pathAlreadyInPrev ) {
133+ graph . getElements ( ) . forEach ( ( element : any ) => {
134+ if ( prevLinkIds . has ( element . id ) && ! pLinkIds . has ( element . id ) ) {
135+ element . isPathSelected = false
115136 }
116- }
137+ } )
138+ } else {
139+ const staleElements = graph . getElements ( ) . filter ( ( e : any ) => prevIds . has ( e . id ) && ! pIds . has ( e . id ) )
140+ staleElements . forEach ( ( e : any ) => {
141+ e . isPath = false
142+ e . isPathSelected = false
143+ if ( "source" in e ) {
144+ e . color = "#999999"
145+ }
146+ } )
117147 }
118- return p
119- } )
120- if ( isPathResponse && paths . length > 0 && paths . some ( ( path ) => [ ...path . nodes , ...path . links ] . every ( ( e : any ) => [ ...p . nodes , ...p . links ] . some ( ( el : any ) => el . id === e . id ) ) ) ) {
121- graph . Elements . links . forEach ( e => {
122- if ( p . links . some ( el => el . id === e . id ) ) {
148+ }
149+
150+ setSelectedPath ( p )
151+
152+ // --- Set new path on graph elements ---
153+ if ( isPathResponse && paths . length > 0 && paths . some ( path =>
154+ [ ...path . nodes , ...path . links ] . every ( ( e : any ) => pIds . has ( e . id ) )
155+ ) ) {
156+ graph . Elements . links . forEach ( ( e : any ) => {
157+ if ( pLinkIds . has ( e . id ) ) {
123158 e . isPathSelected = true
124159 }
125160 } )
126161 } else {
127- const elements : PathData = { nodes : [ ] , links : [ ] } ;
128- p . nodes . forEach ( node => {
129- let element = graph . Elements . nodes . find ( n => n . id === node . id )
130- if ( ! element ) {
131- elements . nodes . push ( node )
132- }
133- } )
134- p . links . forEach ( link => {
135- let element = graph . Elements . links . find ( l => l . id === link . id )
136- if ( ! element ) {
137- elements . links . push ( link )
138- }
139- } )
162+ const existingNodeIds = new Set < number > ( graph . Elements . nodes . map ( ( n : any ) => n . id ) )
163+ const existingLinkIds = new Set < number > ( graph . Elements . links . map ( ( l : any ) => l . id ) )
164+ const elements : PathData = {
165+ nodes : p . nodes . filter ( node => ! existingNodeIds . has ( node . id ) ) ,
166+ links : p . links . filter ( link => ! existingLinkIds . has ( link . id ) ) ,
167+ }
140168 graph . extend ( elements , true , { start : p . nodes [ 0 ] , end : p . nodes [ p . nodes . length - 1 ] } )
141- graph . getElements ( ) . filter ( e => "source" in e ? p . links . some ( l => l . id === e . id ) : p . nodes . some ( n => n . id === e . id ) ) . forEach ( e => {
142- if ( e . id === p . nodes [ 0 ] . id || e . id === p . nodes [ p . nodes . length - 1 ] . id || "source" in e ) {
143- e . isPathSelected = true
144- } else {
145- e . isPath = true
146- }
147- } ) ;
169+ graph . getElements ( )
170+ . filter ( ( e : any ) => "source" in e ? pLinkIds . has ( e . id ) : pNodeIds . has ( e . id ) )
171+ . forEach ( ( e : any ) => {
172+ if ( e . id === firstNodeId || e . id === lastNodeId || "source" in e ) {
173+ e . isPathSelected = true
174+ } else {
175+ e . isPath = true
176+ }
177+ } )
148178 }
149179
150180 const currentData = canvas . getGraphData ( ) ;
151181
152- const nodesSet = new Set < number > ( p . nodes . map ( ( n : Node ) => n . id ) ) ;
153- const linksSet = new Set < number > ( p . links . map ( ( l : any ) => l . id ) ) ;
182+ // --- Unset canvas data for previous path elements no longer in the new path ---
183+ if ( selectedPath ) {
184+ currentData . nodes . forEach ( ( n : any ) => {
185+ if ( prevNodeIds . has ( n . id ) && ! pNodeIds . has ( n . id ) ) {
186+ if ( isPathResponse ) {
187+ // keep isPath + PATH_COLOR border, just deselect
188+ n . data . isPathSelected = false
189+ } else {
190+ n . data . isPath = false
191+ n . data . isPathSelected = false
192+ }
193+ }
194+ } )
195+ currentData . links . forEach ( ( l : any ) => {
196+ if ( prevLinkIds . has ( l . id ) && ! pLinkIds . has ( l . id ) ) {
197+ if ( isPathResponse ) {
198+ // keep color + dashed path line, just deselect
199+ l . data . isPathSelected = false
200+ } else {
201+ l . data . isPathSelected = false
202+ l . color = "#999999"
203+ }
204+ }
205+ } )
206+ }
154207
155- currentData . nodes . forEach ( n => {
156- if ( nodesSet . has ( n . id ) ) {
157- if ( n . id === p . nodes [ 0 ] . id || n . id === p . nodes [ p . nodes . length - 1 ] . id ) {
208+ // --- Set canvas data for new path elements ---
209+ currentData . nodes . forEach ( ( n : any ) => {
210+ if ( pNodeIds . has ( n . id ) ) {
211+ if ( n . id === firstNodeId || n . id === lastNodeId ) {
158212 n . data . isPathSelected = true ;
159213 } else {
160214 n . data . isPath = true ;
161215 }
162216 }
163217 } ) ;
164- currentData . links . forEach ( l => {
165- if ( linksSet . has ( l . id ) ) {
218+ currentData . links . forEach ( ( l : any ) => {
219+ if ( pLinkIds . has ( l . id ) ) {
166220 l . data . isPathSelected = true ;
167221 l . color = PATH_COLOR ;
168222 }
@@ -171,7 +225,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
171225 canvas . setGraphData ( currentData )
172226
173227 setTimeout ( ( ) => {
174- canvas . zoomToFit ( 2 , ( n : GraphNode ) => p . nodes . some ( node => node . id === n . id ) ) ;
228+ canvas . zoomToFit ( 2 , ( n : GraphNode ) => pNodeIds . has ( n . id ) ) ;
175229 } , 0 )
176230 setChatOpen && setChatOpen ( false )
177231 }
@@ -244,14 +298,24 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
244298 } , { type : MessageTypes . Path } ]
245299
246300 setPath ( undefined )
247- setMessages ( ( prev ) => [ ...RemoveLastPath ( prev ) , { type : MessageTypes . Pending } ] )
301+ let insertIndex = 0
302+ setMessages ( ( prev ) => {
303+ const { messages, insertIndex : idx } = RemoveLastPath ( prev )
304+ insertIndex = idx
305+ const pending : Message = { type : MessageTypes . Pending }
306+ return [ ...messages . slice ( 0 , insertIndex ) , pending , ...messages . slice ( insertIndex ) ]
307+ } )
248308
249309 const result = await fetch ( `/api/repo/${ prepareArg ( repo ) } /${ prepareArg ( String ( path . start . id ) ) } /?targetId=${ prepareArg ( String ( path . end . id ) ) } ` , {
250310 method : 'POST'
251311 } )
252312
253313 if ( ! result . ok ) {
254- setMessages ( ( prev ) => [ ...prev . slice ( 0 , - 1 ) , ...pathMessage ] )
314+ setMessages ( ( prev ) => [
315+ ...prev . slice ( 0 , insertIndex ) ,
316+ ...pathMessage ,
317+ ...prev . slice ( insertIndex + 1 ) ,
318+ ] )
255319 setPath ( { } )
256320 toast ( {
257321 variant : "destructive" ,
@@ -264,7 +328,11 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
264328 const json = await result . json ( )
265329
266330 if ( json . result . paths . length === 0 ) {
267- setMessages ( ( prev ) => [ ...prev . slice ( 0 , - 1 ) , ...pathMessage ] )
331+ setMessages ( ( prev ) => [
332+ ...prev . slice ( 0 , insertIndex ) ,
333+ ...pathMessage ,
334+ ...prev . slice ( insertIndex + 1 ) ,
335+ ] )
268336 setPath ( { } )
269337 toast ( {
270338 title : `No path found` ,
@@ -284,7 +352,11 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
284352 { nodes : [ ] , links : [ ] }
285353 )
286354 setPaths ( formattedPaths )
287- setMessages ( ( prev ) => [ ...prev . slice ( 0 , - 1 ) , { type : MessageTypes . PathResponse , paths : formattedPaths , graphName : graph . Id } ] ) ;
355+ setMessages ( ( prev ) => [
356+ ...prev . slice ( 0 , insertIndex ) ,
357+ { type : MessageTypes . PathResponse , paths : formattedPaths , graphName : graph . Id } ,
358+ ...prev . slice ( insertIndex + 1 ) ,
359+ ] ) ;
288360 setIsPathResponse ( true )
289361
290362 const currentData = canvas . getGraphData ( ) ;
@@ -371,10 +443,11 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set
371443 if ( ! canvas ) return
372444
373445 setSugOpen ( false )
374- setMessages ( prev => [
375- ...RemoveLastPath ( prev ) ,
376- { type : MessageTypes . Query , text : "Create a path" } ,
377- ] )
446+ setMessages ( prev => {
447+ const { messages, insertIndex } = RemoveLastPath ( prev )
448+ const queryMsg : Message = { type : MessageTypes . Query , text : "Create a path" }
449+ return [ ...messages . slice ( 0 , insertIndex ) , queryMsg , ...messages . slice ( insertIndex ) ]
450+ } )
378451
379452 if ( isPathResponse ) {
380453 setIsPathResponse ( false )
0 commit comments