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:
parent
74000b4548
commit
e7644f4b3a
129 changed files with 7342 additions and 3810 deletions
3
cognee-frontend/.prettierignore
Normal file
3
cognee-frontend/.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
coverage
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
16
cognee-frontend/eslint.config.mjs
Normal file
16
cognee-frontend/eslint.config.mjs
Normal 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;
|
||||
4757
cognee-frontend/package-lock.json
generated
4757
cognee-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
cognee-frontend/public/images/cognee-logo-with-text.png
Normal file
BIN
cognee-frontend/public/images/cognee-logo-with-text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
cognee-frontend/public/images/crewai.png
Normal file
BIN
cognee-frontend/public/images/crewai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
53
cognee-frontend/public/images/deepnote.svg
Normal file
53
cognee-frontend/public/images/deepnote.svg
Normal 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 |
3
cognee-frontend/public/images/lancedb.svg
Normal file
3
cognee-frontend/public/images/lancedb.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
BIN
cognee-frontend/public/images/neo4j.png
Normal file
BIN
cognee-frontend/public/images/neo4j.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
54
cognee-frontend/src/app/(graph)/ActivityLog.tsx
Normal file
54
cognee-frontend/src/app/(graph)/ActivityLog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
25
cognee-frontend/src/app/(graph)/GraphLegend.tsx
Normal file
25
cognee-frontend/src/app/(graph)/GraphLegend.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
226
cognee-frontend/src/app/(graph)/GraphVisualization.tsx
Normal file
226
cognee-frontend/src/app/(graph)/GraphVisualization.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
cognee-frontend/src/app/(graph)/getColorForNodeType.ts
Normal file
22
cognee-frontend/src/app/(graph)/getColorForNodeType.ts
Normal 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];
|
||||
}
|
||||
82
cognee-frontend/src/app/auth/AuthForm.tsx
Normal file
82
cognee-frontend/src/app/auth/AuthForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
31
cognee-frontend/src/app/auth/layout.tsx
Normal file
31
cognee-frontend/src/app/auth/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
cognee-frontend/src/app/auth/login/LoginPage.tsx
Normal file
40
cognee-frontend/src/app/auth/login/LoginPage.tsx
Normal 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;
|
||||
}
|
||||
1
cognee-frontend/src/app/auth/login/page.tsx
Normal file
1
cognee-frontend/src/app/auth/login/page.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./LoginPage";
|
||||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuthPage';
|
||||
export { default } from "./AuthPage";
|
||||
|
|
|
|||
31
cognee-frontend/src/app/auth/signup/SignUpPage.tsx
Normal file
31
cognee-frontend/src/app/auth/signup/SignUpPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
cognee-frontend/src/app/auth/signup/page.tsx
Normal file
1
cognee-frontend/src/app/auth/signup/page.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SignUpPage";
|
||||
17
cognee-frontend/src/app/auth/token/route.ts
Normal file
17
cognee-frontend/src/app/auth/token/route.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
export { default } from "./(graph)/GraphView";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
.files {
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -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]}`;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './AddStep';
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CognifyStep';
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ConfigStep';
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ExploreStep';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
29
cognee-frontend/src/middleware.ts
Normal file
29
cognee-frontend/src/middleware.ts
Normal 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).*)",
|
||||
],
|
||||
};
|
||||
8
cognee-frontend/src/modules/auth/auth0.ts
Normal file
8
cognee-frontend/src/modules/auth/auth0.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
|
|
@ -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());
|
||||
}
|
||||
123
cognee-frontend/src/modules/chat/hooks/useChat.ts
Normal file
123
cognee-frontend/src/modules/chat/hooks/useChat.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
// });
|
||||
}
|
||||
|
|
|
|||
12
cognee-frontend/src/modules/datasets/createDataset.ts
Normal file
12
cognee-frontend/src/modules/datasets/createDataset.ts
Normal 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());
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
.dataPreviewModal {
|
||||
left: 5% !important;
|
||||
padding: 0 !important;
|
||||
max-width: 90% !important;
|
||||
height: 80%;
|
||||
top: 5% !important;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default, type Data } from './DataView';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './DatasetsView';
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
7
cognee-frontend/src/ui/Icons/AddIcon.tsx
Normal file
7
cognee-frontend/src/ui/Icons/AddIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
8
cognee-frontend/src/ui/Icons/CaretIcon.tsx
Normal file
8
cognee-frontend/src/ui/Icons/CaretIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
cognee-frontend/src/ui/Icons/SearchIcon.tsx
Normal file
9
cognee-frontend/src/ui/Icons/SearchIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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's reasoning</label>
|
||||
<TextArea id="feedback" name="feedback" type="text" placeholder="Your feedback" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
// )
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
// )
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default as WizardHeading } from './WizardHeading';
|
||||
export { default as WizardContent } from './WizardContent/WizardContent';
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
8
cognee-frontend/src/ui/elements/GhostButton.tsx
Normal file
8
cognee-frontend/src/ui/elements/GhostButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} />
|
||||
)
|
||||
}
|
||||
|
|
|
|||
12
cognee-frontend/src/ui/elements/Modal.tsx
Normal file
12
cognee-frontend/src/ui/elements/Modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
41
cognee-frontend/types/d3-force-3d.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
|
|
@ -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=["*"],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})."
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue