feat: migrate new UI to cognee (#966)

<!-- .github/pull_request_template.md -->

## Description
<!-- Provide a clear description of the changes in this PR -->

## DCO Affirmation
I affirm that all code in every commit of this pull request conforms to
the terms of the Topoteretes Developer Certificate of Origin.

---------

Co-authored-by: Igor Ilic <igorilic03@gmail.com>
This commit is contained in:
Boris 2025-06-18 20:56:44 +02:00 committed by GitHub
parent 74000b4548
commit e7644f4b3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
129 changed files with 7342 additions and 3810 deletions

View file

@ -0,0 +1,3 @@
node_modules
dist
coverage

View file

@ -1,5 +1,5 @@
# Use an official Node.js runtime as a parent image
FROM node:18-alpine
FROM node:22-alpine
# Set the working directory to /app
WORKDIR /app
@ -9,12 +9,14 @@ COPY package.json package-lock.json ./
# Install any needed packages specified in package.json
RUN npm ci
# RUN npm rebuild lightningcss
# Copy the rest of the application code to the working directory
COPY src ./src
COPY public ./public
COPY next.config.mjs .
COPY postcss.config.mjs .
COPY tsconfig.json .
# Build the app and run it
CMD npm run dev
CMD ["npm", "run", "dev"]

View file

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
];
export default eslintConfig;

File diff suppressed because it is too large Load diff

View file

@ -9,24 +9,28 @@
"lint": "next lint"
},
"dependencies": {
"@auth0/nextjs-auth0": "^4.6.0",
"classnames": "^2.5.1",
"culori": "^4.0.1",
"d3-force-3d": "^3.0.6",
"next": "15.3.2",
"ohmy-ui": "^0.0.6",
"react": "^18",
"react-dom": "^18",
"next": "15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-force-graph-2d": "^1.27.1",
"tailwindcss": "^4.1.7",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.7",
"@types/culori": "^4.0.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^9.0.8",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"eslint": "^9",
"eslint-config-next": "^15.3.3",
"eslint-config-prettier": "^10.1.5",
"tailwindcss": "^4.1.7",
"typescript": "^5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 694.2 120" style="enable-background:new 0 0 694.2 120;" xml:space="preserve">
<style type="text/css">
.st0{fill:#012447;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#3793EF;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#012447;}
.st3{fill-rule:evenodd;clip-rule:evenodd;fill:#1560AB;}
</style>
<g>
<path class="st0" d="M160.2,19.96h32.4c27.48,0,40.1,13.96,40.1,39.23s-15.14,40.56-40.1,40.56h-32.4V19.96z M175.33,32.59v54.65
h16.07c18.72,0,26.15-10.11,26.15-28.06c0-18.75-7.83-26.6-26.95-26.6H175.33z"/>
<path class="st0" d="M240.25,69.29c0-21.14,11.95-33.64,31.47-33.64c18.85,0,28.01,11.44,28.01,28.59c0,2.39,0,5.19-0.26,8.38
h-45.01c1.06,11.3,6.77,17.02,17.39,17.02c9.96,0,13.41-4.79,15.13-10.77l12.22,3.46c-2.92,11.3-10.75,19.02-27.48,19.02
C252.6,101.34,240.25,90.17,240.25,69.29z M254.59,63.04h32c-0.53-10.77-5.31-16.36-15.27-16.36
C261.63,46.69,255.92,51.87,254.59,63.04z"/>
<path class="st0" d="M307.7,69.29c0-21.14,11.95-33.64,31.47-33.64c18.85,0,28.01,11.44,28.01,28.59c0,2.39,0,5.19-0.26,8.38
h-45.01c1.06,11.3,6.77,17.02,17.39,17.02c9.96,0,13.41-4.79,15.13-10.77l12.22,3.46c-2.92,11.3-10.75,19.02-27.48,19.02
C320.05,101.34,307.7,90.17,307.7,69.29z M322.04,63.04h32c-0.53-10.77-5.31-16.36-15.27-16.36
C329.08,46.69,323.37,51.87,322.04,63.04z"/>
<path class="st0" d="M415.64,101.47c-11.42,0-18.72-7.45-21.77-18.35h-0.93v36.83h-14.21V37.25h14.21v16.22h0.93
c3.32-10.77,10.22-17.82,21.51-17.82c14.74,0,22.97,11.7,22.97,32.98S429.72,101.47,415.64,101.47z M424.27,68.63
c0-13.56-4.78-20.08-15.27-20.08c-9.82,0-16.06,6.92-16.06,17.82v4.79c0,10.51,6.37,17.69,15.93,17.69
C419.36,88.84,424.27,82.06,424.27,68.63z"/>
<path class="st0" d="M463.84,99.74h-14.21v-62.5h13.94v16.49h1.2c3.45-11.57,11.42-18.09,21.91-18.09
c13.01,0,19.25,8.78,19.25,22.34v41.76h-14.21V61.71c0-7.58-3.32-13.17-11.95-13.17c-9.56,0-15.93,6.38-15.93,15.82V99.74z"/>
<path class="st0" d="M516.55,68.36c0-20.61,12.35-32.71,31.73-32.71c19.12,0,31.47,12.1,31.47,32.71
c0,20.48-11.68,33.11-31.47,33.11C528.23,101.47,516.55,88.84,516.55,68.36z M530.89,68.23c0,12.9,5.31,20.88,17.26,20.88
c11.82,0,17.39-7.98,17.39-20.88s-5.84-20.48-17.39-20.48S530.89,55.33,530.89,68.23z"/>
<path class="st0" d="M594.62,48.81h-8.76V37.25h8.76V20.36h14.34v16.89h18.32v11.57h-18.32v29.92c0,6.12,2.26,9.31,8.5,9.31
c3.19,0,5.97-0.66,9.03-1.6l1.73,12.77c-4.65,1.46-7.83,2.26-14.07,2.26c-13.67,0-19.52-9.04-19.52-20.74V48.81z"/>
<path class="st0" d="M634.72,69.29c0-21.14,11.95-33.64,31.47-33.64c18.85,0,28.01,11.44,28.01,28.59c0,2.39,0,5.19-0.26,8.38
h-45.01c1.06,11.3,6.77,17.02,17.39,17.02c9.96,0,13.41-4.79,15.13-10.77l12.22,3.46c-2.92,11.3-10.75,19.02-27.48,19.02
C647.06,101.34,634.72,90.17,634.72,69.29z M649.06,63.04h32c-0.53-10.77-5.31-16.36-15.27-16.36
C656.09,46.69,650.38,51.87,649.06,63.04z"/>
</g>
<g>
<path class="st1" d="M42.35,0c-2.18,0.01-3.14,0-3.14,0C23.9,22.23,15.31,34.69,0,56.92c0,0,19.14,0,36.08,0
c3.07,0,6.44,0.11,9.91,0.45L35.29,72.31c0,0,0,0,0,0l0,0L0,120h50.2l0,0l0,0c0,0,0.03-0.01,0.05-0.01c-0.02,0-0.05,0.01-0.05,0.01
s69.8-2.14,69.8-63.4C120,2.93,57.73-0.06,42.35,0z"/>
<polygon class="st2" points="50.2,120 50.2,120 50.2,120 "/>
<path class="st3" d="M50.2,120c0,0,29.15-5.48,29.15-32.64c0-22.25-17.66-28.47-33.36-29.99L35.29,72.31c0,0,0,0,0,0l0,0L0,120
H50.2L50.2,120 M65.43,105.99c0.03-0.05,0.06-0.09,0.09-0.13C65.49,105.9,65.46,105.95,65.43,105.99z"/>
<g>
<path class="st2" d="M35.29,72.31c22.15,0,36.64,10.38,33.71,24.93C66.06,111.79,50.2,120,50.2,120c0,0,29.15-5.48,29.15-32.64
c0-22.25-17.66-28.47-33.36-29.99L35.29,72.31z"/>
<polygon class="st2" points="50.2,120 50.2,120 50.2,120 "/>
</g>
<path class="st1" d="M0,56.92C15.31,34.69,23.9,22.23,39.22,0c0,0,0.96,0.01,3.14,0C57.73-0.06,120,2.93,120,56.6
c0,61.27-69.8,63.4-69.8,63.4s29.15-5.47,29.15-32.64c0-27.17-26.33-30.44-43.27-30.44S0,56.92,0,56.92z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,54 @@
"use client";
import { useCallback, useImperativeHandle, useState } from "react";
type ActivityLog = {
id: string;
timestamp: number;
activity: string;
};
export interface ActivityLogAPI {
updateActivityLog: (activityLog: ActivityLog[]) => void;
}
interface ActivityLogProps {
ref: React.RefObject<ActivityLogAPI>;
}
const formatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "short", timeStyle: "medium" });
export default function ActivityLog({ ref }: ActivityLogProps) {
const [activityLog, updateActivityLog] = useState<ActivityLog[]>([]);
const handleActivityLogUpdate = useCallback(
(newActivities: ActivityLog[]) => {
updateActivityLog([...activityLog, ...newActivities]);
const activityLogContainer = document.getElementById("activityLogContainer");
if (activityLogContainer) {
activityLogContainer.scrollTo({ top: 0, behavior: "smooth" });
}
},
[activityLog],
);
useImperativeHandle(ref, () => ({
updateActivityLog: handleActivityLogUpdate,
}));
return (
<div className="overflow-y-auto max-h-96" id="activityLogContainer">
<div className="flex flex-col-reverse gap-2">
{activityLog.map((activity) => (
<div key={activity.id} className="flex gap-2 items-top">
<span className="flex-1/3 text-xs text-gray-300 whitespace-nowrap mt-1.5">{formatter.format(activity.timestamp)}: </span>
<span className="flex-2/3 text-white whitespace-normal">{activity.activity}</span>
</div>
))}
{!activityLog.length && <span className="text-white">No activity logged.</span>}
</div>
</div>
);
}

View file

@ -1,27 +1,36 @@
"use client";
import { ChangeEvent, useEffect } from "react";
import { CTAButton, StatusIndicator } from "@/ui/elements";
import { SearchView } from "@/ui/Partials";
import { LoadingIndicator } from "@/ui/App";
import { AddIcon, SearchIcon } from "@/ui/Icons";
import { CTAButton, GhostButton, Modal, NeutralButton, StatusIndicator } from "@/ui/elements";
import { useBoolean } from "@/utils";
import addData from "@/modules/ingestion/addData";
import cognifyDataset from "@/modules/datasets/cognifyDataset";
import useDatasets from "@/modules/ingestion/useDatasets";
import getDatasetGraph from '@/modules/datasets/getDatasetGraph';
import createDataset from "@/modules/datasets/createDataset";
import getDatasetGraph from "@/modules/datasets/getDatasetGraph";
import useDatasets, { Dataset } from "@/modules/ingestion/useDatasets";
export interface NodesAndEdges {
export interface NodesAndLinks {
nodes: { id: string; label: string }[];
links: { source: string; target: string; label: string }[];
}
export interface NodesAndEdges {
nodes: { id: string; label: string }[];
edges: { source: string; target: string; label: string }[];
}
interface CogneeAddWidgetProps {
onData: (data: NodesAndEdges) => void;
onData: (data: NodesAndLinks) => void;
}
export default function CogneeAddWidget({ onData }: CogneeAddWidgetProps) {
const {
datasets,
addDataset,
removeDataset,
refreshDatasets,
} = useDatasets();
@ -38,52 +47,113 @@ export default function CogneeAddWidget({ onData }: CogneeAddWidgetProps) {
}));
}
});
}, [refreshDatasets]);
}, [onData, refreshDatasets]);
const handleAddFiles = (dataset: { id?: string, name?: string }, event: ChangeEvent<HTMLInputElement>) => {
const {
value: isProcessingFiles,
setTrue: setProcessingFilesInProgress,
setFalse: setProcessingFilesDone,
} = useBoolean(false);
const handleAddFiles = (dataset: Dataset, event: ChangeEvent<HTMLInputElement>) => {
event.stopPropagation();
if (!event.currentTarget.files) {
throw new Error("Error: No files added to the uploader input.");
if (isProcessingFiles) {
return;
}
const files: File[] = Array.from(event.currentTarget.files);
setProcessingFilesInProgress();
if (!event.target.files) {
return;
}
const files: File[] = Array.from(event.target.files);
if (!files.length) {
return;
}
return addData(dataset, files)
.then(() => {
console.log("Data added successfully.");
const onUpdate = (data: any) => {
const onUpdate = (data: NodesAndEdges) => {
onData({
nodes: data.payload.nodes,
links: data.payload.edges,
nodes: data.nodes,
links: data.edges,
});
setProcessingFilesDone();
};
return cognifyDataset(dataset, onUpdate)
.then((data) => console.log(data));
.then(() => {
refreshDatasets();
});
});
};
const handleAddFilesNoDataset = (event: ChangeEvent<HTMLInputElement>) => {
event.stopPropagation();
if (isProcessingFiles) {
return;
}
setProcessingFilesInProgress();
createDataset({ name: "main_dataset" })
.then((newDataset: Dataset) => {
return handleAddFiles(newDataset, event);
});
};
const {
value: isSearchModalOpen,
setTrue: openSearchModal,
setFalse: closeSearchModal,
} = useBoolean(false);
const handleSearchClick = () => {
openSearchModal();
};
return (
<div className="flex flex-col gap-4 mb-4">
<div className="flex flex-col gap-4">
{datasets.length ? datasets.map((dataset) => (
<div key={dataset.id} className="flex gap-8 items-center">
<div key={dataset.id} className="flex gap-8 items-center justify-between">
<div className="flex flex-row gap-4 items-center">
<StatusIndicator status={dataset.status} />
<span className="text-white">{dataset.name}</span>
</div>
<CTAButton type="button" className="relative">
<input type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>+ Add Data</span>
</CTAButton>
<div className="flex gap-2">
<CTAButton type="button" className="relative">
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span className="flex flex-row gap-2 items-center">
<AddIcon />
{isProcessingFiles && <LoadingIndicator />}
</span>
</CTAButton>
<NeutralButton onClick={handleSearchClick} type="button">
<SearchIcon />
</NeutralButton>
</div>
</div>
)) : (
<CTAButton type="button" className="relative">
<input type="file" multiple onChange={handleAddFiles.bind(null, { name: "main_dataset" })} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>+ Add Data</span>
<CTAButton type="button" className="relative" disabled={isProcessingFiles}>
<input disabled={isProcessingFiles} tabIndex={-1} type="file" multiple onChange={handleAddFilesNoDataset} className="absolute w-full h-full cursor-pointer opacity-0" />
<span className="flex flex-row gap-2 items-center">
+ Add Data
{isProcessingFiles && <LoadingIndicator />}
</span>
</CTAButton>
)}
<Modal isOpen={isSearchModalOpen}>
<div className="relative w-full max-w-3xl h-full max-h-5/6">
<GhostButton onClick={closeSearchModal} className="absolute right-2 top-2">
<AddIcon className="rotate-45" />
</GhostButton>
<SearchView />
</div>
</Modal>
</div>
);
}

View file

@ -1,4 +1,7 @@
import { useState } from "react";
import { fetch } from "@/utils";
import { v4 as uuid4 } from "uuid";
import { LoadingIndicator } from "@/ui/App";
import { CTAButton, Input } from "@/ui/elements";
interface CrewAIFormPayload extends HTMLFormElement {
@ -6,21 +9,108 @@ interface CrewAIFormPayload extends HTMLFormElement {
username2: HTMLInputElement;
}
export default function CrewAITrigger() {
interface CrewAITriggerProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onData: (data: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onActivity: (activities: any) => void;
}
export default function CrewAITrigger({ onData, onActivity }: CrewAITriggerProps) {
const [isCrewAIRunning, setIsCrewAIRunning] = useState(false);
const handleRunCrewAI = (event: React.FormEvent<CrewAIFormPayload>) => {
fetch("/v1/crew-ai/run", {
event.preventDefault();
const formElements = event.currentTarget;
const crewAIConfig = {
username1: formElements.username1.value,
username2: formElements.username2.value,
};
const websocket = new WebSocket("ws://localhost:8000/api/v1/crewai/subscribe");
onActivity([{ id: uuid4(), timestamp: Date.now(), activity: "Dispatching hiring crew agents" }]);
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.status === "PipelineRunActivity") {
onActivity([data.payload]);
return;
}
onData({
nodes: data.payload.nodes,
links: data.payload.edges,
});
const nodes_type_map: { [key: string]: number } = {};
for (let i = 0; i < data.payload.nodes.length; i++) {
const node = data.payload.nodes[i];
if (!nodes_type_map[node.type]) {
nodes_type_map[node.type] = 0;
}
nodes_type_map[node.type] += 1;
}
const activityMessage = Object.entries(nodes_type_map).reduce((message, [type, count]) => {
return `${message}\n | ${type}: ${count}`;
}, "Graph updated:");
onActivity([{
id: uuid4(),
timestamp: Date.now(),
activity: activityMessage,
}]);
if (data.status === "PipelineRunCompleted") {
websocket.close();
}
};
onData(null);
setIsCrewAIRunning(true);
return fetch("/v1/crewai/run", {
method: "POST",
body: new FormData(event.currentTarget),
body: JSON.stringify(crewAIConfig),
headers: {
"Content-Type": "application/json",
},
})
.then(response => response.json())
.then((data) => console.log(data));
.then(() => {
onActivity([{ id: uuid4(), timestamp: Date.now(), activity: "Hiring crew agents made a decision" }]);
})
.catch(() => {
onActivity([{ id: uuid4(), timestamp: Date.now(), activity: "Hiring crew agents had problems while executing" }]);
})
.finally(() => {
websocket.close();
setIsCrewAIRunning(false);
});
};
return (
<form className="w-full flex flex-row gap-2 items-center" onSubmit={handleRunCrewAI}>
<Input type="text" placeholder="Github Username" required />
<Input type="text" placeholder="Github Username" required />
<CTAButton type="submit" className="whitespace-nowrap">Run CrewAI</CTAButton>
<form className="w-full flex flex-col gap-2" onSubmit={handleRunCrewAI}>
<h1 className="text-2xl text-white">Cognee Dev Mexican Standoff</h1>
<span className="text-white">Agents compare GitHub profiles, and make a decision who is a better developer</span>
<div className="flex flex-row gap-2">
<div className="flex flex-col w-full flex-1/2">
<label className="block mb-1 text-white" htmlFor="username1">GitHub username</label>
<Input name="username1" type="text" placeholder="Github Username" required defaultValue="hajdul88" />
</div>
<div className="flex flex-col w-full flex-1/2">
<label className="block mb-1 text-white" htmlFor="username2">GitHub username</label>
<Input name="username2" type="text" placeholder="Github Username" required defaultValue="lxobr" />
</div>
</div>
<CTAButton type="submit" disabled={isCrewAIRunning} className="whitespace-nowrap">
Start Mexican Standoff
{isCrewAIRunning && <LoadingIndicator />}
</CTAButton>
</form>
);
}

View file

@ -1,15 +1,19 @@
"use client";
import { v4 as uuid4 } from "uuid";
import classNames from "classnames";
import { NodeObject } from "react-force-graph-2d";
import { ChangeEvent, useImperativeHandle, useState } from "react";
// import classNames from "classnames";
import { NodeObject, LinkObject } from "react-force-graph-2d";
import { ChangeEvent, useEffect, useImperativeHandle, useRef, useState } from "react";
import { DeleteIcon } from "@/ui/Icons";
import { FeedbackForm } from "@/ui/Partials";
// import { FeedbackForm } from "@/ui/Partials";
import { CTAButton, Input, NeutralButton, Select } from "@/ui/elements";
interface GraphControlsProps {
data?: {
nodes: NodeObject[];
links: LinkObject[];
};
isAddNodeFormOpen: boolean;
ref: React.RefObject<GraphControlsAPI>;
onFitIntoView: () => void;
@ -21,29 +25,55 @@ export interface GraphControlsAPI {
getSelectedNode: () => NodeObject | null;
}
type ActivityLog = {
id: string;
timestamp: number;
activity: string;
}[];
// type ActivityLog = {
// id: string;
// timestamp: number;
// activity: string;
// };
type NodeProperties = {
type NodeProperty = {
id: string;
name: string;
value: string;
}[];
};
export default function GraphControls({ isAddNodeFormOpen, onGraphShapeChange, onFitIntoView, ref }: GraphControlsProps) {
// const formatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "short", timeStyle: "medium" });
const DEFAULT_GRAPH_SHAPE = "lr";
const GRAPH_SHAPES = [{
value: "none",
label: "None",
}, {
value: "td",
label: "Top-down",
}, {
value: "bu",
label: "Bottom-up",
}, {
value: "lr",
label: "Left-right",
}, {
value: "rl",
label: "Right-left",
}, {
value: "radialin",
label: "Radial-in",
}, {
value: "radialout",
label: "Radial-out",
}];
export default function GraphControls({ data, isAddNodeFormOpen, onGraphShapeChange, onFitIntoView, ref }: GraphControlsProps) {
const [selectedNode, setSelectedNode] = useState<NodeObject | null>(null);
const [activityLog, setActivityLog] = useState<ActivityLog>([]);
const [nodeProperties, setNodeProperties] = useState<NodeProperties>([]);
const [newProperty, setNewProperty] = useState<NodeProperties[0]>({
const [nodeProperties, setNodeProperties] = useState<NodeProperty[]>([]);
const [newProperty, setNewProperty] = useState<NodeProperty>({
id: uuid4(),
name: "",
value: "",
});
const handlePropertyChange = (property: NodeProperties[0], property_key: string, event: ChangeEvent<HTMLInputElement>) => {
const handlePropertyChange = (property: NodeProperty, property_key: string, event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setNodeProperties(nodeProperties.map((nodeProperty) => (nodeProperty.id === property.id ? {...nodeProperty, [property_key]: value } : nodeProperty)));
@ -58,11 +88,11 @@ export default function GraphControls({ isAddNodeFormOpen, onGraphShapeChange, o
}
};
const handlePropertyDelete = (property: NodeProperties[0]) => {
const handlePropertyDelete = (property: NodeProperty) => {
setNodeProperties(nodeProperties.filter((nodeProperty) => nodeProperty.id !== property.id));
};
const handleNewPropertyChange = (property: NodeProperties[0], property_key: string, event: ChangeEvent<HTMLInputElement>) => {
const handleNewPropertyChange = (property: NodeProperty, property_key: string, event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setNewProperty({...property, [property_key]: value });
@ -73,111 +103,139 @@ export default function GraphControls({ isAddNodeFormOpen, onGraphShapeChange, o
getSelectedNode: () => selectedNode,
}));
const [selectedTab, setSelectedTab] = useState("nodeDetails");
// const [selectedTab, setSelectedTab] = useState("nodeDetails");
const handleGraphShapeControl = (event: ChangeEvent<HTMLSelectElement>) => {
setIsAuthShapeChangeEnabled(false);
onGraphShapeChange(event.target.value);
};
const [isAuthShapeChangeEnabled, setIsAuthShapeChangeEnabled] = useState(true);
const shapeChangeTimeout = useRef<number | null>();
useEffect(() => {
onGraphShapeChange(DEFAULT_GRAPH_SHAPE);
const graphShapesNum = GRAPH_SHAPES.length;
function switchShape(shapeIndex: number) {
if (!isAuthShapeChangeEnabled || !data) {
if (shapeChangeTimeout.current) {
clearTimeout(shapeChangeTimeout.current);
shapeChangeTimeout.current = null;
}
return;
}
shapeChangeTimeout.current = setTimeout(() => {
const newValue = GRAPH_SHAPES[shapeIndex].value;
onGraphShapeChange(newValue);
const graphShapeSelectElement = document.getElementById("graph-shape-select") as HTMLSelectElement;
graphShapeSelectElement.value = newValue;
switchShape((shapeIndex + 1) % graphShapesNum);
}, 5000) as unknown as number;
};
switchShape(0);
setTimeout(() => {
onFitIntoView();
}, 500);
return () => {
if (shapeChangeTimeout.current) {
clearTimeout(shapeChangeTimeout.current);
shapeChangeTimeout.current = null;
}
};
}, [data, isAuthShapeChangeEnabled, onFitIntoView, onGraphShapeChange]);
return (
<>
<div className="flex">
<button onClick={() => setSelectedTab("nodeDetails")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30", { "border-b-indigo-600 text-white": selectedTab === "nodeDetails" })}>
<span className="whitespace-nowrap">Node Details</span>
</button>
<button onClick={() => setSelectedTab("activityLog")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30", { "border-b-indigo-600 text-white": selectedTab === "activityLog" })}>
<span className="whitespace-nowrap">Activity Log</span>
</button>
<button onClick={() => setSelectedTab("feedback")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30", { "border-b-indigo-600 text-white": selectedTab === "feedback" })}>
<div className="flex w-full">
{/* <button onClick={() => setSelectedTab("nodeDetails")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30 flex-1/3", { "border-b-indigo-600 text-white": selectedTab === "nodeDetails" })}> */}
<span className="whitespace-nowrap text-white">Node Details</span>
{/* </button> */}
{/* <button onClick={() => setSelectedTab("feedback")} className={classNames("cursor-pointer pt-4 pb-4 align-center text-gray-300 border-b-2 w-30 flex-1/3", { "border-b-indigo-600 text-white": selectedTab === "feedback" })}>
<span className="whitespace-nowrap">Feedback</span>
</button>
</button> */}
</div>
<div className="pt-4">
{selectedTab === "nodeDetails" && (
<>
<div className="w-full flex flex-row gap-2 items-center mb-4">
<label className="text-gray-300 whitespace-nowrap">Graph Shape:</label>
<Select onChange={handleGraphShapeControl}>
<option selected value="none">None</option>
<option value="td">Top-down</option>
<option value="bu">Bottom-up</option>
<option value="lr">Left-right</option>
<option value="rl">Right-left</option>
<option value="radialin">Radial-in</option>
<option value="radialout">Radial-out</option>
</Select>
</div>
{/* {selectedTab === "nodeDetails" && ( */}
<>
<div className="w-full flex flex-row gap-2 items-center mb-4">
<label className="text-gray-300 whitespace-nowrap flex-1/5">Graph Shape:</label>
<Select defaultValue={DEFAULT_GRAPH_SHAPE} onChange={handleGraphShapeControl} id="graph-shape-select" className="flex-2/5">
{GRAPH_SHAPES.map((shape) => (
<option key={shape.value} value={shape.value}>{shape.label}</option>
))}
</Select>
<NeutralButton onClick={onFitIntoView} className="flex-2/5 whitespace-nowrap">Fit Graph into View</NeutralButton>
</div>
<NeutralButton onClick={onFitIntoView} className="mb-4">Fit Graph into View</NeutralButton>
{isAddNodeFormOpen ? (
<form className="flex flex-col gap-4" onSubmit={() => {}}>
<div className="flex flex-row gap-4 items-center">
<span className="text-gray-300 whitespace-nowrap">Source Node ID:</span>
<Input readOnly type="text" defaultValue={selectedNode!.id} />
{isAddNodeFormOpen ? (
<form className="flex flex-col gap-4" onSubmit={() => {}}>
<div className="flex flex-row gap-4 items-center">
<span className="text-gray-300 whitespace-nowrap">Source Node ID:</span>
<Input readOnly type="text" defaultValue={selectedNode!.id} />
</div>
<div className="flex flex-col gap-4 items-end">
{nodeProperties.map((property) => (
<div key={property.id} className="w-full flex flex-row gap-2 items-center">
<Input className="flex-1/3" type="text" placeholder="Property name" required value={property.name} onChange={handlePropertyChange.bind(null, property, "name")} />
<Input className="flex-2/3" type="text" placeholder="Property value" required value={property.value} onChange={handlePropertyChange.bind(null, property, "value")} />
<button className="border-1 border-white p-2 rounded-sm" onClick={handlePropertyDelete.bind(null, property)}>
<DeleteIcon width={16} height={18} color="white" />
</button>
</div>
))}
<div className="w-full flex flex-row gap-2 items-center">
<Input className="flex-1/3" type="text" placeholder="Property name" required value={newProperty.name} onChange={handleNewPropertyChange.bind(null, newProperty, "name")} />
<Input className="flex-2/3" type="text" placeholder="Property value" required value={newProperty.value} onChange={handleNewPropertyChange.bind(null, newProperty, "value")} />
<NeutralButton type="button" className="" onClick={handlePropertyAdd}>Add</NeutralButton>
</div>
<div className="flex flex-col gap-4 items-end">
{nodeProperties.map((property) => (
<div key={property.id} className="w-full flex flex-row gap-2 items-center">
<Input className="flex-1/3" type="text" placeholder="Property name" required value={property.name} onChange={handlePropertyChange.bind(null, property, "name")} />
<Input className="flex-2/3" type="text" placeholder="Property value" required value={property.value} onChange={handlePropertyChange.bind(null, property, "value")} />
<button className="border-1 border-white p-2 rounded-sm" onClick={handlePropertyDelete.bind(null, property)}>
<DeleteIcon width={16} height={18} color="white" />
</button>
</div>
<CTAButton type="submit">Add Node</CTAButton>
</form>
) : (
selectedNode ? (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2 overflow-y-auto max-h-96 pr-2">
<div className="flex gap-2 items-top">
<span className="text-gray-300">ID:</span>
<span className="text-white">{selectedNode.id}</span>
</div>
<div className="flex gap-2 items-top">
<span className="text-gray-300">Label:</span>
<span className="text-white">{selectedNode.label}</span>
</div>
{Object.entries(selectedNode.properties).map(([key, value]) => (
<div key={key} className="flex gap-2 items-top">
<span className="text-gray-300">{key.charAt(0).toUpperCase() + key.slice(1)}:</span>
<span className="text-white">{typeof value === "object" ? JSON.stringify(value) : value as string}</span>
</div>
))}
<div className="w-full flex flex-row gap-2 items-center">
<Input className="flex-1/3" type="text" placeholder="Property name" required value={newProperty.name} onChange={handleNewPropertyChange.bind(null, newProperty, "name")} />
<Input className="flex-2/3" type="text" placeholder="Property value" required value={newProperty.value} onChange={handleNewPropertyChange.bind(null, newProperty, "value")} />
<NeutralButton type="button" className="" onClick={handlePropertyAdd}>Add</NeutralButton>
</div>
</div>
<CTAButton type="submit">Add Node</CTAButton>
</form>
) : (
selectedNode ? (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<span className="text-gray-300">ID:</span>
<span className="text-white">{selectedNode.id}</span>
</div>
{Object.entries(selectedNode.properties).map(([key, value]) => (
<div key={key} className="flex gap-2 items-center">
<span className="text-gray-300">{key}:</span>
<span className="text-white">{typeof value === "object" ? JSON.stringify(value) : value as string}</span>
</div>
))}
</div>
<CTAButton type="button" onClick={() => {}}>Edit Node</CTAButton>
</div>
) : (
<span className="text-white">No node selected.</span>
)
)}
</>
)}
{selectedTab === "activityLog" && (
<div className="flex flex-col gap-2">
{activityLog.map((activity) => (
<div key={activity.id} className="flex gap-2 items-center">
<span className="text-gray-300">{activity.timestamp}</span>
<span className="text-white">{activity.activity}</span>
{/* <CTAButton type="button" onClick={() => {}}>Edit Node</CTAButton> */}
</div>
))}
{!activityLog.length && <span className="text-white">No activity logged.</span>}
</div>
)}
) : (
<span className="text-white">No node selected.</span>
)
)}
</>
{/* )} */}
{selectedTab === "feedback" && (
{/* {selectedTab === "feedback" && (
<div className="flex flex-col gap-2">
<FeedbackForm onSuccess={() => {}} />
</div>
)}
)} */}
</div>
</>
);

View file

@ -0,0 +1,25 @@
import { NodeObject } from "react-force-graph-2d";
import getColorForNodeType from './getColorForNodeType';
interface GraphLegendProps {
data?: NodeObject[];
}
export default function GraphLegend({ data }: GraphLegendProps) {
const legend: Set<string> = new Set();
for (let i = 0; i < Math.min(data?.length || 0, 100); i++) {
legend.add(data![i].type);
}
return (
<div className="flex flex-col gap-1">
{Array.from(legend).map((nodeType) => (
<div key={nodeType} className="flex flex-row items-center gap-2">
<span className="w-2 h-2 rounded-2xl" style={{ backgroundColor: getColorForNodeType(nodeType) }} />
<span className="text-white">{nodeType}</span>
</div>
))}
</div>
);
}

View file

@ -1,24 +1,24 @@
"use client";
import { forceCollide, forceManyBody } from "d3-force-3d";
import { useEffect, useRef, useState } from "react";
import ForceGraph, { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-2d";
import { useCallback, useRef, useState, MutableRefObject } from "react";
import Link from "next/link";
import { TextLogo } from "@/ui/App";
import { Divider } from "@/ui/Layout";
import { Footer } from "@/ui/Partials";
import CrewAITrigger from "./CrewAITrigger";
import CogneeAddWidget, { NodesAndEdges } from "./CogneeAddWidget";
import GraphLegend from "./GraphLegend";
import { DiscordIcon, GithubIcon } from "@/ui/Icons";
import ActivityLog, { ActivityLogAPI } from "./ActivityLog";
import GraphControls, { GraphControlsAPI } from "./GraphControls";
import CogneeAddWidget, { NodesAndLinks } from "./CogneeAddWidget";
import GraphVisualization, { GraphVisualizationAPI } from "./GraphVisualization";
import { useBoolean } from "@/utils";
// import exampleData from "./example_data.json";
interface GraphNode {
id: string | number;
label: string;
properties?: {};
properties?: object;
}
interface GraphData {
@ -29,246 +29,95 @@ interface GraphData {
export default function GraphView() {
const {
value: isAddNodeFormOpen,
setTrue: enableAddNodeForm,
setFalse: disableAddNodeForm,
} = useBoolean(false);
const [data, updateData] = useState<GraphData | null>(null);
const [data, updateData] = useState<GraphData>();
const onDataChange = (newData: NodesAndEdges) => {
if (data === null) {
updateData({
nodes: newData.nodes,
links: newData.links,
});
} else {
updateData({
nodes: [...data.nodes, ...newData.nodes],
links: [...data.links, ...newData.links],
});
}
};
const graphRef = useRef<ForceGraphMethods>();
const graphControls = useRef<GraphControlsAPI>(null);
const handleNodeClick = (node: NodeObject) => {
graphControls.current?.setSelectedNode(node);
graphRef.current?.d3ReheatSimulation();
};
const textSize = 6;
const nodeSize = 15;
const addNodeDistanceFromSourceNode = 15;
const handleBackgroundClick = (event: MouseEvent) => {
const graphBoundingBox = document.getElementById("graph-container")?.querySelector("canvas")?.getBoundingClientRect();
const x = event.clientX - graphBoundingBox!.x;
const y = event.clientY - graphBoundingBox!.y;
const graphClickCoords = graphRef.current!.screen2GraphCoords(x, y);
const selectedNode = graphControls.current?.getSelectedNode();
if (!selectedNode) {
const onDataChange = useCallback((newData: NodesAndLinks) => {
if (newData === null) {
// Requests for resetting the data
updateData(undefined);
return;
}
const distanceFromAddNode = Math.sqrt(
Math.pow(graphClickCoords.x - (selectedNode!.x! + addNodeDistanceFromSourceNode), 2)
+ Math.pow(graphClickCoords.y - (selectedNode!.y! + addNodeDistanceFromSourceNode), 2)
);
if (distanceFromAddNode <= 10) {
enableAddNodeForm();
} else {
disableAddNodeForm();
graphControls.current?.setSelectedNode(null);
}
};
function renderNode(node: NodeObject, ctx: CanvasRenderingContext2D, globalScale: number) {
const selectedNode = graphControls.current?.getSelectedNode();
ctx.save();
if (node.id === selectedNode?.id) {
ctx.fillStyle = "gray";
ctx.beginPath();
ctx.arc(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode, 10, 0, 2 * Math.PI);
ctx.fill();
ctx.beginPath();
ctx.moveTo(node.x! + addNodeDistanceFromSourceNode - 5, node.y! + addNodeDistanceFromSourceNode)
ctx.lineTo(node.x! + addNodeDistanceFromSourceNode - 5 + 10, node.y! + addNodeDistanceFromSourceNode);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode - 5)
ctx.lineTo(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode - 5 + 10);
ctx.stroke();
if (!newData.nodes.length && !newData.links.length) {
return;
}
// ctx.beginPath();
// ctx.arc(node.x, node.y, nodeSize, 0, 2 * Math.PI);
// ctx.fill();
updateData(newData);
}, []);
// draw text label (with background rect)
const textPos = {
x: node.x!,
y: node.y!,
};
ctx.translate(textPos.x, textPos.y);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#333333";
ctx.font = `${textSize}px Sans-Serif`;
ctx.fillText(node.label, 0, 0);
const graphRef = useRef<GraphVisualizationAPI>();
ctx.restore();
}
const graphControls = useRef<GraphControlsAPI>();
function renderLink(link: LinkObject, ctx: CanvasRenderingContext2D) {
const MAX_FONT_SIZE = 4;
const LABEL_NODE_MARGIN = nodeSize * 1.5;
const activityLog = useRef<ActivityLogAPI>();
const start = link.source;
const end = link.target;
// ignore unbound links
if (typeof start !== "object" || typeof end !== "object") return;
const textPos = {
x: start.x! + (end.x! - start.x!) / 2,
y: start.y! + (end.y! - start.y!) / 2,
};
const relLink = { x: end.x! - start.x!, y: end.y! - start.y! };
const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2;
let textAngle = Math.atan2(relLink.y, relLink.x);
// maintain label vertical orientation for legibility
if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle);
if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle);
const label = link.label
// estimate fontSize to fit in link length
ctx.font = "1px Sans-Serif";
const fontSize = Math.min(MAX_FONT_SIZE, maxTextLength / ctx.measureText(label).width);
ctx.font = `${fontSize}px Sans-Serif`;
const textWidth = ctx.measureText(label).width;
const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
// draw text label (with background rect)
ctx.save();
ctx.translate(textPos.x, textPos.y);
ctx.rotate(textAngle);
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, bckgDimensions[0], bckgDimensions[1]);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "darkgrey";
ctx.fillText(label, 0, 0);
ctx.restore();
}
function handleDagError(loopNodeIds: (string | number)[]) {
console.log(loopNodeIds);
}
useEffect(() => {
// add collision force
graphRef.current!.d3Force("collision", forceCollide(nodeSize * 1.5));
graphRef.current!.d3Force("charge", forceManyBody().strength(-1500).distanceMin(300).distanceMax(900));
}, [data]);
const [graphShape, setGraphShape] = useState<string | undefined>(undefined);
return (
<main className="flex flex-col h-full">
<div className="pt-6 pr-3 pb-3 pl-6">
<div className="flex flex-row justify-between items-center pt-6 pr-6 pb-6 pl-6">
<TextLogo width={86} height={24} />
<span className="flex flex-row items-center gap-8">
<Link target="_blank" href="https://www.cognee.ai/">
<span>Cognee Home</span>
</Link>
<Link target="_blank" href="https://github.com/topoteretes/cognee">
<GithubIcon color="black" />
</Link>
<Link target="_blank" href="https://discord.gg/m63hxKsp4p">
<DiscordIcon color="black" />
</Link>
</span>
</div>
<Divider />
<div className="w-full h-full relative overflow-hidden">
<div className="w-full h-full" id="graph-container">
{data ? (
<ForceGraph
ref={graphRef}
dagMode={graphShape as undefined}
dagLevelDistance={300}
onDagError={handleDagError}
graphData={data}
<GraphVisualization
key={data?.nodes.length}
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
data={data}
graphControls={graphControls as MutableRefObject<GraphControlsAPI>}
/>
nodeLabel="label"
nodeRelSize={nodeSize}
nodeCanvasObject={renderNode}
nodeCanvasObjectMode={() => "after"}
nodeAutoColorBy="group"
<div className="absolute top-2 left-2 flex flex-col gap-2">
<div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-sm">
<CogneeAddWidget onData={onDataChange} />
</div>
{/* <div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-sm">
<CrewAITrigger onData={onDataChange} onActivity={(activities) => activityLog.current?.updateActivityLog(activities)} />
</div> */}
<div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-sm">
<h2 className="text-xl text-white mb-4">Activity Log</h2>
<ActivityLog ref={activityLog as MutableRefObject<ActivityLogAPI>} />
</div>
</div>
linkLabel="label"
linkCanvasObject={renderLink}
linkCanvasObjectMode={() => "after"}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
onNodeClick={handleNodeClick}
onBackgroundClick={handleBackgroundClick}
d3VelocityDecay={0.3}
/>
) : (
<ForceGraph
ref={graphRef}
dagMode="lr"
dagLevelDistance={100}
graphData={{
nodes: [{ id: 1, label: "Add" }, { id: 2, label: "Cognify" }, { id: 3, label: "Search" }],
links: [{ source: 1, target: 2, label: "but don't forget to" }, { source: 2, target: 3, label: "and after that you can" }],
}}
nodeLabel="label"
nodeRelSize={20}
nodeCanvasObject={renderNode}
nodeCanvasObjectMode={() => "after"}
nodeAutoColorBy="group"
linkLabel="label"
linkCanvasObject={renderLink}
linkCanvasObjectMode={() => "after"}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
<div className="absolute top-2 right-2 flex flex-col gap-2 items-end">
<div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-110">
<GraphControls
data={data}
ref={graphControls as MutableRefObject<GraphControlsAPI>}
isAddNodeFormOpen={isAddNodeFormOpen}
onFitIntoView={() => graphRef.current!.zoomToFit(1000, 50)}
onGraphShapeChange={(shape) => graphRef.current!.setGraphShape(shape)}
/>
</div>
{data?.nodes.length && (
<div className="bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md w-48">
<GraphLegend data={data?.nodes} />
</div>
)}
</div>
<div className="absolute top-2 left-2 bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md max-w-2xl">
<CogneeAddWidget onData={onDataChange} />
<CrewAITrigger />
</div>
<div className="absolute top-2 right-2 bg-gray-500 pt-4 pr-4 pb-4 pl-4 rounded-md">
<GraphControls
ref={graphControls}
isAddNodeFormOpen={isAddNodeFormOpen}
onFitIntoView={() => graphRef.current?.zoomToFit(1000, 50)}
onGraphShapeChange={setGraphShape}
/>
</div>
</div>
<Divider />
<div className="pl-6 pr-6">
<Footer>
<div className="flex flex-row items-center gap-6">
<span>Nodes: {data?.nodes.length}</span>
<span>Edges: {data?.links.length}</span>
</div>
{(data?.nodes.length || data?.links.length) && (
<div className="flex flex-row items-center gap-6">
<span>Nodes: {data?.nodes.length || 0}</span>
<span>Edges: {data?.links.length || 0}</span>
</div>
)}
</Footer>
</div>
</main>

View file

@ -0,0 +1,226 @@
"use client";
import { MutableRefObject, useEffect, useImperativeHandle, useRef, useState } from "react";
import { forceCollide, forceManyBody } from "d3-force-3d";
import ForceGraph, { ForceGraphMethods, GraphData, LinkObject, NodeObject } from "react-force-graph-2d";
import { GraphControlsAPI } from "./GraphControls";
import getColorForNodeType from "./getColorForNodeType";
interface GraphVisuzaliationProps {
ref: MutableRefObject<GraphVisualizationAPI>;
data?: GraphData<NodeObject, LinkObject>;
graphControls: MutableRefObject<GraphControlsAPI>;
}
export interface GraphVisualizationAPI {
zoomToFit: ForceGraphMethods["zoomToFit"];
setGraphShape: (shape: string) => void;
}
export default function GraphVisualization({ ref, data, graphControls }: GraphVisuzaliationProps) {
const textSize = 6;
const nodeSize = 15;
// const addNodeDistanceFromSourceNode = 15;
const handleNodeClick = (node: NodeObject) => {
graphControls.current?.setSelectedNode(node);
// ref.current?.d3ReheatSimulation()
}
const handleBackgroundClick = (/* event: MouseEvent */) => {
const selectedNode = graphControls.current?.getSelectedNode();
if (!selectedNode) {
return;
}
graphControls.current?.setSelectedNode(null);
// const graphBoundingBox = document.getElementById("graph-container")?.querySelector("canvas")?.getBoundingClientRect();
// const x = event.clientX - graphBoundingBox!.x;
// const y = event.clientY - graphBoundingBox!.y;
// const graphClickCoords = graphRef.current!.screen2GraphCoords(x, y);
// const distanceFromAddNode = Math.sqrt(
// Math.pow(graphClickCoords.x - (selectedNode!.x! + addNodeDistanceFromSourceNode), 2)
// + Math.pow(graphClickCoords.y - (selectedNode!.y! + addNodeDistanceFromSourceNode), 2)
// );
// if (distanceFromAddNode <= 10) {
// enableAddNodeForm();
// } else {
// disableAddNodeForm();
// graphControls.current?.setSelectedNode(null);
// }
};
function renderNode(node: NodeObject, ctx: CanvasRenderingContext2D, globalScale: number, renderType: string = "replace") {
// const selectedNode = graphControls.current?.getSelectedNode();
ctx.save();
// if (node.id === selectedNode?.id) {
// ctx.fillStyle = "gray";
// ctx.beginPath();
// ctx.arc(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode, 10, 0, 2 * Math.PI);
// ctx.fill();
// ctx.beginPath();
// ctx.moveTo(node.x! + addNodeDistanceFromSourceNode - 5, node.y! + addNodeDistanceFromSourceNode)
// ctx.lineTo(node.x! + addNodeDistanceFromSourceNode - 5 + 10, node.y! + addNodeDistanceFromSourceNode);
// ctx.stroke();
// ctx.beginPath();
// ctx.moveTo(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode - 5)
// ctx.lineTo(node.x! + addNodeDistanceFromSourceNode, node.y! + addNodeDistanceFromSourceNode - 5 + 10);
// ctx.stroke();
// }
if (renderType === "replace") {
ctx.beginPath();
ctx.fillStyle = getColorForNodeType(node.type);
ctx.arc(node.x!, node.y!, nodeSize, 0, 2 * Math.PI);
ctx.fill();
}
// draw text label (with background rect)
const textPos = {
x: node.x!,
y: node.y!,
};
ctx.translate(textPos.x, textPos.y);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#333333";
ctx.font = `${textSize}px Sans-Serif`;
ctx.fillText(node.label, 0, 0);
ctx.restore();
}
function renderLink(link: LinkObject, ctx: CanvasRenderingContext2D) {
const MAX_FONT_SIZE = 4;
const LABEL_NODE_MARGIN = nodeSize * 1.5;
const start = link.source;
const end = link.target;
// ignore unbound links
if (typeof start !== "object" || typeof end !== "object") return;
const textPos = {
x: start.x! + (end.x! - start.x!) / 2,
y: start.y! + (end.y! - start.y!) / 2,
};
const relLink = { x: end.x! - start.x!, y: end.y! - start.y! };
const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2;
let textAngle = Math.atan2(relLink.y, relLink.x);
// maintain label vertical orientation for legibility
if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle);
if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle);
const label = link.label
// estimate fontSize to fit in link length
ctx.font = "1px Sans-Serif";
const fontSize = Math.min(MAX_FONT_SIZE, maxTextLength / ctx.measureText(label).width);
ctx.font = `${fontSize}px Sans-Serif`;
const textWidth = ctx.measureText(label).width;
const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
// draw text label (with background rect)
ctx.save();
ctx.translate(textPos.x, textPos.y);
ctx.rotate(textAngle);
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, bckgDimensions[0], bckgDimensions[1]);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "darkgrey";
ctx.fillText(label, 0, 0);
ctx.restore();
}
function renderInitialNode(node: NodeObject, ctx: CanvasRenderingContext2D, globalScale: number) {
renderNode(node, ctx, globalScale, "after");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function handleDagError(loopNodeIds: (string | number)[]) {}
const graphRef = useRef<ForceGraphMethods>();
useEffect(() => {
if (typeof window !== "undefined" && data && graphRef.current) {
// add collision force
graphRef.current.d3Force("collision", forceCollide(nodeSize * 1.5));
graphRef.current.d3Force("charge", forceManyBody().strength(-1500).distanceMin(300).distanceMax(900));
}
}, [data, graphRef]);
const [graphShape, setGraphShape] = useState<string>();
useImperativeHandle(ref, () => ({
zoomToFit: graphRef.current!.zoomToFit,
setGraphShape: setGraphShape,
}));
return (
<div className="w-full h-full" id="graph-container">
{(data && typeof window !== "undefined") ? (
<ForceGraph
ref={graphRef}
dagMode={graphShape as unknown as undefined}
dagLevelDistance={300}
onDagError={handleDagError}
graphData={data}
nodeLabel="label"
nodeRelSize={nodeSize}
nodeCanvasObject={renderNode}
nodeCanvasObjectMode={() => "replace"}
linkLabel="label"
linkCanvasObject={renderLink}
linkCanvasObjectMode={() => "after"}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
onNodeClick={handleNodeClick}
onBackgroundClick={handleBackgroundClick}
d3VelocityDecay={0.3}
/>
) : (
<ForceGraph
ref={graphRef}
dagMode={graphShape as unknown as undefined}
dagLevelDistance={100}
graphData={{
nodes: [{ id: 1, label: "Add" }, { id: 2, label: "Cognify" }, { id: 3, label: "Search" }],
links: [{ source: 1, target: 2, label: "but don't forget to" }, { source: 2, target: 3, label: "and after that you can" }],
}}
nodeLabel="label"
nodeRelSize={20}
nodeCanvasObject={renderInitialNode}
nodeCanvasObjectMode={() => "after"}
nodeAutoColorBy="type"
linkLabel="label"
linkCanvasObject={renderLink}
linkCanvasObjectMode={() => "after"}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
/>
)}
</div>
);
}

View file

@ -0,0 +1,22 @@
import colors from "tailwindcss/colors";
import { formatHex } from "culori";
const NODE_COLORS = {
TextDocument: formatHex(colors.blue[500]),
DocumentChunk: formatHex(colors.green[500]),
TextSummary: formatHex(colors.orange[500]),
Entity: formatHex(colors.yellow[300]),
EntityType: formatHex(colors.purple[800]),
NodeSet: formatHex(colors.indigo[300]),
GitHubUser: formatHex(colors.gray[300]),
Comment: formatHex(colors.amber[500]),
Issue: formatHex(colors.red[500]),
Repository: formatHex(colors.stone[400]),
Commit: formatHex(colors.teal[500]),
File: formatHex(colors.emerald[500]),
FileChange: formatHex(colors.sky[500]),
};
export default function getColorForNodeType(type: string) {
return NODE_COLORS[type as keyof typeof NODE_COLORS] || colors.gray[500];
}

View file

@ -0,0 +1,82 @@
"use client";
import { useState } from "react";
import { fetch, useBoolean } from "@/utils";
import { CTAButton, Input } from "@/ui/elements";
import { LoadingIndicator } from '@/ui/App';
interface AuthFormPayload extends HTMLFormElement {
email: HTMLInputElement;
password: HTMLInputElement;
}
const errorsMap = {
LOGIN_BAD_CREDENTIALS: "Invalid username or password",
REGISTER_USER_ALREADY_EXISTS: "User already exists",
};
const defaultFormatPayload: (data: { email: string; password: string; }) => object = (data) => data;
export default function AuthForm({
submitButtonText = "Sign in",
authUrl = "/v1/auth/login",
formatPayload = defaultFormatPayload,
onSignInSuccess = () => window.location.href = "/",
}) {
const {
value: isSigningIn,
setTrue: disableSignIn,
setFalse: enableSignIn,
} = useBoolean(false);
const [signInError, setSignInError] = useState<string | null>(null);
const signIn = (event: React.FormEvent<AuthFormPayload>) => {
event.preventDefault();
const formElements = event.currentTarget;
// Backend expects username and password fields
const authCredentials = {
email: formElements.email.value,
password: formElements.password.value,
};
setSignInError(null);
disableSignIn();
const formattedPayload = formatPayload(authCredentials);
fetch(authUrl, {
method: "POST",
body: formattedPayload instanceof URLSearchParams ? formattedPayload.toString() : JSON.stringify(formattedPayload),
headers: {
"Content-Type": formattedPayload instanceof URLSearchParams ? "application/x-www-form-urlencoded" : "application/json",
},
})
.then(() => {
onSignInSuccess();
})
.catch(error => setSignInError(errorsMap[error.detail as keyof typeof errorsMap] || error.message))
.finally(() => enableSignIn());
};
return (
<form onSubmit={signIn} className="flex flex-col gap-4">
<label className="flex flex-col gap-1">
Email address*
<Input type="email" name="email" required placeholder="Email address*" defaultValue="default_user@example.com" />
</label>
<label className="flex flex-col gap-1">
Password*
<Input type="password" name="password" required placeholder="Password*" defaultValue="default_password" />
</label>
<CTAButton className="mt-6 mb-2" type="submit">
{submitButtonText}
{isSigningIn && <LoadingIndicator />}
</CTAButton>
{signInError && (
<span className="text-s text-red-500 mb-4">{signInError}</span>
)}
</form>
);
}

View file

@ -1,24 +1,40 @@
import { TextLogo } from "@/ui/App";
import { Divider } from "@/ui/Layout";
import Footer from "@/ui/Partials/Footer/Footer";
import SignInForm from "@/ui/Partials/SignInForm/SignInForm";
import Link from "next/link";
import { auth0 } from "@/modules/auth/auth0";
import { CTAButton } from "@/ui/elements";
export default async function AuthPage() {
const session = await auth0.getSession();
export default function AuthPage() {
return (
<main className="flex flex-col h-full">
<div className="pt-6 pr-3 pb-3 pl-6">
<TextLogo width={86} height={24} />
<div className="flex flex-col m-auto max-w-md h-full gap-8 pb-12 pt-6">
<h1><span className="text-xl">Welcome to cognee</span></h1>
{session ? (
<div className="flex flex-col gap-8">
<span className="text-lg">Hello, {session.user.name}!</span>
<Link href="/auth/logout">
<CTAButton>
Log out
</CTAButton>
</Link>
</div>
<Divider />
<div className="w-full max-w-md pt-12 pb-6 m-auto">
<div className="flex flex-col w-full gap-8">
<h1><span className="text-xl">Sign in</span></h1>
<SignInForm />
</div>
) : (
<div className="flex flex-row h-full gap-8">
<Link href="/auth/login?screen_hint=signup">
<CTAButton>
Sign up
</CTAButton>
</Link>
<Link href="/auth/login">
<CTAButton>
Log in
</CTAButton>
</Link>
</div>
<div className="pl-6 pr-6">
<Footer />
</div>
</main>
)}
</div>
)
}

View file

@ -0,0 +1,31 @@
import type { Metadata } from "next";
import { TextLogo } from "@/ui/App";
import { Divider } from "@/ui/Layout";
import { Footer } from "@/ui/Partials";
export const metadata: Metadata = {
title: "Cognee",
description: "Cognee authentication",
};
export default function AuthLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<main className="flex flex-col h-full">
<div className="pt-6 pr-3 pb-3 pl-6">
<TextLogo width={86} height={24} />
</div>
<Divider />
{children}
<Divider />
<div className="pl-6 pr-6">
<Footer />
</div>
</main>
);
}

View file

@ -0,0 +1,40 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import AuthForm from "../AuthForm";
export default function LoginPage() {
return (
<div className="m-auto w-full max-w-md shadow-xl rounded-xl">
<div className="flex flex-col px-10 py-16 bg-white border-1 rounded-xl border-indigo-600 overflow-hidden">
<Image src="/images/cognee-logo-with-text.png" alt="Cognee logo" width={176} height={46} className="h-12 w-44 self-center mb-16" />
<h1 className="self-center text-xl mb-4">Welcome</h1>
<p className="self-center mb-10">Log in to continue with Cognee</p>
<AuthForm
authUrl="/v1/auth/login"
submitButtonText="Login"
formatPayload={formatPayload}
/>
<p className="text-center mt-2 text-sm">
<Link href="/auth/signup">
{"Or go to Sign up ->"}
</Link>
</p>
</div>
</div>
);
}
function formatPayload(data: { email: string, password: string }) {
const payload = new URLSearchParams();
payload.append("username", data.email);
payload.append("password", data.password);
return payload;
}

View file

@ -0,0 +1 @@
export { default } from "./LoginPage";

View file

@ -1 +1 @@
export { default } from './AuthPage';
export { default } from "./AuthPage";

View file

@ -0,0 +1,31 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import AuthForm from "../AuthForm";
export default function SignUpPage() {
return (
<div className="m-auto w-full max-w-md shadow-xl rounded-xl">
<div className="flex flex-col px-10 py-16 bg-white border-1 rounded-xl border-indigo-600 overflow-hidden">
<Image src="/images/cognee-logo-with-text.png" alt="Cognee logo" width={176} height={46} className="h-12 w-44 self-center mb-16" />
<h1 className="self-center text-xl mb-4">Welcome</h1>
<p className="self-center mb-10">Sign up to start using Cognee</p>
<AuthForm
authUrl="/v1/auth/register"
submitButtonText="Sign up"
onSignInSuccess={() => window.location.href = "/auth/login"}
/>
<p className="text-center mt-2 text-sm">
<Link href="/auth/login">
{"Or go to Login ->"}
</Link>
</p>
</div>
</div>
);
}

View file

@ -0,0 +1 @@
export { default } from "./SignUpPage";

View file

@ -0,0 +1,17 @@
import { redirect } from "next/navigation";
import { auth0 } from "@/modules/auth/auth0";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function GET(request: Request) {
const accessToken = await auth0.getAccessToken();
if (accessToken) {
const response = new Response();
response.headers.set("Set-Cookie", `${process.env.AUTH_TOKEN_COOKIE_NAME}=${accessToken.token}; Expires=${new Date(accessToken.expiresAt * 1000).toUTCString()}; Path=/; SameSite=Lax; Domain=localhost; HttpOnly`);
return response;
} else {
redirect("/auth");
}
}

View file

@ -5,8 +5,8 @@ import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Cognee",
description: "Cognee Dev Mexican Standoff",
};
export default function RootLayout({

View file

@ -1,130 +0,0 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import styles from "./page.module.css";
import { GhostButton, Notification, NotificationContainer, Spacer, Stack, Text, useBoolean, useNotifications } from 'ohmy-ui';
import useDatasets from '@/modules/ingestion/useDatasets';
import DataView, { Data } from '@/modules/ingestion/DataView';
import DatasetsView from '@/modules/ingestion/DatasetsView';
import classNames from 'classnames';
import addData from '@/modules/ingestion/addData';
import cognifyDataset from '@/modules/datasets/cognifyDataset';
import getDatasetData from '@/modules/datasets/getDatasetData';
import { Footer, SettingsModal } from '@/ui/Partials';
import { TextLogo } from '@/ui/App';
import { SettingsIcon } from '@/ui/Icons';
export default function Home() {
const {
datasets,
refreshDatasets,
} = useDatasets();
const [datasetData, setDatasetData] = useState<Data[]>([]);
const [selectedDataset, setSelectedDataset] = useState<string | null>(null);
useEffect(() => {
refreshDatasets();
}, [refreshDatasets]);
const openDatasetData = (dataset: { id: string }) => {
getDatasetData(dataset)
.then(setDatasetData)
.then(() => setSelectedDataset(dataset.id));
};
const closeDatasetData = () => {
setDatasetData([]);
setSelectedDataset(null);
};
const { notifications, showNotification } = useNotifications();
const onDataAdd = useCallback((dataset: { id: string }, files: File[]) => {
return addData(dataset, files)
.then(() => {
showNotification("Data added successfully. Please run \"Cognify\" when ready.", 5000);
openDatasetData(dataset);
});
}, [showNotification])
const onDatasetCognify = useCallback((dataset: { id: string, name: string }) => {
showNotification(`Cognification started for dataset "${dataset.name}".`, 5000);
return cognifyDataset(dataset)
.then(() => {
showNotification(`Dataset "${dataset.name}" cognified.`, 5000);
})
.catch(() => {
showNotification(`Dataset "${dataset.name}" cognification failed. Please try again.`, 5000);
});
}, [showNotification]);
const onCognify = useCallback(() => {
const dataset = datasets.find((dataset) => dataset.id === selectedDataset);
return onDatasetCognify({
id: dataset!.id,
name: dataset!.name,
});
}, [datasets, onDatasetCognify, selectedDataset]);
const {
value: isSettingsModalOpen,
setTrue: openSettingsModal,
setFalse: closeSettingsModal,
} = useBoolean(false);
return (
<main className={styles.main}>
<Spacer inset vertical="2" horizontal="2">
<Stack orientation="horizontal" gap="between" align="center">
<TextLogo width={158} height={44} color="white" />
<GhostButton hugContent onClick={openSettingsModal}>
<SettingsIcon />
</GhostButton>
</Stack>
</Spacer>
<SettingsModal isOpen={isSettingsModalOpen} onClose={closeSettingsModal} />
<Spacer inset vertical="1" horizontal="3">
<div className={styles.data}>
<div className={classNames(styles.datasetsView, {
[styles.openDatasetData]: datasetData.length > 0,
})}>
<DatasetsView
datasets={datasets}
onDatasetClick={openDatasetData}
onDatasetCognify={onDatasetCognify}
/>
</div>
{datasetData.length > 0 && selectedDataset && (
<div className={styles.dataView}>
<DataView
data={datasetData}
datasetId={selectedDataset}
onClose={closeDatasetData}
onDataAdd={onDataAdd}
onCognify={onCognify}
/>
</div>
)}
</div>
</Spacer>
<Spacer inset horizontal="3" wrap>
<Footer />
</Spacer>
<NotificationContainer gap="1" bottom right>
{notifications.map((notification, index: number) => (
<Notification
key={notification.id}
isOpen={notification.isOpen}
style={{ top: `${index * 60}px` }}
expireIn={notification.expireIn}
onClose={notification.delete}
>
<Text nowrap>{notification.message}</Text>
</Notification>
))}
</NotificationContainer>
</main>
);
}

View file

@ -1 +1,3 @@
export { default } from "./(graph)/GraphView";
export const dynamic = "force-dynamic";

View file

@ -1,8 +0,0 @@
.files {
width: 100%;
padding: 4px;
}
.fileSize {
display: block;
}

View file

@ -1,97 +0,0 @@
import { useCallback, useState } from 'react';
import { CTAButton, GhostButton, Stack, Text, TrashIcon, UploadIcon, UploadInput, useBoolean } from 'ohmy-ui';
import { Divider } from '@/ui/Layout';
import addData from '@/modules/ingestion/addData';
import { LoadingIndicator } from '@/ui/App';
import styles from './AddStep.module.css';
import { WizardHeading } from '@/ui/Partials/Wizard';
interface ConfigStepProps {
onNext: () => void;
}
export default function AddStep({ onNext }: ConfigStepProps) {
const [files, setFiles] = useState<File[]>([]);
const {
value: isUploading,
setTrue: disableUploading,
setFalse: enableUploading,
} = useBoolean(false);
const uploadFiles = useCallback(() => {
disableUploading()
addData({ name: 'main' }, files)
.then(() => {
onNext();
})
.finally(() => enableUploading());
}, [disableUploading, enableUploading, files, onNext]);
const addFiles = useCallback((files: File[]) => {
setFiles((existingFiles) => {
const newFiles = files.filter((file) => !existingFiles.some((existingFile) => existingFile.name === file.name));
return [...existingFiles, ...newFiles]
});
}, []);
const removeFile = useCallback((file: File) => {
setFiles((files) => files.filter((f) => f !== file));
}, []);
return (
<Stack orientation="vertical" gap="6">
<WizardHeading><Text light size="large">Step 2/3</Text> Add knowledge</WizardHeading>
<Divider />
<Text align="center">
Cognee lets you process your personal data, books, articles or company data.
Simply add datasets to get started.
</Text>
<Stack gap="1">
<UploadInput onChange={addFiles}>
<Stack gap="2" orientation="horizontal" align="center/center">
<UploadIcon key={files.length} />
<Text>Upload your data</Text>
</Stack>
</UploadInput>
<Stack gap="3" className={styles.files}>
{files.map((file, index) => (
<Stack gap="between" orientation="horizontal" align="center/" key={index}>
<div key={index}>
<Text bold>{file.name}</Text>
<Text className={styles.fileSize} size="small">
{getBiggestUnitSize(file.size)}
</Text>
</div>
<GhostButton hugContent onClick={() => removeFile(file)}>
<TrashIcon />
</GhostButton>
</Stack>
))}
</Stack>
</Stack>
<Stack align="/end">
<CTAButton disabled={isUploading || files.length === 0} onClick={uploadFiles}>
<Stack gap="2" orientation="horizontal" align="center/center">
<Text>Next</Text>
{isUploading && (
<LoadingIndicator />
)}
</Stack>
</CTAButton>
</Stack>
</Stack>
)
}
function getBiggestUnitSize(sizeInBytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
while (sizeInBytes >= 1024 && i < units.length - 1) {
sizeInBytes /= 1024;
i++;
}
return `${sizeInBytes.toFixed(2)} ${units[i]}`;
}

View file

@ -1 +0,0 @@
export { default } from './AddStep';

View file

@ -1,51 +0,0 @@
import { useEffect, useRef } from 'react';
import { CTAButton, Stack, Text, useBoolean } from 'ohmy-ui';
import { Divider } from '@/ui/Layout';
import { CognifyLoadingIndicator } from '@/ui/App';
import { WizardHeading } from '@/ui/Partials/Wizard';
import cognifyDataset from '@/modules/datasets/cognifyDataset';
interface ConfigStepProps {
onNext: () => void;
dataset: { name: string }
}
export default function CognifyStep({ onNext, dataset }: ConfigStepProps) {
const {
value: isCognifyRunning,
setFalse: stopCognifyIndicator,
} = useBoolean(true);
const cognifyPromise = useRef<Promise<void>>()
useEffect(() => {
if (cognifyPromise.current) {
return;
}
cognifyPromise.current = cognifyDataset(dataset)
.then(() => {
stopCognifyIndicator();
});
}, [stopCognifyIndicator, dataset]);
return (
<Stack orientation="vertical" gap="6">
<WizardHeading><Text light size="large">Step 3/3</Text> Cognify</WizardHeading>
<Divider />
<Stack align="/center">
<CognifyLoadingIndicator isLoading={isCognifyRunning} />
</Stack>
<Text align="center">
Cognee decomposes your data into facts and connects them in relevant clusters,
so that you can navigate your knowledge better.
</Text>
<CTAButton disabled={isCognifyRunning} onClick={onNext}>
<Stack gap="2" orientation="horizontal" align="center/center">
<Text>Explore data</Text>
</Stack>
</CTAButton>
</Stack>
)
}

View file

@ -1 +0,0 @@
export { default } from './CognifyStep';

View file

@ -1,22 +0,0 @@
import { Stack, Text } from 'ohmy-ui';
import { Divider } from '@/ui/Layout';
import Settings from '@/ui/Partials/SettingsModal/Settings';
import { WizardContent, WizardHeading } from '@/ui/Partials/Wizard';
interface ConfigStepProps {
onNext: () => void;
}
export default function ConfigStep({ onNext }: ConfigStepProps) {
return (
<Stack orientation="vertical" gap="6">
<WizardHeading><Text light size="large">Step 1/3</Text> Basic configuration</WizardHeading>
<Divider />
<Text align="center">
Cognee helps you process your data and create a mind-like structure you can explore.
To get started you need an OpenAI API key.
</Text>
<Settings onDone={onNext} submitButtonText="Next" />
</Stack>
)
}

View file

@ -1 +0,0 @@
export { default } from './ConfigStep';

View file

@ -1,14 +0,0 @@
import { Explorer } from '@/ui/Partials';
import { Spacer } from 'ohmy-ui';
interface ExploreStepProps {
dataset: { name: string };
}
export default function ExploreStep({ dataset }: ExploreStepProps) {
return (
<Spacer horizontal="3">
<Explorer dataset={dataset} />
</Spacer>
)
}

View file

@ -1 +0,0 @@
export { default } from './ExploreStep';

View file

@ -1,13 +0,0 @@
.main {
display: flex;
flex-direction: row;
flex-direction: column;
padding: 0;
min-height: 100vh;
}
.wizardContainer {
flex: 1;
display: flex;
padding: 24px 0;
}

View file

@ -1,83 +0,0 @@
import { useState } from 'react';
import { CloseIcon, GhostButton, Spacer, Stack, useBoolean } from 'ohmy-ui';
import { TextLogo } from '@/ui/App';
import { SettingsIcon } from '@/ui/Icons';
import { Footer, SettingsModal } from '@/ui/Partials';
import ConfigStep from './ConfigStep';
import AddStep from './AddStep';
import CognifyStep from './CognifyStep';
import ExploreStep from './ExploreStep';
import { WizardContent } from '@/ui/Partials/Wizard';
import styles from './WizardPage.module.css';
import { Divider } from '@/ui/Layout';
import { useSearchParams } from 'next/navigation';
interface WizardPageProps {
onFinish: () => void;
}
export default function WizardPage({
onFinish,
}: WizardPageProps) {
const searchParams = useSearchParams()
const presetWizardStep = searchParams.get('step') as 'config';
const [wizardStep, setWizardStep] = useState<'config' | 'add' | 'cognify' | 'explore'>(presetWizardStep || 'config');
const {
value: isSettingsModalOpen,
setTrue: openSettingsModal,
setFalse: closeSettingsModal,
} = useBoolean(false);
const dataset = { name: 'main' };
return (
<main className={styles.main}>
<Spacer inset vertical="2" horizontal="2">
<Stack orientation="horizontal" gap="between" align="center">
<TextLogo width={158} height={44} color="white" />
{wizardStep === 'explore' && (
<GhostButton hugContent onClick={onFinish}>
<CloseIcon />
</GhostButton>
)}
{wizardStep === 'add' && (
<GhostButton hugContent onClick={openSettingsModal}>
<SettingsIcon />
</GhostButton>
)}
</Stack>
</Spacer>
<Divider />
<SettingsModal isOpen={isSettingsModalOpen} onClose={closeSettingsModal} />
<div className={styles.wizardContainer}>
{wizardStep === 'config' && (
<WizardContent>
<ConfigStep onNext={() => setWizardStep('add')} />
</WizardContent>
)}
{wizardStep === 'add' && (
<WizardContent>
<AddStep onNext={() => setWizardStep('cognify')} />
</WizardContent>
)}
{wizardStep === 'cognify' && (
<WizardContent>
<CognifyStep dataset={dataset} onNext={() => setWizardStep('explore')} />
</WizardContent>
)}
{wizardStep === 'explore' && (
<Spacer inset top="4" bottom="1" horizontal="4">
<ExploreStep dataset={dataset} />
</Spacer>
)}
</div>
<Spacer inset horizontal="3" wrap>
<Footer />
</Spacer>
</main>
)
}

View file

@ -1,18 +0,0 @@
'use client';
import { Suspense, useCallback } from 'react';
import WizardPage from './WizardPage';
export default function Page() {
const finishWizard = useCallback(() => {
window.location.href = '/';
}, []);
return (
<Suspense>
<WizardPage
onFinish={finishWizard}
/>
</Suspense>
);
}

View file

@ -0,0 +1,29 @@
import { NextResponse, type NextRequest } from "next/server";
// import { auth0 } from "./modules/auth/auth0";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function middleware(request: NextRequest) {
// if (process.env.USE_AUTH0_AUTHORIZATION?.toLowerCase() === "true") {
// if (request.nextUrl.pathname === "/auth/token") {
// return NextResponse.next();
// }
// const response: NextResponse = await auth0.middleware(request);
// return response;
// }
return NextResponse.next();
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
};

View file

@ -0,0 +1,8 @@
import { Auth0Client } from "@auth0/nextjs-auth0/server";
export const auth0 = new Auth0Client({
authorizationParameters: {
scope: "openid profile email",
audience: "cognee:api",
},
});

View file

@ -1,8 +1,8 @@
import { fetch } from '@/utils';
import { fetch } from "@/utils";
export default function getHistory() {
return fetch(
'/v1/search',
"/v1/search",
)
.then((response) => response.json());
}

View file

@ -0,0 +1,123 @@
import { v4 } from "uuid";
import { useCallback, useState } from "react";
import { fetch, useBoolean } from "@/utils";
import { Dataset } from "@/modules/ingestion/useDatasets";
interface ChatMessage {
id: string;
user: "user" | "system";
text: string;
}
const fetchMessages = () => {
return fetch("/v1/search/")
.then(response => response.json());
};
const sendMessage = (message: string, searchType: string) => {
return fetch("/v1/search/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: message,
searchType,
datasets: ["main_dataset"],
}),
})
.then(response => response.json());
};
// Will be used in the future.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function useChat(dataset: Dataset) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const {
value: isSearchRunning,
setTrue: disableSearchRun,
setFalse: enableSearchRun,
} = useBoolean(false);
const refreshChat = useCallback(async () => {
const data = await fetchMessages();
return setMessages(data);
}, []);
const handleMessageSending = useCallback((message: string, searchType: string) => {
const sentMessageId = v4();
setMessages((messages) => [
...messages,
{
id: sentMessageId,
user: "user",
text: message,
},
]);
disableSearchRun();
return sendMessage(message, searchType)
.then(newMessages => {
setMessages((messages) => [
...messages,
...newMessages.map((newMessage: string | []) => ({
id: v4(),
user: "system",
text: convertToSearchTypeOutput(newMessage, searchType),
})),
]);
})
.catch(() => {
setMessages(
(messages) => messages.filter(message => message.id !== sentMessageId),
);
throw new Error("Failed to send message. Please try again. If the issue persists, please contact support.")
})
.finally(() => enableSearchRun());
}, [disableSearchRun, enableSearchRun]);
return {
messages,
refreshChat,
sendMessage: handleMessageSending,
isSearchRunning,
};
}
interface Node {
name: string;
}
interface Relationship {
relationship_name: string;
}
type InsightMessage = [Node, Relationship, Node];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function convertToSearchTypeOutput(systemMessage: any[] | any, searchType: string): string {
if (Array.isArray(systemMessage) && systemMessage.length === 1 && typeof(systemMessage[0]) === "string") {
return systemMessage[0];
}
switch (searchType) {
case "INSIGHTS":
return systemMessage.map((message: InsightMessage) => {
const [node1, relationship, node2] = message;
if (node1.name && node2.name) {
return `${node1.name} ${relationship.relationship_name} ${node2.name}.`;
}
return "";
}).join("\n");
case "SUMMARIES":
return systemMessage.map((message: { text: string }) => message.text).join("\n");
case "CHUNKS":
return systemMessage.map((message: { text: string }) => message.text).join("\n");
default:
return systemMessage;
}
}

View file

@ -1,43 +1,58 @@
import { fetch } from '@/utils';
import { fetch } from "@/utils";
import getDatasetGraph from "./getDatasetGraph";
import { Dataset } from "../ingestion/useDatasets";
export default function cognifyDataset(dataset: { id?: string, name?: string }, onUpdate = (data: []) => {}) {
return fetch('/v1/cognify', {
method: 'POST',
interface GraphData {
nodes: { id: string; label: string; properties?: object }[];
edges: { source: string; target: string; label: string }[];
}
export default async function cognifyDataset(dataset: Dataset, onUpdate: (data: GraphData) => void) {
// const data = await (
return fetch("/v1/cognify", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
datasets: [dataset.id || dataset.name],
datasetIds: [dataset.id],
runInBackground: false,
}),
})
.then((response) => response.json())
.then((data) => {
const websocket = new WebSocket(`ws://localhost:8000/api/v1/cognify/subscribe/${data.pipeline_run_id}`);
websocket.onopen = () => {
websocket.send(JSON.stringify({
"Authorization": `Bearer ${localStorage.getItem("access_token")}`,
}));
};
let isCognifyDone = false;
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
onUpdate(data);
if (data.status === "PipelineRunCompleted") {
isCognifyDone = true;
websocket.close();
}
};
return new Promise(async (resolve) => {
while (!isCognifyDone) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
resolve(true);
.then((response) => response.json())
.then(() => {
return getDatasetGraph(dataset)
.then((data) => {
onUpdate({
nodes: data.nodes,
edges: data.edges,
});
});
});
});
// )
// const websocket = new WebSocket(`ws://localhost:8000/api/v1/cognify/subscribe/${data.pipeline_run_id}`);
// let isCognifyDone = false;
// websocket.onmessage = (event) => {
// const data = JSON.parse(event.data);
// onUpdate?.({
// nodes: data.payload.nodes,
// edges: data.payload.edges,
// });
// if (data.status === "PipelineRunCompleted") {
// isCognifyDone = true;
// websocket.close();
// }
// };
// return new Promise(async (resolve) => {
// while (!isCognifyDone) {
// await new Promise(resolve => setTimeout(resolve, 1000));
// }
// resolve(true);
// });
}

View file

@ -0,0 +1,12 @@
import { fetch } from "@/utils";
export default function createDataset(dataset: { name: string }) {
return fetch(`/v1/datasets/`, {
method: "POST",
body: JSON.stringify(dataset),
headers: {
"Content-Type": "application/json",
}
})
.then((response) => response.json());
}

View file

@ -1,6 +1,6 @@
import { fetch } from '@/utils';
export default function getExplorationGraphUrl(dataset: { name: string }) {
export default function getExplorationGraphUrl(/* dataset: { name: string } */) {
return fetch('/v1/visualize')
.then(async (response) => {
if (response.status !== 200) {

View file

@ -1,24 +0,0 @@
.tableContainer {
overflow: auto;
padding-bottom: 32px;
min-height: 300px;
}
.datasetMenu {
background-color: var(--global-background-default);
border-radius: var(--border-radius);
padding: 4px;
}
.dataTable {
color: white;
border-collapse: collapse;
}
.dataTable td, .dataTable th {
vertical-align: top;
padding: 8px;
border: 1px solid white;
margin: 0;
white-space: nowrap;
}

View file

@ -1,143 +0,0 @@
import { useCallback, useState } from 'react';
import {
DropdownMenu,
GhostButton,
Stack,
Text,
UploadInput,
CloseIcon,
CTAButton,
useBoolean,
} from "ohmy-ui";
import { fetch } from '@/utils';
import RawDataPreview from './RawDataPreview';
import styles from "./DataView.module.css";
export interface Data {
id: string;
name: string;
mimeType: string;
extension: string;
rawDataLocation: string;
}
interface DatasetLike {
id: string;
}
interface DataViewProps {
data: Data[];
datasetId: string;
onClose: () => void;
onDataAdd: (dataset: DatasetLike, files: File[]) => void;
onCognify: () => Promise<any>;
}
export default function DataView({ datasetId, data, onClose, onDataAdd, onCognify }: DataViewProps) {
// const handleDataDelete = () => {};
const [rawData, setRawData] = useState<ArrayBuffer | null>(null);
const [selectedData, setSelectedData] = useState<Data | null>(null);
const showRawData = useCallback((dataItem: Data) => {
setSelectedData(dataItem);
fetch(`/v1/datasets/${datasetId}/data/${dataItem.id}/raw`)
.then((response) => response.arrayBuffer())
.then(setRawData);
document.body.click(); // Close the dropdown menu.
}, [datasetId]);
const resetDataPreview = useCallback(() => {
setSelectedData(null);
setRawData(null);
}, []);
const handleDataAdd = (files: File[]) => {
onDataAdd({ id: datasetId }, files);
};
const {
value: isCognifyButtonDisabled,
setTrue: disableCognifyButton,
setFalse: enableCognifyButton,
} = useBoolean(false);
const handleCognify = () => {
disableCognifyButton();
onCognify()
.finally(() => enableCognifyButton());
};
return (
<Stack orientation="vertical" gap="4">
<Stack gap="2" orientation="horizontal" align="/end">
<div>
<UploadInput onChange={handleDataAdd}>
<Text>Add data</Text>
</UploadInput>
</div>
<div>
<CTAButton disabled={isCognifyButtonDisabled} onClick={handleCognify}>
<Text>Cognify</Text>
</CTAButton>
</div>
<GhostButton hugContent onClick={onClose}>
<CloseIcon />
</GhostButton>
</Stack>
{rawData && selectedData && (
<RawDataPreview
fileName={selectedData.name}
rawData={rawData}
onClose={resetDataPreview}
/>
)}
<div className={styles.tableContainer}>
<table className={styles.dataTable}>
<thead>
<tr>
<th>Actions</th>
<th>ID</th>
<th>Name</th>
<th>File path</th>
<th>MIME type</th>
</tr>
</thead>
<tbody>
{data.map((dataItem) => (
<tr key={dataItem.id}>
<td>
<Stack orientation="horizontal" gap="2" align="center">
<DropdownMenu position="right">
<Stack gap="1" className={styles.datasetMenu} orientation="vertical">
<GhostButton onClick={() => showRawData(dataItem)}>
<Text>View raw data</Text>
</GhostButton>
{/* <NegativeButton onClick={handleDataDelete}>
<Text>Delete</Text>
</NegativeButton> */}
</Stack>
</DropdownMenu>
</Stack>
</td>
<td>
<Text>{dataItem.id}</Text>
</td>
<td>
<Text>{dataItem.name}.{dataItem.extension}</Text>
</td>
<td>
<Text>{dataItem.rawDataLocation}</Text>
</td>
<td>
<Text>{dataItem.mimeType}</Text>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Stack>
);
}

View file

@ -1,8 +0,0 @@
.dataPreviewModal {
left: 5% !important;
padding: 0 !important;
max-width: 90% !important;
height: 80%;
top: 5% !important;
}

View file

@ -1,37 +0,0 @@
import { IFrameView } from '@/ui/Partials';
import { CloseIcon, GhostButton, Modal, Spacer, Stack, Text } from 'ohmy-ui';
import styles from './RawDataPreview.module.css';
interface RawDataPreviewProps {
fileName: string;
rawData: ArrayBuffer;
onClose: () => void;
}
const file_header = ';headers=filename%3D';
export default function RawDataPreview({ fileName, rawData, onClose }: RawDataPreviewProps) {
const src = `data:application/pdf;base64,${arrayBufferToBase64(rawData)}`.replace(';', file_header + encodeURIComponent(fileName) + ';');
return (
<Modal isOpen onClose={onClose} className={styles.dataPreviewModal}>
<Spacer horizontal="2" vertical="3" wrap>
<Text>{fileName}</Text>
</Spacer>
<IFrameView src={src} />
</Modal>
);
}
function arrayBufferToBase64(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa(binary);
}

View file

@ -1 +0,0 @@
export { default, type Data } from './DataView';

View file

@ -1,16 +0,0 @@
.datasetMenu {
background-color: var(--global-background-default);
border-radius: var(--border-radius);
padding: 4px;
}
.explorerModal {
left: 5% !important;
padding: 0 !important;
max-width: 90% !important;
height: 80%;
top: 5% !important;
display: flex;
flex-direction: column;
}

View file

@ -1,106 +0,0 @@
import { useState } from 'react';
import Link from 'next/link';
import { Explorer } from '@/ui/Partials';
import StatusIcon from '@/ui/elements/StatusIndicator';
import { LoadingIndicator } from '@/ui/App';
import { DropdownMenu, GhostButton, Stack, Text, CTAButton, useBoolean, Modal, Spacer } from "ohmy-ui";
import styles from "./DatasetsView.module.css";
interface Dataset {
id: string;
name: string;
status: string;
}
const DatasetItem = GhostButton.remix({ Component: 'div' });
interface DatasetsViewProps {
datasets: Dataset[];
onDatasetClick: (dataset: Dataset) => void;
onDatasetCognify: (dataset: Dataset) => Promise<void>;
}
export default function DatasetsView({
datasets,
onDatasetClick,
onDatasetCognify,
}: DatasetsViewProps) {
const {
value: isCognifyRunning,
setTrue: disableCognifyRun,
setFalse: enableCognifyRun,
} = useBoolean(false);
const handleCognifyDataset = (event: React.MouseEvent<HTMLButtonElement>, dataset: Dataset) => {
event.stopPropagation();
disableCognifyRun();
onDatasetCognify(dataset)
.finally(() => enableCognifyRun());
}
const [dataset, setExplorationDataset] = useState<{ id: string, name: string } | null>(null);
const {
value: isExplorationWindowShown,
setTrue: showExplorationWindow,
setFalse: hideExplorationWindow,
} = useBoolean(false);
const handleExploreDataset = (event: React.MouseEvent<HTMLButtonElement>, dataset: Dataset) => {
event.stopPropagation();
setExplorationDataset(dataset);
showExplorationWindow();
}
return (
<>
<Stack orientation="vertical" gap="4">
{datasets.map((dataset) => (
<DatasetItem key={dataset.id} onClick={() => onDatasetClick(dataset)}>
<Stack orientation="horizontal" gap="between" align="start/center">
<Text>{dataset.name}</Text>
<Stack orientation="horizontal" gap="2" align="center">
<StatusIcon status={dataset.status} />
<DropdownMenu>
<Stack gap="1" className={styles.datasetMenu} orientation="vertical">
{dataset.status === 'DATASET_PROCESSING_COMPLETED' ? (
<CTAButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => handleExploreDataset(event, dataset)}
>
<Text>Explore</Text>
</CTAButton>
) : (
<CTAButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => handleCognifyDataset(event, dataset)}
>
<Stack gap="2" orientation="horizontal" align="center/center">
<Text>Cognify</Text>
{isCognifyRunning && (
<LoadingIndicator />
)}
</Stack>
</CTAButton>
)}
<Link href="/wizard?step=add">
<GhostButton>
<Text>Add data</Text>
</GhostButton>
</Link>
</Stack>
</DropdownMenu>
</Stack>
</Stack>
</DatasetItem>
))}
</Stack>
<Modal closeOnBackdropClick={false} onClose={hideExplorationWindow} isOpen={isExplorationWindowShown} className={styles.explorerModal}>
<Spacer horizontal="2" vertical="3" wrap>
<Text>{dataset?.name}</Text>
</Spacer>
<Explorer dataset={dataset!} />
</Modal>
</>
);
}

View file

@ -1 +0,0 @@
export { default } from './DatasetsView';

View file

@ -1,19 +1,19 @@
import { fetch } from '@/utils';
import { fetch } from "@/utils";
export default function addData(dataset: { id?: string, name?: string }, files: File[]) {
const formData = new FormData();
files.forEach((file) => {
formData.append('data', file, file.name);
formData.append("data", file, file.name);
})
if (dataset.id) {
formData.append('datasetId', dataset.id);
formData.append("datasetId", dataset.id);
}
if (dataset.name) {
formData.append('datasetName', dataset.name);
formData.append("datasetName", dataset.name);
}
return fetch('/v1/add', {
method: 'POST',
return fetch("/v1/add", {
method: "POST",
body: formData,
}).then((response) => response.json());
}

View file

@ -12,16 +12,12 @@ export interface Dataset {
function useDatasets() {
const [datasets, setDatasets] = useState<Dataset[]>([]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusTimeout = useRef<any>(null);
const fetchDatasetStatuses = useCallback((datasets: Dataset[]) => {
fetch(
`/v1/datasets/status?dataset=${datasets.map(d => d.id).join('&dataset=')}`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
},
},
)
.then((response) => response.json())
.then((statuses) => setDatasets(
@ -42,7 +38,7 @@ function useDatasets() {
statusTimeout.current = setTimeout(() => {
checkDatasetStatuses(datasets);
}, 5000);
}, 50000);
}, [fetchDatasetStatuses]);
useEffect(() => {
@ -73,11 +69,7 @@ function useDatasets() {
}, []);
const fetchDatasets = useCallback(() => {
return fetch('/v1/datasets', {
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
},
})
return fetch('/v1/datasets')
.then((response) => response.json())
.then((datasets) => {
setDatasets(datasets);

View file

@ -0,0 +1,7 @@
export default function SearchIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) {
return (
<svg width={width} height={height} viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M24.9999 46L24.9999 4M46.0049 25.005L4.00488 25.005" stroke={color} strokeWidth="8" strokeLinecap="round"/>
</svg>
);
}

View file

@ -0,0 +1,8 @@
export default function CaretIcon({ width = 50, height = 36, color = "currentColor", className = "" }) {
return (
<svg width={width} height={height} viewBox="0 0 50 36" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M4 32L25 5" stroke={color} strokeWidth="8" strokeLinecap="round"/>
<path d="M46 32L25 5" stroke={color} strokeWidth="8" strokeLinecap="round"/>
</svg>
);
}

View file

@ -0,0 +1,9 @@
export default function SearchIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) {
return (
<svg width={width} height={height} viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="19.5" cy="19.5" r="17" stroke={color} strokeWidth="5"/>
<path d="M8 19.5C8 13.1487 13.1487 8 19.5 8" stroke={color}/>
<path d="M43.2782 48.9312C44.897 50.4344 47.428 50.3406 48.9312 48.7218C50.4344 47.103 50.3406 44.572 48.7218 43.0688L43.2782 48.9312ZM46 46L48.7218 43.0688L34.7218 30.0688L32 33L29.2782 35.9312L43.2782 48.9312L46 46Z" fill={color}/>
</svg>
);
}

View file

@ -1,3 +1,6 @@
export { default as AddIcon } from './AddIcon';
export { default as CaretIcon } from './CaretIcon';
export { default as SearchIcon } from './SearchIcon';
export { default as DeleteIcon } from './DeleteIcon';
export { default as GithubIcon } from './GitHubIcon';
export { default as DiscordIcon } from './DiscordIcon';

View file

@ -1,21 +0,0 @@
.explorer {
flex: 1;
min-height: 100%;
flex-direction: column;
}
.explorerContent {
flex: 1;
}
.graphExplorer {
width: 65%;
overflow: hidden;
border-radius: var(--border-radius);
}
.chat {
width: 35%;
display: flex;
}

View file

@ -1,61 +0,0 @@
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import { Spacer, Stack, Text } from 'ohmy-ui';
import { LoadingIndicator } from '@/ui/App';
import { IFrameView, SearchView } from '@/ui/Partials';
import { getExplorationGraphUrl } from '@/modules/exploration';
import styles from './Explorer.module.css';
interface ExplorerProps {
dataset: { name: string };
className?: string;
style?: React.CSSProperties;
}
export default function Explorer({ dataset, className, style }: ExplorerProps) {
const [error, setError] = useState<Error | null>(null);
const [graphHtml, setGraphHtml] = useState<string | null>(null);
const exploreData = useCallback(() => {
getExplorationGraphUrl(dataset)
.then((graphHtml) => {
setError(null);
setGraphHtml(graphHtml);
})
.catch((error) => {
setError(error);
});
}, [dataset]);
useEffect(() => {
exploreData();
}, [exploreData]);
return (
<Stack
gap="6"
style={style}
orientation="horizontal"
className={classNames(styles.explorerContent, className)}
>
<div className={styles.graphExplorer}>
{error ? (
<Text color="red">{error.message}</Text>
) : (
<>
{!graphHtml ? (
<Spacer horizontal="2" wrap>
<LoadingIndicator />
</Spacer>
) : (
<IFrameView src="http://127.0.0.1:8000/api/v1/visualize" />
)}
</>
)}
</div>
<div className={styles.chat}>
<SearchView />
</div>
</Stack>
)
}

View file

@ -26,19 +26,22 @@ export default function FeedbackForm({ onSuccess }: FeedbackFormProps) {
event.preventDefault();
const formElements = event.currentTarget;
const authCredentials = new FormData();
authCredentials.append("feedback", formElements.feedback.value);
setFeedbackError(null);
disableFeedbackSubmit();
fetch("/v1/feedback/reasoning", {
fetch("/v1/crewai/feedback", {
method: "POST",
body: authCredentials,
body: JSON.stringify({
feedback: formElements.feedback.value,
}),
headers: {
"Content-Type": "application/json",
},
})
.then(response => response.json())
.then(() => {
onSuccess();
formElements.feedback.value = "";
})
.catch(error => setFeedbackError(error.detail))
.finally(() => enableFeedbackSubmit());
@ -48,7 +51,7 @@ export default function FeedbackForm({ onSuccess }: FeedbackFormProps) {
<form onSubmit={signIn} className="flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="mb-4">
<label className="block text-white" htmlFor="feedback">Your feedback on agents reasoning</label>
<label className="block text-white" htmlFor="feedback">Feedback on agent&apos;s reasoning</label>
<TextArea id="feedback" name="feedback" type="text" placeholder="Your feedback" />
</div>
</div>

View file

@ -1,29 +1,7 @@
.searchViewContainer {
flex: 1;
padding: 16px;
overflow: hidden;
border: 1px solid white;
background: var(--global-background-default);
border-radius: var(--border-radius);
.userMessage + .systemMessage {
margin-top: 2rem;
}
.messagesContainer {
flex-basis: 400px;
flex-grow: 1;
overflow-y: auto;
}
.messages {
padding-top: 24px;
padding-bottom: 24px;
}
.message {
padding: 16px;
border-radius: var(--border-radius);
}
.userMessage {
align-self: flex-end;
background-color: #5858ff;
.systemMessage + .userMessage {
margin-top: 2rem;
}

View file

@ -1,209 +1,145 @@
'use client';
"use client";
import { v4 } from 'uuid';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import { CTAButton, Stack, Text, DropdownSelect, TextArea, useBoolean, Input } from 'ohmy-ui';
import { fetch } from '@/utils';
import styles from './SearchView.module.css';
import getHistory from '@/modules/chat/getHistory';
import classNames from "classnames";
import { useCallback, useEffect, useRef, useState } from "react";
interface Message {
id: string;
user: 'user' | 'system';
text: any;
}
import { LoadingIndicator } from "@/ui/App";
import { CTAButton, Select, TextArea } from "@/ui/elements";
import useChat from "@/modules/chat/hooks/useChat";
import styles from "./SearchView.module.css";
interface SelectOption {
value: string;
label: string;
}
interface SearchFormPayload extends HTMLFormElement {
chatInput: HTMLInputElement;
}
const MAIN_DATASET = {
id: "",
data: [],
status: "",
name: "main_dataset",
};
export default function SearchView() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState<string>("");
const handleInputChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(event.target.value);
}, []);
const searchOptions = [{
value: 'GRAPH_COMPLETION',
label: 'Completion using Cognee\'s graph based memory',
const searchOptions: SelectOption[] = [{
value: "GRAPH_COMPLETION",
label: "GraphRAG Completion",
}, {
value: 'RAG_COMPLETION',
label: 'Completion using RAG',
}, {
value: 'GRAPH_COMPLETION_COT',
label: 'Cognee\'s Chain of Thought search',
}, {
value: 'GRAPH_COMPLETION_CONTEXT_EXTENSION',
label: 'Cognee\'s Multi-Hop search',
value: "RAG_COMPLETION",
label: "RAG Completion",
}];
const [searchType, setSearchType] = useState(searchOptions[0]);
const [rangeValue, setRangeValue] = useState(10);
const scrollToBottom = useCallback(() => {
setTimeout(() => {
const messagesContainerElement = document.getElementById('messages');
const messagesContainerElement = document.getElementById("messages");
if (messagesContainerElement) {
const messagesElements = messagesContainerElement.children[0];
if (messagesElements) {
messagesContainerElement.scrollTo({
top: messagesElements.scrollHeight,
behavior: 'smooth',
behavior: "smooth",
});
}
}
}, 300);
}, []);
useEffect(() => {
getHistory()
.then((history) => {
setMessages(history);
scrollToBottom();
});
}, [scrollToBottom]);
// Hardcoded to `main_dataset` for now, change when multiple datasets are supported.
const { messages, refreshChat, sendMessage, isSearchRunning } = useChat(MAIN_DATASET);
const handleSearchSubmit = useCallback((event: React.FormEvent<HTMLFormElement>) => {
useEffect(() => {
refreshChat()
.then(() => scrollToBottom());
}, [refreshChat, scrollToBottom]);
const [searchInputValue, setSearchInputValue] = useState("");
const handleSearchInputChange = useCallback((value: string) => {
setSearchInputValue(value);
}, []);
const handleChatMessageSubmit = useCallback((event: React.FormEvent<SearchFormPayload>) => {
event.preventDefault();
if (inputValue.trim() === '') {
const formElements = event.currentTarget;
const searchType = formElements.searchType.value;
const chatInput = searchInputValue.trim();
if (chatInput === "") {
return;
}
setMessages((currentMessages) => [
...currentMessages,
{
id: v4(),
user: 'user',
text: inputValue,
},
]);
scrollToBottom();
setInputValue('');
setSearchInputValue("");
const searchTypeValue = searchType.value;
sendMessage(chatInput, searchType)
.then(scrollToBottom)
}, [scrollToBottom, sendMessage, searchInputValue]);
fetch('/v1/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: inputValue.trim(),
searchType: searchTypeValue,
topK: rangeValue,
}),
})
.then((response) => response.json())
.then((systemMessage) => {
setMessages((currentMessages) => [
...currentMessages,
{
id: v4(),
user: 'system',
text: convertToSearchTypeOutput(systemMessage, searchTypeValue),
},
]);
scrollToBottom();
})
.catch(() => {
setInputValue(inputValue);
});
}, [inputValue, rangeValue, scrollToBottom, searchType.value]);
const {
value: isInputExpanded,
setTrue: expandInput,
setFalse: contractInput,
} = useBoolean(false);
const chatFormRef = useRef<HTMLFormElement>(null);
const handleSubmitOnEnter = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
handleSearchSubmit(event as unknown as React.FormEvent<HTMLFormElement>);
if (event.key === "Enter" && !event.shiftKey) {
chatFormRef.current?.requestSubmit();
}
};
const handleRangeValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRangeValue(parseInt(event.target.value));
};
return (
<Stack className={styles.searchViewContainer}>
<DropdownSelect<SelectOption>
value={searchType}
options={searchOptions}
onChange={setSearchType}
/>
<div className={styles.messagesContainer} id="messages">
<Stack gap="2" className={styles.messages} align="end">
{messages.map((message) => (
<Text
key={message.id}
className={classNames(styles.message, {
[styles.userMessage]: message.user === "user",
})}
>
{message?.text && (
typeof(message.text) == "string" ? message.text : JSON.stringify(message.text)
)}
</Text>
))}
</Stack>
</div>
<form onSubmit={handleSearchSubmit}>
<Stack orientation="vertical" gap="2">
<TextArea onKeyUp={handleSubmitOnEnter} style={{ transition: 'height 0.3s ease', height: isInputExpanded ? '128px' : '38px' }} onFocus={expandInput} onBlur={contractInput} value={inputValue} onChange={handleInputChange} name="searchInput" placeholder="Search" />
<Stack orientation="horizontal" gap="between">
<Stack orientation="horizontal" gap="2" align="center">
<label><Text>Search range: </Text></label>
<Input style={{ maxWidth: "90px" }} value={rangeValue} onChange={handleRangeValueChange} type="number" />
</Stack>
<CTAButton hugContent type="submit">Search</CTAButton>
</Stack>
</Stack>
<div className="flex flex-col h-full bg-gray-500 p-6 pt-16 rounded-3xl border-indigo-600 border-2 shadow-xl">
<form onSubmit={handleChatMessageSubmit} ref={chatFormRef} className="flex flex-col gap-4 h-full">
<div className="h-full overflow-y-auto" id="messages">
<div className="flex flex-col gap-2 items-end justify-end min-h-full px-6 pb-4">
{messages.map((message) => (
<p
key={message.id}
className={classNames({
[classNames("ml-12 px-6 py-4 bg-gray-300 rounded-xl", styles.userMessage)]: message.user === "user",
[classNames("text-gray-200", styles.systemMessage)]: message.user !== "user",
})}
>
{message?.text && (
typeof(message.text) == "string" ? message.text : JSON.stringify(message.text)
)}
</p>
))}
</div>
</div>
<div className="p-4 bg-gray-300 rounded-xl flex flex-col gap-2">
<TextArea
value={searchInputValue}
onChange={handleSearchInputChange}
onKeyUp={handleSubmitOnEnter}
isAutoExpanding
name="chatInput"
placeholder="Ask anything"
contentEditable={true}
className="resize-none min-h-14 max-h-96 overflow-y-auto"
/>
<div className="flex flex-row items-center justify-between gap-4">
<div className="flex flex-row items-center gap-2">
<label className="text-gray-600 whitespace-nowrap">Search type:</label>
<Select name="searchType" defaultValue={searchOptions[0].value} className="max-w-2xs">
{searchOptions.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</Select>
</div>
<CTAButton disabled={isSearchRunning} type="submit">
{isSearchRunning? "Searching..." : "Search"}
{isSearchRunning && <LoadingIndicator />}
</CTAButton>
</div>
</div>
</form>
</Stack>
</div>
);
}
interface Node {
name: string;
}
interface Relationship {
relationship_name: string;
}
type InsightMessage = [Node, Relationship, Node];
function convertToSearchTypeOutput(systemMessages: any[], searchType: string): string {
if (systemMessages.length > 0 && typeof(systemMessages[0]) === "string") {
return systemMessages[0];
}
switch (searchType) {
case 'INSIGHTS':
return systemMessages.map((message: InsightMessage) => {
const [node1, relationship, node2] = message;
if (node1.name && node2.name) {
return `${node1.name} ${relationship.relationship_name} ${node2.name}.`;
}
return '';
}).join('\n');
case 'SUMMARIES':
return systemMessages.map((message: { text: string }) => message.text).join('\n');
case 'CHUNKS':
return systemMessages.map((message: { text: string }) => message.text).join('\n');
default:
return "";
}
}

View file

@ -1,200 +1,200 @@
import { useCallback, useEffect, useState } from 'react';
import {
CTAButton,
DropdownSelect,
FormGroup,
FormInput,
FormLabel,
Input,
Spacer,
Stack,
useBoolean,
} from 'ohmy-ui';
import { LoadingIndicator } from '@/ui/App';
import { fetch } from '@/utils';
// import { useCallback, useEffect, useState } from 'react';
// import {
// CTAButton,
// DropdownSelect,
// FormGroup,
// FormInput,
// FormLabel,
// Input,
// Spacer,
// Stack,
// useBoolean,
// } from 'ohmy-ui';
// import { LoadingIndicator } from '@/ui/App';
// import { fetch } from '@/utils';
interface SelectOption {
label: string;
value: string;
}
// interface SelectOption {
// label: string;
// value: string;
// }
interface SettingsForm extends HTMLFormElement {
vectorDBUrl: HTMLInputElement;
vectorDBApiKey: HTMLInputElement;
llmProvider: HTMLInputElement;
llmModel: HTMLInputElement;
llmApiKey: HTMLInputElement;
llmEndpoint: HTMLInputElement;
llmApiVersion: HTMLInputElement;
}
// interface SettingsForm extends HTMLFormElement {
// vectorDBUrl: HTMLInputElement;
// vectorDBApiKey: HTMLInputElement;
// llmProvider: HTMLInputElement;
// llmModel: HTMLInputElement;
// llmApiKey: HTMLInputElement;
// llmEndpoint: HTMLInputElement;
// llmApiVersion: HTMLInputElement;
// }
const defaultProvider = {
label: 'OpenAI',
value: 'openai',
};
// const defaultProvider = {
// label: 'OpenAI',
// value: 'openai',
// };
const defaultModel = {
label: 'gpt-4o-mini',
value: 'gpt-4o-mini',
};
// const defaultModel = {
// label: 'gpt-4o-mini',
// value: 'gpt-4o-mini',
// };
export default function Settings({ onDone = () => {}, submitButtonText = 'Save' }) {
const [llmConfig, setLLMConfig] = useState<{
apiKey: string;
model: string;
endpoint: string;
apiVersion: string;
provider: string;
}>();
const [vectorDBConfig, setVectorDBConfig] = useState<{
url: string;
apiKey: string;
provider: SelectOption;
providers: SelectOption[];
}>();
// export default function Settings({ onDone = () => {}, submitButtonText = 'Save' }) {
// const [llmConfig, setLLMConfig] = useState<{
// apiKey: string;
// model: string;
// endpoint: string;
// apiVersion: string;
// provider: string;
// }>();
// const [vectorDBConfig, setVectorDBConfig] = useState<{
// url: string;
// apiKey: string;
// provider: SelectOption;
// providers: SelectOption[];
// }>();
const {
value: isSaving,
setTrue: startSaving,
setFalse: stopSaving,
} = useBoolean(false);
// const {
// value: isSaving,
// setTrue: startSaving,
// setFalse: stopSaving,
// } = useBoolean(false);
const saveConfig = (event: React.FormEvent<SettingsForm>) => {
event.preventDefault();
const formElements = event.currentTarget;
// const saveConfig = (event: React.FormEvent<SettingsForm>) => {
// event.preventDefault();
// const formElements = event.currentTarget;
const newVectorConfig = {
provider: vectorDBConfig?.provider.value,
url: formElements.vectorDBUrl.value,
apiKey: formElements.vectorDBApiKey.value,
};
// const newVectorConfig = {
// provider: vectorDBConfig?.provider.value,
// url: formElements.vectorDBUrl.value,
// apiKey: formElements.vectorDBApiKey.value,
// };
const newLLMConfig = {
provider: formElements.llmProvider.value,
model: formElements.llmModel.value,
apiKey: formElements.llmApiKey.value,
endpoint: formElements.llmEndpoint.value,
apiVersion: formElements.llmApiVersion.value,
};
// const newLLMConfig = {
// provider: formElements.llmProvider.value,
// model: formElements.llmModel.value,
// apiKey: formElements.llmApiKey.value,
// endpoint: formElements.llmEndpoint.value,
// apiVersion: formElements.llmApiVersion.value,
// };
startSaving();
// startSaving();
fetch('/v1/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
llm: newLLMConfig,
vectorDb: newVectorConfig,
}),
})
.then(() => {
onDone();
})
.finally(() => stopSaving());
};
// fetch('/v1/settings', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({
// llm: newLLMConfig,
// vectorDb: newVectorConfig,
// }),
// })
// .then(() => {
// onDone();
// })
// .finally(() => stopSaving());
// };
const handleVectorDBChange = useCallback((newVectorDBProvider: SelectOption) => {
setVectorDBConfig((config) => {
if (config?.provider !== newVectorDBProvider) {
return {
...config,
providers: config?.providers || [],
provider: newVectorDBProvider,
url: '',
apiKey: '',
};
}
return config;
});
}, []);
// const handleVectorDBChange = useCallback((newVectorDBProvider: SelectOption) => {
// setVectorDBConfig((config) => {
// if (config?.provider !== newVectorDBProvider) {
// return {
// ...config,
// providers: config?.providers || [],
// provider: newVectorDBProvider,
// url: '',
// apiKey: '',
// };
// }
// return config;
// });
// }, []);
useEffect(() => {
const fetchConfig = async () => {
const response = await fetch('/v1/settings');
const settings = await response.json();
// useEffect(() => {
// const fetchConfig = async () => {
// const response = await fetch('/v1/settings');
// const settings = await response.json();
if (!settings.llm.provider) {
settings.llm.provider = settings.llm.providers[0].value;
}
if (!settings.llm.model) {
settings.llm.model = settings.llm.models[settings.llm.provider][0].value;
}
if (!settings.vectorDb.provider) {
settings.vectorDb.provider = settings.vectorDb.providers[0];
} else {
settings.vectorDb.provider = settings.vectorDb.providers.find((provider: SelectOption) => provider.value === settings.vectorDb.provider);
}
setLLMConfig(settings.llm);
setVectorDBConfig(settings.vectorDb);
};
fetchConfig();
}, []);
// if (!settings.llm.provider) {
// settings.llm.provider = settings.llm.providers[0].value;
// }
// if (!settings.llm.model) {
// settings.llm.model = settings.llm.models[settings.llm.provider][0].value;
// }
// if (!settings.vectorDb.provider) {
// settings.vectorDb.provider = settings.vectorDb.providers[0];
// } else {
// settings.vectorDb.provider = settings.vectorDb.providers.find((provider: SelectOption) => provider.value === settings.vectorDb.provider);
// }
// setLLMConfig(settings.llm);
// setVectorDBConfig(settings.vectorDb);
// };
// fetchConfig();
// }, []);
return (
<form onSubmit={saveConfig} style={{ width: "100%", overflowY: "auto", maxHeight: "500px" }}>
<Stack gap="4" orientation="vertical">
<Stack gap="4" orientation="vertical">
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM provider:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.provider} name="llmProvider" placeholder="LLM provider" />
</FormInput>
</FormGroup>
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM model:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.model} name="llmModel" placeholder="LLM model" />
</FormInput>
</FormGroup>
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM endpoint:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.endpoint} name="llmEndpoint" placeholder="LLM endpoint url" />
</FormInput>
</FormGroup>
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM API key:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.apiKey} name="llmApiKey" placeholder="LLM API key" />
</FormInput>
</FormGroup>
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>LLM API version:</FormLabel>
<FormInput>
<Input defaultValue={llmConfig?.apiVersion} name="llmApiVersion" placeholder="LLM API version" />
</FormInput>
</FormGroup>
</Stack>
// return (
// <form onSubmit={saveConfig} style={{ width: "100%", overflowY: "auto", maxHeight: "500px" }}>
// <Stack gap="4" orientation="vertical">
// <Stack gap="4" orientation="vertical">
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM provider:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.provider} name="llmProvider" placeholder="LLM provider" />
// </FormInput>
// </FormGroup>
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM model:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.model} name="llmModel" placeholder="LLM model" />
// </FormInput>
// </FormGroup>
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM endpoint:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.endpoint} name="llmEndpoint" placeholder="LLM endpoint url" />
// </FormInput>
// </FormGroup>
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM API key:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.apiKey} name="llmApiKey" placeholder="LLM API key" />
// </FormInput>
// </FormGroup>
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>LLM API version:</FormLabel>
// <FormInput>
// <Input defaultValue={llmConfig?.apiVersion} name="llmApiVersion" placeholder="LLM API version" />
// </FormInput>
// </FormGroup>
// </Stack>
<Stack gap="2" orientation="vertical">
<FormGroup orientation="vertical" align="center/" gap="2">
<FormLabel>Vector DB provider:</FormLabel>
<DropdownSelect
value={vectorDBConfig?.provider || null}
options={vectorDBConfig?.providers || []}
onChange={handleVectorDBChange}
/>
</FormGroup>
<FormInput>
<Input defaultValue={vectorDBConfig?.url} name="vectorDBUrl" placeholder="Vector DB instance url" />
</FormInput>
<FormInput>
<Input defaultValue={vectorDBConfig?.apiKey} name="vectorDBApiKey" placeholder="Vector DB API key" />
</FormInput>
<Stack align="/end">
<Spacer top="2">
<CTAButton type="submit">
<Stack gap="2" orientation="vertical" align="center/">
{submitButtonText}
{isSaving && <LoadingIndicator />}
</Stack>
</CTAButton>
</Spacer>
</Stack>
</Stack>
</Stack>
</form>
)
}
// <Stack gap="2" orientation="vertical">
// <FormGroup orientation="vertical" align="center/" gap="2">
// <FormLabel>Vector DB provider:</FormLabel>
// <DropdownSelect
// value={vectorDBConfig?.provider || null}
// options={vectorDBConfig?.providers || []}
// onChange={handleVectorDBChange}
// />
// </FormGroup>
// <FormInput>
// <Input defaultValue={vectorDBConfig?.url} name="vectorDBUrl" placeholder="Vector DB instance url" />
// </FormInput>
// <FormInput>
// <Input defaultValue={vectorDBConfig?.apiKey} name="vectorDBApiKey" placeholder="Vector DB API key" />
// </FormInput>
// <Stack align="/end">
// <Spacer top="2">
// <CTAButton type="submit">
// <Stack gap="2" orientation="vertical" align="center/">
// {submitButtonText}
// {isSaving && <LoadingIndicator />}
// </Stack>
// </CTAButton>
// </Spacer>
// </Stack>
// </Stack>
// </Stack>
// </form>
// )
// }

View file

@ -1,10 +1,10 @@
import { Modal } from 'ohmy-ui';
import Settings from './Settings';
// import { Modal } from 'ohmy-ui';
// import Settings from './Settings';
export default function SettingsModal({ isOpen = false, onClose = () => {} }) {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<Settings onDone={onClose} />
</Modal>
)
}
// export default function SettingsModal({ isOpen = false, onClose = () => {} }) {
// return (
// <Modal isOpen={isOpen} onClose={onClose}>
// <Settings onDone={onClose} />
// </Modal>
// )
// }

View file

@ -40,12 +40,10 @@ export default function SignInForm({ onSignInSuccess = () => window.location.hre
method: "POST",
body: authCredentials,
})
.then(response => response.json())
.then((bearer) => {
window.localStorage.setItem("access_token", bearer.access_token);
.then(() => {
onSignInSuccess();
})
.catch(error => setSignInError(errorsMap[error.detail as keyof typeof errorsMap]))
.catch(error => setSignInError(errorsMap[error.detail as keyof typeof errorsMap] || error.message))
.finally(() => enableSignIn());
};

View file

@ -1,24 +0,0 @@
.wizardContent {
width: 100%;
max-width: 400px;
height: max-content;
background: linear-gradient(90deg, #6510F4 0.52%, #0DFF00 103.83%);
padding: 24px;
margin: 0 auto;
position: relative;
}
.wizardContent::before {
content: '';
width: calc(100% - 6px);
height: calc(100% - 6px);
max-width: 394px;
position: absolute;
top: 3px;
left: 3px;
background-color: #351A4B;
}
.wizardContent > * {
position: relative;
}

View file

@ -1,6 +0,0 @@
import { withStyles } from 'ohmy-ui';
import styles from './WizardContent.module.css';
const WizardContent = withStyles<{ children: React.ReactNode }>('div', { className: styles.wizardContent });
export default WizardContent;

View file

@ -1,11 +0,0 @@
import { H1 } from 'ohmy-ui';
interface WizardHeadingProps {
children: React.ReactNode;
}
export default function WizardHeading({ children, ...props }: WizardHeadingProps) {
return (
<H1 {...props} align="center" size="small" style={{ color: '#40A9FF' }}>{children}</H1>
);
}

View file

@ -1,2 +0,0 @@
export { default as WizardHeading } from './WizardHeading';
export { default as WizardContent } from './WizardContent/WizardContent';

View file

@ -1,6 +1,6 @@
export { default as Footer } from "./Footer/Footer";
export { default as SettingsModal } from "./SettingsModal/SettingsModal";
// export { default as SettingsModal } from "./SettingsModal/SettingsModal";
export { default as SearchView } from "./SearchView/SearchView";
export { default as IFrameView } from "./IFrameView/IFrameView";
export { default as Explorer } from "./Explorer/Explorer";
// export { default as Explorer } from "./Explorer/Explorer";
export { default as FeedbackForm } from "./FeedbackForm";

View file

@ -3,6 +3,6 @@ import { ButtonHTMLAttributes } from "react";
export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-3xl bg-indigo-600 px-4 py-3 text-white hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
);
}

View file

@ -0,0 +1,8 @@
import classNames from 'classnames';
import { ButtonHTMLAttributes } from "react";
export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-3xl bg-transparent px-4 py-3 text-white shadow-xs border-1 hover:bg-gray-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
);
}

View file

@ -3,6 +3,6 @@ import { InputHTMLAttributes } from "react"
export default function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
return (
<input className={classNames("block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6", className)} {...props} />
<input className={classNames("block w-full rounded-md bg-white px-4 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600", className)} {...props} />
)
}

View file

@ -0,0 +1,12 @@
interface ModalProps {
isOpen: boolean;
children: React.ReactNode;
}
export default function Modal({ isOpen, children }: ModalProps) {
return isOpen && (
<div className="fixed top-0 left-0 right-0 bottom-0 backdrop-blur-lg z-1 flex items-center justify-center">
{children}
</div>
);
}

View file

@ -3,6 +3,6 @@ import { ButtonHTMLAttributes } from "react";
export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-md bg-transparent px-3 py-2 text-sm font-semibold text-white shadow-xs border-1 border-white hover:bg-gray-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-3xl bg-transparent px-4 py-3 text-white shadow-xs border-1 border-white hover:bg-gray-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
);
}

View file

@ -1,10 +1,24 @@
import classNames from "classnames";
import { SelectHTMLAttributes } from "react";
import { CaretIcon } from "../Icons";
export default function Select({ children, className, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {
return (
<select className={classNames("block w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6", className)} {...props}>
{children}
</select>
<div className="relative">
<select
className={
classNames(
"block w-full appearance-none rounded-md bg-white pl-4 pr-8 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600",
className,
)
}
{...props}
>
{children}
</select>
<span className="pointer-events-none absolute top-1/2 -mt-0.5 right-3 text-indigo-600 rotate-180">
<CaretIcon height={8} width={12} />
</span>
</div>
);
}

View file

@ -1,7 +1,103 @@
import { InputHTMLAttributes } from "react"
"use client";
export default function TextArea(props: InputHTMLAttributes<HTMLTextAreaElement>) {
return (
<textarea className="block w-full mt-2 rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" {...props} />
import classNames from "classnames";
import { InputHTMLAttributes, useCallback, useEffect, useLayoutEffect, useRef } from "react"
interface TextAreaProps extends Omit<InputHTMLAttributes<HTMLTextAreaElement>, "onChange"> {
isAutoExpanding?: boolean; // Set to true to enable auto-expanding text area behavior. Default is false.
value: string;
onChange: (value: string) => void;
}
export default function TextArea({
isAutoExpanding,
style,
name,
value,
onChange,
className,
placeholder = "",
onKeyUp,
...props
}: TextAreaProps) {
const handleTextChange = useCallback((event: Event) => {
const fakeTextAreaElement = event.target as HTMLDivElement;
const newValue = fakeTextAreaElement.innerText;
if (newValue !== value) {
onChange?.(newValue);
}
}, [onChange, value]);
const handleKeyUp = useCallback((event: Event) => {
if (onKeyUp) {
onKeyUp(event as unknown as React.KeyboardEvent<HTMLTextAreaElement>);
}
}, [onKeyUp]);
const handleTextAreaFocus = (event: React.FocusEvent<HTMLDivElement>) => {
if (event.target.innerText.trim() === placeholder) {
event.target.innerText = "";
}
};
const handleTextAreaBlur = (event: React.FocusEvent<HTMLDivElement>) => {
if (value === "") {
event.target.innerText = placeholder;
}
};
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(event.target.value);
};
const fakeTextAreaRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const fakeTextAreaElement = fakeTextAreaRef.current;
if (fakeTextAreaElement) {
fakeTextAreaElement.innerText = placeholder;
fakeTextAreaElement.addEventListener("input", handleTextChange);
fakeTextAreaElement.addEventListener("keyup", handleKeyUp);
}
return () => {
if (fakeTextAreaElement) {
fakeTextAreaElement.removeEventListener("input", handleTextChange);
fakeTextAreaElement.removeEventListener("keyup", handleKeyUp);
}
};
}, []);
useEffect(() => {
const fakeTextAreaElement = fakeTextAreaRef.current;
const textAreaText = fakeTextAreaElement?.innerText;
if (fakeTextAreaElement && textAreaText !== value && textAreaText !== placeholder) {
fakeTextAreaElement.innerText = value;
}
}, [value]);
return isAutoExpanding ? (
<>
<div
ref={fakeTextAreaRef}
contentEditable="true"
role="textbox"
aria-multiline="true"
className={classNames("block w-full rounded-md bg-white px-4 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600", className)}
onFocus={handleTextAreaFocus}
onBlur={handleTextAreaBlur}
/>
</>
) : (
<textarea
name={name}
style={style}
value={value}
placeholder={placeholder}
className={classNames("block w-full rounded-md bg-white px-4 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600", className)}
onChange={handleChange}
{...props}
/>
)
}

View file

@ -1,6 +1,8 @@
export { default as Modal } from "./Modal";
export { default as Input } from "./Input";
export { default as Select } from "./Select";
export { default as TextArea } from "./TextArea";
export { default as CTAButton } from "./CTAButton";
export { default as GhostButton } from "./GhostButton";
export { default as NeutralButton } from "./NeutralButton";
export { default as StatusIndicator } from "./StatusIndicator";

View file

@ -1,12 +1,47 @@
import handleServerErrors from './handleServerErrors';
import handleServerErrors from "./handleServerErrors";
export default function fetch(url: string, options: RequestInit = {}): Promise<Response> {
return global.fetch('http://localhost:8000/api' + url, {
let numberOfRetries = 0;
const isAuth0Enabled = process.env.USE_AUTH0_AUTHORIZATION?.toLowerCase() === "true"
export default async function fetch(url: string, options: RequestInit = {}): Promise<Response> {
function retry(lastError: Response) {
if (!isAuth0Enabled) {
return Promise.reject(lastError);
}
if (numberOfRetries >= 1) {
return Promise.reject(lastError);
}
numberOfRetries += 1;
return window.fetch("/auth/token")
.then(() => {
return fetch(url, options);
});
}
return global.fetch("http://localhost:8000/api" + url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
},
credentials: "include",
})
.then(handleServerErrors);
.then((response) => handleServerErrors(response, retry))
.then((response) => {
numberOfRetries = 0;
return response;
})
.catch((error) => {
if (error.detail === undefined) {
return Promise.reject(
new Error("No connection to the server.")
);
}
if (error.status === 401) {
return retry(error);
}
return Promise.reject(error);
});
}

View file

@ -1,13 +1,25 @@
export default function handleServerErrors(response: Response): Promise<Response> {
import { redirect } from "next/navigation";
export default function handleServerErrors(response: Response, retry?: (response: Response) => Promise<Response>): Promise<Response> {
return new Promise((resolve, reject) => {
if (response.status === 401) {
window.location.href = '/auth';
return;
if (retry) {
return retry(response)
.catch(() => {
return redirect("/auth/login");
});
} else {
return redirect("/auth/login");
}
}
if (!response.ok) {
return response.json().then(error => reject(error));
}
return resolve(response);
if (response.status >= 200 && response.status < 300) {
return resolve(response);
}
return reject(response);
});
}

View file

@ -1,10 +1,10 @@
import { useState } from "react";
import { useCallback, useState } from "react";
export default function useBoolean(initialValue: boolean) {
const [value, setValue] = useState(initialValue);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return {
value,

41
cognee-frontend/types/d3-force-3d.d.ts vendored Normal file
View file

@ -0,0 +1,41 @@
declare module "d3-force-3d" {
// Import types from d3-force if needed
import {
SimulationNodeDatum,
SimulationLinkDatum,
Force,
Simulation,
} from "d3-force";
export interface SimulationNodeDatum3D extends SimulationNodeDatum {
x: number;
y: number;
z: number;
vx: number;
vy: number;
vz: number;
fx?: number | null;
fy?: number | null;
fz?: number | null;
}
export function forceSimulation<NodeDatum extends SimulationNodeDatum3D>(
nodes?: NodeDatum[]
): Simulation<NodeDatum, undefined>;
export function forceCenter(x: number, y: number, z: number): Force<SimulationNodeDatum3D, any>;
export function forceManyBody(): Force<SimulationNodeDatum3D, any>;
export function forceLink<NodeDatum extends SimulationNodeDatum3D, Links extends SimulationLinkDatum<NodeDatum>[] = SimulationLinkDatum<NodeDatum>[]>(
links?: Links
): Force<NodeDatum, SimulationLinkDatum<NodeDatum>>;
export function forceCollide(radius?: number): Force<SimulationNodeDatum3D, any>;
export function forceRadial(radius: number, x?: number, y?: number, z?: number): Force<SimulationNodeDatum3D, any>;
export function forceX(x?: number): Force<SimulationNodeDatum3D, any>;
export function forceY(y?: number): Force<SimulationNodeDatum3D, any>;
export function forceZ(z?: number): Force<SimulationNodeDatum3D, any>;
}

View file

@ -68,7 +68,7 @@ app = FastAPI(debug=app_environment != "prod", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
allow_credentials=True,
allow_methods=["OPTIONS", "GET", "POST", "DELETE"],
allow_headers=["*"],

View file

@ -17,10 +17,10 @@ logger = get_logger()
def get_add_router() -> APIRouter:
router = APIRouter()
@router.post("/", response_model=dict)
@router.post("", response_model=dict)
async def add(
data: List[UploadFile],
datasetName: str,
datasetName: Optional[str] = Form(default=None),
datasetId: Optional[UUID] = Form(default=None),
user: User = Depends(get_authenticated_user),
):

View file

@ -1,3 +1,4 @@
import os
import asyncio
from uuid import UUID
from pydantic import BaseModel
@ -6,37 +7,53 @@ from fastapi.responses import JSONResponse
from fastapi import APIRouter, WebSocket, Depends, WebSocketDisconnect
from starlette.status import WS_1000_NORMAL_CLOSURE, WS_1008_POLICY_VIOLATION
from cognee.api.DTO import InDTO
from cognee.modules.pipelines.methods import get_pipeline_run
from cognee.modules.users.models import User
from cognee.shared.data_models import KnowledgeGraph
from cognee.modules.users.methods import get_authenticated_user
from cognee.modules.users.get_user_db import get_user_db_context
from cognee.modules.graph.methods import get_formatted_graph_data
from cognee.modules.users.get_user_manager import get_user_manager_context
from cognee.infrastructure.databases.relational import get_relational_engine
from cognee.modules.users.authentication.default.default_jwt_strategy import DefaultJWTStrategy
from cognee.modules.pipelines.models.PipelineRunInfo import PipelineRunCompleted, PipelineRunInfo
from cognee.modules.graph.utils import deduplicate_nodes_and_edges, get_graph_from_model
from cognee.modules.pipelines.queues.pipeline_run_info_queues import (
get_from_queue,
initialize_queue,
remove_queue,
)
from cognee.shared.logging_utils import get_logger
class CognifyPayloadDTO(BaseModel):
datasets: List[str]
logger = get_logger("api.cognify")
class CognifyPayloadDTO(InDTO):
datasets: Optional[List[str]] = None
dataset_ids: Optional[List[UUID]] = None
graph_model: Optional[BaseModel] = KnowledgeGraph
run_in_background: Optional[bool] = False
def get_cognify_router() -> APIRouter:
router = APIRouter()
@router.post("/", response_model=None)
@router.post("", response_model=None)
async def cognify(payload: CognifyPayloadDTO, user: User = Depends(get_authenticated_user)):
"""This endpoint is responsible for the cognitive processing of the content."""
if not payload.datasets and not payload.dataset_ids:
return JSONResponse(
status_code=400, content={"error": "No datasets or dataset_ids provided"}
)
from cognee.api.v1.cognify import cognify as cognee_cognify
try:
datasets = payload.dataset_ids if payload.dataset_ids else payload.datasets
cognify_run = await cognee_cognify(
datasets, user, payload.graph_model, run_in_background=True
datasets, user, payload.graph_model, run_in_background=payload.run_in_background
)
return cognify_run.model_dump()
@ -47,16 +64,33 @@ def get_cognify_router() -> APIRouter:
async def subscribe_to_cognify_info(websocket: WebSocket, pipeline_run_id: str):
await websocket.accept()
auth_message = await websocket.receive_json()
access_token = websocket.cookies.get(os.getenv("AUTH_TOKEN_COOKIE_NAME", "auth_token"))
try:
await get_authenticated_user(auth_message.get("Authorization"))
except Exception:
secret = os.getenv("FASTAPI_USERS_JWT_SECRET", "super_secret")
strategy = DefaultJWTStrategy(secret, lifetime_seconds=3600)
db_engine = get_relational_engine()
async with db_engine.get_async_session() as session:
async with get_user_db_context(session) as user_db:
async with get_user_manager_context(user_db) as user_manager:
user = await get_authenticated_user(
cookie=access_token,
strategy_cookie=strategy,
user_manager=user_manager,
bearer=None,
)
except Exception as error:
logger.error(f"Authentication failed: {str(error)}")
await websocket.close(code=WS_1008_POLICY_VIOLATION, reason="Unauthorized")
return
pipeline_run_id = UUID(pipeline_run_id)
pipeline_run = await get_pipeline_run(pipeline_run_id)
initialize_queue(pipeline_run_id)
while True:
@ -74,9 +108,7 @@ def get_cognify_router() -> APIRouter:
{
"pipeline_run_id": str(pipeline_run_info.pipeline_run_id),
"status": pipeline_run_info.status,
"payload": await get_nodes_and_edges(pipeline_run_info.payload)
if pipeline_run_info.payload
else None,
"payload": await get_formatted_graph_data(pipeline_run.dataset_id, user.id),
}
)
@ -89,53 +121,3 @@ def get_cognify_router() -> APIRouter:
break
return router
async def get_nodes_and_edges(data_points):
nodes = []
edges = []
added_nodes = {}
added_edges = {}
visited_properties = {}
results = await asyncio.gather(
*[
get_graph_from_model(
data_point,
added_nodes=added_nodes,
added_edges=added_edges,
visited_properties=visited_properties,
)
for data_point in data_points
]
)
for result_nodes, result_edges in results:
nodes.extend(result_nodes)
edges.extend(result_edges)
nodes, edges = deduplicate_nodes_and_edges(nodes, edges)
return {
"nodes": list(
map(
lambda node: {
"id": str(node.id),
"label": node.name if hasattr(node, "name") else f"{node.type}_{str(node.id)}",
"properties": {},
},
nodes,
)
),
"edges": list(
map(
lambda edge: {
"source": str(edge[0]),
"target": str(edge[1]),
"label": edge[2],
},
edges,
)
),
}

View file

@ -1,17 +1,25 @@
from cognee.shared.logging_utils import get_logger
from fastapi import APIRouter
from datetime import datetime
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel
from typing import List, Optional
from typing_extensions import Annotated
from fastapi import status
from fastapi import APIRouter
from fastapi import HTTPException, Query, Depends
from fastapi.responses import JSONResponse, FileResponse
from pydantic import BaseModel
from cognee.api.DTO import OutDTO
from cognee.infrastructure.databases.exceptions import EntityNotFoundError
from cognee.api.DTO import InDTO, OutDTO
from cognee.infrastructure.databases.relational import get_relational_engine
from cognee.modules.data.methods import create_dataset, get_datasets_by_name
from cognee.shared.logging_utils import get_logger
from cognee.api.v1.delete.exceptions import DataNotFoundError, DatasetNotFoundError
from cognee.modules.users.models import User
from cognee.modules.users.methods import get_authenticated_user
from cognee.modules.users.permissions.methods import (
get_all_user_permission_datasets,
give_permission_on_dataset,
)
from cognee.modules.graph.methods import get_formatted_graph_data
from cognee.modules.pipelines.models import PipelineRunStatus
logger = get_logger()
@ -56,21 +64,53 @@ class GraphDTO(OutDTO):
edges: List[GraphEdgeDTO]
class DatasetCreationPayload(InDTO):
name: str
def get_datasets_router() -> APIRouter:
router = APIRouter()
@router.get("/", response_model=list[DatasetDTO])
@router.get("", response_model=list[DatasetDTO])
async def get_datasets(user: User = Depends(get_authenticated_user)):
try:
from cognee.modules.data.methods import get_datasets
datasets = await get_datasets(user.id)
datasets = await get_all_user_permission_datasets(user, "read")
return datasets
except Exception as error:
logger.error(f"Error retrieving datasets: {str(error)}")
raise HTTPException(
status_code=500, detail=f"Error retrieving datasets: {str(error)}"
status_code=status.HTTP_418_IM_A_TEAPOT,
detail=f"Error retrieving datasets: {str(error)}",
) from error
@router.post("", response_model=DatasetDTO)
async def create_new_dataset(
dataset_data: DatasetCreationPayload, user: User = Depends(get_authenticated_user)
):
try:
datasets = await get_datasets_by_name([dataset_data.name], user.id)
if datasets:
return datasets[0]
db_engine = get_relational_engine()
async with db_engine.get_async_session() as session:
dataset = await create_dataset(
dataset_name=dataset_data.name, user=user, session=session
)
await give_permission_on_dataset(user, dataset.id, "read")
await give_permission_on_dataset(user, dataset.id, "write")
await give_permission_on_dataset(user, dataset.id, "share")
await give_permission_on_dataset(user, dataset.id, "delete")
return dataset
except Exception as error:
logger.error(f"Error creating dataset: {str(error)}")
raise HTTPException(
status_code=status.HTTP_418_IM_A_TEAPOT,
detail=f"Error creating dataset: {str(error)}",
) from error
@router.delete(
@ -82,7 +122,7 @@ def get_datasets_router() -> APIRouter:
dataset = await get_dataset(user.id, dataset_id)
if dataset is None:
raise EntityNotFoundError(message=f"Dataset ({str(dataset_id)}) not found.")
raise DatasetNotFoundError(message=f"Dataset ({str(dataset_id)}) not found.")
await delete_dataset(dataset)
@ -100,54 +140,30 @@ def get_datasets_router() -> APIRouter:
# Check if user has permission to access dataset and data by trying to get the dataset
dataset = await get_dataset(user.id, dataset_id)
# TODO: Handle situation differently if user doesn't have permission to access data?
if dataset is None:
raise EntityNotFoundError(message=f"Dataset ({str(dataset_id)}) not found.")
raise DatasetNotFoundError(message=f"Dataset ({str(dataset_id)}) not found.")
data = await get_data(user.id, data_id)
if data is None:
raise EntityNotFoundError(message=f"Data ({str(data_id)}) not found.")
raise DataNotFoundError(message=f"Data ({str(data_id)}) not found.")
await delete_data(data)
@router.get("/{dataset_id}/graph", response_model=GraphDTO)
async def get_dataset_graph(dataset_id: UUID, user: User = Depends(get_authenticated_user)):
from cognee.infrastructure.databases.graph import get_graph_engine
try:
graph_client = await get_graph_engine()
(nodes, edges) = await graph_client.get_graph_data()
from cognee.modules.data.methods import get_dataset
dataset = await get_dataset(user.id, dataset_id)
formatted_graph_data = await get_formatted_graph_data(dataset.id, user.id)
return JSONResponse(
status_code=200,
content={
"nodes": list(
map(
lambda node: {
"id": str(node[0]),
"label": node[1]["name"]
if hasattr(node[1], "name")
else f"{node[1]['type']}_{str(node[0])}",
"properties": {},
},
nodes,
)
),
"edges": list(
map(
lambda edge: {
"source": str(edge[0]),
"target": str(edge[1]),
"label": edge[2],
},
edges,
)
),
},
content=formatted_graph_data,
)
except Exception as error:
print(error)
except Exception:
return JSONResponse(
status_code=409,
content="Error retrieving dataset graph data.",
@ -207,20 +223,20 @@ def get_datasets_router() -> APIRouter:
dataset_data = await get_dataset_data(dataset.id)
if dataset_data is None:
raise EntityNotFoundError(message=f"No data found in dataset ({dataset_id}).")
raise DataNotFoundError(message=f"No data found in dataset ({dataset_id}).")
matching_data = [data for data in dataset_data if data.id == data_id]
# Check if matching_data contains an element
if len(matching_data) == 0:
raise EntityNotFoundError(
raise DataNotFoundError(
message=f"Data ({data_id}) not found in dataset ({dataset_id})."
)
data = await get_data(user.id, data_id)
if data is None:
raise EntityNotFoundError(
raise DataNotFoundError(
message=f"Data ({data_id}) not found in dataset ({dataset_id})."
)

View file

@ -26,6 +26,18 @@ class DatasetNotFoundError(CogneeApiError):
super().__init__(message, name, status_code)
class DataNotFoundError(CogneeApiError):
"""Raised when a dataset cannot be found."""
def __init__(
self,
message: str = "Data not found.",
name: str = "DataNotFoundError",
status_code: int = status.HTTP_404_NOT_FOUND,
):
super().__init__(message, name, status_code)
class DocumentSubgraphNotFoundError(CogneeApiError):
"""Raised when a document's subgraph cannot be found in the graph database."""

View file

@ -14,7 +14,7 @@ logger = get_logger()
def get_delete_router() -> APIRouter:
router = APIRouter()
@router.delete("/", response_model=None)
@router.delete("", response_model=None)
async def delete(
data: List[UploadFile],
dataset_name: str = Form("main_dataset"),

View file

@ -5,6 +5,7 @@ from fastapi import Depends, APIRouter
from fastapi.responses import JSONResponse
from cognee.modules.search.types import SearchType
from cognee.api.DTO import InDTO, OutDTO
from cognee.modules.users.exceptions.exceptions import PermissionDeniedError
from cognee.modules.users.models import User
from cognee.modules.search.operations import get_history
from cognee.modules.users.methods import get_authenticated_user
@ -29,16 +30,16 @@ def get_search_router() -> APIRouter:
user: str
created_at: datetime
@router.get("/", response_model=list[SearchHistoryItem])
@router.get("", response_model=list[SearchHistoryItem])
async def get_search_history(user: User = Depends(get_authenticated_user)):
try:
history = await get_history(user.id)
history = await get_history(user.id, limit=0)
return history
except Exception as error:
return JSONResponse(status_code=500, content={"error": str(error)})
@router.post("/", response_model=list)
@router.post("", response_model=list)
async def search(payload: SearchPayloadDTO, user: User = Depends(get_authenticated_user)):
"""This endpoint is responsible for searching for nodes in the graph."""
from cognee.api.v1.search import search as cognee_search
@ -54,6 +55,8 @@ def get_search_router() -> APIRouter:
)
return results
except PermissionDeniedError:
return []
except Exception as error:
return JSONResponse(status_code=409, content={"error": str(error)})

View file

@ -46,13 +46,13 @@ class SettingsPayloadDTO(InDTO):
def get_settings_router() -> APIRouter:
router = APIRouter()
@router.get("/", response_model=SettingsDTO)
@router.get("", response_model=SettingsDTO)
async def get_settings(user: User = Depends(get_authenticated_user)):
from cognee.modules.settings import get_settings as get_cognee_settings
return get_cognee_settings()
@router.post("/", response_model=None)
@router.post("", response_model=None)
async def save_settings(
new_settings: SettingsPayloadDTO, user: User = Depends(get_authenticated_user)
):

View file

@ -1,7 +1,7 @@
from cognee.modules.users.get_fastapi_users import get_fastapi_users
from cognee.modules.users.authentication.get_auth_backend import get_auth_backend
from cognee.modules.users.authentication.get_client_auth_backend import get_client_auth_backend
def get_auth_router():
auth_backend = get_auth_backend()
auth_backend = get_client_auth_backend()
return get_fastapi_users().get_auth_router(auth_backend)

View file

@ -8,7 +8,7 @@ logger = get_logger()
def get_visualize_router() -> APIRouter:
router = APIRouter()
@router.get("/", response_model=None)
@router.get("", response_model=None)
async def visualize():
"""This endpoint is responsible for adding data to the graph."""
from cognee.api.v1.visualize import visualize_graph

View file

@ -46,7 +46,12 @@ class KuzuAdapter(GraphDBInterface):
def _initialize_connection(self) -> None:
"""Initialize the Kuzu database connection and schema."""
try:
os.makedirs(self.db_path, exist_ok=True)
try:
os.makedirs(self.db_path, exist_ok=True)
except FileExistsError:
os.remove(self.db_path)
os.makedirs(self.db_path, exist_ok=True)
self.db = Database(self.db_path)
self.db.init_database()
self.connection = Connection(self.db)
@ -1044,7 +1049,7 @@ class KuzuAdapter(GraphDBInterface):
return [], []
edges_query = """
MATCH (n:Node)-[r:EDGE]->(m:Node)
MATCH (n:Node)-[r]->(m:Node)
RETURN n.id, m.id, r.relationship_name, r.properties
"""
edges = await self.query(edges_query)

Some files were not shown because too many files have changed in this diff Show more