From 5fb6d5107750e1ab6ae0ae8188c459aaeed70ae8 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 30 Sep 2024 16:55:17 +0300 Subject: [PATCH 01/20] commit --- app/components/chat.tsx | 39 ++++++++++------ app/components/code-graph.tsx | 85 ++++++++++++++++------------------- app/globals.css | 37 +++++++-------- app/page.tsx | 15 ++++--- public/falkordb-circle.svg | 21 +++++++++ public/falkordb-white.svg | 18 ++++++++ 6 files changed, 130 insertions(+), 85 deletions(-) create mode 100644 public/falkordb-circle.svg create mode 100644 public/falkordb-white.svg diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 04025737..f7cb87c0 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react"; import { QUESTIONS } from "../api/repo/questions"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import Image from "next/image"; +import { SendHorizonal } from "lucide-react"; enum MessageTypes { Query, @@ -74,6 +75,7 @@ export function Chat(props: { repo: string }) { } return [...messages, { text: data.result, type: MessageTypes.Response }]; }); + setQuery("") }).catch((error) => { setMessages(function (messages) { if (messages[messages.length - 1].type === MessageTypes.Pending) { @@ -82,6 +84,7 @@ export function Chat(props: { repo: string }) { } return messages }); + setQuery("") toast({ variant: "destructive", title: "Uh oh! Something went wrong.", @@ -109,23 +112,29 @@ export function Chat(props: { repo: string }) { return ( <> -
+
+ +

Set your query

