Skip to content

Commit e58cc3b

Browse files
authored
Merge pull request #515 from FalkorDB/fix-ai-comments
Fix ai comments
2 parents 44ae35e + 8746724 commit e58cc3b

14 files changed

Lines changed: 181 additions & 107 deletions

File tree

.github/workflows/playwright.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
npm run build
3939
NEXTAUTH_SECRET=SECRET npm start & npx playwright test --shard=${{ matrix.shard }}/2 --reporter=dot,list
4040
- name: Ensure required directories exist
41+
if: always()
4142
run: |
4243
mkdir -p playwright-report
4344
mkdir -p playwright-report/artifacts

app/components/chat.tsx

Lines changed: 141 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { 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";
33
import Image from "next/image";
44
import { 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";
66
import Input from "./Input";
77
import { Graph, GraphData, Node } from "./model";
88
import { cn, GraphRef } from "@/lib/utils";
@@ -32,7 +32,6 @@ interface Props {
3232
setPaths: Dispatch<SetStateAction<PathData[]>>
3333
}
3434

35-
const PATH_COLOR = "#ffde21";
3635
const 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

5570
export 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)

app/components/code-graph.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ export function CodeGraph({
105105
useEffect(() => {
106106
const handleKeyDown = (event: KeyboardEvent) => {
107107
if (event.key === 'Delete') {
108-
if (selectedObjects.length === 0 && !selectedObj) return
108+
if (selectedObjects.length === 0 && (!selectedObj || "source" in selectedObj)) return
109+
109110
handleRemove([...selectedObjects.map(obj => obj.id), selectedObj?.id].filter(id => id !== undefined), "nodes");
110111
}
111112
};
@@ -174,7 +175,7 @@ export function CodeGraph({
174175

175176
async function handleSelectedValue(value: string) {
176177
setGraphName(value)
177-
onFetchGraph(value)
178+
await onFetchGraph(value)
178179
}
179180

180181
const deleteNeighbors = (nodes: Node[]) => {

app/components/combobox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ interface Props {
66
options: string[]
77
setOptions: (options: string[]) => void
88
selectedValue: string
9-
onSelectedValue: (value: string) => void
9+
onSelectedValue: (value: string) => Promise<void>
1010

1111
}
1212

app/components/dataPanel.tsx

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export default function DataPanel({ obj, setObj, url }: Props) {
7070
>
7171
{value}
7272
</SyntaxHighlighter>
73-
: typeof value === "object" ?
73+
: typeof value === "object" && value !== null ?
7474
<JSONTree
7575
data={Object.fromEntries(Object.entries(value).filter(([k]) => !excludedProperties.includes(k)))}
7676
theme={{
@@ -91,22 +91,22 @@ export default function DataPanel({ obj, setObj, url }: Props) {
9191
base0E: '#ae81ff',
9292
base0F: '#cc6633'
9393
}}
94-
valueRenderer={(valueAsString, value, keyPath) => {
94+
valueRenderer={(_valueAsString, value, keyPath) => {
9595
if (keyPath === "src") {
9696
return <SyntaxHighlighter
9797
language="python"
9898
style={{
99-
...dark,
100-
hljs: {
101-
...dark.hljs,
102-
maxHeight: `9rem`,
103-
background: '#343434',
104-
padding: 2,
105-
}
106-
}}
107-
>
108-
{value as string}
109-
</SyntaxHighlighter>
99+
...dark,
100+
hljs: {
101+
...dark.hljs,
102+
maxHeight: `9rem`,
103+
background: '#343434',
104+
padding: 2,
105+
}
106+
}}
107+
>
108+
{value as string}
109+
</SyntaxHighlighter>
110110
}
111111
return <span className="text-white">{value as string}</span>
112112
}}
@@ -132,18 +132,9 @@ export default function DataPanel({ obj, setObj, url }: Props) {
132132
<a
133133
className="flex items-center gap-2 p-2"
134134
href={url}
135+
rel="noopener noreferrer"
135136
target="_blank"
136137
title="Go to repo"
137-
onClick={() => {
138-
const newTab = window.open(url, '_blank');
139-
140-
if (!obj.data.src_start || !obj.data.src_end || !newTab) return
141-
142-
newTab.scroll({
143-
top: obj.data.src_start,
144-
behavior: 'smooth'
145-
})
146-
}}
147138
>
148139
<SquareArrowOutUpRight color="white" />
149140
Go to repo

0 commit comments

Comments
 (0)