+
+
{ messages.map((message, index) => { if (message.type === MessageTypes.Query) { - return (
-
-

{message.text}

+ return (
+
+
+

{message.text}

) } else if (message.type === MessageTypes.Response) { - return (
-
-

{message.text}

+ return (
+
+
+

{message.text}

) } else { - return (
+ return (
Waiting for response
@@ -134,10 +143,10 @@ export function Chat(props: { repo: string }) { }) }
-
+
{props.repo && -
- @@ -148,9 +157,11 @@ export function Chat(props: { repo: string }) { }) } - - - + */} + +
}
diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 35db7e49..28b69fea 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -1,4 +1,3 @@ -import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import CytoscapeComponent from 'react-cytoscapejs' import { useContext, useEffect, useRef, useState } from "react"; @@ -6,12 +5,11 @@ import { Category, Node } from "./model"; import { RESPOSITORIES } from "../api/repo/repositories"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { GraphContext } from "./provider"; - import cytoscape from 'cytoscape'; import fcose from 'cytoscape-fcose'; -import { Skeleton } from "@/components/ui/skeleton"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; +import { GitFork, Search } from "lucide-react"; const LIMITED_MODE = process.env.NEXT_PUBLIC_MODE?.toLowerCase() === 'limited'; @@ -118,11 +116,11 @@ export function CodeGraph(parmas: { onFetchGraph: (url: string) => void, onFetch parmas.onFetchGraph(value) } - const defaultRepo = RESPOSITORIES[0]; - // Fetch the default graph on first render - useEffect(() => { - onRepoSelected(defaultRepo) - }, []); + // const defaultRepo = RESPOSITORIES[0]; + // // Fetch the default graph on first render + // useEffect(() => { + // onRepoSelected(defaultRepo) + // }, []); function onCategoryClick(category: Category) { let chart = chartRef.current @@ -140,37 +138,40 @@ export function CodeGraph(parmas: { onFetchGraph: (url: string) => void, onFetch } return ( - <> -
-
- - { - !LIMITED_MODE && - <> - - - - } -
+
+
+

Knowledge Graph

+
-
+
{graph.Id ? ( <> -
+
- + +
+ { + !LIMITED_MODE && +
+ + +
+ } +
void, onFetch ) : ( -
-
- Loading Repository Graph... -
-
- -
- - -
-
+
+ +

Select a repo to show its graph here

) }
- +
) } \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 6a757250..00f62bf7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,7 +1,7 @@ @tailwind base; @tailwind components; @tailwind utilities; - + @layer base { :root { --background: 0 0% 100%; @@ -9,67 +9,68 @@ --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; - + --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - + --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; - + --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; - + --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - + --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; - + --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; - + --radius: 0.5rem; } - + .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; - + --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; - + --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - + --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; - + --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; - + --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; - + --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; - + --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - + --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } - + @layer base { * { @apply border-border; } + body { @apply bg-background text-foreground; } diff --git a/app/page.tsx b/app/page.tsx index 3981a1cc..95c4b4b7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -9,6 +9,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { CodeGraph } from './components/code-graph'; import { toast } from '@/components/ui/use-toast'; import { GraphContext } from './components/provider'; +import Image from 'next/image'; export default function Home() { @@ -70,28 +71,28 @@ export default function Home() { return (
-
+
- + FalkorDB

Code Graph by FalkorDB

- - + + - - + + diff --git a/public/falkordb-circle.svg b/public/falkordb-circle.svg new file mode 100644 index 00000000..cebff978 --- /dev/null +++ b/public/falkordb-circle.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/falkordb-white.svg b/public/falkordb-white.svg new file mode 100644 index 00000000..1afdce2a --- /dev/null +++ b/public/falkordb-white.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + From b652d8b11d19c13357efd059ac5c0a21c663730c Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 30 Sep 2024 17:02:46 +0300 Subject: [PATCH 02/20] remove header border --- app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index 95c4b4b7..b31b651c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -71,7 +71,7 @@ export default function Home() { return (
-
+
FalkorDB From a80b1742bd249b676f7e0805a6536a0f4f0dc331 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 28 Oct 2024 18:01:28 +0200 Subject: [PATCH 03/20] restyle --- app/api/repo/[graph]/[node]/route.ts | 57 ++- app/api/repo/[graph]/route.ts | 296 -------------- app/api/repo/[graph]/route.tsx | 89 +++++ app/api/repo/graph_ops.ts | 552 -------------------------- app/api/repo/questions.ts | 7 - app/api/repo/repositories.ts | 5 - app/api/repo/route.ts | 332 ++-------------- app/components/Input.tsx | 150 ++++++++ app/components/chat.tsx | 357 ++++++++++++++--- app/components/code-graph.tsx | 555 +++++++++++++++++++++------ app/components/combobox.tsx | 28 ++ app/components/dataPanel.tsx | 74 ++++ app/components/elementMenu.tsx | 92 +++++ app/components/elementTooltip.tsx | 30 ++ app/components/labels.tsx | 42 +- app/components/model.ts | 73 +++- app/components/toolbar.tsx | 54 ++- app/globals.css | 1 + app/layout.tsx | 3 +- app/page.tsx | 245 +++++++++--- components/ui/checkbox.tsx | 30 ++ components/ui/dialog.tsx | 122 ++++++ components/ui/dropdown-menu.tsx | 200 ++++++++++ components/ui/select.tsx | 2 +- package-lock.json | 463 ++++++++++++++++++++++ package.json | 3 + public/color-logo.svg | 23 ++ public/falkordb-white.svg | 18 - 28 files changed, 2396 insertions(+), 1507 deletions(-) delete mode 100644 app/api/repo/[graph]/route.ts create mode 100644 app/api/repo/[graph]/route.tsx delete mode 100644 app/api/repo/graph_ops.ts delete mode 100644 app/api/repo/questions.ts delete mode 100644 app/api/repo/repositories.ts create mode 100644 app/components/Input.tsx create mode 100644 app/components/combobox.tsx create mode 100644 app/components/dataPanel.tsx create mode 100644 app/components/elementMenu.tsx create mode 100644 app/components/elementTooltip.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 public/color-logo.svg delete mode 100644 public/falkordb-white.svg diff --git a/app/api/repo/[graph]/[node]/route.ts b/app/api/repo/[graph]/[node]/route.ts index 277e3945..fa22cfbf 100644 --- a/app/api/repo/[graph]/[node]/route.ts +++ b/app/api/repo/[graph]/[node]/route.ts @@ -1,24 +1,51 @@ -import { FalkorDB, Graph } from "falkordb"; import { NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest, { params }: { params: { graph: string, node: string } }) { - - const nodeId = parseInt(params.node); + + const nodeId = parseInt(params.node); const graphId = params.graph; + try { + + const result = await fetch(`http://localhost:5000/get_neighbors?repo=${graphId}&node_id=${nodeId}`, { + method: 'GET', + }) + + const json = await result.json() + + return NextResponse.json({ result: json }, { status: 200 }) + } catch (err) { + return NextResponse.json({ massage: (err as Error).message }, { status: 400 }) + } +} + +export async function POST(request: NextRequest, { params }: { params: { graph: string, node: string } }) { + + const nodeId = params.node; + const graphId = params.graph; + const targetId = request.nextUrl.searchParams.get('targetId') + + try { - const db = await FalkorDB.connect({url: process.env.FALKORDB_URL || 'falkor://localhost:6379',}); - const graph = db.selectGraph(graphId); + const result = await fetch(`http://localhost:5000/find_paths`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + repo: graphId, + src: Number(nodeId), + dest: Number(targetId!) + }) + }) - // Get node's neighbors - const q_params = {nodeId: nodeId}; - const query = `MATCH (src)-[e]-(n) - WHERE ID(src) = $nodeId - RETURN collect(distinct { label:labels(n)[0], id:ID(n), name: n.name } ) as nodes, - collect( { src: ID(startNode(e)), id: ID(e), dest: ID(endNode(e)), type: type(e) } ) as edges`; + if (!result.ok) { + throw new Error(await result.text()) + } - let res: any = await graph.query(query, { params: q_params }); - let nodes = res.data[0]['nodes']; - let edges = res.data[0]['edges']; + const json = await result.json() - return NextResponse.json({ id: graphId, nodes: nodes, edges: edges }, { status: 200 }) + return NextResponse.json({ result: json }, { status: 200 }) + } catch (err) { + return NextResponse.json({ massage: (err as Error).message }, { status: 200 }) + } } \ No newline at end of file diff --git a/app/api/repo/[graph]/route.ts b/app/api/repo/[graph]/route.ts deleted file mode 100644 index 1fe88b8b..00000000 --- a/app/api/repo/[graph]/route.ts +++ /dev/null @@ -1,296 +0,0 @@ -import OpenAI from "openai"; -import { QUESTIONS } from '../questions'; -import { graphSchema } from "../graph_ops"; -import { FalkorDB, Graph } from 'falkordb'; -import { NextRequest, NextResponse } from "next/server"; -import { ChatCompletionCreateParams, ChatCompletionMessageParam, ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources/chat/completions.mjs'; - -// convert a structured graph schema into a string representation -// used in a model prompt -async function GraphSchemaToPrompt( - graph: Graph, - graphId: string, - db: FalkorDB -) { - // Retrieve graph schema - let schema: any = await graphSchema(graphId, db); - - // Build a string description of graph schema - let desc: string = "The knowladge graph schema is as follows:\n"; - - //------------------------------------------------------------------------- - // Describe labels - //------------------------------------------------------------------------- - - // list labels - desc = desc + "The graph contains the following node labels:\n"; - for (const lbl in schema["labels"]) { - desc = desc + `${lbl}\n`; - } - - // specify attributes associated with each label - for (const lbl in schema["labels"]) { - let node_count = schema["labels"][lbl]['node_count']; - let attributes = schema["labels"][lbl]['attributes']; - let attr_count = Object.keys(attributes).length; - - if (attr_count == 0) { - desc = desc + `the ${lbl} label has ${node_count} nodes and has no attributes\n`; - } else { - desc = desc + `the ${lbl} label has ${node_count} nodes and is associated with the following attribute(s):\n`; - for (const attr in attributes) { - let type = attributes[attr]['type']; - desc = desc + `'${attr}' which is of type ${type}\n`; - } - } - } - - desc = desc + "The graph contains the following relationship types:\n" - - //------------------------------------------------------------------------- - // Describe relationships - //------------------------------------------------------------------------- - - // list relations - for (const relation in schema["relations"]) { - desc = desc + `${relation}\n`; - } - - // specify attributes associated with each relationship - for (const relation in schema["relations"]) { - let connect = schema["relations"][relation]['connect']; - let edge_count = schema["relations"][relation]['edge_count']; - let attributes = schema["relations"][relation]['attributes']; - let attr_count = Object.keys(attributes).length; - - if (attr_count == 0) { - desc = desc + `the ${relation} relationship has ${edge_count} edges and has no attributes\n`; - } else { - desc = desc + `the ${relation} relationship has ${edge_count} edges and is associated with the following attribute(s):\n`; - for (const attr in attributes) { - let type = attributes[attr]['type']; - desc = desc + `'${attr}' which is of type ${type}\n`; - } - } - - if (connect.length > 0) { - desc = desc + `the ${relation} relationship connects the following labels:\n` - for (let i = 0; i < connect.length; i += 2) { - let src = connect[i]; - let dest = connect[i + 1]; - desc = desc + `${src} is connected via ${relation} to ${dest}\n`; - } - } - } - - desc = desc + `This is the end of the knowladge graph schema description.\n` - - //------------------------------------------------------------------------- - // include graph indices - //------------------------------------------------------------------------- - - // vector indices - let query = `CALL db.indexes() YIELD label, properties, types, entitytype`; - let res = await graph.query(query); - - // process indexes - let indexes: any = res.data; - if (indexes.length > 0) { - let index_prompt = "The knowladge graph contains the following indexes:\n" - for (let i = 0; i < indexes.length; i++) { - const index = indexes[i]; - const label: string = index['label']; - const entityType: string = index['entitytype']; - const props = index['properties']; - const types = index['types']; - - for (const prop of props) { - const propTypes: string[] = types[prop]; - for (let j = 0; j < propTypes.length; j++) { - const idxType: string = propTypes[j]; - index_prompt += `${entityType} of type ${label} have a ${idxType} index indexing its ${prop} attribute\n`; - } - } - } - index_prompt += `This is the end of our indexes list - To use a Vector index use the following procedure: - CALL db.idx.vector.queryNodes(
- + ); } diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 28b69fea..2aa0902b 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -1,17 +1,31 @@ -import { Input } from "@/components/ui/input"; import CytoscapeComponent from 'react-cytoscapejs' -import { useContext, useEffect, useRef, useState } from "react"; -import { Category, Node } from "./model"; -import { RESPOSITORIES } from "../api/repo/repositories"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Dispatch, MutableRefObject, SetStateAction, useContext, useEffect, useRef, useState } from "react"; +import { Node } from "./model"; import { GraphContext } from "./provider"; -import cytoscape from 'cytoscape'; +import cytoscape, { ElementDefinition, EventObject, Position } from 'cytoscape'; import fcose from 'cytoscape-fcose'; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; -import { GitFork, Search } from "lucide-react"; +import { ChevronLeft, ChevronRight, GitFork, Search } from "lucide-react"; +import ElementMenu from "./elementMenu"; +import ElementTooltip from "./elementTooltip"; +import Combobox from "./combobox"; +import { toast } from '@/components/ui/use-toast'; +import { cn } from '@/lib/utils'; +import { Path } from '../page'; +import Input from './Input'; -const LIMITED_MODE = process.env.NEXT_PUBLIC_MODE?.toLowerCase() === 'limited'; +interface Props { + onFetchGraph: (graphName: string) => void, + onFetchNode: (node: Node) => Promise, + options: string[] + isShowPath: boolean + setPath: Dispatch> + chartRef: MutableRefObject + selectedValue: string + setSelectedPathId: (selectedPathId: string) => void + isPathResponse: boolean +} // The stylesheet for the graph const STYLESHEET: cytoscape.Stylesheet[] = [ @@ -35,16 +49,19 @@ const STYLESHEET: cytoscape.Stylesheet[] = [ selector: "node", style: { label: "data(name)", + "color": "black", "text-valign": "center", - "text-halign": "center", + "text-wrap": "ellipsis", + "text-max-width": "10rem", shape: "ellipse", - height: 10, - width: 10, - "border-width": 0.15, + height: "15rem", + width: "15rem", + "border-width": 0.3, + "border-color": "black", "border-opacity": 0.5, "background-color": "data(color)", - "font-size": "3", - "overlay-padding": "1px", + "font-size": "3rem", + "overlay-padding": "1rem", }, }, { @@ -53,6 +70,30 @@ const STYLESHEET: cytoscape.Stylesheet[] = [ "overlay-opacity": 0, // hide gray box around active node }, }, + { + selector: "node:selected", + style: { + 'border-width': 0.5, + 'border-color': 'black', + 'border-opacity': 1, + }, + }, + { + selector: "node[?isPath]", + style: { + 'border-width': 0.5, + 'border-color': 'pink', + 'border-opacity': 1, + }, + }, + { + selector: "node[?isPathStartEnd]", + style: { + 'border-width': 1, + 'border-color': 'pink', + 'border-opacity': 1, + }, + }, { selector: "edge", style: { @@ -60,6 +101,7 @@ const STYLESHEET: cytoscape.Stylesheet[] = [ "line-color": "#ccc", "arrow-scale": 0.3, "target-arrow-shape": "triangle", + "target-arrow-color": "#ccc", label: "data(label)", 'curve-style': 'straight', "text-background-color": "#ffffff", @@ -68,67 +110,132 @@ const STYLESHEET: cytoscape.Stylesheet[] = [ "overlay-padding": "2px", }, }, + { + selector: "edge:active", + style: { + "overlay-opacity": 0, // hide gray box around active node + }, + }, + { + selector: "edge[?isPath]", + style: { + "line-style": "dashed", + "line-color": "pink", + "target-arrow-color": "pink", + }, + } ] - cytoscape.use(fcose); -const LAYOUT = { +export const LAYOUT = { name: "fcose", fit: true, - padding: 30, + padding: 80, avoidOverlap: true, } -export function CodeGraph(parmas: { onFetchGraph: (url: string) => void, onFetchNode: (node: Node) => Promise }) { +const COMMIT_LIMIT = 7 - let graph = useContext(GraphContext) +export function CodeGraph({ + onFetchGraph, + onFetchNode, + options, + isShowPath, + setPath, + chartRef, + selectedValue, + setSelectedPathId, + isPathResponse +}: Props) { - // Holds the user input while typing - const [url, setURL] = useState(''); + let graph = useContext(GraphContext) - // A reference to the chart container to allowing zooming and editing - const chartRef = useRef(null) + const [url, setURL] = useState(""); + const [selectedObj, setSelectedObj] = useState(); + const [tooltipLabel, setTooltipLabel] = useState(); + const [position, setPosition] = useState(); + const [tooltipPosition, setTooltipPosition] = useState(); + const [graphName, setGraphName] = useState(""); + const [searchNodeName, setSearchNodeName] = useState(""); + const [commits, setCommits] = useState([]); + const [nodesCount, setNodesCount] = useState(0); + const [edgesCount, setEdgesCount] = useState(0); + const [commitIndex, setCommitIndex] = useState(0); + const [currentCommit, setCurrentCommit] = useState(0); + const containerRef = useRef(null); - // A function that handles the change event of the url input box - async function handleRepoInputChange(event: any) { + async function fetchCount() { + const result = await fetch(`/api/repo/${graphName}`, { + method: 'POST' + }) - // If the user pressed enter, submit the URL - if (event.key === "Enter") { - await handleSubmit(event); + if (!result.ok) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: await result.text(), + }) + return } - // Get the new value of the input box - let value: string = event.target.value; + const json = await result.json() - // Update the url state - setURL(value); + setNodesCount(json.result.info.node_count) + setEdgesCount(json.result.info.edge_count) + setURL(json.result.info.repo_url) } - // A function that handles the click event - async function handleSubmit(event: any) { - event.preventDefault(); - parmas.onFetchGraph(url); - } + useEffect(() => { + if (!selectedValue) return + handelSelectedValue(selectedValue) + }, [selectedValue]) - function onRepoSelected(value: string): void { - setURL(value) - parmas.onFetchGraph(value) - } + useEffect(() => { + if (!graphName) return + + const run = async () => { + fetchCount() + const result = await fetch(`/api/repo/${graphName}/?type=commit`, { + method: 'POST' + }) - // const defaultRepo = RESPOSITORIES[0]; - // // Fetch the default graph on first render - // useEffect(() => { - // onRepoSelected(defaultRepo) - // }, []); + if (!result.ok) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: await result.text(), + }) + return + } - function onCategoryClick(category: Category) { + const json = await result.json() + const commitsArr = json.result.commits + setCommits(commitsArr) + setCurrentCommit(commitsArr[commitsArr.length - 1].hash) + setCommitIndex(commitsArr.length) + } + + run() + }, [graphName]) + + function handelSelectedValue(value: string) { + setGraphName(value) + onFetchGraph(value) + } + + function onCategoryClick(name: string, show: boolean) { let chart = chartRef.current if (chart) { - let elements = chart.elements(`node[category = "${category.name}"]`) - category.show = !category.show + let elements = chart.elements(`node[category = "${name}"]`) + + graph.Categories.forEach((category) => { + if (category.name === name) { + category.show = show + } + }) - if (category.show) { + if (show) { elements.style({ display: 'element' }) } else { elements.style({ display: 'none' }) @@ -137,78 +244,292 @@ export function CodeGraph(parmas: { onFetchGraph: (url: string) => void, onFetch } } + const deleteNeighbors = (node: Node, chart: cytoscape.Core) => { + const neighbors = chart.elements(`#${node.id}`).outgoers() + neighbors.forEach((n) => { + const id = n.id() + const index = graph.Elements.findIndex(e => e.data.id === id); + const element = graph.Elements[index] + + if (index === -1 || !element.data.collapsed) return + + const type = "category" in element.data + + if (element.data.expand) { + deleteNeighbors(element.data, chart) + } + + graph.Elements.splice(index, 1); + + if (type) { + graph.NodesMap.delete(Number(id)) + } else { + graph.EdgesMap.delete(Number(id.split('')[1])) + } + + chart.remove(`#${id}`) + }) + + } + + const handleDoubleTap = async (evt?: EventObject) => { + + const chart = chartRef.current + + if (!chart) return + + let node: Node + let elements: ElementDefinition[] + + if (evt) { + const { target } = evt + target.unselect() + node = target.json().data; + } else { + node = selectedObj! + } + + const graphNode = graph.Elements.find(e => e.data.id === node.id); + + if (!graphNode) return + + if (!graphNode.data.expand) { + elements = await onFetchNode(node) + + if (elements.length === 0) { + toast({ + title: "No neighbors found", + }) + return + } + + chart.add(elements); + chart.elements().layout(LAYOUT).run(); + } else { + deleteNeighbors(node, chart) + } + + graphNode.data.expand = !graphNode.data.expand; + + setSelectedObj(undefined) + } + + const handelTap = (evt: EventObject) => { + const chart = chartRef.current + + if (!chart) return + + const { target } = evt + setTooltipLabel(undefined) + + if (isShowPath) { + setPath(prev => { + if (!prev?.start?.name || (prev.end?.name && prev.end?.name !== "")) { + return ({ start: { id: Number(target.id()), name: target.data().name as string } }) + } else { + return ({ end: { id: Number(target.id()), name: target.data().name as string }, start: prev.start }) + } + }) + return + } + + const position = target.renderedPosition() + setPosition(() => position ? { x: position.x, y: position.y + chart.zoom() * 8 } : { x: 0, y: 0 }); + setSelectedObj(target.json().data) + } + + const handelSearchSubmit = (node: any) => { + const chart = chartRef.current + + if (!chart) return + + let chartNode = chart.elements(`node[name = "${node.properties.name}"]`) + + if (chartNode.length === 0) { + const [newNode] = graph.extend({ nodes: [node], edges: [] }) + chartNode = chart.add(newNode) + } + + chartNode.select() + const layout = { ...LAYOUT, padding: 250 } + chartNode.layout(layout).run() + } + return ( -
+
-

Knowledge Graph

- +
-
- {graph.Id ? - ( - <> -
- - -
- { - !LIMITED_MODE && -
- - -
- } -
-
+
+ +
+ { + graph.Id ? +
+
+ setSearchNodeName(node.name!)} + icon={} + handelSubmit={handelSearchSubmit} + /> + +
+
+
+

{nodesCount} Nodes

+

{edgesCount} Edges

+
+ +
+ + + { + chartRef.current = cy + + // Make sure no previous listeners are attached + cy.removeAllListeners(); + + // Listen to the click event on nodes for expanding the node + cy.on('dbltap', 'node', handleDoubleTap); + + cy.on('mousedown', (evt) => { + setTooltipLabel(undefined) + const { target } = evt + + if (target !== cy && !target.isEdge()) return; + + setSelectedObj(undefined) + }) + + cy.on('mouseout', (evt) => { + const { target } = evt + + if (target === cy || target.isEdge()) { + setTooltipLabel(undefined) + return + } + + setTooltipLabel(undefined) + + if (selectedObj) return - { - chartRef.current = cy - - // Make sure no previous listeners are attached - cy.removeAllListeners(); - - // Listen to the click event on nodes for expanding the node - cy.on('dbltap', 'node', async function (evt) { - var node: Node = evt.target.json().data; - let elements = await parmas.onFetchNode(node); - //cy.add(elements).layout(LAYOUT).run() - - // adjust entire graph. - if (elements.length > 0) { - cy.add(elements); - cy.elements().layout(LAYOUT).run(); - } - }); - }} - stylesheet={STYLESHEET} - elements={graph.Elements} - layout={LAYOUT} - className="w-full h-full" - /> - - ) : - ( -
- -

Select a repo to show its graph here

-
- ) + target.unselect() + }) + + cy.on('scrollzoom', () => { + setSelectedObj(undefined) + setTooltipLabel(undefined) + }); + + cy.on('mouseover', 'node', (evt) => { + const { target } = evt + target.select() + + if (selectedObj) return + + const position = target.renderedPosition() + + setTooltipPosition(() => ({ x: position.x, y: position.y + cy.zoom() * 8 })); + setTooltipLabel(() => target.json().data.name); + }) + + cy.on('tap', 'node', handelTap); + + cy.on('drag', 'node', () => { + setTooltipLabel(undefined) + setSelectedObj(undefined) + }); + + cy.on('tap', 'edge', (evt) => { + const { target } = evt + + if (!isPathResponse) return + + setSelectedPathId(target.id()) + }); + }} + stylesheet={STYLESHEET} + elements={graph.Elements} + layout={LAYOUT} + className="h-full w-full" + /> +
+ :
+ +

Select a repo to show its graph here

+
+ } +
+ { + graph.Id && +
+ +
    + { + commits.slice(commitIndex - COMMIT_LIMIT, commitIndex).map((commit: any) => { + const date = new Date(commit.date * 1000) + const month = `${date.getDate()} ${date.toLocaleString('default', { month: 'long' })}` + const hour = `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}` + return ( +
  • + +
  • + ) + }) + } +
+ +
} -
+
) } \ No newline at end of file diff --git a/app/components/combobox.tsx b/app/components/combobox.tsx new file mode 100644 index 00000000..59020d6c --- /dev/null +++ b/app/components/combobox.tsx @@ -0,0 +1,28 @@ +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface Props { + options: string[] + selectedValue: string + onSelectedValue: (value: string) => void + +} + +export default function Combobox({ options, selectedValue, onSelectedValue }: Props) { + return ( + + ) +} \ No newline at end of file diff --git a/app/components/dataPanel.tsx b/app/components/dataPanel.tsx new file mode 100644 index 00000000..611a4aa8 --- /dev/null +++ b/app/components/dataPanel.tsx @@ -0,0 +1,74 @@ +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +import { Node } from "./model"; +import { ChevronLeft, ChevronRight, Copy, SquareArrowOutUpRight, X } from "lucide-react"; + +interface Props { + obj: Node | undefined; + setObj: Dispatch>; + url: string; +} + +const excludedProperties = [ + "category", + "color", + "expand", + "collapsed", + "isPath", +] + +export default function DataPanel({ obj, setObj, url }: Props) { + + if (!obj) return null; + + const label = `${obj.category}: ${obj.name}` + const object = Object.fromEntries(Object.entries(obj).filter(([k]) => !excludedProperties.includes(k))) + + return ( +
+
+
+

{label.toUpperCase()}

+
+ +
+
+
+                    {JSON.stringify(object, null, 1)
+                        .replace(/({|}|\[|\]|,)/g, match => match === '}' || match === "]" ? `\n${match}` : `${match}\n`).slice(1, -1)
+                    }
+                
+
+ +
+ ) +} \ No newline at end of file diff --git a/app/components/elementMenu.tsx b/app/components/elementMenu.tsx new file mode 100644 index 00000000..7feac1b3 --- /dev/null +++ b/app/components/elementMenu.tsx @@ -0,0 +1,92 @@ +"use client" + +import { useEffect, useState } from "react"; +import { Node } from "./model"; +import { ChevronLeft, ChevronRight, ChevronsLeftRight, ChevronsRightLeft, Copy, Globe, Maximize2 } from "lucide-react"; +import DataPanel from "./dataPanel"; +import { Position } from "cytoscape"; + +interface Props { + obj: Node | undefined; + position: Position | undefined; + url: string; + handelMaximize: () => void; + parentWidth: number; +} + + +export default function ElementMenu({ obj, position, url, handelMaximize, parentWidth }: Props) { + const [currentObj, setCurrentObj] = useState(); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + setCurrentObj(undefined) + }, [obj]) + + if (!obj || !position) return null + + const objURL = obj.category === "File" + ? `${url}/tree/master/${obj.path}/${obj.name}` + : `${url}/tree/master/${obj.path}#L${obj.src_start}-L${obj.src_end + 1}` + + return ( + <> +
{ + if (!ref) return + setContainerWidth(ref.clientWidth) + }} + className="absolute z-10 bg-black rounded-lg shadow-lg flex" + style={{ + left: position.x - containerWidth / 2, + top: position.y + 5 + }} + > + + { + const newTab = window.open(objURL, '_blank'); + + if (!obj.src_start || !obj.src_end || !newTab) return + + newTab.scroll({ + top: obj.src_start, + left: obj.src_end, + behavior: 'smooth' + }) + }} + > + + + + +
+ + + ) +} \ No newline at end of file diff --git a/app/components/elementTooltip.tsx b/app/components/elementTooltip.tsx new file mode 100644 index 00000000..4fcd1390 --- /dev/null +++ b/app/components/elementTooltip.tsx @@ -0,0 +1,30 @@ +import { Position } from "cytoscape"; +import { useState } from "react"; + +interface Props { + label: string | undefined; + position: Position | undefined; + parentWidth: number; +} + +export default function ElementTooltip({ label, position }: Props) { + const [containerWidth, setContainerWidth] = useState(0); + + if (!label || !position) return null + + return ( +
{ + if (!ref) return + setContainerWidth(ref.clientWidth) + }} + className="absolute z-10 bg-white rounded-lg shadow-lg p-3" + style={{ + left: position.x - containerWidth / 2, + top: position.y + }} + > + {label} +
+ ) +} \ No newline at end of file diff --git a/app/components/labels.tsx b/app/components/labels.tsx index 605b538d..716c2a62 100644 --- a/app/components/labels.tsx +++ b/app/components/labels.tsx @@ -1,32 +1,28 @@ -import { Category, getCategoryColorName} from "./model"; +import { Category, getCategoryColorName } from "./model"; import { cn } from "@/lib/utils"; -import { Minus, Plus } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; import { useState } from "react"; -import { Button } from "@/components/ui/button"; -export function Labels(params: { categories: Category[], className?: string, onClick: (category: Category) => void }) { +export function Labels(params: { categories: Category[], className?: string, onClick: (name: string, show: boolean) => void }) { - // fake stae to force reload const [reload, setReload] = useState(false) return ( -
- {params.categories.map((category) => { - return ( -
- -

{category.name}

-
- ) - })} -
+
+ {params.categories.map((category) => +
+ { + params.onClick(category.name, checked as boolean) + setReload(!reload) + }} + checked={category.show} + /> +

{category.name}

+
+ ) + } +
) } \ No newline at end of file diff --git a/app/components/model.ts b/app/components/model.ts index 0afda744..984a633e 100644 --- a/app/components/model.ts +++ b/app/components/model.ts @@ -1,4 +1,5 @@ import twcolors from 'tailwindcss/colors' +import { Path } from '../page' export interface Category { name: string, @@ -11,12 +12,14 @@ export interface Node { name: string, category: string, color: string, + [key: string]: any, } export interface Edge { source: number, target: number, label: string, + [key: string]: any, } const COLORS_ORDER = [ @@ -34,17 +37,23 @@ const COLORS_ORDER = [ "pink", ] -export function getCategoryColorName(index: number): string { - index = index; + private categoriesMap: Map; private nodesMap: Map; private edgesMap: Map; private constructor(id: string, categories: Category[], elements: any[], - categoriesMap: Map, nodesMap: Map, edgesMap: Map) { + categoriesMap: Map, nodesMap: Map, edgesMap: Map) { this.id = id; this.categories = categories; this.elements = elements; @@ -76,25 +85,38 @@ export class Graph { return this.categories; } + get CategoriesMap(): Map { + return this.categoriesMap; + } + get Elements(): any[] { return this.elements; } + get EdgesMap(): Map { + return this.edgesMap; + } + + get NodesMap(): Map { + return this.nodesMap; + } + public static empty(): Graph { - return new Graph("", [], [], new Map(), new Map(), new Map()) + return new Graph("", [], [], new Map(), new Map(), new Map()) } - public static create(results: any): Graph { + public static create(results: any, graphName: string): Graph { let graph = Graph.empty() graph.extend(results) - graph.id = results.id + graph.id = graphName return graph } - public extend(results: any): any[] { + public extend(results: any, collapsed = false, path?: Path): any[] { let newElements: any[] = [] + results.nodes.forEach((nodeData: any) => { - let label = nodeData.label; + let label = nodeData.labels[0]; // check if category already exists in categories let category = this.categoriesMap.get(label) if (!category) { @@ -106,6 +128,11 @@ export class Graph { // check if node already exists in nodes let node = this.nodesMap.get(nodeData.id) if (node) { + node.isPath = !!path + if (path?.start?.id == nodeData.id || path?.end?.id == nodeData.id) { + node.isPathStartEnd = true + } + node.isPath = !!path return } @@ -114,7 +141,16 @@ export class Graph { name: nodeData.name, color: getCategoryColorValue(category.index), category: category.name, + expand: false, + collapsed, + isPath: !!path, + } + if (path?.start?.id == nodeData.id || path?.end?.id == nodeData.id) { + node.isPathStartEnd = true } + Object.entries(nodeData.properties).forEach(([key, value]) => { + node[key] = value + }) this.nodesMap.set(nodeData.id, node) this.elements.push({ data: node }) newElements.push({ data: node }) @@ -123,16 +159,21 @@ export class Graph { results.edges.forEach((edgeData: any) => { let edge = this.edgesMap.get(edgeData.id) if (edge) { + edge.isPath = !!path return } - let sourceId = edgeData.src.toString(); - let destinationId = edgeData.dest.toString() + let sourceId = edgeData.src_node.toString(); + let destinationId = edgeData.dest_node.toString() edge = { + id: `_${edgeData.id}`, source: sourceId, target: destinationId, - label: edgeData.type, + label: edgeData.relation, + expand: false, + collapsed, + isPath: !!path, } this.edgesMap.set(edgeData.id, edge) this.elements.push({ data: edge }) diff --git a/app/components/toolbar.tsx b/app/components/toolbar.tsx index c497039d..7c948797 100644 --- a/app/components/toolbar.tsx +++ b/app/components/toolbar.tsx @@ -1,5 +1,4 @@ -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { CircleDot, XCircle, ZoomIn, ZoomOut } from "lucide-react"; +import { CircleDot, Minus, Plus } from "lucide-react"; import { cn } from "@/lib/utils" export function Toolbar(params: { @@ -16,39 +15,34 @@ export function Toolbar(params: { function handleCenterClick() { let chart = params.chartRef.current if (chart) { - chart.fit() + chart.fit(undefined, 80) chart.center() } } return ( -
- - - handleZoomClick(1.1)}> - - - -

Zoom In

-
-
- - handleZoomClick(0.9)}> - - - -

Zoom Out

-
-
- - - - - -

Center

-
-
-
+
+ + +
) } \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 00f62bf7..e22d9a6b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -63,6 +63,7 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; + --radius: 0.5rem; } } diff --git a/app/layout.tsx b/app/layout.tsx index 6afa69ae..7d2475e9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from 'next/font/google' import './globals.css' import { Toaster } from '@/components/ui/toaster' import GoogleAnalytics from './components/GoogleAnalytics' +import { cn } from '@/lib/utils' const inter = Inter({ subsets: ['latin'] }) @@ -18,7 +19,7 @@ export default function RootLayout({ }) { return ( - + {process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS ? ( diff --git a/app/page.tsx b/app/page.tsx index b31b651c..4ccf65f9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,99 +1,232 @@ 'use client' -import { createContext, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Chat } from './components/chat'; import { Graph, Node } from './components/model'; -import { Github, HomeIcon } from 'lucide-react'; +import { BookOpen, Github, HomeIcon } from 'lucide-react'; import Link from 'next/link'; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { CodeGraph } from './components/code-graph'; import { toast } from '@/components/ui/use-toast'; import { GraphContext } from './components/provider'; import Image from 'next/image'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; + +export type PathNode = { + id?: number + name?: string +} + +export type Path = { + start?: PathNode, + end?: PathNode +} export default function Home() { const [graph, setGraph] = useState(Graph.empty()); + const [selectedValue, setSelectedValue] = useState(""); + const [selectedPathId, setSelectedPathId] = useState(); + const [isPathResponse, setIsPathResponse] = useState(false); + const [createURL, setCreateURL] = useState("https://github.com/FalkorDB/GraphRAG-SDK") + const [createOpen, setCreateOpen] = useState(false) + const [options, setOptions] = useState([]); + const [path, setPath] = useState(); + const chartRef = useRef(null) + + useEffect(() => { + const run = async () => { + const result = await fetch(`/api/repo`, { + method: 'GET', + }) - function onFetchGraph(url: string) { - let value = url; - if (!value || value.length === 0) { - value = 'https://github.com/falkorDB/falkordb-py'; + if (!result.ok) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: await result.text(), + }) + return + } + + const json = await result.json() + setOptions(json.result) } - setGraph(Graph.empty()) + run() + }, []) - // Send the user query to the server to fetch a repo graph - fetch('/api/repo', { + async function onCreateRepo(e: React.FormEvent) { + e.preventDefault() + + if (!createURL) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: "Please enter a URL.", + }) + return + } + + const result = await fetch(`/api/repo/?url=${createURL}`, { method: 'POST', - body: JSON.stringify({ - url: value + }) + + if (!result.ok) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: await result.text(), }) - }).then(async (result) => { - if (result.status >= 300) { - throw Error(await result.text()) - } - return result.json() - }).then(data => { - let graph = Graph.create(data); - setGraph(graph); - }).catch((error) => { + return + } + + const graphName = createURL.split('/').pop()! + + setOptions(prev => [...prev, graphName]) + setSelectedValue(graphName) + setCreateURL("") + setCreateOpen(false) + + toast({ + title: "Success", + description: `Project ${graphName} created successfully`, + }) + } + + async function onFetchGraph(graphName: string) { + + setGraph(Graph.empty()) + + const result = await fetch(`/api/repo/${graphName}`, { + method: 'GET' + }) + + if (!result.ok) { toast({ variant: "destructive", title: "Uh oh! Something went wrong.", - description: error.message, - }); - }); + description: await result.text(), + }) + return + } + + const json = await result.json() + setGraph(Graph.create(json.result.entities, graphName)) } // Send the user query to the server to expand a node async function onFetchNode(node: Node) { - return fetch(`/api/repo/${graph.Id}/${node.id}`, { + const result = await fetch(`/api/repo/${graph.Id}/${node.id}`, { method: 'GET' - }).then(async (result) => { - if (result.status >= 300) { - throw Error(await result.text()) - } - return result.json() - }).then(data => { - let newElements = graph.extend(data) - setGraph(graph) - return newElements - }).catch((error) => { + }) + + if (!result.ok) { toast({ variant: "destructive", title: "Uh oh! Something went wrong.", - description: error.message, + description: await result.text(), }) - return [] as any[] - }) + return [] + } + + const json = await result.json() + + return graph.extend(json.result.neighbors, true) } return (
-
- - FalkorDB - -

- Code Graph by FalkorDB -

- +
+
+
+ + FalkorDB + +

+ CODE GRAPH BY FALKORDB +

+
+
    + + +

    Home

    + + + +

    Github

    + + + +

    Tip

    + + + + + + + + CREATE A NEW PROJECT + + + Please provide the URL of the model to connect and start querying data + +
    + setCreateURL(e.target.value)} + placeholder="Type URL" + /> +
    + +
    +
    +
    +
    +
+
+
- - - + + - + - - - + + +
diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx new file mode 100644 index 00000000..df61a138 --- /dev/null +++ b/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 00000000..01ff19c7 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..6373043b --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/select.tsx b/components/ui/select.tsx index cbe5a36b..de308396 100644 --- a/components/ui/select.tsx +++ b/components/ui/select.tsx @@ -26,7 +26,7 @@ const SelectTrigger = React.forwardRef< > {children} - + )) diff --git a/package-lock.json b/package-lock.json index 5bd6d4fd..46ee6bfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "code-graph", "version": "0.1.0", "dependencies": { + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", @@ -513,6 +516,72 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz", + "integrity": "sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", @@ -569,6 +638,165 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", + "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.6.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -611,6 +839,48 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz", + "integrity": "sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.2", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", @@ -669,6 +939,169 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz", + "integrity": "sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.6.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", @@ -772,6 +1205,36 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz", diff --git a/package.json b/package.json index 8b9c360d..7d85c1bb 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", diff --git a/public/color-logo.svg b/public/color-logo.svg new file mode 100644 index 00000000..3632ebf0 --- /dev/null +++ b/public/color-logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/falkordb-white.svg b/public/falkordb-white.svg deleted file mode 100644 index 1afdce2a..00000000 --- a/public/falkordb-white.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - From ec11a2a357809b6b6b56e68507eab8fc40804bbe Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 29 Oct 2024 15:54:19 +0200 Subject: [PATCH 04/20] add hover card and fix show path and search --- app/api/chat/[graph]/route.ts | 27 ++++++++ app/components/Input.tsx | 17 +++-- app/components/chat.tsx | 86 +++++++++++++------------ app/components/code-graph.tsx | 82 +++++++++++------------- components/ui/hover-card.tsx | 29 +++++++++ package-lock.json | 117 ++++++++++++++++++++++++++++++++++ package.json | 1 + 7 files changed, 267 insertions(+), 92 deletions(-) create mode 100644 app/api/chat/[graph]/route.ts create mode 100644 components/ui/hover-card.tsx diff --git a/app/api/chat/[graph]/route.ts b/app/api/chat/[graph]/route.ts new file mode 100644 index 00000000..a69ba2ad --- /dev/null +++ b/app/api/chat/[graph]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server" + +export async function POST(request: NextRequest, { params }: { params: { graph: string } }) { + + const graphName = params.graph + const msg = request.nextUrl.searchParams.get('msg') + + try { + const result = await fetch(`http://localhost:5000/chat`, { + method: 'POST', + body: JSON.stringify({ repo: graphName, msg}), + headers: { + "Content-Type": 'application/json' + } + }) + + if (!result.ok) { + throw new Error(await result.text()) + } + + const json = await result.json() + + return NextResponse.json({ result: json }, { status: 200 }) + } catch (err) { + return NextResponse.json({ message: (err as Error).message }, { status: 400 }) + } +} \ No newline at end of file diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 314a1c16..bfe03c3b 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -11,9 +11,10 @@ interface Props extends React.InputHTMLAttributes { handelSubmit?: (node: any) => void icon?: React.ReactNode node?: PathNode + parentClassName?: string } -export default function Input({ value, onValueChange, handelSubmit, graph, icon, node, ...props }: Props) { +export default function Input({ value, onValueChange, handelSubmit, graph, icon, node, className, parentClassName, ...props }: Props) { const [open, setOpen] = useState(false) const [options, setOptions] = useState([]) @@ -60,8 +61,9 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, case "Enter": { e.preventDefault() const option = options.find((o, i) => i === selectedOption) - onValueChange(option) + onValueChange({ name: option.properties.name, id: option.id }) handelSubmit && handelSubmit(option) + setOpen(false) return } case "ArrowUp": { @@ -78,7 +80,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, case "Space": { if (e.ctrlKey) { e.preventDefault() - setOpen(prev => true) + setOpen(true) } return } @@ -91,12 +93,12 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, return (
{ const newVal = e.target.value @@ -107,7 +109,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, { open &&
setSelectedOption(index)} onMouseLeave={() => setSelectedOption(-1)} onClick={() => { - onValueChange(option) + onValueChange({ name: option.properties.name, id: option.id }) handelSubmit && handelSubmit(option) + setOpen(false) }} key={option.id} > diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 16d044c0..06b9d9d7 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -123,50 +123,35 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set title: "Uh oh! Something went wrong.", description: "Please enter a question.", }) - setQuery("") return } - setMessages((messages) => [...messages, { text: q, type: MessageTypes.Query }, { text: "", type: MessageTypes.Pending }]); + setQuery("") - return fetch(`/api/repo/${repo}?q=${encodeURIComponent(q)}&type=text`, { - method: 'GET' - }).then(async (result) => { - if (result.status >= 300) { - throw Error(await result.text()) + setMessages((messages) => [...messages, { text: q, type: MessageTypes.Query }, { text: "", type: MessageTypes.Pending }]); - } + const result = await fetch(`/api/chat/${repo}?msg=${encodeURIComponent(q)}`, { + method: 'POST' + }) - return result.json() - }).then(data => { - // Create an array of messages from the current messages remove the last pending message and add the new response - setMessages(function (messages) { - if (messages[messages.length - 1].type === MessageTypes.Pending) { - // Remove the last pending message if exists - messages = messages.slice(0, -1); - } - return [...messages, { text: data.result, type: MessageTypes.Response }]; - }); - setQuery("") - }).catch((error) => { - setMessages(function (messages) { - if (messages[messages.length - 1].type === MessageTypes.Pending) { - // Remove the last pending message if exists - return messages.slice(0, -1); - } - return messages - }); - setQuery("") - toast({ - variant: "destructive", - title: "Uh oh! Something went wrong.", - description: error.message, + if (!result.ok) { + setMessages((prev) => { + prev = [...prev.slice(0, -1)]; + return [...prev, { type: MessageTypes.Response, text: "Sorry but I couldn't answer your question, please try rephrasing." }]; }); + return + } + + const json = await result.json() + + setMessages((prev) => { + prev = prev.slice(0, -1); + return [...prev, { text: json.result.response, type: MessageTypes.Response }]; }); } // A function that handles the click event - async function handleQueryClick(event: any) { + const handleQueryClick = async (event: any) => { event.preventDefault(); return sendQuery(query.trim()); } @@ -200,12 +185,36 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set const addedElements: ElementDefinition[] = [] formattedPaths.forEach((p: any) => addedElements.push(...graph.extend(p, false, path))) formattedPaths.forEach(p => p.edges.forEach(e => e.id = `_${e.id}`)) - console.log(formattedPaths); + console.log(formattedPaths.flatMap(p => p.edges.map(e => e.id))); + chart.add(addedElements) graph.Elements.forEach((element: any) => { const { id } = element.data - if (!formattedPaths.some((p: any) => [...p.nodes, ...p.edges].some((el: any) => el.id == id))) { - const e = chart.elements().filter(el => el.id() == id) + const e = chart.elements().filter(el => el.id() == id) + console.log(id); + if (id == path.start?.id || id == path.end?.id) { + e.style({ + 'border-width': 1, + 'border-color': 'pink', + 'border-opacity': 1, + }); + } else if (formattedPaths.some((p: any) => [...p.nodes, ...p.edges].some((el: any) => el.id == id))) { + if (e.isNode()) { + e.style({ + 'border-width': 0.5, + 'border-color': 'pink', + 'border-opacity': 1, + }); + } + if (e.isEdge()) { + e.style({ + "line-style": "dashed", + "line-color": "pink", + "target-arrow-color": "pink", + "opacity": 1 + }); + } + } else { if (e.isNode()) { e.style({ "color": "gray", @@ -223,10 +232,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set } } }) - chart.add(addedElements) - console.log(formattedPaths.flatMap(p => [...p.nodes, ...p.edges].map(e => e.id))); const elements = chart.elements().filter((element) => { - console.log(element.id()); return formattedPaths.some(p => [...p.nodes, ...p.edges].some((node) => node.id == element.id())) }); elements.layout(LAYOUT).run() @@ -263,6 +269,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set return (
setPath(prev => ({ start: { name, id }, end: prev?.end }))} value={path?.start?.name} @@ -272,6 +279,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set node={path?.start} /> setPath(prev => ({ end: { name, id }, start: prev?.start }))} diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 2aa0902b..dcbc3837 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -14,6 +14,7 @@ import { toast } from '@/components/ui/use-toast'; import { cn } from '@/lib/utils'; import { Path } from '../page'; import Input from './Input'; +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; interface Props { onFetchGraph: (graphName: string) => void, @@ -78,22 +79,6 @@ const STYLESHEET: cytoscape.Stylesheet[] = [ 'border-opacity': 1, }, }, - { - selector: "node[?isPath]", - style: { - 'border-width': 0.5, - 'border-color': 'pink', - 'border-opacity': 1, - }, - }, - { - selector: "node[?isPathStartEnd]", - style: { - 'border-width': 1, - 'border-color': 'pink', - 'border-opacity': 1, - }, - }, { selector: "edge", style: { @@ -115,14 +100,6 @@ const STYLESHEET: cytoscape.Stylesheet[] = [ style: { "overlay-opacity": 0, // hide gray box around active node }, - }, - { - selector: "edge[?isPath]", - style: { - "line-style": "dashed", - "line-color": "pink", - "target-arrow-color": "pink", - }, } ] @@ -264,7 +241,7 @@ export function CodeGraph({ if (type) { graph.NodesMap.delete(Number(id)) } else { - graph.EdgesMap.delete(Number(id.split('')[1])) + graph.EdgesMap.delete(Number(id.split('_')[1])) } chart.remove(`#${id}`) @@ -295,7 +272,7 @@ export function CodeGraph({ if (!graphNode.data.expand) { elements = await onFetchNode(node) - + console.log(elements); if (elements.length === 0) { toast({ title: "No neighbors found", @@ -304,14 +281,13 @@ export function CodeGraph({ } chart.add(elements); - chart.elements().layout(LAYOUT).run(); } else { deleteNeighbors(node, chart) } graphNode.data.expand = !graphNode.data.expand; - setSelectedObj(undefined) + chart.elements().layout(LAYOUT).run(); } const handelTap = (evt: EventObject) => { @@ -481,49 +457,63 @@ export function CodeGraph({ graph.Id &&
-
    +
      { commits.slice(commitIndex - COMMIT_LIMIT, commitIndex).map((commit: any) => { const date = new Date(commit.date * 1000) const month = `${date.getDate()} ${date.toLocaleString('default', { month: 'long' })}` const hour = `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}` return ( -
    • - -
    • + +
    • + +
    • +
      + +
      +

      {commit.author}:

      +

      {commit.message}

      +

      {commit.hash}

      +
      +
      + ) }) }
    diff --git a/components/ui/hover-card.tsx b/components/ui/hover-card.tsx new file mode 100644 index 00000000..e54d91cf --- /dev/null +++ b/components/ui/hover-card.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/package-lock.json b/package-lock.json index 46ee6bfb..9cc07005 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", @@ -921,6 +922,122 @@ } } }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.2.tgz", + "integrity": "sha512-Y5w0qGhysvmqsIy6nQxaPa6mXNKznfoGjOfBgzOjocLxr2XlSjqBMYQQL+FfyogsMuX+m8cZyQGYhJxvxUzO4w==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", diff --git a/package.json b/package.json index 7d85c1bb..cf38f37d 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", From ecbef2a238031e8b479f70c3c2ae3b5d22b089dc Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Fri, 1 Nov 2024 06:43:48 -0700 Subject: [PATCH 05/20] Fix Typo in Chat Component --- app/components/chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 06b9d9d7..ba1796b2 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -322,7 +322,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set >
    -

    Show unreadable code

    +

    Show unreachable code

    Remove it if unnecessary or fix logic issues.

    From e7c8d76a7cbecb893f2f7e04490aa43e1433b0bd Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 5 Nov 2024 14:25:07 +0200 Subject: [PATCH 06/20] replace localhost in ip --- app/api/chat/[graph]/route.ts | 2 +- app/api/repo/[graph]/[node]/route.ts | 4 +- app/api/repo/[graph]/route.tsx | 171 ++++++++++++++++++++++++++- app/api/repo/route.ts | 4 +- 4 files changed, 171 insertions(+), 10 deletions(-) diff --git a/app/api/chat/[graph]/route.ts b/app/api/chat/[graph]/route.ts index a69ba2ad..6431fe56 100644 --- a/app/api/chat/[graph]/route.ts +++ b/app/api/chat/[graph]/route.ts @@ -6,7 +6,7 @@ export async function POST(request: NextRequest, { params }: { params: { graph: const msg = request.nextUrl.searchParams.get('msg') try { - const result = await fetch(`http://localhost:5000/chat`, { + const result = await fetch(`http://127.0.0.1:5000/chat`, { method: 'POST', body: JSON.stringify({ repo: graphName, msg}), headers: { diff --git a/app/api/repo/[graph]/[node]/route.ts b/app/api/repo/[graph]/[node]/route.ts index fa22cfbf..9c18d64e 100644 --- a/app/api/repo/[graph]/[node]/route.ts +++ b/app/api/repo/[graph]/[node]/route.ts @@ -6,7 +6,7 @@ export async function GET(request: NextRequest, { params }: { params: { graph: s const graphId = params.graph; try { - const result = await fetch(`http://localhost:5000/get_neighbors?repo=${graphId}&node_id=${nodeId}`, { + const result = await fetch(`http://127.0.0.1:5000/get_neighbors?repo=${graphId}&node_id=${nodeId}`, { method: 'GET', }) @@ -26,7 +26,7 @@ export async function POST(request: NextRequest, { params }: { params: { graph: try { - const result = await fetch(`http://localhost:5000/find_paths`, { + const result = await fetch(`http://127.0.0.1:5000/find_paths`, { method: 'POST', headers: { 'Content-Type': 'application/json' diff --git a/app/api/repo/[graph]/route.tsx b/app/api/repo/[graph]/route.tsx index b6487228..ebab0af4 100644 --- a/app/api/repo/[graph]/route.tsx +++ b/app/api/repo/[graph]/route.tsx @@ -5,7 +5,7 @@ export async function GET(request: NextRequest, { params }: { params: { graph: s const graphName = params.graph try { - const result = await fetch(`http://localhost:5000/graph_entities?repo=${graphName}`, { + const result = await fetch(`http://127.0.0.1:5000/graph_entities?repo=${graphName}`, { method: 'GET', }) @@ -29,7 +29,7 @@ export async function POST(request: NextRequest, { params }: { params: { graph: try { switch (type) { case "commit": { - const result = await fetch(`http://localhost:5000/list_commits`, { + const result = await fetch(`http://127.0.0.1:5000/list_commits`, { method: 'POST', body: JSON.stringify({ repo: graphName }), headers: { @@ -45,11 +45,172 @@ export async function POST(request: NextRequest, { params }: { params: { graph: return NextResponse.json({ result: json }, { status: 200 }) } + case "switchCommit": { + return NextResponse.json({ + result: { + deletions: { + 'nodes': [ + { + "alias": "", + "id": 2, + "labels": [ + "Function" + ], + "properties": { + "args": [ + [ + "cls", + "Unknown" + ] + ], + "name": "setUpClass", + "path": "tests/test_kg_gemini.py", + "src": "def setUpClass(cls):\n\n cls.ontology = Ontology([], [])\n\n cls.ontology.add_entity(\n Entity(\n label=\"Actor\",\n attributes=[\n Attribute(\n name=\"name\",\n attr_type=AttributeType.STRING,\n unique=True,\n required=True,\n ),\n ],\n )\n )\n cls.ontology.add_entity(\n Entity(\n label=\"Movie\",\n attributes=[\n Attribute(\n name=\"title\",\n attr_type=AttributeType.STRING,\n unique=True,\n required=True,\n ),\n ],\n )\n )\n cls.ontology.add_relation(\n Relation(\n label=\"ACTED_IN\",\n source=\"Actor\",\n target=\"Movie\",\n attributes=[\n Attribute(\n name=\"role\",\n attr_type=AttributeType.STRING,\n unique=False,\n required=False,\n ),\n ],\n )\n )\n\n cls.graph_name = \"IMDB_gemini\"\n\n model = GeminiGenerativeModel(model_name=\"gemini-1.5-flash-001\")\n cls.kg = KnowledgeGraph(\n name=cls.graph_name,\n ontology=cls.ontology,\n model_config=KnowledgeGraphModelConfig.with_model(model),\n )", + "src_end": 82, + "src_start": 29 + } + }, + { + "alias": "", + "id": 13, + "labels": [ + "Function" + ], + "properties": { + "args": [ + [ + "self", + "Unknown" + ], + [ + "restaurants_kg", + "KnowledgeGraph" + ], + [ + "attractions_kg", + "KnowledgeGraph" + ] + ], + "name": "import_data", + "path": "tests/test_multi_agent.py", + "src": "def import_data(\n self,\n restaurants_kg: KnowledgeGraph,\n attractions_kg: KnowledgeGraph,\n ):\n with open(\"tests/data/cities.json\") as f:\n cities = loads(f.read())\n with open(\"tests/data/restaurants.json\") as f:\n restaurants = loads(f.read())\n with open(\"tests/data/attractions.json\") as f:\n attractions = loads(f.read())\n\n for city in cities:\n restaurants_kg.add_node(\n \"City\",\n {\n \"name\": city[\"name\"],\n \"weather\": city[\"weather\"],\n \"population\": city[\"population\"],\n },\n )\n restaurants_kg.add_node(\"Country\", {\"name\": city[\"country\"]})\n restaurants_kg.add_edge(\n \"IN_COUNTRY\",\n \"City\",\n \"Country\",\n {\"name\": city[\"name\"]},\n {\"name\": city[\"country\"]},\n )\n\n attractions_kg.add_node(\n \"City\",\n {\n \"name\": city[\"name\"],\n \"weather\": city[\"weather\"],\n \"population\": city[\"population\"],\n },\n )\n attractions_kg.add_node(\"Country\", {\"name\": city[\"country\"]})\n attractions_kg.add_edge(\n \"IN_COUNTRY\",\n \"City\",\n \"Country\",\n {\"name\": city[\"name\"]},\n {\"name\": city[\"country\"]},\n )\n\n for restaurant in restaurants:\n restaurants_kg.add_node(\n \"Restaurant\",\n {\n \"name\": restaurant[\"name\"],\n \"description\": restaurant[\"description\"],\n \"rating\": restaurant[\"rating\"],\n \"food_type\": restaurant[\"food_type\"],\n },\n )\n restaurants_kg.add_edge(\n \"IN_CITY\",\n \"Restaurant\",\n \"City\",\n {\"name\": restaurant[\"name\"]},\n {\"name\": restaurant[\"city\"]},\n )\n\n for attraction in attractions:\n attractions_kg.add_node(\n \"Attraction\",\n {\n \"name\": attraction[\"name\"],\n \"description\": attraction[\"description\"],\n \"type\": attraction[\"type\"],\n },\n )\n attractions_kg.add_edge(\n \"IN_CITY\",\n \"Attraction\",\n \"City\",\n {\"name\": attraction[\"name\"]},\n {\"name\": attraction[\"city\"]},\n )", + "src_end": 310, + "src_start": 230 + } + }, + ], + 'edges': [ + { + "alias": "", + "dest_node": 13, + "id": 460, + "properties": {}, + "relation": "CALLS", + "src_node": 2 + }, + ] + }, + additions: { + 'nodes': [ + { + "alias": "", + "id": 13, + "labels": [ + "File" + ], + "properties": { + "ext": ".py", + "name": "test_kg_gemini.py", + "path": "tests" + } + }, + { + "alias": "", + "id": 2, + "labels": [ + "Class" + ], + "properties": { + "doc": "\"\"\"\n Test Knowledge Graph\n \"\"\"", + "name": "TestKGGemini", + "path": "tests/test_kg_gemini.py", + "src_end": 106, + "src_start": 23 + } + }, + ], + 'edges': [ + { + "alias": "", + "dest_node": 13, + "id": 460, + "properties": {}, + "relation": "DEFINES", + "src_node": 2 + }, + ], + }, + modifications: { + 'nodes': [ + { + "alias": "", + "id": 3, + "labels": [ + "Function" + ], + "properties": { + "args": [ + [] + ], + "name": "", + "path": "", + "src": "", + "src_end": 0, + "src_start": 0 + } + }, + { + "alias": "", + "id": 61, + "labels": [ + "Function" + ], + "properties": { + "args": [ + [], + [] + ], + "doc": "", + "name": "", + "path": "", + "ret_type": "", + "src": "", + "src_end": 0, + "src_start": 0 + } + }, + ], + 'edges': [ + { + "alias": "", + "dest_node": 61, + "id": 439, + "properties": { + name: "Source" + }, + "relation": "CALLS", + "src_node": 3 + }, + ] + } + } + }, { status: 200 }) + } case "autoComplete": { const prefix = request.nextUrl.searchParams.get('prefix')! - const result = await fetch(`http://localhost:5000/auto_complete`, { + const result = await fetch(`http://127.0.0.1:5000/auto_complete`, { method: 'POST', - body: JSON.stringify({ repo: graphName, prefix}), + body: JSON.stringify({ repo: graphName, prefix }), headers: { "Content-Type": 'application/json' } @@ -64,7 +225,7 @@ export async function POST(request: NextRequest, { params }: { params: { graph: return NextResponse.json({ result: json }, { status: 200 }) } default: { - const result = await fetch(`http://localhost:5000/repo_info`, { + const result = await fetch(`http://127.0.0.1:5000/repo_info`, { method: 'POST', body: JSON.stringify({ repo: graphName }), headers: { diff --git a/app/api/repo/route.ts b/app/api/repo/route.ts index 1544626d..83f339e4 100644 --- a/app/api/repo/route.ts +++ b/app/api/repo/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; export async function GET() { try { - const result = await fetch(`http://localhost:5000/list_repos`, { + const result = await fetch(`http://127.0.0.1:5000/list_repos`, { method: 'GET', }) @@ -23,7 +23,7 @@ export async function POST(request: NextRequest) { const url = request.nextUrl.searchParams.get('url'); try { - const result = await fetch(`http://localhost:5000/process_repo`, { + const result = await fetch(`http://127.0.0.1:5000/process_repo`, { method: 'POST', body: JSON.stringify({ repo_url: url, ignore: ["./.github", "./sbin", "./.git", "./deps", "./bin", "./build"] }), headers: { From fbe93b20501069f7c0b8636118af3cb5f2b1f408 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 5 Nov 2024 14:34:46 +0200 Subject: [PATCH 07/20] improve styling and add multi paths messages --- .env.local.template | 6 - app/components/Input.tsx | 41 ++++-- app/components/chat.tsx | 237 +++++++++++++++++++++++---------- app/components/code-graph.tsx | 140 +++++++++---------- app/components/commitList.tsx | 144 ++++++++++++++++++++ app/components/dataPanel.tsx | 32 +++-- app/components/elementMenu.tsx | 24 ++-- app/components/model.ts | 2 +- app/globals.css | 31 +++++ app/page.tsx | 32 ++--- public/color-logo.svg | 23 ---- public/logo_02.svg | 11 ++ 12 files changed, 484 insertions(+), 239 deletions(-) delete mode 100644 .env.local.template create mode 100644 app/components/commitList.tsx delete mode 100644 public/color-logo.svg create mode 100644 public/logo_02.svg diff --git a/.env.local.template b/.env.local.template deleted file mode 100644 index ab2504cc..00000000 --- a/.env.local.template +++ /dev/null @@ -1,6 +0,0 @@ -NEXT_PUBLIC_MODE=UNLIMITED # Optional values LIMITED/UNLIMITED - -FALKORDB_URL=falkordb://localhost:6379 # FALKORDB_URL - -OPENAI_API_KEY=[API_KEY] # Set you openai api key here - diff --git a/app/components/Input.tsx b/app/components/Input.tsx index bfe03c3b..2e3282a7 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -1,8 +1,11 @@ import { toast } from "@/components/ui/use-toast" -import { getCategoryColorName, Graph } from "./model" +import { getCategoryColorName, getCategoryColorValue, Graph } from "./model" import { useEffect, useRef, useState } from "react" import { PathNode } from "../page" import { cn } from "@/lib/utils" +import twcolors from 'tailwindcss/colors' + +let colors = twcolors as any interface Props extends React.InputHTMLAttributes { value?: string @@ -12,9 +15,10 @@ interface Props extends React.InputHTMLAttributes { icon?: React.ReactNode node?: PathNode parentClassName?: string + scrollToBottom?: () => void } -export default function Input({ value, onValueChange, handelSubmit, graph, icon, node, className, parentClassName, ...props }: Props) { +export default function Input({ value, onValueChange, handelSubmit, graph, icon, node, className, parentClassName, scrollToBottom, ...props }: Props) { const [open, setOpen] = useState(false) const [options, setOptions] = useState([]) @@ -23,6 +27,10 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, useEffect(() => { setSelectedOption(0) + + if (open) { + scrollToBottom && scrollToBottom() + } }, [open]) useEffect(() => { @@ -48,9 +56,6 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, const { completions } = json.result setOptions(completions) setOpen(true) - setTimeout(() => { - inputRef?.current?.focus(); - }, 0); }, 500) return () => clearTimeout(timeout) @@ -60,7 +65,9 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, switch (e.code) { case "Enter": { e.preventDefault() + if (!open) return const option = options.find((o, i) => i === selectedOption) + if (!option) return onValueChange({ name: option.properties.name, id: option.id }) handelSubmit && handelSubmit(option) setOpen(false) @@ -93,7 +100,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, return (
    setOpen(false)} {...props} /> { open &&
    { const label = option.labels[0] - const color = getCategoryColorName(graph.CategoriesMap.get(label)?.index) + const name = option.properties.name + const path = option.properties.path + const colorName = getCategoryColorName(graph.CategoriesMap.get(label)?.index) return ( ) }) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index ba1796b2..f37f8254 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,12 +1,11 @@ import { toast } from "@/components/ui/use-toast"; -import { Dispatch, MutableRefObject, SetStateAction, use, useEffect, useRef, useState } from "react"; +import { Dispatch, MutableRefObject, SetStateAction, useEffect, useRef, useState } from "react"; import Image from "next/image"; -import { AlignLeft, ArrowRight, ChevronDown, Lightbulb, Redo2, SendHorizonal, Undo2 } from "lucide-react"; +import { AlignLeft, ArrowRight, ChevronDown, Lightbulb, Undo2 } from "lucide-react"; import { Path } from "../page"; import Input from "./Input"; import { Graph } from "./model"; import { cn } from "@/lib/utils"; -import { ElementDefinition } from "cytoscape"; import { LAYOUT } from "./code-graph"; enum MessageTypes { @@ -22,6 +21,7 @@ enum MessageTypes { interface Message { type: MessageTypes; text?: string; + paths?: { nodes: any[], edges: any[] }[]; } interface Props { @@ -31,13 +31,25 @@ interface Props { graph: Graph chartRef: MutableRefObject selectedPathId: string | undefined - setIsPathResponse: (isPathResponse: boolean) => void + isPath: boolean + setIsPath: (isPathResponse: boolean) => void } -export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, setIsPathResponse }: Props) { +const RemoveLastPath = (messages: Message[]) => { + const index = messages.findIndex((m) => m.type === MessageTypes.Path) + + if (index !== -1) { + messages = [...messages.slice(0, index - 2), ...messages.slice(index + 1)]; + messages = RemoveLastPath(messages) + } + + return messages +} + +export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isPath, setIsPath }: Props) { // Holds the messages in the chat - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([{ type: MessageTypes.Tip }]); // Holds the messages in the chat const [paths, setPaths] = useState<{ nodes: any[], edges: any[] }[]>([]); @@ -47,58 +59,124 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set // Holds the user input while typing const [query, setQuery] = useState(''); + const [isPathResponse, setIsPathResponse] = useState(false); + // A reference to the chat container to allow scrolling to the bottom const containerRef: React.RefObject = useRef(null); useEffect(() => { - const path = paths.find((p) => [...p.edges, ...p.nodes].some((e: any) => e.id === selectedPathId)) + const p = paths.find((path) => [...path.edges, ...path.nodes].some((e: any) => e.id === selectedPathId)) - if (!path) return + if (!p) return - handelSetSelectedPath(path) + handelSetSelectedPath(p) }, [selectedPathId]) useEffect(() => { handelSubmit() }, [path]) + useEffect(() => { + if (isPath) return + setIsPathResponse(false) + setSelectedPath(undefined) + setPaths([]) + }, [isPath]) + + useEffect(() => { + setIsPath(isPathResponse) + }, [isPathResponse]) + const handelSetSelectedPath = (p: { nodes: any[], edges: any[] }) => { const chart = chartRef.current if (!chart) return setSelectedPath(prev => { - prev?.edges.forEach(element => { - const e = chart.elements().filter(el => el.id() == element.id) - if (!p.edges.some(e => e.id === element.id)) { - e.style({ - width: 0.5, - "line-style": "dashed", - "line-color": "pink", - "arrow-scale": 0.3, - "target-arrow-color": "pink", + if (prev) { + if (isPathResponse && paths.some((path) => [...path.nodes, ...path.edges].every((e: any) => [...prev.nodes, ...prev.edges].some((el: any) => el.id === e.id)))) { + chart.edges().forEach(e => { + const id = e.id() + + if (prev.edges.some(el => el.id == id) && !p.edges.some(el => el.id == id)) { + e.style({ + width: 0.5, + "line-style": "dashed", + "line-color": "#FF66B3", + "arrow-scale": 0.3, + "target-arrow-color": "#FF66B3", + }) + } }) + } else { + const elements = chart.elements().filter(e => [...prev.edges, ...prev.nodes].some(el => el.id == e.id())).removeStyle() + if (isPathResponse) { + elements.forEach(e => { + if (e.isNode()) { + e.style({ + "border-width": 0.5, + "color": "gray", + "border-color": "black", + "background-color": "gray", + "opacity": 0.5 + }); + } + + if (e.isEdge()) { + e.style({ + "line-color": "gray", + "target-arrow-color": "gray", + "opacity": 0.5, + }); + } + }) + } } - }) - + } return p - }); - - p.edges.forEach(element => { - const e = chart.elements().filter(el => el.id() == element.id) - e.style({ - width: 1, - "line-style": "solid", - "line-color": "pink", - "arrow-scale": 0.6, - "target-arrow-color": "pink", - }) }) - const elementsInPath = chart.elements().filter((element) => { - return [...p.nodes, ...p.edges].some((node) => node.id == element.id()) - }); - elementsInPath.layout(LAYOUT).run(); + if (isPathResponse && paths.some((path) => [...path.nodes, ...path.edges].every((e: any) => [...p.nodes, ...p.edges].some((el: any) => el.id === e.id)))) { + chart.edges().forEach(e => { + const id = e.id() + + if (p.edges.some(el => el.id == id)) { + e.style({ + width: 1, + "line-style": "solid", + "line-color": "#FF66B3", + "arrow-scale": 0.6, + "target-arrow-color": "#FF66B3", + }) + } + }) + chart.elements().filter(el => [...p.nodes, ...p.edges].some(e => e.id == el.id())).layout(LAYOUT).run(); + } else { + chart.elements().filter(el => [...p.nodes, ...p.edges].some(e => e.id == el.id())).forEach(el => { + if (el.id() == p.nodes[0].id || el.id() == p.nodes[p.nodes.length - 1].id) { + el.removeStyle().style({ + "border-width": 1, + "border-color": "#FF66B3", + "border-opacity": 1, + }); + } else if (el.isNode()) { + el.removeStyle().style({ + "border-width": 0.5, + "border-color": "#FF66B3", + "border-opacity": 1, + }); + } + if (el.isEdge()) { + el.removeStyle().style({ + width: 1, + "line-style": "solid", + "line-color": "#FF66B3", + "arrow-scale": 0.6, + "target-arrow-color": "#FF66B3", + }) + } + }).layout(LAYOUT).run(); + } } // A function that handles the change event of the url input box @@ -127,9 +205,10 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set } setQuery("") - setMessages((messages) => [...messages, { text: q, type: MessageTypes.Query }, { text: "", type: MessageTypes.Pending }]); + containerRef.current?.scrollTo(0, containerRef.current?.scrollHeight); + const result = await fetch(`/api/chat/${repo}?msg=${encodeURIComponent(q)}`, { method: 'POST' }) @@ -162,6 +241,8 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set }, [messages]); const handelSubmit = async () => { + setSelectedPath(undefined) + const chart = chartRef?.current if (!chart || !path?.start?.id || !path.end?.id) return @@ -181,27 +262,31 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set const json = await result.json() + if (json.result.paths.length === 0) { + toast({ + title: `No path found`, + description: `no path found between node ${path.start.name} - ${path.end.name}`, + }) + return + } + const formattedPaths: { nodes: any[], edges: any[] }[] = json.result.paths.map((p: any) => ({ nodes: p.filter((node: any, i: number) => i % 2 === 0), edges: p.filter((edge: any, i: number) => i % 2 !== 0) })) - const addedElements: ElementDefinition[] = [] - formattedPaths.forEach((p: any) => addedElements.push(...graph.extend(p, false, path))) + chart.add(formattedPaths.flatMap((p: any) => graph.extend(p, false, path))) formattedPaths.forEach(p => p.edges.forEach(e => e.id = `_${e.id}`)) - console.log(formattedPaths.flatMap(p => p.edges.map(e => e.id))); - chart.add(addedElements) graph.Elements.forEach((element: any) => { const { id } = element.data const e = chart.elements().filter(el => el.id() == id) - console.log(id); if (id == path.start?.id || id == path.end?.id) { e.style({ 'border-width': 1, - 'border-color': 'pink', + 'border-color': '#FF66B3', 'border-opacity': 1, }); } else if (formattedPaths.some((p: any) => [...p.nodes, ...p.edges].some((el: any) => el.id == id))) { if (e.isNode()) { e.style({ 'border-width': 0.5, - 'border-color': 'pink', + 'border-color': '#FF66B3', 'border-opacity': 1, }); } @@ -209,15 +294,17 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set if (e.isEdge()) { e.style({ "line-style": "dashed", - "line-color": "pink", - "target-arrow-color": "pink", + "line-color": "#FF66B3", + "target-arrow-color": "#FF66B3", "opacity": 1 }); } } else { if (e.isNode()) { e.style({ + "border-width": 0.5, "color": "gray", + "border-color": "black", "background-color": "gray", "opacity": 0.5 }); @@ -227,7 +314,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set e.style({ "line-color": "gray", "target-arrow-color": "gray", - "opacity": 0.5 + "opacity": 0.5, }); } } @@ -237,7 +324,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set }); elements.layout(LAYOUT).run() setPaths(formattedPaths) - setMessages(prev => [...prev.slice(0, -2), { type: MessageTypes.PathResponse }]) + setMessages(prev => [...prev.slice(0, -2), { type: MessageTypes.PathResponse, paths: formattedPaths }]) setPath(undefined) setIsPathResponse(true) } @@ -250,7 +337,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set

    You

    -

    {message.text}

    +

    {message.text}

    ) case MessageTypes.Response: return ( @@ -259,7 +346,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set

    Answer

-

{message.text}

+

{message.text?.replaceAll('"', "")}

) case MessageTypes.Text: return ( @@ -277,6 +364,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set type="text" icon={} node={path?.start} + scrollToBottom={() => containerRef.current?.scrollTo(0, containerRef.current?.scrollHeight)} /> } node={path?.end} + scrollToBottom={() => containerRef.current?.scrollTo(0, containerRef.current?.scrollHeight)} />
) @@ -294,14 +383,18 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, set case MessageTypes.PathResponse: return (
{ - paths.map((p, i: number) => ( + message.paths && + message.paths.map((p, i: number) => ( )) }
) case MessageTypes.Tip: return ( -
+
diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index dcbc3837..a3baff4d 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -6,15 +6,15 @@ import cytoscape, { ElementDefinition, EventObject, Position } from 'cytoscape'; import fcose from 'cytoscape-fcose'; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; -import { ChevronLeft, ChevronRight, GitFork, Search } from "lucide-react"; +import { GitFork, Search, X } from "lucide-react"; import ElementMenu from "./elementMenu"; import ElementTooltip from "./elementTooltip"; import Combobox from "./combobox"; import { toast } from '@/components/ui/use-toast'; -import { cn } from '@/lib/utils'; import { Path } from '../page'; import Input from './Input'; -import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; +import CommitList from './commitList'; +import { Checkbox } from '@/components/ui/checkbox'; interface Props { onFetchGraph: (graphName: string) => void, @@ -26,6 +26,7 @@ interface Props { selectedValue: string setSelectedPathId: (selectedPathId: string) => void isPathResponse: boolean + setIsPathResponse: Dispatch> } // The stylesheet for the graph @@ -112,8 +113,6 @@ export const LAYOUT = { avoidOverlap: true, } -const COMMIT_LIMIT = 7 - export function CodeGraph({ onFetchGraph, onFetchNode, @@ -123,7 +122,8 @@ export function CodeGraph({ chartRef, selectedValue, setSelectedPathId, - isPathResponse + isPathResponse, + setIsPathResponse }: Props) { let graph = useContext(GraphContext) @@ -329,6 +329,7 @@ export function CodeGraph({ chartNode.select() const layout = { ...LAYOUT, padding: 250 } chartNode.layout(layout).run() + setSearchNodeName("") } return ( @@ -346,22 +347,55 @@ export function CodeGraph({ { graph.Id ?
-
- setSearchNodeName(node.name!)} - icon={} - handelSubmit={handelSearchSubmit} - /> - +
+
+ setSearchNodeName(node.name!)} + icon={} + handelSubmit={handelSearchSubmit} + /> + +
+ { + isPathResponse && + + }
-
+

{nodesCount} Nodes

{edgesCount} Edges

- +
+ {/*
+
+ +

Display Changes

+
+
+
+

Were added

+
+
+
+

Were edited

+
+
*/} + +
{ graph.Id && -
- -
    - { - commits.slice(commitIndex - COMMIT_LIMIT, commitIndex).map((commit: any) => { - const date = new Date(commit.date * 1000) - const month = `${date.getDate()} ${date.toLocaleString('default', { month: 'long' })}` - const hour = `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}` - return ( - - -
  • - -
  • -
    - -
    -

    {commit.author}:

    -

    {commit.message}

    -

    {commit.hash}

    -
    -
    -
    - ) - }) - } -
- -
+ }
diff --git a/app/components/commitList.tsx b/app/components/commitList.tsx new file mode 100644 index 00000000..43d46140 --- /dev/null +++ b/app/components/commitList.tsx @@ -0,0 +1,144 @@ +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" +import { toast } from "@/components/ui/use-toast" +import { cn } from "@/lib/utils" +import cytoscape from "cytoscape" +import { ChevronLeft, ChevronRight } from "lucide-react" +import { Dispatch, MutableRefObject, SetStateAction, useState } from "react" +import { Graph } from "./model" +import { LAYOUT } from "./code-graph" + +interface Props { + commits: any[] + currentCommit: number + setCurrentCommit: Dispatch> + commitIndex: number + setCommitIndex: Dispatch> + graph: Graph, + chartRef: MutableRefObject +} + +const COMMIT_LIMIT = 7 + +export default function CommitList({ commitIndex, commits, currentCommit, setCommitIndex, setCurrentCommit, graph, chartRef }: Props) { + + const [commitChanges, setCommitChanges] = useState() + + const handelCommitChange = async (commit: any) => { + const chart = chartRef.current + + if (!chart) return + + const result = await fetch(`api/repo/${graph.Id}/?type=switchCommit`, { + method: 'POST', + }) + + if (!result.ok) { + toast({ + title: "Uh oh! Something went wrong", + description: (await result.text()), + }) + return + } + + const json = await result.json() + + json.result.deletions.nodes.forEach((e: any) => { + chart.remove(`#${e.id}`) + graph.NodesMap.delete(e.id) + graph.Elements.splice(graph.Elements.findIndex((el) => el.data.id === e.id), 1) + }) + + json.result.deletions.edges.forEach((e: any) => { + chart.remove(`#_${e.id}`) + graph.EdgesMap.delete(e.id) + graph.Elements.splice(graph.Elements.findIndex((el) => el.data.id === e.id), 1) + }) + + const additionsIds = chart.add(graph.extend(json.result.additions)) + .filter((e) => e.isNode()).style({ "border-color": "pink", "border-width": 2, "border-opacity": 1 }) + .map((e) => e.id())! + + const g = Graph.empty() + g.extend(json.result.modifications) + + const modifiedIds = g.Elements.map((e) => { + const graphElement = graph.Elements.find((el) => el.data.id === e.data.id) + graphElement.data = e.data + + if ("category" in e.data) { + chart.$(`#${e.data.id}`).data(e.data).style({ "border-color": "blue", "border-width": 2, "border-opacity": 1 }) + } + + return e.data.id + }) + + chart.layout(LAYOUT).run() + + setCommitChanges({ additionsIds, modifiedIds }) + setCurrentCommit(commit.hash) + } + + return ( +
+ +
    + { + commits.slice(commitIndex - COMMIT_LIMIT, commitIndex).map((commit: any) => { + const date = new Date(commit.date * 1000) + const month = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}` + const hour = `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}` + return ( + + +
  • + +
  • +
    + +

    {commit.author}

    +

    {commit.message}

    +

    {commit.hash}

    +
    +
    + ) + }) + } +
+ +
+ ) +} \ No newline at end of file diff --git a/app/components/dataPanel.tsx b/app/components/dataPanel.tsx index 611a4aa8..7aefeb7d 100644 --- a/app/components/dataPanel.tsx +++ b/app/components/dataPanel.tsx @@ -1,6 +1,6 @@ -import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +import { Dispatch, SetStateAction } from "react"; import { Node } from "./model"; -import { ChevronLeft, ChevronRight, Copy, SquareArrowOutUpRight, X } from "lucide-react"; +import { Copy, SquareArrowOutUpRight, X } from "lucide-react"; interface Props { obj: Node | undefined; @@ -14,6 +14,7 @@ const excludedProperties = [ "expand", "collapsed", "isPath", + "isPathStartEnd" ] export default function DataPanel({ obj, setObj, url }: Props) { @@ -21,11 +22,11 @@ export default function DataPanel({ obj, setObj, url }: Props) { if (!obj) return null; const label = `${obj.category}: ${obj.name}` - const object = Object.fromEntries(Object.entries(obj).filter(([k]) => !excludedProperties.includes(k))) + const object = Object.entries(obj).filter(([k]) => !excludedProperties.includes(k)) return ( -
-
+
+

{label.toUpperCase()}

@@ -33,18 +34,21 @@ export default function DataPanel({ obj, setObj, url }: Props) {
-
-
-                    {JSON.stringify(object, null, 1)
-                        .replace(/({|}|\[|\]|,)/g, match => match === '}' || match === "]" ? `\n${match}` : `${match}\n`).slice(1, -1)
-                    }
-                
+
+ { + object.map(([key, value]) => ( +
+

{key}:

+

{value}

+
+ )) + }
-
+
- + CREATE A NEW PROJECT @@ -178,7 +178,7 @@ export default function Home() {
setCreateURL(e.target.value)} @@ -186,7 +186,7 @@ export default function Home() { />
-
@@ -464,8 +464,8 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP return (
-

WELCOME TO OUR ASSISTANCE SERVICE

- +

WELCOME TO OUR ASSISTANCE SERVICE

+ We can help you access and update only the needed data via paths, optimizing network requests with batching and catching for better performance. diff --git a/app/components/labels.tsx b/app/components/labels.tsx index 716c2a62..132ab856 100644 --- a/app/components/labels.tsx +++ b/app/components/labels.tsx @@ -12,7 +12,7 @@ export function Labels(params: { categories: Category[], className?: string, onC {params.categories.map((category) =>
{ params.onClick(category.name, checked as boolean) setReload(!reload) diff --git a/app/components/model.ts b/app/components/model.ts index aa144950..a2f713a5 100644 --- a/app/components/model.ts +++ b/app/components/model.ts @@ -22,39 +22,24 @@ export interface Edge { [key: string]: any, } -const COLORS_ORDER = [ - "rose", +const COLORS_ORDER_NAME = [ + "pink", "yellow", - "teal", - "fuchsia", "blue", - "violet", - "slate", - "cyan", - "orange", - "red", - "green", - "pink", ] -export function getCategoryColorName(index = -1): string { - if (index === -1) return "gray" - - index = index < COLORS_ORDER.length ? index : 0 +const COLORS_ORDER = [ + "#F43F5F", + "#E9B306", + "#15B8A6", +] - return COLORS_ORDER[index] +export function getCategoryColorValue(index: number): string { + return COLORS_ORDER[index % COLORS_ORDER.length] } -export function getCategoryColorValue(index = -1): string { - let colors = twcolors as any - - if (index === -1) return colors["gray"] - - index = index < COLORS_ORDER.length ? index : 0 - let colorName = COLORS_ORDER[index] - let color = colors[colorName] - - return color["500"] +export function getCategoryColorName(index: number): string { + return COLORS_ORDER_NAME[index % COLORS_ORDER.length] } export class Graph { diff --git a/app/globals.css b/app/globals.css index a3c89fab..7a32f2b4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -71,11 +71,32 @@ * { @apply border-border; } - + body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-roberto; } - + +} + +.Tip { + @apply flex gap-2 p-4 border border-[#0000001A] bg-[#F9F9F9] hover:bg-[#ECECEC] rounded-md text-left; + .label { + @apply text-[16px] font-bold leading-[14px] + } + .text { + @apply text-[16px] font-normal leading-[20px] text-[#7D7D7D] + } + div { + @apply flex flex-col gap-2 text-start + } +} + +.font-roberto { + font-family: 'Roboto', sans-serif; +} + +.font-oswald { + font-family: 'Oswald', sans-serif; } ::-webkit-scrollbar { @@ -95,15 +116,5 @@ ::-webkit-scrollbar-thumb:hover { background-color: #a8bbbf; - -} - -.font-roberto { - font-family: 'Roboto', sans-serif; - font-style: italic; -} -.title { - font-weight: 700; - font-size: 22px; } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 0946cae0..a5061d72 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -141,9 +141,9 @@ export default function Home() {
- FalkorDB + FalkorDB -

+

CODE GRAPH

diff --git a/tailwind.config.js b/tailwind.config.js index a14e83b2..f0c4cb6f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -23,6 +23,9 @@ module.exports = { }, extend: { colors: { + pink: "#F43F5F", + yellow: "#E9B306", + blue: "#15B8A6", border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", From 98f3cb690f25e91e223d50f9a4e2a0896461e4e0 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 5 Nov 2024 15:19:42 +0200 Subject: [PATCH 09/20] fix type err --- app/components/Input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 2e3282a7..4d36afa5 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -127,7 +127,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, const label = option.labels[0] const name = option.properties.name const path = option.properties.path - const colorName = getCategoryColorName(graph.CategoriesMap.get(label)?.index) + const colorName = getCategoryColorName(graph.CategoriesMap.get(label)!.index) return (
-

{message.text}

+

{message.text}

) case MessageTypes.Response: return (
-

Answer

+

Answer

-

{message.text?.replaceAll('"', "")}

+
) case MessageTypes.Text: return ( @@ -408,51 +415,6 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP }
) - case MessageTypes.Tip: return ( -
- - - -
- ) default: return (
Waiting for response @@ -463,28 +425,121 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP return (
-
-

WELCOME TO OUR ASSISTANCE SERVICE

- - We can help you access and update only the needed - data via paths, optimizing network requests with - batching and catching for better performance. - +
+ { + messages.length === 0 && + <> +

WELCOME TO OUR ASSISTANCE SERVICE

+ + We can help you access and update only the needed + data via paths, optimizing network requests with + batching and catching for better performance. + + + + + + } { messages.map((message, index) => { return getMessage(message, index) }) } + { + tipOpen && +
setTipOpen(false)}> + + + +
+ }
- {repo && + { + repo &&
- - - - diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index b626fd10..bc49a25c 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -487,7 +487,7 @@ export function CodeGraph({
}
- { + {/* { graph.Id && - } + } */}
) diff --git a/package-lock.json b/package-lock.json index 9cc07005..3f4547f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "react-cytoscapejs": "^2.0.0", "react-dom": "^18", "react-resizable-panels": "^2.0.20", + "react-type-animation": "^3.2.0", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "web-tree-sitter": "^0.22.6" @@ -5925,6 +5926,16 @@ } } }, + "node_modules/react-type-animation": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-type-animation/-/react-type-animation-3.2.0.tgz", + "integrity": "sha512-WXTe0i3rRNKjmggPvT5ntye1QBt0ATGbijeW6V3cQe2W0jaMABXXlPPEdtofnS9tM7wSRHchEvI9SUw+0kUohw==", + "peerDependencies": { + "prop-types": "^15.5.4", + "react": ">= 15.0.0", + "react-dom": ">= 15.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index cf38f37d..e466c31d 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "react-cytoscapejs": "^2.0.0", "react-dom": "^18", "react-resizable-panels": "^2.0.20", + "react-type-animation": "^3.2.0", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "web-tree-sitter": "^0.22.6" diff --git a/repositories/GraphRAG-SDK b/repositories/GraphRAG-SDK new file mode 160000 index 00000000..52e3042b --- /dev/null +++ b/repositories/GraphRAG-SDK @@ -0,0 +1 @@ +Subproject commit 52e3042ba79d83489d4ceff6108973f0f2e43442 From 1426b64387204401248f67e0b04abb83cb5288a5 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 12 Nov 2024 12:15:58 +0200 Subject: [PATCH 14/20] final fix --- app/components/Input.tsx | 3 +- app/components/chat.tsx | 191 ++++++++++++++++------------------ app/components/code-graph.tsx | 6 +- app/page.tsx | 1 + 4 files changed, 96 insertions(+), 105 deletions(-) diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 1fa3d960..078141c3 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -133,6 +133,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, const name = option.properties.name const path = option.properties.path const colorName = getCategoryColorName(graph.CategoriesMap.get(label)!.index) + const color = getCategoryColorValue(graph.CategoriesMap.get(label)!.index) return ( + + + + const getMessage = (message: Message, index?: number) => { switch (message.type) { case MessageTypes.Query: return ( @@ -349,8 +414,9 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP

Answer

@@ -394,8 +460,9 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP message.paths.map((p, i: number) => ( - - + {getTip()} } { @@ -485,48 +512,8 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP } { tipOpen && -
setTipOpen(false)}> - - - +
setTipOpen(false)}> + {getTip()}
}
diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index bc49a25c..7394ab7d 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -24,6 +24,7 @@ interface Props { setPath: Dispatch> chartRef: MutableRefObject selectedValue: string + selectedPathId: string | undefined setSelectedPathId: (selectedPathId: string) => void isPathResponse: boolean setIsPathResponse: Dispatch> @@ -123,7 +124,8 @@ export function CodeGraph({ selectedValue, setSelectedPathId, isPathResponse, - setIsPathResponse + setIsPathResponse, + selectedPathId }: Props) { let graph = useContext(GraphContext) @@ -470,7 +472,7 @@ export function CodeGraph({ cy.on('tap', 'edge', (evt) => { const { target } = evt - if (!isPathResponse) return + if (!isPathResponse || selectedPathId === target.id()) return setSelectedPathId(target.id()) }); diff --git a/app/page.tsx b/app/page.tsx index a5061d72..a9aac303 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -211,6 +211,7 @@ export default function Home() { setPath={setPath} isShowPath={!!path} selectedValue={selectedValue} + selectedPathId={selectedPathId} setSelectedPathId={setSelectedPathId} isPathResponse={isPathResponse} setIsPathResponse={setIsPathResponse} From 6c72394d89a439aab1b0ab7cb63617799d87c58f Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 12 Nov 2024 14:09:03 +0200 Subject: [PATCH 15/20] commit --- app/page.tsx | 57 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index a9aac303..a4a81851 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -11,6 +11,7 @@ import { toast } from '@/components/ui/use-toast'; import { GraphContext } from './components/provider'; import Image from 'next/image'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Progress } from '@/components/ui/progress'; export type PathNode = { id?: number @@ -28,10 +29,11 @@ export default function Home() { const [selectedValue, setSelectedValue] = useState(""); const [selectedPathId, setSelectedPathId] = useState(); const [isPathResponse, setIsPathResponse] = useState(false); - const [createURL, setCreateURL] = useState("https://github.com/FalkorDB/GraphRAG-SDK") + const [createURL, setCreateURL] = useState("") const [createOpen, setCreateOpen] = useState(false) const [options, setOptions] = useState([]); const [path, setPath] = useState(); + const [isSubmit, setIsSubmit] = useState(false); const chartRef = useRef(null) useEffect(() => { @@ -59,6 +61,8 @@ export default function Home() { async function onCreateRepo(e: React.FormEvent) { e.preventDefault() + setIsSubmit(true) + if (!createURL) { toast({ variant: "destructive", @@ -87,6 +91,7 @@ export default function Home() { setSelectedValue(graphName) setCreateURL("") setCreateOpen(false) + setIsSubmit(false) toast({ title: "Success", @@ -171,29 +176,37 @@ export default function Home() { - CREATE A NEW PROJECT + {!isSubmit ? "CREATE A NEW PROJECT" : "THANK YOU FOR A NEW REQUEST"} - Please provide the URL of the model to connect and start querying data + { + !isSubmit + ? "Please provide the URL of the model to connect and start querying data" + : "Processing your graph, this could take a while. We appreciate your patience" + } -
- setCreateURL(e.target.value)} - placeholder="Type URL" - /> -
- -
-
+ { + !isSubmit ? +
+ setCreateURL(e.target.value)} + placeholder="Type URL" + /> +
+ +
+
+ : + }
@@ -215,7 +228,7 @@ export default function Home() { setSelectedPathId={setSelectedPathId} isPathResponse={isPathResponse} setIsPathResponse={setIsPathResponse} - /> + /> From 96e29aa786264d6004f901145ac3fb59d95ed3c3 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 12 Nov 2024 14:09:51 +0200 Subject: [PATCH 16/20] commit --- app/components/Input.tsx | 12 ++++++++---- app/components/chat.tsx | 36 +++++++---------------------------- app/components/code-graph.tsx | 15 +++++++++------ components/ui/progress.tsx | 28 +++++++++++++++++++++++++++ package-lock.json | 24 +++++++++++++++++++++++ package.json | 1 + 6 files changed, 77 insertions(+), 39 deletions(-) create mode 100644 components/ui/progress.tsx diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 078141c3..42df1fc1 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -36,7 +36,11 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, useEffect(() => { const timeout = setTimeout(async () => { - if (!value || node?.id) return + if (!value || node?.id) { + setOptions([]) + setOpen(false) + return + } const result = await fetch(`/api/repo/${graph.Id}/?prefix=${value}&type=autoComplete`, { method: 'POST' @@ -71,10 +75,10 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, if (!option) return if (handelSubmit) { handelSubmit(option) + } else { + if (!open) return onValueChange({ name: option.properties.name, id: option.id }) - } - if (!open) return - onValueChange({ name: option.properties.name, id: option.id }) + } setOpen(false) return } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 2c550325..069ed4cf 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -344,18 +344,6 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP const getTip = () => <> - - const getMessage = (message: Message, index?: number) => { diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 7394ab7d..9e32a3e7 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -11,7 +11,7 @@ import ElementMenu from "./elementMenu"; import ElementTooltip from "./elementTooltip"; import Combobox from "./combobox"; import { toast } from '@/components/ui/use-toast'; -import { Path } from '../page'; +import { Path, PathNode } from '../page'; import Input from './Input'; import CommitList from './commitList'; import { Checkbox } from '@/components/ui/checkbox'; @@ -136,7 +136,7 @@ export function CodeGraph({ const [position, setPosition] = useState(); const [tooltipPosition, setTooltipPosition] = useState(); const [graphName, setGraphName] = useState(""); - const [searchNodeName, setSearchNodeName] = useState(""); + const [searchNode, setSearchNode] = useState({}); const [commits, setCommits] = useState([]); const [nodesCount, setNodesCount] = useState(0); const [edgesCount, setEdgesCount] = useState(0); @@ -321,7 +321,9 @@ export function CodeGraph({ if (!chart) return - let chartNode = chart.elements(`node[name = "${node.properties.name}"]`) + const n = { name: node.properties.name, id: node.id } + + let chartNode = chart.elements(`node[name = "${n.name}"]`) if (chartNode.length === 0) { const [newNode] = graph.extend({ nodes: [node], edges: [] }) @@ -331,7 +333,7 @@ export function CodeGraph({ chartNode.select() const layout = { ...LAYOUT, padding: 250 } chartNode.layout(layout).run() - setSearchNodeName("") + setSearchNode(n) } return ( @@ -353,10 +355,11 @@ export function CodeGraph({
setSearchNodeName(node.name!)} + value={searchNode.name} + onValueChange={({ name }) => setSearchNode({ name })} icon={} handelSubmit={handelSearchSubmit} + node={searchNode} />
diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx new file mode 100644 index 00000000..5c87ea48 --- /dev/null +++ b/components/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/package-lock.json b/package-lock.json index 3f4547f9..6f8bab75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", @@ -1323,6 +1324,29 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", + "integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==", + "dependencies": { + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", diff --git a/package.json b/package.json index e466c31d..26bc32a5 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", From e4470541e32cd4ef8ddb0a33fd5232f5f5d6e64b Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 12 Nov 2024 15:43:45 +0200 Subject: [PATCH 17/20] fix search input --- app/components/Input.tsx | 6 ++++-- app/components/code-graph.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 42df1fc1..fd8ddc8b 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -37,7 +37,9 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, const timeout = setTimeout(async () => { if (!value || node?.id) { - setOptions([]) + if (!value) { + setOptions([]) + } setOpen(false) return } @@ -120,7 +122,6 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, const newVal = e.target.value onValueChange({ name: newVal }) }} - onBlur={() => setOpen(false)} {...props} /> { @@ -147,6 +148,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, onMouseEnter={() => setSelectedOption(index)} onMouseLeave={() => setSelectedOption(-1)} onClick={() => { + debugger onValueChange({ name: option.properties.name, id: option.id }) handelSubmit && handelSubmit(option) setOpen(false) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 9e32a3e7..025395a3 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -323,7 +323,7 @@ export function CodeGraph({ const n = { name: node.properties.name, id: node.id } - let chartNode = chart.elements(`node[name = "${n.name}"]`) + let chartNode = chart.elements(`node[id = "${n.id}"]`) if (chartNode.length === 0) { const [newNode] = graph.extend({ nodes: [node], edges: [] }) From 938dec2d6d0eb5a387f47829eeb6c6ac4c5315ac Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Thu, 14 Nov 2024 13:07:43 +0200 Subject: [PATCH 18/20] commit --- app/api/chat/[graph]/route.ts | 6 ++- app/api/repo/[graph]/[node]/route.ts | 10 ++-- app/api/repo/[graph]/{route.tsx => route.ts} | 14 +++-- app/api/repo/route.ts | 10 ++-- app/components/Input.tsx | 8 +-- app/components/chat.tsx | 54 +++++++++----------- app/components/code-graph.tsx | 6 +-- 7 files changed, 61 insertions(+), 47 deletions(-) rename app/api/repo/[graph]/{route.tsx => route.ts} (95%) diff --git a/app/api/chat/[graph]/route.ts b/app/api/chat/[graph]/route.ts index 6431fe56..d6ef73a6 100644 --- a/app/api/chat/[graph]/route.ts +++ b/app/api/chat/[graph]/route.ts @@ -1,15 +1,17 @@ import { NextRequest, NextResponse } from "next/server" + export async function POST(request: NextRequest, { params }: { params: { graph: string } }) { const graphName = params.graph const msg = request.nextUrl.searchParams.get('msg') try { - const result = await fetch(`http://127.0.0.1:5000/chat`, { + const result = await fetch(`${process.env.BEAKEND_URL}/chat`, { method: 'POST', - body: JSON.stringify({ repo: graphName, msg}), + body: JSON.stringify({ repo: graphName, msg }), headers: { + "Authorization": process.env.SECRET_TOKEN!, "Content-Type": 'application/json' } }) diff --git a/app/api/repo/[graph]/[node]/route.ts b/app/api/repo/[graph]/[node]/route.ts index 9c18d64e..4b4f0143 100644 --- a/app/api/repo/[graph]/[node]/route.ts +++ b/app/api/repo/[graph]/[node]/route.ts @@ -6,8 +6,11 @@ export async function GET(request: NextRequest, { params }: { params: { graph: s const graphId = params.graph; try { - const result = await fetch(`http://127.0.0.1:5000/get_neighbors?repo=${graphId}&node_id=${nodeId}`, { + const result = await fetch(`${process.env.BEAKEND_URL}/get_neighbors?repo=${graphId}&node_id=${nodeId}`, { method: 'GET', + headers: { + "Authorization": process.env.SECRET_TOKEN!, + } }) const json = await result.json() @@ -23,12 +26,13 @@ export async function POST(request: NextRequest, { params }: { params: { graph: const nodeId = params.node; const graphId = params.graph; const targetId = request.nextUrl.searchParams.get('targetId') - + try { - const result = await fetch(`http://127.0.0.1:5000/find_paths`, { + const result = await fetch(`${process.env.BEAKEND_URL}/find_paths`, { method: 'POST', headers: { + "Authorization": process.env.SECRET_TOKEN!, 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/app/api/repo/[graph]/route.tsx b/app/api/repo/[graph]/route.ts similarity index 95% rename from app/api/repo/[graph]/route.tsx rename to app/api/repo/[graph]/route.ts index ebab0af4..a4a2dd7e 100644 --- a/app/api/repo/[graph]/route.tsx +++ b/app/api/repo/[graph]/route.ts @@ -5,8 +5,11 @@ export async function GET(request: NextRequest, { params }: { params: { graph: s const graphName = params.graph try { - const result = await fetch(`http://127.0.0.1:5000/graph_entities?repo=${graphName}`, { + const result = await fetch(`${process.env.BEAKEND_URL}/graph_entities?repo=${graphName}`, { method: 'GET', + headers: { + "Authorization": process.env.SECRET_TOKEN!, + } }) if (!result.ok) { @@ -29,10 +32,11 @@ export async function POST(request: NextRequest, { params }: { params: { graph: try { switch (type) { case "commit": { - const result = await fetch(`http://127.0.0.1:5000/list_commits`, { + const result = await fetch(`${process.env.BEAKEND_URL}/list_commits`, { method: 'POST', body: JSON.stringify({ repo: graphName }), headers: { + "Authorization": process.env.SECRET_TOKEN!, "Content-Type": 'application/json' } }) @@ -208,10 +212,11 @@ export async function POST(request: NextRequest, { params }: { params: { graph: } case "autoComplete": { const prefix = request.nextUrl.searchParams.get('prefix')! - const result = await fetch(`http://127.0.0.1:5000/auto_complete`, { + const result = await fetch(`${process.env.BEAKEND_URL}/auto_complete`, { method: 'POST', body: JSON.stringify({ repo: graphName, prefix }), headers: { + "Authorization": process.env.SECRET_TOKEN!, "Content-Type": 'application/json' } }) @@ -225,10 +230,11 @@ export async function POST(request: NextRequest, { params }: { params: { graph: return NextResponse.json({ result: json }, { status: 200 }) } default: { - const result = await fetch(`http://127.0.0.1:5000/repo_info`, { + const result = await fetch(`${process.env.BEAKEND_URL}/repo_info`, { method: 'POST', body: JSON.stringify({ repo: graphName }), headers: { + "Authorization": process.env.SECRET_TOKEN!, "Content-Type": 'application/json' } }) diff --git a/app/api/repo/route.ts b/app/api/repo/route.ts index 83f339e4..06d6199c 100644 --- a/app/api/repo/route.ts +++ b/app/api/repo/route.ts @@ -2,8 +2,11 @@ import { NextRequest, NextResponse } from "next/server"; export async function GET() { try { - const result = await fetch(`http://127.0.0.1:5000/list_repos`, { + const result = await fetch(`${process.env.BEAKEND_URL}/list_repos`, { method: 'GET', + headers: { + "Authorization": process.env.SECRET_TOKEN!, + } }) if (!result.ok) { @@ -21,12 +24,13 @@ export async function GET() { export async function POST(request: NextRequest) { const url = request.nextUrl.searchParams.get('url'); - + try { - const result = await fetch(`http://127.0.0.1:5000/process_repo`, { + const result = await fetch(`${process.env.BEAKEND_URL}/process_repo`, { method: 'POST', body: JSON.stringify({ repo_url: url, ignore: ["./.github", "./sbin", "./.git", "./deps", "./bin", "./build"] }), headers: { + "Authorization": process.env.SECRET_TOKEN!, 'Content-Type': 'application/json' } }) diff --git a/app/components/Input.tsx b/app/components/Input.tsx index fd8ddc8b..51b0c981 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -58,11 +58,14 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, } const json = await result.json() - const { completions } = json.result - setOptions(completions) + + setOptions(completions || []) + if (completions?.length > 0) { setOpen(true) + } else { + setOpen(false) } }, 500) @@ -86,7 +89,6 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, } case "ArrowUp": { e.preventDefault() - console.log(selectedOption <= 0 ? selectedOption : selectedOption - 1); setSelectedOption(prev => prev <= 0 ? options.length - 1 : prev - 1) return } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 069ed4cf..29aa3674 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,5 +1,5 @@ import { toast } from "@/components/ui/use-toast"; -import { Dispatch, MutableRefObject, SetStateAction, useEffect, useRef, useState } from "react"; +import { Dispatch, FormEvent, MutableRefObject, SetStateAction, useEffect, useRef, useState } from "react"; import Image from "next/image"; import { AlignLeft, ArrowRight, ChevronDown, Lightbulb, Undo2 } from "lucide-react"; import { Path } from "../page"; @@ -68,6 +68,8 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP const tipRef: React.RefObject = useRef(null); + const isSendMessage = messages.some(m => m.type === MessageTypes.Pending) || (messages.some(m => m.text === "Please select a starting point and the end point. Select or press relevant item on the graph") && !messages.some(m => m.type === MessageTypes.Path)) + useEffect(() => { if (tipOpen) { tipRef.current?.focus() @@ -192,10 +194,6 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP // A function that handles the change event of the url input box async function handleQueryInputChange(event: any) { - if (event.key === "Enter") { - await handleQueryClick(event); - } - // Get the new value of the input box const value = event.target.value; @@ -204,7 +202,25 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP } // Send the user query to the server - async function sendQuery(q: string) { + async function sendQuery(event: FormEvent) { + + event.preventDefault(); + + const q = query.trim() + + if (!q) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: "Please enter a question.", + }) + return + } + + setQuery("") + + setMessages((messages) => [...messages, { text: q, type: MessageTypes.Query }, { type: MessageTypes.Pending }]); + const result = await fetch(`/api/chat/${repo}?msg=${encodeURIComponent(q)}`, { method: 'POST' }) @@ -223,27 +239,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP prev = prev.slice(0, -1); return [...prev, { text: json.result.response, type: MessageTypes.Response }]; }); - } - - // A function that handles the click event - const handleQueryClick = async (event: any) => { - event.preventDefault(); - - const q = query.trim() - - if (!q) { - toast({ - variant: "destructive", - title: "Uh oh! Something went wrong.", - description: "Please enter a question.", - }) - return - } - - setQuery("") - setMessages((messages) => [...messages, { text: q, type: MessageTypes.Query }, { text: "", type: MessageTypes.Pending }]); - return await sendQuery(q); } // Scroll to the bottom of the chat on new message @@ -502,9 +498,9 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP -
- -
diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 025395a3..df796dfe 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -274,7 +274,7 @@ export function CodeGraph({ if (!graphNode.data.expand) { elements = await onFetchNode(node) - console.log(elements); + if (elements.length === 0) { toast({ title: "No neighbors found", @@ -492,7 +492,7 @@ export function CodeGraph({ } - {/* { + { graph.Id && - } */} + } ) From 679b236378272e9da4926bfffc9dd3863ddd1cc8 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Thu, 14 Nov 2024 14:45:02 +0200 Subject: [PATCH 19/20] fix commit list --- app/components/code-graph.tsx | 42 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index df796dfe..63a402da 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -191,8 +191,11 @@ export function CodeGraph({ const json = await result.json() const commitsArr = json.result.commits setCommits(commitsArr) - setCurrentCommit(commitsArr[commitsArr.length - 1].hash) - setCommitIndex(commitsArr.length) + + if (commitsArr.length > 0) { + setCurrentCommit(commitsArr[commitsArr.length - 1].hash) + setCommitIndex(commitsArr.length) + } } run() @@ -383,22 +386,25 @@ export function CodeGraph({

{edgesCount} Edges

- {/*
-
- -

Display Changes

-
-
-
-

Were added

-
-
-
-

Were edited

+ { + commitIndex !== commits.length && +
+
+ +

Display Changes

+
+
+
+

Were added

+
+
+
+

Were edited

+
-
*/} + }
@@ -493,7 +499,7 @@ export function CodeGraph({ } { - graph.Id && + graph.Id && commits.length > 0 && Date: Thu, 14 Nov 2024 15:02:08 +0200 Subject: [PATCH 20/20] commit --- app/components/Input.tsx | 21 +++++++++++++++++---- app/components/chat.tsx | 10 +++++----- repositories/GraphRAG-SDK | 1 - 3 files changed, 22 insertions(+), 10 deletions(-) delete mode 160000 repositories/GraphRAG-SDK diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 51b0c981..d314fe8e 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -24,6 +24,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, const [options, setOptions] = useState([]) const [selectedOption, setSelectedOption] = useState(0) const inputRef = useRef(null) + const containerRef = useRef(null) useEffect(() => { setSelectedOption(0) @@ -37,7 +38,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, const timeout = setTimeout(async () => { if (!value || node?.id) { - if (!value) { + if (!value) { setOptions([]) } setOpen(false) @@ -59,7 +60,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, const json = await result.json() const { completions } = json.result - + setOptions(completions || []) if (completions?.length > 0) { @@ -89,12 +90,18 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, } case "ArrowUp": { e.preventDefault() - setSelectedOption(prev => prev <= 0 ? options.length - 1 : prev - 1) + setSelectedOption(prev => { + containerRef.current?.scrollTo({ behavior: 'smooth', top: (prev <= 0 ? options.length - 1 : prev - 1) * 64 }) + return prev <= 0 ? options.length - 1 : prev - 1 + }) return } case "ArrowDown": { e.preventDefault() - setSelectedOption(prev => (prev + 1) % options.length) + setSelectedOption(prev => { + containerRef.current?.scrollTo({ behavior: 'smooth', top: ((prev + 1) % options.length) * 64 }) + return (prev + 1) % options.length + }) return } case "Space": { @@ -125,10 +132,16 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, onValueChange({ name: newVal }) }} {...props} + onBlur={(e) => { + if (!containerRef.current?.contains(e.relatedTarget as Node)) { + setOpen(false) + } + }} /> { open &&
[...messages, { text: q, type: MessageTypes.Query }, { type: MessageTypes.Pending }]); - + const result = await fetch(`/api/chat/${repo}?msg=${encodeURIComponent(q)}`, { method: 'POST' }) @@ -495,7 +495,7 @@ export function Chat({ repo, path, setPath, graph, chartRef, selectedPathId, isP { repo &&
-
diff --git a/repositories/GraphRAG-SDK b/repositories/GraphRAG-SDK deleted file mode 160000 index 52e3042b..00000000 --- a/repositories/GraphRAG-SDK +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 52e3042ba79d83489d4ceff6108973f0f2e43442