Merge branch 'dev' into COG-2826
This commit is contained in:
commit
0b6db2c23f
142 changed files with 4674 additions and 237 deletions
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
Before Width: | Height: | Size: 629 B |
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import classNames from "classnames";
|
||||
import { MutableRefObject, useEffect, useImperativeHandle, useRef, useState, useCallback } from "react";
|
||||
import { forceCollide, forceManyBody } from "d3-force-3d";
|
||||
import ForceGraph, { ForceGraphMethods, GraphData, LinkObject, NodeObject } from "react-force-graph-2d";
|
||||
|
|
@ -10,6 +11,7 @@ interface GraphVisuzaliationProps {
|
|||
ref: MutableRefObject<GraphVisualizationAPI>;
|
||||
data?: GraphData<NodeObject, LinkObject>;
|
||||
graphControls: MutableRefObject<GraphControlsAPI>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface GraphVisualizationAPI {
|
||||
|
|
@ -17,7 +19,7 @@ export interface GraphVisualizationAPI {
|
|||
setGraphShape: (shape: string) => void;
|
||||
}
|
||||
|
||||
export default function GraphVisualization({ ref, data, graphControls }: GraphVisuzaliationProps) {
|
||||
export default function GraphVisualization({ ref, data, graphControls, className }: GraphVisuzaliationProps) {
|
||||
const textSize = 6;
|
||||
const nodeSize = 15;
|
||||
// const addNodeDistanceFromSourceNode = 15;
|
||||
|
|
@ -201,7 +203,7 @@ export default function GraphVisualization({ ref, data, graphControls }: GraphVi
|
|||
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));
|
||||
graphRef.current.d3Force("charge", forceManyBody().strength(-10).distanceMin(10).distanceMax(50));
|
||||
}
|
||||
}, [data, graphRef]);
|
||||
|
||||
|
|
@ -213,7 +215,7 @@ export default function GraphVisualization({ ref, data, graphControls }: GraphVi
|
|||
}));
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-full" id="graph-container">
|
||||
<div ref={containerRef} className={classNames("w-full h-full", className)} id="graph-container">
|
||||
{(data && typeof window !== "undefined") ? (
|
||||
<ForceGraph
|
||||
ref={graphRef}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@ 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]),
|
||||
TextDocument: formatHex(colors.stone[200]),
|
||||
DocumentChunk: formatHex(colors.stone[300]),
|
||||
TextSummary: formatHex(colors.blue[300]),
|
||||
Entity: formatHex(colors.indigo[300]),
|
||||
EntityType: formatHex(colors.indigo[400]),
|
||||
NodeSet: formatHex(colors.indigo[400]),
|
||||
GitHubUser: formatHex(colors.gray[200]),
|
||||
Comment: formatHex(colors.blue[300]),
|
||||
Issue: formatHex(colors.red[200]),
|
||||
Repository: formatHex(colors.stone[200]),
|
||||
Commit: formatHex(colors.teal[300]),
|
||||
File: formatHex(colors.emerald[300]),
|
||||
FileChange: formatHex(colors.sky[300]),
|
||||
};
|
||||
|
||||
export default function getColorForNodeType(type: string) {
|
||||
|
|
|
|||
51
cognee-frontend/src/app/account/Account.tsx
Normal file
51
cognee-frontend/src/app/account/Account.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Link from "next/link";
|
||||
import { BackIcon } from "@/ui/Icons";
|
||||
import { CTAButton } from "@/ui/elements";
|
||||
import Header from "@/ui/Layout/Header";
|
||||
|
||||
export default function Account() {
|
||||
const account = {
|
||||
name: "John Doe",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute top-0 right-0 bottom-0 left-0 flex flex-row gap-2.5">
|
||||
<div className="flex-1/5 bg-gray-100 h-full"></div>
|
||||
<div className="flex-1/5 bg-gray-100 h-full"></div>
|
||||
<div className="flex-1/5 bg-gray-100 h-full"></div>
|
||||
<div className="flex-1/5 bg-gray-100 h-full"></div>
|
||||
<div className="flex-1/5 bg-gray-100 h-full"></div>
|
||||
</div>
|
||||
|
||||
<Header />
|
||||
|
||||
<div className="relative flex flex-row items-start gap-2.5">
|
||||
<Link href="/dashboard" className="flex-1/5 py-4 px-5 flex flex-row items-center gap-5">
|
||||
<BackIcon />
|
||||
<span>back</span>
|
||||
</Link>
|
||||
<div className="flex-1/5 flex flex-col gap-2.5">
|
||||
<div className="py-4 px-5 rounded-xl bg-white">
|
||||
<div>Account</div>
|
||||
<div className="text-sm text-gray-400 mb-8">Manage your account's settings.</div>
|
||||
<div>{account.name}</div>
|
||||
</div>
|
||||
<div className="py-4 px-5 rounded-xl bg-white">
|
||||
<div>Plan</div>
|
||||
<div className="text-sm text-gray-400 mb-8">You are using open-source version. Subscribe to get access to hosted cognee with your data!</div>
|
||||
<Link href="/plan">
|
||||
<CTAButton><span className="">Select a plan</span></CTAButton>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1/5 py-4 px-5 rounded-xl">
|
||||
</div>
|
||||
<div className="flex-1/5 py-4 px-5 rounded-xl">
|
||||
</div>
|
||||
<div className="flex-1/5 py-4 px-5 rounded-xl">
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
cognee-frontend/src/app/account/page.tsx
Normal file
1
cognee-frontend/src/app/account/page.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./Account";
|
||||
104
cognee-frontend/src/app/dashboard/AddDataToCognee.tsx
Normal file
104
cognee-frontend/src/app/dashboard/AddDataToCognee.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { FormEvent, useCallback, useState } from "react";
|
||||
import { CloseIcon, PlusIcon } from "@/ui/Icons";
|
||||
import { useModal } from "@/ui/elements/Modal";
|
||||
import { CTAButton, GhostButton, IconButton, Modal, Select } from "@/ui/elements";
|
||||
|
||||
import addData from "@/modules/ingestion/addData";
|
||||
import { Dataset } from "@/modules/ingestion/useDatasets";
|
||||
|
||||
interface AddDataToCogneeProps {
|
||||
datasets: Dataset[];
|
||||
refreshDatasets: () => void;
|
||||
}
|
||||
|
||||
export default function AddDataToCognee({ datasets, refreshDatasets }: AddDataToCogneeProps) {
|
||||
const [filesForUpload, setFilesForUpload] = useState<FileList | null>(null);
|
||||
|
||||
const prepareFiles = useCallback((event: FormEvent<HTMLInputElement>) => {
|
||||
const formElements = event.currentTarget;
|
||||
const files = formElements.files;
|
||||
|
||||
setFilesForUpload(files);
|
||||
}, []);
|
||||
|
||||
const processDataWithCognee = useCallback((state: object, event?: FormEvent<HTMLFormElement>) => {
|
||||
event!.preventDefault();
|
||||
|
||||
if (!filesForUpload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formElements = event!.currentTarget;
|
||||
const datasetId = formElements.datasetName.value;
|
||||
|
||||
return addData(
|
||||
datasetId ? {
|
||||
id: datasetId,
|
||||
} : {
|
||||
name: "main_dataset",
|
||||
},
|
||||
Array.from(filesForUpload)
|
||||
)
|
||||
.then(() => {
|
||||
refreshDatasets();
|
||||
setFilesForUpload(null);
|
||||
});
|
||||
}, [filesForUpload, refreshDatasets]);
|
||||
|
||||
const {
|
||||
isModalOpen: isAddDataModalOpen,
|
||||
openModal: openAddDataModal,
|
||||
closeModal: closeAddDataModal,
|
||||
isActionLoading: isProcessingDataWithCognee,
|
||||
confirmAction: submitDataToCognee,
|
||||
} = useModal(false, processDataWithCognee);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GhostButton onClick={openAddDataModal} className="mb-5 py-1.5 !px-2 text-sm w-full items-center justify-start">
|
||||
<PlusIcon />
|
||||
Add data to cognee
|
||||
</GhostButton>
|
||||
|
||||
<Modal isOpen={isAddDataModalOpen}>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-2xl">Add new data to a dataset?</span>
|
||||
<IconButton onClick={closeAddDataModal}><CloseIcon /></IconButton>
|
||||
</div>
|
||||
<div className="mt-8 mb-6">Please select a dataset to add data in.<br/> If you don't have any, don't worry, we will create one for you.</div>
|
||||
<form onSubmit={submitDataToCognee}>
|
||||
<div className="max-w-md flex flex-col gap-4">
|
||||
<Select name="datasetName">
|
||||
<option value="">select a dataset</option>
|
||||
{datasets.map((dataset: Dataset) => <option key={dataset.id} value={dataset.id}>{dataset.name}</option>)}
|
||||
</Select>
|
||||
|
||||
<GhostButton className="w-full relative justify-start pl-4">
|
||||
<input onChange={prepareFiles} required name="files" tabIndex={-1} type="file" multiple className="absolute w-full h-full cursor-pointer opacity-0" />
|
||||
<span>select files</span>
|
||||
</GhostButton>
|
||||
|
||||
{filesForUpload?.length && (
|
||||
<div className="pt-4 mt-4 border-t-1 border-t-gray-100">
|
||||
<div className="mb-1.5">selected files:</div>
|
||||
{Array.from(filesForUpload || []).map((file) => (
|
||||
<div key={file.name} className="py-1.5 pl-2">
|
||||
<span className="text-sm">{file.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 mt-4 justify-end">
|
||||
<GhostButton type="button" onClick={() => closeAddDataModal()}>cancel</GhostButton>
|
||||
<CTAButton disabled={isProcessingDataWithCognee} type="submit">
|
||||
{isProcessingDataWithCognee ? "processing..." : "add"}
|
||||
</CTAButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import { useBoolean } from "@/utils";
|
||||
import { Accordion } from "@/ui/elements";
|
||||
|
||||
interface CogneeInstancesAccordionProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function CogneeInstancesAccordion({
|
||||
children,
|
||||
}: CogneeInstancesAccordionProps) {
|
||||
const {
|
||||
value: isInstancesPanelOpen,
|
||||
setTrue: openInstancesPanel,
|
||||
setFalse: closeInstancesPanel,
|
||||
} = useBoolean(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion
|
||||
title={<span>Cognee Instances</span>}
|
||||
isOpen={isInstancesPanelOpen}
|
||||
openAccordion={openInstancesPanel}
|
||||
closeAccordion={closeInstancesPanel}
|
||||
>
|
||||
{children}
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
}
|
||||
140
cognee-frontend/src/app/dashboard/Dashboard.tsx
Normal file
140
cognee-frontend/src/app/dashboard/Dashboard.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Header } from "@/ui/Layout";
|
||||
import { SearchIcon } from "@/ui/Icons";
|
||||
import { Notebook } from "@/ui/elements";
|
||||
import { Notebook as NotebookType } from "@/ui/elements/Notebook/types";
|
||||
import { Dataset } from "@/modules/ingestion/useDatasets";
|
||||
import useNotebooks from "@/modules/notebooks/useNotebooks";
|
||||
|
||||
import NotebooksAccordion from "./NotebooksAccordion";
|
||||
import CogneeInstancesAccordion from "./CogneeInstancesAccordion";
|
||||
import AddDataToCognee from "./AddDataToCognee";
|
||||
import InstanceDatasetsAccordion from "./InstanceDatasetsAccordion";
|
||||
|
||||
export default function Dashboard() {
|
||||
const {
|
||||
notebooks,
|
||||
refreshNotebooks,
|
||||
runCell,
|
||||
addNotebook,
|
||||
updateNotebook,
|
||||
saveNotebook,
|
||||
removeNotebook,
|
||||
} = useNotebooks();
|
||||
|
||||
useEffect(() => {
|
||||
if (!notebooks.length) {
|
||||
refreshNotebooks()
|
||||
.then((notebooks) => {
|
||||
if (notebooks[0]) {
|
||||
setSelectedNotebookId(notebooks[0].id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [notebooks.length, refreshNotebooks]);
|
||||
|
||||
const [selectedNotebookId, setSelectedNotebookId] = useState<string | null>(null);
|
||||
|
||||
const handleNotebookRemove = useCallback((notebookId: string) => {
|
||||
setSelectedNotebookId((currentSelectedNotebookId) => (
|
||||
currentSelectedNotebookId === notebookId ? null : currentSelectedNotebookId
|
||||
));
|
||||
return removeNotebook(notebookId);
|
||||
}, [removeNotebook]);
|
||||
|
||||
const saveNotebookTimeoutRef = useRef<number | null>(null);
|
||||
const saveNotebookThrottled = useCallback((notebook: NotebookType) => {
|
||||
const throttleTime = 1000;
|
||||
|
||||
if (saveNotebookTimeoutRef.current) {
|
||||
clearTimeout(saveNotebookTimeoutRef.current);
|
||||
saveNotebookTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
saveNotebookTimeoutRef.current = setTimeout(() => {
|
||||
saveNotebook(notebook);
|
||||
}, throttleTime) as unknown as number;
|
||||
}, [saveNotebook]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (saveNotebookTimeoutRef.current) {
|
||||
clearTimeout(saveNotebookTimeoutRef.current);
|
||||
saveNotebookTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleNotebookUpdate = useCallback((notebook: NotebookType) => {
|
||||
updateNotebook(notebook);
|
||||
saveNotebookThrottled(notebook);
|
||||
}, [saveNotebookThrottled, updateNotebook]);
|
||||
|
||||
const selectedNotebook = notebooks.find((notebook) => notebook.id === selectedNotebookId);
|
||||
|
||||
// ############################
|
||||
// Datasets logic
|
||||
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const refreshDatasetsRef = useRef(() => {});
|
||||
|
||||
const handleDatasetsChange = useCallback((payload: { datasets: Dataset[], refreshDatasets: () => void }) => {
|
||||
const {
|
||||
datasets,
|
||||
refreshDatasets,
|
||||
} = payload;
|
||||
|
||||
refreshDatasetsRef.current = refreshDatasets;
|
||||
setDatasets(datasets);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-gray-200">
|
||||
<Header />
|
||||
|
||||
<div className="relative flex-1 flex flex-row gap-2.5 items-start w-full max-w-[1920px] max-h-[calc(100% - 3.5rem)] overflow-hidden mx-auto px-2.5 py-2.5">
|
||||
<div className="px-5 py-4 lg:w-96 bg-white rounded-xl min-h-full">
|
||||
<div className="relative mb-2">
|
||||
<label htmlFor="search-input"><SearchIcon className="absolute left-3 top-[10px] cursor-text" /></label>
|
||||
<input id="search-input" className="text-xs leading-3 w-full h-8 flex flex-row items-center gap-2.5 rounded-3xl pl-9 placeholder-gray-300 border-gray-300 border-[1px] focus:outline-indigo-600" placeholder="Search datasets..." />
|
||||
</div>
|
||||
|
||||
<AddDataToCognee
|
||||
datasets={datasets}
|
||||
refreshDatasets={refreshDatasetsRef.current}
|
||||
/>
|
||||
|
||||
<NotebooksAccordion
|
||||
notebooks={notebooks}
|
||||
addNotebook={addNotebook}
|
||||
removeNotebook={handleNotebookRemove}
|
||||
openNotebook={setSelectedNotebookId}
|
||||
/>
|
||||
|
||||
<div className="mt-7 mb-14">
|
||||
<CogneeInstancesAccordion>
|
||||
<InstanceDatasetsAccordion
|
||||
onDatasetsChange={handleDatasetsChange}
|
||||
/>
|
||||
</CogneeInstancesAccordion>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col justify-between h-full overflow-y-auto">
|
||||
{selectedNotebook && (
|
||||
<Notebook
|
||||
key={selectedNotebook.id}
|
||||
notebook={selectedNotebook}
|
||||
updateNotebook={handleNotebookUpdate}
|
||||
saveNotebook={saveNotebook}
|
||||
runCell={runCell}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
346
cognee-frontend/src/app/dashboard/DatasetsAccordion.tsx
Normal file
346
cognee-frontend/src/app/dashboard/DatasetsAccordion.tsx
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
"use client";
|
||||
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from "react";
|
||||
import { useBoolean } from "@/utils";
|
||||
import { Accordion, CTAButton, GhostButton, IconButton, Input, Modal, PopupMenu } from "@/ui/elements";
|
||||
import { AccordionProps } from "@/ui/elements/Accordion";
|
||||
import { CloseIcon, DatasetIcon, MinusIcon, PlusIcon } from "@/ui/Icons";
|
||||
import useDatasets, { Dataset } from "@/modules/ingestion/useDatasets";
|
||||
import addData from "@/modules/ingestion/addData";
|
||||
import cognifyDataset from "@/modules/datasets/cognifyDataset";
|
||||
import { DataFile } from '@/modules/ingestion/useData';
|
||||
import { LoadingIndicator } from '@/ui/App';
|
||||
|
||||
interface DatasetsChangePayload {
|
||||
datasets: Dataset[]
|
||||
refreshDatasets: () => void;
|
||||
}
|
||||
|
||||
export interface DatasetsAccordionProps extends Omit<AccordionProps, "isOpen" | "openAccordion" | "closeAccordion" | "children"> {
|
||||
onDatasetsChange?: (payload: DatasetsChangePayload) => void;
|
||||
}
|
||||
|
||||
export default function DatasetsAccordion({
|
||||
title,
|
||||
tools,
|
||||
switchCaretPosition = false,
|
||||
className,
|
||||
contentClassName,
|
||||
onDatasetsChange,
|
||||
}: DatasetsAccordionProps) {
|
||||
const {
|
||||
value: isDatasetsPanelOpen,
|
||||
setTrue: openDatasetsPanel,
|
||||
setFalse: closeDatasetsPanel,
|
||||
} = useBoolean(true);
|
||||
|
||||
const {
|
||||
datasets,
|
||||
refreshDatasets,
|
||||
addDataset,
|
||||
removeDataset,
|
||||
getDatasetData,
|
||||
removeDatasetData,
|
||||
} = useDatasets();
|
||||
|
||||
useEffect(() => {
|
||||
if (datasets.length === 0) {
|
||||
refreshDatasets();
|
||||
}
|
||||
}, [datasets.length, refreshDatasets]);
|
||||
|
||||
const [openDatasets, openDataset] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleDataset = (id: string) => {
|
||||
openDataset((prev) => {
|
||||
const newState = new Set(prev);
|
||||
|
||||
if (newState.has(id)) {
|
||||
newState.delete(id)
|
||||
} else {
|
||||
getDatasetData(id)
|
||||
.then(() => {
|
||||
newState.add(id);
|
||||
});
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
const refreshOpenDatasetsData = useCallback(() => {
|
||||
return Promise.all(
|
||||
openDatasets.values().map(
|
||||
(datasetId) => getDatasetData(datasetId)
|
||||
)
|
||||
);
|
||||
}, [getDatasetData, openDatasets]);
|
||||
|
||||
const refreshDatasetsAndData = useCallback(() => {
|
||||
refreshDatasets()
|
||||
.then(refreshOpenDatasetsData);
|
||||
}, [refreshDatasets, refreshOpenDatasetsData]);
|
||||
|
||||
useEffect(() => {
|
||||
onDatasetsChange?.({
|
||||
datasets,
|
||||
refreshDatasets: refreshDatasetsAndData,
|
||||
});
|
||||
}, [datasets, onDatasetsChange, refreshDatasets, refreshDatasetsAndData]);
|
||||
|
||||
const {
|
||||
value: isNewDatasetModalOpen,
|
||||
setTrue: openNewDatasetModal,
|
||||
setFalse: closeNewDatasetModal,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleDatasetAdd = () => {
|
||||
openNewDatasetModal();
|
||||
};
|
||||
|
||||
const [newDatasetError, setNewDatasetError] = useState("");
|
||||
|
||||
const handleNewDatasetSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setNewDatasetError("");
|
||||
|
||||
const formElements = event.currentTarget;
|
||||
|
||||
const datasetName = formElements.datasetName.value;
|
||||
|
||||
if (datasetName.trim().length === 0) {
|
||||
setNewDatasetError("Dataset name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (datasetName.includes(" ") || datasetName.includes(".")) {
|
||||
setNewDatasetError("Dataset name cannot contain spaces or periods.");
|
||||
return;
|
||||
}
|
||||
|
||||
addDataset(datasetName)
|
||||
.then(() => {
|
||||
closeNewDatasetModal();
|
||||
refreshDatasetsAndData();
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
value: isRemoveDatasetModalOpen,
|
||||
setTrue: openRemoveDatasetModal,
|
||||
setFalse: closeRemoveDatasetModal,
|
||||
} = useBoolean(false);
|
||||
|
||||
const [datasetToRemove, setDatasetToRemove] = useState<Dataset | null>(null);
|
||||
|
||||
const handleDatasetRemove = (dataset: Dataset) => {
|
||||
setDatasetToRemove(dataset);
|
||||
openRemoveDatasetModal();
|
||||
};
|
||||
|
||||
const handleDatasetRemoveCancel = () => {
|
||||
setDatasetToRemove(null);
|
||||
closeRemoveDatasetModal();
|
||||
};
|
||||
|
||||
const handleRemoveDatasetConfirm = (event: React.FormEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (datasetToRemove) {
|
||||
removeDataset(datasetToRemove.id)
|
||||
.then(() => {
|
||||
closeRemoveDatasetModal();
|
||||
setDatasetToRemove(null);
|
||||
refreshDatasetsAndData();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
value: isProcessingFiles,
|
||||
setTrue: setProcessingFilesInProgress,
|
||||
setFalse: setProcessingFilesDone,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleAddFiles = (dataset: Dataset, event: ChangeEvent<HTMLInputElement>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (isProcessingFiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessingFilesInProgress();
|
||||
|
||||
if (!event.target.files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files: File[] = Array.from(event.target.files);
|
||||
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return addData(dataset, files)
|
||||
.then(async () => {
|
||||
await getDatasetData(dataset.id);
|
||||
|
||||
const onUpdate = () => {};
|
||||
|
||||
return cognifyDataset(dataset, onUpdate)
|
||||
.finally(() => {
|
||||
setProcessingFilesDone();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const [dataToRemove, setDataToRemove] = useState<DataFile | null>(null);
|
||||
const {
|
||||
value: isRemoveDataModalOpen,
|
||||
setTrue: openRemoveDataModal,
|
||||
setFalse: closeRemoveDataModal,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleDataRemove = (data: DataFile) => {
|
||||
setDataToRemove(data);
|
||||
|
||||
openRemoveDataModal();
|
||||
};
|
||||
const handleDataRemoveCancel = () => {
|
||||
setDataToRemove(null);
|
||||
closeRemoveDataModal();
|
||||
};
|
||||
const handleDataRemoveConfirm = (event: React.FormEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (dataToRemove) {
|
||||
removeDatasetData(dataToRemove.datasetId, dataToRemove.id)
|
||||
.then(() => {
|
||||
closeRemoveDataModal();
|
||||
setDataToRemove(null);
|
||||
refreshDatasetsAndData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion
|
||||
title={title || <span>Datasets</span>}
|
||||
isOpen={isDatasetsPanelOpen}
|
||||
openAccordion={openDatasetsPanel}
|
||||
closeAccordion={closeDatasetsPanel}
|
||||
tools={tools || <IconButton onClick={handleDatasetAdd}><PlusIcon /></IconButton>}
|
||||
switchCaretPosition={switchCaretPosition}
|
||||
className={className}
|
||||
contentClassName={contentClassName}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{datasets.length === 0 && (
|
||||
<div className="flex flex-row items-baseline-last text-sm text-gray-400 mt-2 px-2">
|
||||
<span>No datasets here, add one by clicking +</span>
|
||||
</div>
|
||||
)}
|
||||
{datasets.map((dataset) => {
|
||||
return (
|
||||
<Accordion
|
||||
key={dataset.id}
|
||||
title={(
|
||||
<div className="flex flex-row gap-2 items-center py-1.5 cursor-pointer">
|
||||
{isProcessingFiles ? <LoadingIndicator /> : <DatasetIcon />}
|
||||
<span className="text-xs">{dataset.name}</span>
|
||||
</div>
|
||||
)}
|
||||
isOpen={openDatasets.has(dataset.id)}
|
||||
openAccordion={() => toggleDataset(dataset.id)}
|
||||
closeAccordion={() => toggleDataset(dataset.id)}
|
||||
tools={(
|
||||
<IconButton className="relative">
|
||||
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
|
||||
<PopupMenu>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer relative">
|
||||
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
|
||||
<span>add data</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 items-start">
|
||||
<div onClick={() => handleDatasetRemove(dataset)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete</div>
|
||||
</div>
|
||||
</PopupMenu>
|
||||
</IconButton>
|
||||
)}
|
||||
className="first:pt-1.5"
|
||||
switchCaretPosition={true}
|
||||
>
|
||||
<>
|
||||
{dataset.data?.length === 0 && (
|
||||
<div className="flex flex-row items-baseline-last text-sm text-gray-400 mt-2 px-2">
|
||||
<span>No data in a dataset, add by clicking "add data" in a dropdown menu</span>
|
||||
</div>
|
||||
)}
|
||||
{dataset.data?.map((data) => (
|
||||
<div key={data.id} className="flex flex-row gap-2 items-center justify-between py-1.5 pl-6 last:pb-2.5">
|
||||
<span className="text-xs">{data.name}</span>
|
||||
<div>
|
||||
<IconButton onClick={() => handleDataRemove(data)}><MinusIcon /></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
<Modal isOpen={isNewDatasetModalOpen}>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-2xl">Create a new dataset?</span>
|
||||
<IconButton onClick={closeNewDatasetModal}><CloseIcon /></IconButton>
|
||||
</div>
|
||||
<div className="mt-8 mb-6">Please provide a name for the dataset being created.</div>
|
||||
<form onSubmit={handleNewDatasetSubmit}>
|
||||
<div className="max-w-md">
|
||||
<Input name="datasetName" type="text" placeholder="Dataset name" required />
|
||||
{newDatasetError && <span className="text-sm pl-4 text-gray-400">{newDatasetError}</span>}
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 mt-4 justify-end">
|
||||
<GhostButton type="button" onClick={() => closeNewDatasetModal()}>cancel</GhostButton>
|
||||
<CTAButton type="submit">create</CTAButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal isOpen={isRemoveDatasetModalOpen}>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-2xl">Delete <span className="text-indigo-600">{datasetToRemove?.name}</span> dataset?</span>
|
||||
<IconButton onClick={handleDatasetRemoveCancel}><CloseIcon /></IconButton>
|
||||
</div>
|
||||
<div className="mt-8 mb-6">Are you sure you want to delete <span className="text-indigo-600">{datasetToRemove?.name}</span>? This action cannot be undone.</div>
|
||||
<div className="flex flex-row gap-4 mt-4 justify-end">
|
||||
<GhostButton type="button" onClick={handleDatasetRemoveCancel}>cancel</GhostButton>
|
||||
<CTAButton onClick={handleRemoveDatasetConfirm} type="submit">delete</CTAButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal isOpen={isRemoveDataModalOpen}>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-2xl">Delete <span className="text-indigo-600">{dataToRemove?.name}</span> data?</span>
|
||||
<IconButton onClick={handleDataRemoveCancel}><CloseIcon /></IconButton>
|
||||
</div>
|
||||
<div className="mt-8 mb-6">Are you sure you want to delete <span className="text-indigo-600">{dataToRemove?.name}</span>? This action cannot be undone.</div>
|
||||
<div className="flex flex-row gap-4 mt-4 justify-end">
|
||||
<GhostButton type="button" onClick={handleDataRemoveCancel}>cancel</GhostButton>
|
||||
<CTAButton onClick={handleDataRemoveConfirm} type="submit">delete</CTAButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
102
cognee-frontend/src/app/dashboard/InstanceDatasetsAccordion.tsx
Normal file
102
cognee-frontend/src/app/dashboard/InstanceDatasetsAccordion.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { fetch, useBoolean } from "@/utils";
|
||||
import { checkCloudConnection } from "@/modules/cloud";
|
||||
import { CloseIcon, CloudIcon, LocalCogneeIcon } from "@/ui/Icons";
|
||||
import { CTAButton, GhostButton, IconButton, Input, Modal } from "@/ui/elements";
|
||||
|
||||
import DatasetsAccordion, { DatasetsAccordionProps } from "./DatasetsAccordion";
|
||||
|
||||
type InstanceDatasetsAccordionProps = Omit<DatasetsAccordionProps, "title">;
|
||||
|
||||
export default function InstanceDatasetsAccordion({ onDatasetsChange }: InstanceDatasetsAccordionProps) {
|
||||
const {
|
||||
value: isLocalCogneeConnected,
|
||||
setTrue: setLocalCogneeConnected,
|
||||
} = useBoolean(false);
|
||||
|
||||
const {
|
||||
value: isCloudCogneeConnected,
|
||||
setTrue: setCloudCogneeConnected,
|
||||
} = useBoolean(false);
|
||||
|
||||
const checkConnectionToCloudCognee = useCallback((apiKey: string) => {
|
||||
return checkCloudConnection(apiKey)
|
||||
.then(setCloudCogneeConnected)
|
||||
}, [setCloudCogneeConnected]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkConnectionToLocalCognee = () => {
|
||||
fetch.checkHealth()
|
||||
.then(setLocalCogneeConnected)
|
||||
};
|
||||
|
||||
checkConnectionToLocalCognee();
|
||||
|
||||
checkConnectionToCloudCognee("");
|
||||
}, [checkConnectionToCloudCognee, setCloudCogneeConnected, setLocalCogneeConnected]);
|
||||
|
||||
const {
|
||||
value: isCloudConnectedModalOpen,
|
||||
setTrue: openCloudConnectionModal,
|
||||
setFalse: closeCloudConnectionModal,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleCloudConnectionConfirm = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const apiKeyValue = event.currentTarget.apiKey.value;
|
||||
|
||||
checkConnectionToCloudCognee(apiKeyValue)
|
||||
.then(() => {
|
||||
closeCloudConnectionModal();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DatasetsAccordion
|
||||
title={(
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<LocalCogneeIcon className="text-indigo-700" />
|
||||
<span className="text-xs">local cognee</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
tools={isLocalCogneeConnected ? <span className="text-xs text-indigo-600">Connected</span> : <span className="text-xs text-gray-400">Not connected</span>}
|
||||
switchCaretPosition={true}
|
||||
className="pt-3 pb-1.5"
|
||||
contentClassName="pl-4"
|
||||
onDatasetsChange={onDatasetsChange}
|
||||
/>
|
||||
|
||||
<button className="w-full flex flex-row items-center justify-between py-1.5 cursor-pointer" onClick={!isCloudCogneeConnected ? openCloudConnectionModal : () => {}}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CloudIcon color="#000000" />
|
||||
<span className="text-xs">cloud cognee</span>
|
||||
</div>
|
||||
{isCloudCogneeConnected ? <span className="text-xs text-indigo-600">Connected</span> : <span className="text-xs text-gray-400">Not connected</span>}
|
||||
</button>
|
||||
|
||||
<Modal isOpen={isCloudConnectedModalOpen}>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-2xl">Connect to cloud?</span>
|
||||
<IconButton onClick={closeCloudConnectionModal}><CloseIcon /></IconButton>
|
||||
</div>
|
||||
<div className="mt-8 mb-6">Please provide your API key. You can find it on <a className="!text-indigo-600" href="https://platform.cognee.ai">our platform.</a></div>
|
||||
<form onSubmit={handleCloudConnectionConfirm}>
|
||||
<div className="max-w-md">
|
||||
<Input name="apiKey" type="text" placeholder="cloud API key" required />
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 mt-4 justify-end">
|
||||
<GhostButton type="button" onClick={() => closeCloudConnectionModal()}>cancel</GhostButton>
|
||||
<CTAButton type="submit">connect</CTAButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
150
cognee-frontend/src/app/dashboard/NotebooksAccordion.tsx
Normal file
150
cognee-frontend/src/app/dashboard/NotebooksAccordion.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"use client";
|
||||
|
||||
import { FormEvent, useCallback, useState } from "react";
|
||||
import { useBoolean } from "@/utils";
|
||||
import { Accordion, CTAButton, GhostButton, IconButton, Input, Modal } from "@/ui/elements";
|
||||
import { CloseIcon, MinusIcon, NotebookIcon, PlusIcon } from "@/ui/Icons";
|
||||
import { Notebook } from "@/ui/elements/Notebook/types";
|
||||
import { LoadingIndicator } from "@/ui/App";
|
||||
import { useModal } from "@/ui/elements/Modal";
|
||||
|
||||
interface NotebooksAccordionProps {
|
||||
notebooks: Notebook[];
|
||||
addNotebook: (name: string) => Promise<Notebook>;
|
||||
removeNotebook: (id: string) => Promise<void>;
|
||||
openNotebook: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function NotebooksAccordion({
|
||||
notebooks,
|
||||
addNotebook,
|
||||
removeNotebook,
|
||||
openNotebook,
|
||||
}: NotebooksAccordionProps) {
|
||||
const {
|
||||
value: isNotebookPanelOpen,
|
||||
setTrue: openNotebookPanel,
|
||||
setFalse: closeNotebookPanel,
|
||||
} = useBoolean(true);
|
||||
|
||||
const {
|
||||
value: isNotebookLoading,
|
||||
setTrue: notebookLoading,
|
||||
setFalse: notebookLoaded,
|
||||
} = useBoolean(false);
|
||||
|
||||
// Notebook removal modal
|
||||
const [notebookToRemove, setNotebookToRemove] = useState<Notebook | null>(null);
|
||||
|
||||
const handleNotebookRemove = (notebook: Notebook) => {
|
||||
setNotebookToRemove(notebook);
|
||||
openRemoveNotebookModal();
|
||||
};
|
||||
|
||||
const {
|
||||
value: isRemoveNotebookModalOpen,
|
||||
setTrue: openRemoveNotebookModal,
|
||||
setFalse: closeRemoveNotebookModal,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleNotebookRemoveCancel = () => {
|
||||
closeRemoveNotebookModal();
|
||||
setNotebookToRemove(null);
|
||||
};
|
||||
|
||||
const handleNotebookRemoveConfirm = () => {
|
||||
notebookLoading();
|
||||
removeNotebook(notebookToRemove!.id)
|
||||
.finally(notebookLoaded)
|
||||
.finally(closeRemoveNotebookModal);
|
||||
setNotebookToRemove(null);
|
||||
};
|
||||
|
||||
const handleNotebookAdd = useCallback((_: object, formEvent?: FormEvent<HTMLFormElement>) => {
|
||||
if (!formEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
formEvent.preventDefault();
|
||||
|
||||
const formElements = formEvent.currentTarget;
|
||||
const notebookName = formElements.notebookName.value.trim();
|
||||
|
||||
return addNotebook(notebookName)
|
||||
}, [addNotebook]);
|
||||
|
||||
const {
|
||||
isModalOpen: isNewNotebookModalOpen,
|
||||
openModal: openNewNotebookModal,
|
||||
closeModal: closeNewNotebookModal,
|
||||
confirmAction: handleNewNotebookSubmit,
|
||||
isActionLoading: isNewDatasetLoading,
|
||||
} = useModal<Notebook | void>(false, handleNotebookAdd);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion
|
||||
title={<span>Notebooks</span>}
|
||||
isOpen={isNotebookPanelOpen}
|
||||
openAccordion={openNotebookPanel}
|
||||
closeAccordion={closeNotebookPanel}
|
||||
tools={isNewDatasetLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<IconButton onClick={openNewNotebookModal}><PlusIcon /></IconButton>
|
||||
)}
|
||||
>
|
||||
{notebooks.length === 0 && (
|
||||
<div className="flex flex-row items-baseline-last text-sm text-gray-400 mt-2 px-2">
|
||||
<span>No notebooks here, add one by clicking +</span>
|
||||
</div>
|
||||
)}
|
||||
{notebooks.map((notebook: Notebook) => (
|
||||
<div key={notebook.id} className="flex flex-row gap-2.5 items-center justify-between py-1.5 first:pt-3">
|
||||
<button onClick={() => openNotebook(notebook.id)} className="flex flex-row gap-2 items-center cursor-pointer">
|
||||
{isNotebookLoading ? <LoadingIndicator /> : <NotebookIcon />}
|
||||
<span className="text-xs">{notebook.name}</span>
|
||||
</button>
|
||||
<div>
|
||||
{notebook.deletable && <IconButton onClick={() => handleNotebookRemove(notebook)}><MinusIcon /></IconButton>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<Modal isOpen={isNewNotebookModalOpen}>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-2xl">Create a new notebook?</span>
|
||||
<IconButton onClick={closeNewNotebookModal}><CloseIcon /></IconButton>
|
||||
</div>
|
||||
<div className="mt-8 mb-6">Please provide a name for the notebook being created.</div>
|
||||
<form onSubmit={handleNewNotebookSubmit}>
|
||||
<div className="max-w-md">
|
||||
<Input name="notebookName" type="text" placeholder="Notebook name" required />
|
||||
{/* {newDatasetError && <span className="text-sm pl-4 text-gray-400">{newDatasetError}</span>} */}
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 mt-4 justify-end">
|
||||
<GhostButton type="button" onClick={() => closeNewNotebookModal()}>cancel</GhostButton>
|
||||
<CTAButton type="submit">create</CTAButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal isOpen={isRemoveNotebookModalOpen}>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-2xl">Delete <span className="text-indigo-600">{notebookToRemove?.name}</span> notebook?</span>
|
||||
<IconButton onClick={handleNotebookRemoveCancel}><CloseIcon /></IconButton>
|
||||
</div>
|
||||
<div className="mt-8 mb-6">Are you sure you want to delete <span className="text-indigo-600">{notebookToRemove?.name}</span>? This action cannot be undone.</div>
|
||||
<div className="flex flex-row gap-4 mt-4 justify-end">
|
||||
<GhostButton type="button" onClick={handleNotebookRemoveCancel}>cancel</GhostButton>
|
||||
<CTAButton onClick={handleNotebookRemoveConfirm} type="submit">delete</CTAButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
cognee-frontend/src/app/dashboard/page.tsx
Normal file
1
cognee-frontend/src/app/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./Dashboard";
|
||||
157
cognee-frontend/src/app/plan/Plan.tsx
Normal file
157
cognee-frontend/src/app/plan/Plan.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import Link from "next/link";
|
||||
import { BackIcon, CheckIcon } from "@/ui/Icons";
|
||||
import { CTAButton, NeutralButton } from "@/ui/elements";
|
||||
import Header from "@/ui/Layout/Header";
|
||||
|
||||
export default function Plan() {
|
||||
return (
|
||||
<>
|
||||
<div className="absolute top-0 right-0 bottom-0 left-0 flex flex-row gap-2.5">
|
||||
<div className="flex-1/5 bg-gray-100 h-full"></div>
|
||||
<div className="flex-3/5 h-full flex flex-row gap-2.5">
|
||||
<div className="flex-1/3 bg-gray-100 h-full"></div>
|
||||
<div className="flex-1/3 bg-gray-100 h-full"></div>
|
||||
<div className="flex-1/3 bg-gray-100 h-full"></div>
|
||||
</div>
|
||||
<div className="flex-1/5 bg-gray-100 h-full"></div>
|
||||
</div>
|
||||
|
||||
<Header />
|
||||
|
||||
<div className="relative flex flex-row items-start justify-stretch gap-2.5">
|
||||
<div className="flex-1/5 h-full">
|
||||
<Link href="/dashboard" className="py-4 px-5 flex flex-row items-center gap-5">
|
||||
<BackIcon />
|
||||
<span>back</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex-3/5">
|
||||
<div className="grid grid-cols-3 gap-x-2.5">
|
||||
<div className="pt-13 py-4 px-5 mb-2.5 rounded-tl-xl rounded-tr-xl bg-white h-full">
|
||||
<div>Basic</div>
|
||||
<div className="text-3xl mb-4 font-bold">Free</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-13 py-4 px-5 mb-2.5 rounded-tl-xl rounded-tr-xl bg-white h-full">
|
||||
<div>On-prem Subscription</div>
|
||||
<div className="mb-4"><span className="text-3xl font-bold">$2470</span><span className="text-gray-400"> /per month</span></div>
|
||||
<div className="mb-9"><span className="font-bold">Save 20% </span>yearly</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-13 py-4 px-5 mb-2.5 rounded-tl-xl rounded-tr-xl bg-white h-full">
|
||||
<div>Cloud Subscription</div>
|
||||
<div className="mb-4"><span className="text-3xl font-bold">$25</span><span className="text-gray-400"> /per month</span></div>
|
||||
<div className="mb-9 text-gray-400">(beta pricing)</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-bl-xl rounded-br-xl h-full">
|
||||
<div className="mb-1 invisible">Everything in the free plan, plus...</div>
|
||||
<div className="flex flex-col gap-3 mb-28">
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />License to use Cognee open source</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Cognee tasks and pipelines</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Custom schema and ontology generation</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Integrated evaluations</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />More than 28 data sources supported</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-bl-xl rounded-br-xl h-full">
|
||||
<div className="mb-1 text-gray-400">Everything in the free plan, plus...</div>
|
||||
<div className="flex flex-col gap-3 mb-10">
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />License to use Cognee open source and Cognee Platform</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />1 day SLA</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />On-prem deployment</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Hands-on support</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Architecture review</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Roadmap prioritization</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Knowledge transfer</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-bl-xl rounded-br-xl h-full">
|
||||
<div className="mb-1 text-gray-400">Everything in the free plan, plus...</div>
|
||||
<div className="flex flex-col gap-3 mb-10">
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Fully hosted cloud platform</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Multi-tenant architecture</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Comprehensive API endpoints</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Automated scaling and parallel processing</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Ability to group memories per user and domain</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />Automatic updates and priority support</div>
|
||||
<div className="flex flex-row gap-2"><CheckIcon className="mt-1 shrink-0" />1 GB ingestion + 10,000 API calls</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 pb-14 mb-2.5">
|
||||
<NeutralButton>Try for free</NeutralButton>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 pb-14 mb-2.5">
|
||||
<CTAButton>Talk to us</CTAButton>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 pb-14 mb-2.5">
|
||||
<NeutralButton>Sign up for Cogwit Beta</NeutralButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 py-4 px-5 bg-[rgba(255,255,255,0.5)] mb-12">
|
||||
<div>Feature Comparison</div>
|
||||
<div className="text-center">Basic</div>
|
||||
<div className="text-center">On-prem</div>
|
||||
<div className="text-center">Cloud</div>
|
||||
|
||||
<div className="border-b-[1px] border-b-gray-100 py-3">Data Sources</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">28+</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">28+</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">28+</div>
|
||||
|
||||
<div className="border-b-[1px] border-b-gray-100 py-3">Deployment</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">Self-hosted</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">On-premise</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">Cloud</div>
|
||||
|
||||
<div className="border-b-[1px] border-b-gray-100 py-3">API Calls</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">Limited</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">Unlimited</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">10,000</div>
|
||||
|
||||
<div className="border-b-[1px] border-b-gray-100 py-3">Support</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">Community</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">Hands-on</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">Priority</div>
|
||||
|
||||
<div className="border-b-[1px] border-b-gray-100 py-3">SLA</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">—</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">1 day</div>
|
||||
<div className="text-center border-b-[1px] border-b-gray-100 py-3">Standard</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-2.5 gap-y-2.5 mb-12">
|
||||
<div className="bg-[rgba(255,255,255,0.5)] py-4 px-5">
|
||||
<div>Can I change my plan anytime?</div>
|
||||
<div className="text-gray-500 mt-6">Yes, you can upgrade or downgrade your plan at any time. Changes take effect immediately.</div>
|
||||
</div>
|
||||
<div className="bg-[rgba(255,255,255,0.5)] py-4 px-5">
|
||||
<div>What happens to my data if I downgrade?</div>
|
||||
<div className="text-gray-500 mt-6">Your data is preserved, but features may be limited based on your new plan constraints.</div>
|
||||
</div>
|
||||
<div className="bg-[rgba(255,255,255,0.5)] py-4 px-5">
|
||||
<div>Do you offer educational discounts?</div>
|
||||
<div className="text-gray-500 mt-6">Yes, we offer special pricing for educational institutions and students. Contact us for details.</div>
|
||||
</div>
|
||||
<div className="bg-[rgba(255,255,255,0.5)] py-4 px-5">
|
||||
<div>Is there a free trial for paid plans?</div>
|
||||
<div className="text-gray-500 mt-6">All new accounts start with a 14-day free trial of our Pro plan features.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1/5 h-full text-center flex flex-col self-end mb-12">
|
||||
<span className="text-sm mb-2">Need a custom solution?</span>
|
||||
<CTAButton>Contact us</CTAButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
cognee-frontend/src/app/plan/page.tsx
Normal file
1
cognee-frontend/src/app/plan/page.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./Plan";
|
||||
2
cognee-frontend/src/modules/auth/index.ts
Normal file
2
cognee-frontend/src/modules/auth/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as useAuthenticatedUser } from "./useAuthenticatedUser";
|
||||
export { type User } from "./types";
|
||||
6
cognee-frontend/src/modules/auth/types.ts
Normal file
6
cognee-frontend/src/modules/auth/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatarImagePath: string;
|
||||
}
|
||||
17
cognee-frontend/src/modules/auth/useAuthenticatedUser.ts
Normal file
17
cognee-frontend/src/modules/auth/useAuthenticatedUser.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { fetch } from "@/utils";
|
||||
import { User } from "./types";
|
||||
|
||||
export default function useAuthenticatedUser() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
fetch("/v1/auth/me")
|
||||
.then((response) => response.json())
|
||||
.then((data) => setUser(data));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
return { user };
|
||||
}
|
||||
10
cognee-frontend/src/modules/cloud/checkCloudConnection.ts
Normal file
10
cognee-frontend/src/modules/cloud/checkCloudConnection.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { fetch } from "@/utils";
|
||||
|
||||
export default function checkCloudConnection(apiKey: string) {
|
||||
return fetch("/v1/checks/connection", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Api-Key": apiKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
2
cognee-frontend/src/modules/cloud/index.ts
Normal file
2
cognee-frontend/src/modules/cloud/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as syncData } from "./syncData";
|
||||
export { default as checkCloudConnection } from "./checkCloudConnection";
|
||||
11
cognee-frontend/src/modules/cloud/syncData.ts
Normal file
11
cognee-frontend/src/modules/cloud/syncData.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { fetch } from "@/utils";
|
||||
|
||||
export default function syncData(datasetId?: string) {
|
||||
return fetch("/v1/sync", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
...(datasetId ? { body: JSON.stringify({ datasetId }) } : { body: "{}" }),
|
||||
});
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ export interface DataFile {
|
|||
id: string;
|
||||
name: string;
|
||||
file: File;
|
||||
datasetId: string;
|
||||
}
|
||||
|
||||
const useData = () => {
|
||||
|
|
@ -16,6 +17,7 @@ const useData = () => {
|
|||
id: v4(),
|
||||
name: file.name,
|
||||
file,
|
||||
datasetId: "",
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
import { DataFile } from './useData';
|
||||
|
||||
import { fetch } from '@/utils';
|
||||
import { DataFile } from './useData';
|
||||
import createDataset from "../datasets/createDataset";
|
||||
|
||||
export interface Dataset {
|
||||
id: string;
|
||||
|
|
@ -56,21 +58,24 @@ function useDatasets() {
|
|||
}, []);
|
||||
|
||||
const addDataset = useCallback((datasetName: string) => {
|
||||
setDatasets((datasets) => [
|
||||
...datasets,
|
||||
{
|
||||
id: v4(),
|
||||
name: datasetName,
|
||||
data: [],
|
||||
status: 'DATASET_INITIALIZED',
|
||||
}
|
||||
]);
|
||||
return createDataset({ name: datasetName })
|
||||
.then((dataset) => {
|
||||
setDatasets((datasets) => [
|
||||
...datasets,
|
||||
dataset,
|
||||
]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeDataset = useCallback((datasetId: string) => {
|
||||
setDatasets((datasets) =>
|
||||
datasets.filter((dataset) => dataset.id !== datasetId)
|
||||
);
|
||||
return fetch(`/v1/datasets/${datasetId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
.then(() => {
|
||||
setDatasets((datasets) =>
|
||||
datasets.filter((dataset) => dataset.id !== datasetId)
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchDatasets = useCallback(() => {
|
||||
|
|
@ -94,7 +99,41 @@ function useDatasets() {
|
|||
});
|
||||
}, [checkDatasetStatuses]);
|
||||
|
||||
return { datasets, addDataset, removeDataset, refreshDatasets: fetchDatasets };
|
||||
const getDatasetData = useCallback((datasetId: string) => {
|
||||
return fetch(`/v1/datasets/${datasetId}/data`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const datasetIndex = datasets.findIndex((dataset) => dataset.id === datasetId);
|
||||
|
||||
if (datasetIndex >= 0) {
|
||||
setDatasets((datasets) => [
|
||||
...datasets.slice(0, datasetIndex),
|
||||
{
|
||||
...datasets[datasetIndex],
|
||||
data,
|
||||
},
|
||||
...datasets.slice(datasetIndex + 1),
|
||||
]);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}, [datasets]);
|
||||
|
||||
const removeDatasetData = useCallback((datasetId: string, dataId: string) => {
|
||||
return fetch(`/v1/datasets/${datasetId}/data/${dataId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
datasets,
|
||||
addDataset,
|
||||
removeDataset,
|
||||
getDatasetData,
|
||||
removeDatasetData,
|
||||
refreshDatasets: fetchDatasets,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDatasets;
|
||||
|
|
|
|||
134
cognee-frontend/src/modules/notebooks/useNotebooks.ts
Normal file
134
cognee-frontend/src/modules/notebooks/useNotebooks.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { fetch } from "@/utils";
|
||||
import { Cell, Notebook } from "@/ui/elements/Notebook/types";
|
||||
|
||||
function useNotebooks() {
|
||||
const [notebooks, setNotebooks] = useState<Notebook[]>([]);
|
||||
|
||||
const addNotebook = useCallback((notebookName: string) => {
|
||||
return fetch("/v1/notebooks", {
|
||||
body: JSON.stringify({ name: notebookName }),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((notebook) => {
|
||||
setNotebooks((notebooks) => [
|
||||
...notebooks,
|
||||
notebook,
|
||||
]);
|
||||
|
||||
return notebook;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeNotebook = useCallback((notebookId: string) => {
|
||||
return fetch(`/v1/notebooks/${notebookId}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
.then(() => {
|
||||
setNotebooks((notebooks) =>
|
||||
notebooks.filter((notebook) => notebook.id !== notebookId)
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchNotebooks = useCallback(() => {
|
||||
return fetch("/v1/notebooks", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((notebooks) => {
|
||||
setNotebooks(notebooks);
|
||||
|
||||
return notebooks;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching notebooks:", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateNotebook = useCallback((updatedNotebook: Notebook) => {
|
||||
setNotebooks((existingNotebooks) =>
|
||||
existingNotebooks.map((notebook) =>
|
||||
notebook.id === updatedNotebook.id
|
||||
? updatedNotebook
|
||||
: notebook
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const saveNotebook = useCallback((notebook: Notebook) => {
|
||||
return fetch(`/v1/notebooks/${notebook.id}`, {
|
||||
body: JSON.stringify({
|
||||
name: notebook.name,
|
||||
cells: notebook.cells,
|
||||
}),
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
}, []);
|
||||
|
||||
const runCell = useCallback((notebook: Notebook, cell: Cell) => {
|
||||
setNotebooks((existingNotebooks) =>
|
||||
existingNotebooks.map((existingNotebook) =>
|
||||
existingNotebook.id === notebook.id ? {
|
||||
...existingNotebook,
|
||||
cells: existingNotebook.cells.map((existingCell) =>
|
||||
existingCell.id === cell.id ? {
|
||||
...existingCell,
|
||||
result: undefined,
|
||||
error: undefined,
|
||||
} : existingCell
|
||||
),
|
||||
} : notebook
|
||||
)
|
||||
);
|
||||
|
||||
return fetch(`/v1/notebooks/${notebook.id}/${cell.id}/run`, {
|
||||
body: JSON.stringify({
|
||||
content: cell.content,
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
setNotebooks((existingNotebooks) =>
|
||||
existingNotebooks.map((existingNotebook) =>
|
||||
existingNotebook.id === notebook.id ? {
|
||||
...existingNotebook,
|
||||
cells: existingNotebook.cells.map((existingCell) =>
|
||||
existingCell.id === cell.id ? {
|
||||
...existingCell,
|
||||
result: response.result,
|
||||
error: response.error,
|
||||
} : existingCell
|
||||
),
|
||||
} : notebook
|
||||
)
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
notebooks,
|
||||
addNotebook,
|
||||
saveNotebook,
|
||||
updateNotebook,
|
||||
removeNotebook,
|
||||
refreshNotebooks: fetchNotebooks,
|
||||
runCell,
|
||||
};
|
||||
};
|
||||
|
||||
export default useNotebooks;
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
border: 0.18rem solid white;
|
||||
border: 0.18rem solid var(--color-indigo-600);;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
animation: spin 2s linear infinite;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default function SearchIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) {
|
||||
export default function AddIcon({ 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"/>
|
||||
|
|
|
|||
8
cognee-frontend/src/ui/Icons/BackIcon.tsx
Normal file
8
cognee-frontend/src/ui/Icons/BackIcon.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export default function BackIcon({ width = 16, height = 16, color = "#17191C", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99992 12.6666L3.33325 7.99998L7.99992 3.33331" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M12.6666 8H3.33325" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
export default function CaretIcon({ width = 50, height = 36, color = "currentColor", className = "" }) {
|
||||
export default function CaretIcon({ width = 17, height = 16, color = "#000000", 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 className={className} width={width} height={height} viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.04877 6L8.09755 10L12.1463 6" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
7
cognee-frontend/src/ui/Icons/CheckIcon.tsx
Normal file
7
cognee-frontend/src/ui/Icons/CheckIcon.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function CheckIcon({ width = 17, height = 18, color = "#5C10F4", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 17 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.1693 4.60767L6.41823 12.3587L2.89502 8.83551" stroke={color} strokeWidth="1.40928" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
8
cognee-frontend/src/ui/Icons/CloseIcon.tsx
Normal file
8
cognee-frontend/src/ui/Icons/CloseIcon.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export default function CloseIcon({ width = 29, height = 29, color = "#000000", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.02429 20.0913L20.5737 8.5419" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M9.02441 8.54199L20.5738 20.0914" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
7
cognee-frontend/src/ui/Icons/CloudIcon.tsx
Normal file
7
cognee-frontend/src/ui/Icons/CloudIcon.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function CloudIcon({ width = 16, height = 12, color = "#5C10F4", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.6666 10.6666H5.99994C5.13452 10.6664 4.28621 10.4256 3.54979 9.97096C2.81338 9.51636 2.21789 8.86595 1.82986 8.09239C1.44183 7.31883 1.27654 6.45261 1.35247 5.59053C1.4284 4.72844 1.74256 3.90445 2.25984 3.21063C2.77712 2.51682 3.47714 1.98051 4.28168 1.66164C5.08622 1.34277 5.96357 1.2539 6.81571 1.40496C7.66785 1.55602 8.4612 1.94106 9.1071 2.51705C9.753 3.09304 10.226 3.8373 10.4733 4.66665H11.6666C12.4623 4.66665 13.2253 4.98272 13.7879 5.54533C14.3505 6.10794 14.6666 6.871 14.6666 7.66665C14.6666 8.4623 14.3505 9.22536 13.7879 9.78797C13.2253 10.3506 12.4623 10.6666 11.6666 10.6666Z" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
7
cognee-frontend/src/ui/Icons/CogneeIcon.tsx
Normal file
7
cognee-frontend/src/ui/Icons/CogneeIcon.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function CogneeIcon({ width = 21, height = 24, color="#6510F4", className="" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 21 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M10.2423 2.05148C10.1507 2.15398 10.083 2.30864 10.083 2.49201V21.508C10.083 22.7797 9.14692 24 7.77076 24C6.41621 24 5.45848 22.7869 5.45848 21.508V6.6326C5.45848 6.21738 5.18157 6.05752 5.04152 6.05752C4.97012 6.05752 4.87507 6.08986 4.78377 6.19205C4.69222 6.29455 4.62455 6.44919 4.62455 6.6326V15.9872C4.62455 17.2589 3.68844 18.4792 2.31227 18.4792C0.957707 18.4792 0 17.2661 0 15.9872V11.4632C0 10.0904 1.10659 8.97124 2.4639 8.97124C2.556 8.97124 2.64505 8.98455 2.72924 9.00931V6.6326C2.72924 5.35369 3.68695 4.14057 5.04152 4.14057C6.41768 4.14057 7.3538 5.3609 7.3538 6.6326V21.508C7.3538 21.6913 7.42147 21.846 7.51303 21.9485C7.60433 22.0507 7.69934 22.0831 7.77076 22.0831C7.91081 22.0831 8.18772 21.9232 8.18772 21.508V2.49201C8.18772 1.2131 9.14545 0 10.5 0C11.8762 0 12.8123 1.22033 12.8123 2.49201V21.508C12.8123 21.6913 12.8799 21.846 12.9715 21.9485C13.0628 22.0507 13.1579 22.0831 13.2292 22.0831C13.3693 22.0831 13.6462 21.9232 13.6462 21.508V6.6326C13.6462 5.35369 14.6039 4.14057 15.9585 4.14057C17.3346 4.14057 18.2708 5.3609 18.2708 6.6326V9.00931C18.355 8.98455 18.444 8.97124 18.5361 8.97124C19.8934 8.97124 21 10.0904 21 11.4632V15.9872C21 17.2589 20.0639 18.4792 18.6877 18.4792C17.3332 18.4792 16.3754 17.2661 16.3754 15.9872V6.6326C16.3754 6.21738 16.0986 6.05752 15.9585 6.05752C15.8871 6.05752 15.7921 6.08986 15.7007 6.19205C15.6092 6.29455 15.5415 6.44919 15.5415 6.6326V21.508C15.5415 22.7797 14.6054 24 13.2292 24C11.8747 24 10.917 22.7869 10.917 21.508V2.49201C10.917 2.07679 10.6401 1.91693 10.5 1.91693C10.4286 1.91693 10.3336 1.94928 10.2423 2.05148ZM18.2708 10.8501V15.9872C18.2708 16.1706 18.3384 16.3253 18.43 16.4278C18.5213 16.53 18.6163 16.5623 18.6877 16.5623C18.8278 16.5623 19.1047 16.4024 19.1047 15.9872V11.4632C19.1047 11.1492 18.8466 10.8882 18.5361 10.8882C18.444 10.8882 18.355 10.8749 18.2708 10.8501ZM2.72924 10.8501C2.64505 10.8749 2.556 10.8882 2.4639 10.8882C2.15334 10.8882 1.89531 11.1492 1.89531 11.4632V15.9872C1.89531 16.1706 1.96298 16.3253 2.05453 16.4278C2.14582 16.53 2.24088 16.5623 2.31227 16.5623C2.45235 16.5623 2.72924 16.4024 2.72924 15.9872V10.8501Z" fill={color}/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
9
cognee-frontend/src/ui/Icons/DatasetIcon.tsx
Normal file
9
cognee-frontend/src/ui/Icons/DatasetIcon.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export default function DatasetIcon({ width = 16, height = 16, color = "#000000", className = '' }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99998 6.55042C10.932 6.55042 13.3088 5.53177 13.3088 4.27521C13.3088 3.01865 10.932 2 7.99998 2C5.068 2 2.69116 3.01865 2.69116 4.27521C2.69116 5.53177 5.068 6.55042 7.99998 6.55042Z" stroke={color} strokeWidth="1.17679"/>
|
||||
<path d="M2.69116 8.82568C2.69116 8.82568 2.69116 10.6027 2.69116 11.8593C2.69116 13.1159 5.06801 14.1345 7.99998 14.1345C10.932 14.1345 13.3088 13.1159 13.3088 11.8593C13.3088 11.2321 13.3088 8.82568 13.3088 8.82568" stroke={color} strokeWidth="1.17679" strokeLinecap="square"/>
|
||||
<path d="M2.69116 4.27515C2.69116 4.27515 2.69116 6.81056 2.69116 8.06716C2.69116 9.32376 5.06801 10.3424 7.99998 10.3424C10.932 10.3424 13.3088 9.32376 13.3088 8.06716C13.3088 7.43996 13.3088 4.27515 13.3088 4.27515" stroke={color} strokeWidth="1.17679"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
10
cognee-frontend/src/ui/Icons/LocalCogneeIcon.tsx
Normal file
10
cognee-frontend/src/ui/Icons/LocalCogneeIcon.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export default function LocalCogneeIcon({ width = 16, height = 16, color = "#000000", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6667 8H1.33334" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M3.63334 3.40663L1.33334 7.99996V12C1.33334 12.3536 1.47382 12.6927 1.72387 12.9428C1.97392 13.1928 2.31305 13.3333 2.66668 13.3333H13.3333C13.687 13.3333 14.0261 13.1928 14.2762 12.9428C14.5262 12.6927 14.6667 12.3536 14.6667 12V7.99996L12.3667 3.40663C12.2563 3.18448 12.0861 2.99754 11.8753 2.86681C11.6645 2.73608 11.4214 2.66676 11.1733 2.66663H4.82668C4.57862 2.66676 4.33552 2.73608 4.12471 2.86681C3.91389 2.99754 3.74373 3.18448 3.63334 3.40663Z" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M4 10.6666H4.00667" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M6.66666 10.6666H6.67332" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
9
cognee-frontend/src/ui/Icons/MenuIcon.tsx
Normal file
9
cognee-frontend/src/ui/Icons/MenuIcon.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export default function AddIcon({ width = 16, height = 16, color = "#000000", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="4" r="1" fill={color} />
|
||||
<circle cx="8" cy="8" r="1" fill={color} />
|
||||
<circle cx="8" cy="12" r="1" fill={color} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
7
cognee-frontend/src/ui/Icons/MinusIcon.tsx
Normal file
7
cognee-frontend/src/ui/Icons/MinusIcon.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function MinusIcon({ width = 16, height = 16, color = "#000000", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.09637 8H12.8675" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
8
cognee-frontend/src/ui/Icons/NotebookIcon.tsx
Normal file
8
cognee-frontend/src/ui/Icons/NotebookIcon.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export default function NotebookIcon({ width = 16, height = 16, color = "#000000", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 2V14" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
7
cognee-frontend/src/ui/Icons/PlayIcon.tsx
Normal file
7
cognee-frontend/src/ui/Icons/PlayIcon.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function PlayIcon({ width = 11, height = 14, color = "#000000", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 11 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1L10.3333 7L1 13V1Z" stroke={color} strokeWidth="1.33" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
8
cognee-frontend/src/ui/Icons/PlusIcon.tsx
Normal file
8
cognee-frontend/src/ui/Icons/PlusIcon.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export default function PlusIcon({ width = 16, height = 16, color = "#000000", className = "" }) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.09637 8H12.8675" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8.48193 3.33331V12.6666" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
export default function SearchIcon({ width = 24, height = 24, color = 'currentColor', className = '' }) {
|
||||
export default function SearchIcon({ width = 12, height = 12, color = "#D8D8D8", 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 className={className} width={width} height={height} viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 9.5C7.70914 9.5 9.5 7.70914 9.5 5.5C9.5 3.29086 7.70914 1.5 5.5 1.5C3.29086 1.5 1.5 3.29086 1.5 5.5C1.5 7.70914 3.29086 9.5 5.5 9.5Z" stroke={color} strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M10.5 10.5L8.35001 8.34998" stroke={color} strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
export default function SettingsIcon({ width = 32, height = 33, color = "#E8EAED" }) {
|
||||
export default function SettingsIcon({ width = 16, height = 17, color = "#000000" }) {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox="0 0 54 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.2482 55.75L20.1336 46.8322C19.1495 46.5357 18.0853 46.0691 16.9408 45.4324C15.7964 44.7962 14.8231 44.1145 14.0209 43.3874L5.79611 46.8854L0.0449219 36.8646L7.15432 31.5042C7.06336 30.9472 6.98833 30.3731 6.92923 29.7822C6.86962 29.1912 6.83982 28.6169 6.83982 28.0594C6.83982 27.5414 6.86962 26.9969 6.92923 26.426C6.98833 25.8545 7.06336 25.2111 7.15432 24.4958L0.0449219 19.1354L5.79611 9.23329L13.9615 12.672C14.8824 11.9053 15.8786 11.2136 16.9501 10.5969C18.021 9.98023 19.0624 9.50385 20.0743 9.16777L21.2482 0.25H32.7522L33.8668 9.22713C35.0487 9.64235 36.0932 10.1187 37.0002 10.6562C37.9072 11.1938 38.8412 11.8657 39.8022 12.672L48.2043 9.23329L53.9555 19.1354L46.6087 24.6739C46.7788 25.31 46.8738 25.8941 46.8939 26.426C46.9134 26.9573 46.9232 27.482 46.9232 28C46.9232 28.4784 46.9034 28.9833 46.8638 29.5147C46.8242 30.0466 46.7333 30.69 46.5909 31.4449L53.819 36.8646L48.0678 46.8854L39.8022 43.328C38.8412 44.1343 37.8746 44.826 36.9023 45.4031C35.93 45.9802 34.9182 46.4368 33.8668 46.7729L32.7522 55.75H21.2482ZM23.9169 52.6667H29.9471L31.0856 44.3178C32.6391 43.9067 34.0374 43.3424 35.2805 42.625C36.5241 41.9076 37.7901 40.9243 39.0784 39.675L46.769 42.9542L49.8346 37.7125L43.0867 32.6427C43.3437 31.765 43.5138 30.9577 43.597 30.2208C43.6797 29.4833 43.7211 28.7431 43.7211 28C43.7211 27.2173 43.6797 26.4771 43.597 25.7792C43.5138 25.0819 43.3437 24.3141 43.0867 23.476L49.9533 18.2875L46.8877 13.0458L39.019 16.3427C38.0863 15.319 36.8599 14.3593 35.3398 13.4636C33.8203 12.5684 32.3824 11.9746 31.0263 11.6822L30.0835 3.33333H23.9346L22.9741 11.6229C21.4206 11.9548 19.9925 12.4895 18.6898 13.227C17.3876 13.9639 16.0921 14.9768 14.8033 16.2656L7.11269 13.0458L4.04709 18.2875L10.7356 23.2802C10.4787 23.9719 10.2988 24.7229 10.196 25.5333C10.0932 26.3437 10.0419 27.1857 10.0419 28.0594C10.0419 28.842 10.0932 29.6187 10.196 30.3896C10.2988 31.1604 10.4589 31.9115 10.6763 32.6427L4.04709 37.7125L7.11269 42.9542L14.7439 39.7167C15.9536 40.9382 17.2096 41.9177 18.5118 42.6551C19.8145 43.392 21.2822 43.966 22.9148 44.3771L23.9169 52.6667ZM26.9169 35.7083C29.0676 35.7083 30.8901 34.9611 32.3845 33.4668C33.8783 31.9729 34.6253 30.1506 34.6253 28C34.6253 25.8494 33.8783 24.0271 32.3845 22.5333C30.8901 21.0389 29.0676 20.2917 26.9169 20.2917C24.755 20.2917 22.9297 21.0389 21.4409 22.5333C19.9527 24.0271 19.2086 25.8494 19.2086 28C19.2086 30.1506 19.9527 31.9729 21.4409 33.4668C22.9297 34.9611 24.755 35.7083 26.9169 35.7083Z" fill={color} />
|
||||
<svg width={width} height={height} viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.14667 1.35278H7.85333C7.49971 1.35278 7.16057 1.49326 6.91053 1.74331C6.66048 1.99336 6.52 2.33249 6.52 2.68612V2.80612C6.51976 3.03993 6.45804 3.26958 6.34103 3.47201C6.22401 3.67444 6.05583 3.84254 5.85333 3.95945L5.56667 4.12612C5.36398 4.24314 5.13405 4.30475 4.9 4.30475C4.66595 4.30475 4.43603 4.24314 4.23333 4.12612L4.13333 4.07278C3.82738 3.89629 3.46389 3.84841 3.12267 3.93966C2.78145 4.0309 2.49037 4.25381 2.31333 4.55945L2.16667 4.81278C1.99018 5.11874 1.9423 5.48223 2.03354 5.82345C2.12478 6.16467 2.34769 6.45575 2.65333 6.63278L2.75333 6.69945C2.95485 6.81579 3.12241 6.98284 3.23937 7.184C3.35632 7.38517 3.4186 7.61343 3.42 7.84612V8.18612C3.42093 8.42106 3.35977 8.65209 3.2427 8.85579C3.12563 9.05949 2.95681 9.22864 2.75333 9.34612L2.65333 9.40612C2.34769 9.58315 2.12478 9.87423 2.03354 10.2155C1.9423 10.5567 1.99018 10.9202 2.16667 11.2261L2.31333 11.4795C2.49037 11.7851 2.78145 12.008 3.12267 12.0992C3.46389 12.1905 3.82738 12.1426 4.13333 11.9661L4.23333 11.9128C4.43603 11.7958 4.66595 11.7342 4.9 11.7342C5.13405 11.7342 5.36398 11.7958 5.56667 11.9128L5.85333 12.0795C6.05583 12.1964 6.22401 12.3645 6.34103 12.5669C6.45804 12.7693 6.51976 12.999 6.52 13.2328V13.3528C6.52 13.7064 6.66048 14.0455 6.91053 14.2956C7.16057 14.5456 7.49971 14.6861 7.85333 14.6861H8.14667C8.50029 14.6861 8.83943 14.5456 9.08948 14.2956C9.33953 14.0455 9.48 13.7064 9.48 13.3528V13.2328C9.48024 12.999 9.54196 12.7693 9.65898 12.5669C9.77599 12.3645 9.94418 12.1964 10.1467 12.0795L10.4333 11.9128C10.636 11.7958 10.866 11.7342 11.1 11.7342C11.3341 11.7342 11.564 11.7958 11.7667 11.9128L11.8667 11.9661C12.1726 12.1426 12.5361 12.1905 12.8773 12.0992C13.2186 12.008 13.5096 11.7851 13.6867 11.4795L13.8333 11.2194C14.0098 10.9135 14.0577 10.55 13.9665 10.2088C13.8752 9.86756 13.6523 9.57648 13.3467 9.39945L13.2467 9.34612C13.0432 9.22864 12.8744 9.05949 12.7573 8.85579C12.6402 8.65209 12.5791 8.42106 12.58 8.18612V7.85278C12.5791 7.61784 12.6402 7.38682 12.7573 7.18311C12.8744 6.97941 13.0432 6.81026 13.2467 6.69278L13.3467 6.63278C13.6523 6.45575 13.8752 6.16467 13.9665 5.82345C14.0577 5.48223 14.0098 5.11874 13.8333 4.81278L13.6867 4.55945C13.5096 4.25381 13.2186 4.0309 12.8773 3.93966C12.5361 3.84841 12.1726 3.89629 11.8667 4.07278L11.7667 4.12612C11.564 4.24314 11.3341 4.30475 11.1 4.30475C10.866 4.30475 10.636 4.24314 10.4333 4.12612L10.1467 3.95945C9.94418 3.84254 9.77599 3.67444 9.65898 3.47201C9.54196 3.26958 9.48024 3.03993 9.48 2.80612V2.68612C9.48 2.33249 9.33953 1.99336 9.08948 1.74331C8.83943 1.49326 8.50029 1.35278 8.14667 1.35278Z" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8 10.0195C9.10457 10.0195 10 9.12404 10 8.01947C10 6.9149 9.10457 6.01947 8 6.01947C6.89543 6.01947 6 6.9149 6 8.01947C6 9.12404 6.89543 10.0195 8 10.0195Z" stroke="black" strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
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';
|
||||
export { default as SettingsIcon } from './SettingsIcon';
|
||||
export { default as AddIcon } from "./AddIcon";
|
||||
export { default as BackIcon } from "./BackIcon";
|
||||
export { default as PlayIcon } from "./PlayIcon";
|
||||
export { default as MenuIcon } from "./MenuIcon";
|
||||
export { default as PlusIcon } from "./PlusIcon";
|
||||
export { default as MinusIcon } from "./MinusIcon";
|
||||
export { default as CloseIcon } from "./CloseIcon";
|
||||
export { default as CheckIcon } from "./CheckIcon";
|
||||
export { default as CaretIcon } from "./CaretIcon";
|
||||
export { default as CloudIcon } from "./CloudIcon";
|
||||
export { default as SearchIcon } from "./SearchIcon";
|
||||
export { default as DeleteIcon } from "./DeleteIcon";
|
||||
export { default as GithubIcon } from "./GitHubIcon";
|
||||
export { default as CogneeIcon } from "./CogneeIcon";
|
||||
export { default as DiscordIcon } from "./DiscordIcon";
|
||||
export { default as DatasetIcon } from "./DatasetIcon";
|
||||
export { default as SettingsIcon } from "./SettingsIcon";
|
||||
export { default as NotebookIcon } from "./NotebookIcon";
|
||||
export { default as LocalCogneeIcon } from "./LocalCogneeIcon";
|
||||
|
|
|
|||
74
cognee-frontend/src/ui/Layout/Header.tsx
Normal file
74
cognee-frontend/src/ui/Layout/Header.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useBoolean } from "@/utils";
|
||||
|
||||
import { CloseIcon, CloudIcon, CogneeIcon } from "../Icons";
|
||||
import { CTAButton, GhostButton, IconButton, Modal } from "../elements";
|
||||
import { useAuthenticatedUser } from "@/modules/auth";
|
||||
import syncData from "@/modules/cloud/syncData";
|
||||
|
||||
export default function Header() {
|
||||
const { user } = useAuthenticatedUser();
|
||||
|
||||
const {
|
||||
value: isSyncModalOpen,
|
||||
setTrue: openSyncModal,
|
||||
setFalse: closeSyncModal,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleDataSyncConfirm = () => {
|
||||
syncData()
|
||||
.finally(() => {
|
||||
closeSyncModal();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="relative bg-[rgba(244,244,244,0.5)] flex flex-row h-14 min-h-14 px-5 items-center justify-between w-full max-w-[1920px] mx-auto">
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<CogneeIcon />
|
||||
<div className="text-lg">Cognee Graph Interface</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-2.5">
|
||||
<GhostButton onClick={openSyncModal} className="text-indigo-700 gap-3 pl-4 pr-4">
|
||||
<CloudIcon />
|
||||
<div>Sync</div>
|
||||
</GhostButton>
|
||||
<a href="/plan">
|
||||
<GhostButton className="text-indigo-700 pl-4 pr-4">Premium</GhostButton>
|
||||
</a>
|
||||
{/* <div className="px-2 py-2 mr-3">
|
||||
<SettingsIcon />
|
||||
</div> */}
|
||||
<Link href="/account" className="bg-indigo-600 w-8 h-8 rounded-full overflow-hidden">
|
||||
{user?.avatarImagePath ? (
|
||||
<Image width="32" height="32" alt="Name of the user" src={user.avatarImagePath} />
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full text-white flex items-center justify-center">
|
||||
{user?.email?.charAt(0) || "C"}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Modal isOpen={isSyncModalOpen}>
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-2xl">Sync local datasets with cloud datasets?</span>
|
||||
<IconButton onClick={closeSyncModal}><CloseIcon /></IconButton>
|
||||
</div>
|
||||
<div className="mt-8 mb-6">Are you sure you want to sync local datasets to cloud?</div>
|
||||
<div className="flex flex-row gap-4 mt-4 justify-end">
|
||||
<GhostButton type="button" onClick={closeSyncModal}>cancel</GhostButton>
|
||||
<CTAButton onClick={handleDataSyncConfirm} type="submit">confirm</CTAButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
export { default as Divider } from './Divider/Divider';
|
||||
export { default as Divider } from "./Divider/Divider";
|
||||
export { default as Header } from "./Header";
|
||||
|
|
|
|||
45
cognee-frontend/src/ui/elements/Accordion.tsx
Normal file
45
cognee-frontend/src/ui/elements/Accordion.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import classNames from "classnames";
|
||||
import { CaretIcon } from "../Icons";
|
||||
|
||||
export interface AccordionProps {
|
||||
isOpen: boolean;
|
||||
title: React.ReactNode;
|
||||
openAccordion: () => void;
|
||||
closeAccordion: () => void;
|
||||
tools?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
switchCaretPosition?: boolean;
|
||||
}
|
||||
|
||||
export default function Accordion({ title, tools, children, isOpen, openAccordion, closeAccordion, className, contentClassName, switchCaretPosition = false }: AccordionProps) {
|
||||
return (
|
||||
<div className={classNames("flex flex-col", className)}>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<button className={classNames("flex flex-row items-center pr-2", switchCaretPosition ? "gap-1.5" : "gap-4")} onClick={isOpen ? closeAccordion : openAccordion}>
|
||||
{switchCaretPosition ? (
|
||||
<>
|
||||
<CaretIcon className={classNames("transition-transform", isOpen ? "rotate-360" : "rotate-270")} />
|
||||
{title}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{title}
|
||||
<CaretIcon className={classNames("transition-transform", isOpen ? "rotate-0" : "rotate-180")} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{tools}
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className={classNames("grid transition-[grid-template-rows] duration-300 ease-in-out [grid-template-rows:0fr]", contentClassName, {
|
||||
"[grid-template-rows:1fr]": isOpen,
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
cognee-frontend/src/ui/elements/AvatarImage.tsx
Normal file
0
cognee-frontend/src/ui/elements/AvatarImage.tsx
Normal file
|
|
@ -1,8 +1,8 @@
|
|||
import classNames from 'classnames';
|
||||
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-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>
|
||||
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-3xl bg-indigo-600 px-10 h-8 text-white hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import classNames from 'classnames';
|
||||
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>
|
||||
<button className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-3xl bg-transparent px-10 h-8 text-black hover:bg-gray-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
14
cognee-frontend/src/ui/elements/IconButton.tsx
Normal file
14
cognee-frontend/src/ui/elements/IconButton.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import classNames from "classnames";
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export default function IconButton({ as, children, className, ...props }: ButtonProps) {
|
||||
const Element = as || "button";
|
||||
|
||||
return (
|
||||
<Element className={classNames("flex flex-row justify-center items-center gap-2 cursor-pointer rounded-xl bg-transparent p-[0.5rem] m-[-0.5rem] text-black hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</Element>
|
||||
);
|
||||
}
|
||||
|
|
@ -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-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} />
|
||||
<input className={classNames("block w-full rounded-3xl bg-white px-4 h-10 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} />
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ interface ModalProps {
|
|||
|
||||
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">
|
||||
<div className="fixed top-0 left-0 right-0 bottom-0 backdrop-blur-lg z-20 flex items-center justify-center">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
3
cognee-frontend/src/ui/elements/Modal/index.ts
Normal file
3
cognee-frontend/src/ui/elements/Modal/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { default as Modal } from "./Modal";
|
||||
export { default as useModal } from "./useModal";
|
||||
|
||||
49
cognee-frontend/src/ui/elements/Modal/useModal.ts
Normal file
49
cognee-frontend/src/ui/elements/Modal/useModal.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { FormEvent, useCallback, useState } from "react";
|
||||
import { useBoolean } from "@/utils";
|
||||
|
||||
export default function useModal<ConfirmActionReturnType = void>(initiallyOpen?: boolean, confirmCallback?: (state: object, event?: FormEvent<HTMLFormElement>) => Promise<ConfirmActionReturnType> | ConfirmActionReturnType) {
|
||||
const [modalState, setModalState] = useState<object>({});
|
||||
const [isActionLoading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
value: isModalOpen,
|
||||
setTrue: openModalInternal,
|
||||
setFalse: closeModalInternal,
|
||||
} = useBoolean(initiallyOpen || false);
|
||||
|
||||
const openModal = useCallback((state?: object) => {
|
||||
if (state) {
|
||||
setModalState(state);
|
||||
}
|
||||
openModalInternal();
|
||||
}, [openModalInternal]);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
closeModalInternal();
|
||||
setModalState({});
|
||||
}, [closeModalInternal]);
|
||||
|
||||
const confirmAction = useCallback((event?: FormEvent<HTMLFormElement>) => {
|
||||
if (confirmCallback) {
|
||||
setLoading(true);
|
||||
|
||||
const maybePromise = confirmCallback(modalState, event);
|
||||
|
||||
if (maybePromise instanceof Promise) {
|
||||
return maybePromise
|
||||
.finally(closeModal)
|
||||
.finally(() => setLoading(false));
|
||||
} else {
|
||||
return maybePromise; // Not a promise.
|
||||
}
|
||||
}
|
||||
}, [closeModal, confirmCallback, modalState]);
|
||||
|
||||
return {
|
||||
isModalOpen,
|
||||
openModal,
|
||||
closeModal,
|
||||
confirmAction,
|
||||
isActionLoading,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import classNames from 'classnames';
|
||||
import classNames from "classnames";
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
|
||||
export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
export default function NeutralButton({ 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 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-10 h-8 text-black border-1 border-indigo-600 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", className)} {...props}>{children}</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
342
cognee-frontend/src/ui/elements/Notebook/Notebook.tsx
Normal file
342
cognee-frontend/src/ui/elements/Notebook/Notebook.tsx
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
"use client";
|
||||
|
||||
import { v4 as uuid4 } from "uuid";
|
||||
import classNames from "classnames";
|
||||
import { Fragment, MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { CaretIcon, PlusIcon } from "@/ui/Icons";
|
||||
import { IconButton, PopupMenu, TextArea } from "@/ui/elements";
|
||||
import { GraphControlsAPI } from "@/app/(graph)/GraphControls";
|
||||
import GraphVisualization, { GraphVisualizationAPI } from "@/app/(graph)/GraphVisualization";
|
||||
|
||||
import NotebookCellHeader from "./NotebookCellHeader";
|
||||
import { Cell, Notebook as NotebookType } from "./types";
|
||||
|
||||
interface NotebookProps {
|
||||
notebook: NotebookType;
|
||||
runCell: (notebook: NotebookType, cell: Cell) => Promise<void>;
|
||||
updateNotebook: (updatedNotebook: NotebookType) => void;
|
||||
saveNotebook: (notebook: NotebookType) => void;
|
||||
}
|
||||
|
||||
export default function Notebook({ notebook, updateNotebook, saveNotebook, runCell }: NotebookProps) {
|
||||
const saveCells = useCallback(() => {
|
||||
saveNotebook(notebook);
|
||||
}, [notebook, saveNotebook]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("beforeunload", saveCells);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", saveCells);
|
||||
};
|
||||
}, [saveCells]);
|
||||
|
||||
useEffect(() => {
|
||||
if (notebook.cells.length === 0) {
|
||||
const newCell: Cell = {
|
||||
id: uuid4(),
|
||||
name: "first cell",
|
||||
type: "code",
|
||||
content: "",
|
||||
};
|
||||
updateNotebook({
|
||||
...notebook,
|
||||
cells: [newCell],
|
||||
});
|
||||
}
|
||||
}, [notebook, saveNotebook, updateNotebook]);
|
||||
|
||||
const handleCellRun = useCallback((cell: Cell) => {
|
||||
return runCell(notebook, cell);
|
||||
}, [notebook, runCell]);
|
||||
|
||||
const handleCellAdd = useCallback((afterCellIndex: number, cellType: "markdown" | "code") => {
|
||||
const newCell: Cell = {
|
||||
id: uuid4(),
|
||||
name: "new cell",
|
||||
type: cellType,
|
||||
content: "",
|
||||
};
|
||||
|
||||
const newNotebook = {
|
||||
...notebook,
|
||||
cells: [
|
||||
...notebook.cells.slice(0, afterCellIndex + 1),
|
||||
newCell,
|
||||
...notebook.cells.slice(afterCellIndex + 1),
|
||||
],
|
||||
};
|
||||
|
||||
toggleCellOpen(newCell.id);
|
||||
updateNotebook(newNotebook);
|
||||
}, [notebook, updateNotebook]);
|
||||
|
||||
const handleCellRemove = useCallback((cell: Cell) => {
|
||||
updateNotebook({
|
||||
...notebook,
|
||||
cells: notebook.cells.filter((c: Cell) => c.id !== cell.id),
|
||||
});
|
||||
}, [notebook, updateNotebook]);
|
||||
|
||||
const handleCellInputChange = useCallback((notebook: NotebookType, cell: Cell, value: string) => {
|
||||
const newCell = {...cell, content: value };
|
||||
|
||||
updateNotebook({
|
||||
...notebook,
|
||||
cells: notebook.cells.map((cell: Cell) => (cell.id === newCell.id ? newCell : cell)),
|
||||
});
|
||||
}, [updateNotebook]);
|
||||
|
||||
const handleCellUp = useCallback((cell: Cell) => {
|
||||
const index = notebook.cells.indexOf(cell);
|
||||
|
||||
if (index > 0) {
|
||||
const newCells = [...notebook.cells];
|
||||
newCells[index] = notebook.cells[index - 1];
|
||||
newCells[index - 1] = cell;
|
||||
|
||||
updateNotebook({
|
||||
...notebook,
|
||||
cells: newCells,
|
||||
});
|
||||
}
|
||||
}, [notebook, updateNotebook]);
|
||||
|
||||
const handleCellDown = useCallback((cell: Cell) => {
|
||||
const index = notebook.cells.indexOf(cell);
|
||||
|
||||
if (index < notebook.cells.length - 1) {
|
||||
const newCells = [...notebook.cells];
|
||||
newCells[index] = notebook.cells[index + 1];
|
||||
newCells[index + 1] = cell;
|
||||
|
||||
updateNotebook({
|
||||
...notebook,
|
||||
cells: newCells,
|
||||
});
|
||||
}
|
||||
}, [notebook, updateNotebook]);
|
||||
|
||||
const handleCellRename = useCallback((cell: Cell) => {
|
||||
const newName = prompt("Enter a new name for the cell:");
|
||||
|
||||
if (newName) {
|
||||
updateNotebook({
|
||||
...notebook,
|
||||
cells: notebook.cells.map((c: Cell) => (c.id === cell.id ? {...c, name: newName } : c)),
|
||||
});
|
||||
}
|
||||
}, [notebook, updateNotebook]);
|
||||
|
||||
const [openCells, setOpenCells] = useState(new Set(notebook.cells.map((c: Cell) => c.id)));
|
||||
|
||||
const toggleCellOpen = (id: string) => {
|
||||
setOpenCells((prev) => {
|
||||
const newState = new Set(prev);
|
||||
|
||||
if (newState.has(id)) {
|
||||
newState.delete(id)
|
||||
} else {
|
||||
newState.add(id);
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl flex flex-col gap-0.5 px-7 py-5 flex-1">
|
||||
<div className="mb-5">{notebook.name}</div>
|
||||
|
||||
{notebook.cells.map((cell: Cell, index) => (
|
||||
<Fragment key={cell.id}>
|
||||
<div key={cell.id} className="flex flex-row rounded-xl border-1 border-gray-100">
|
||||
<div className="flex flex-col flex-1 relative">
|
||||
{cell.type === "code" ? (
|
||||
<>
|
||||
<div className="absolute left-[-1.35rem] top-2.5">
|
||||
<IconButton className="p-[0.25rem] m-[-0.25rem]" onClick={toggleCellOpen.bind(null, cell.id)}>
|
||||
<CaretIcon className={classNames("transition-transform", openCells.has(cell.id) ? "rotate-0" : "rotate-180")} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<NotebookCellHeader
|
||||
cell={cell}
|
||||
runCell={handleCellRun}
|
||||
renameCell={handleCellRename}
|
||||
removeCell={handleCellRemove}
|
||||
moveCellUp={handleCellUp}
|
||||
moveCellDown={handleCellDown}
|
||||
className="rounded-tl-xl rounded-tr-xl"
|
||||
/>
|
||||
|
||||
{openCells.has(cell.id) && (
|
||||
<>
|
||||
<TextArea
|
||||
value={cell.content}
|
||||
onChange={handleCellInputChange.bind(null, notebook, cell)}
|
||||
// onKeyUp={handleCellRunOnEnter}
|
||||
isAutoExpanding
|
||||
name="cellInput"
|
||||
placeholder="Type your code here..."
|
||||
contentEditable={true}
|
||||
className="resize-none min-h-36 max-h-96 overflow-y-auto rounded-tl-none rounded-tr-none rounded-bl-xl rounded-br-xl border-0 !outline-0"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col bg-gray-100 overflow-x-auto max-w-full">
|
||||
{cell.result && (
|
||||
<div className="px-2 py-2">
|
||||
output: <CellResult content={cell.result} />
|
||||
</div>
|
||||
)}
|
||||
{cell.error && (
|
||||
<div className="px-2 py-2">
|
||||
error: {cell.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
openCells.has(cell.id) && (
|
||||
<TextArea
|
||||
value={cell.content}
|
||||
onChange={handleCellInputChange.bind(null, notebook, cell)}
|
||||
// onKeyUp={handleCellRunOnEnter}
|
||||
isAutoExpanding
|
||||
name="cellInput"
|
||||
placeholder="Type your text here..."
|
||||
contentEditable={true}
|
||||
className="resize-none min-h-24 max-h-96 overflow-y-auto rounded-tl-none rounded-tr-none rounded-bl-xl rounded-br-xl border-0 !outline-0"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-[-1.35rem]">
|
||||
<PopupMenu
|
||||
openToRight={true}
|
||||
triggerElement={<PlusIcon />}
|
||||
triggerClassName="p-[0.25rem] m-[-0.25rem]"
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<button
|
||||
onClick={() => handleCellAdd(index, "markdown")}
|
||||
className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer"
|
||||
>
|
||||
<span>text</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleCellAdd(index, "code")}
|
||||
className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer"
|
||||
>
|
||||
<span>code</span>
|
||||
</div>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function CellResult({ content = [] }) {
|
||||
const parsedContent = [];
|
||||
|
||||
const graphRef = useRef<GraphVisualizationAPI>();
|
||||
const graphControls = useRef<GraphControlsAPI>({
|
||||
setSelectedNode: () => {},
|
||||
getSelectedNode: () => null,
|
||||
});
|
||||
|
||||
for (const line of content) {
|
||||
try {
|
||||
if (Array.isArray(line)) {
|
||||
for (const item of line) {
|
||||
if (typeof item === "string") {
|
||||
parsedContent.push(
|
||||
<pre key={item.slice(0, -10)}>
|
||||
{item}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
if (typeof item === "object" && item["search_result"] && Array.isArray(item["search_result"])) {
|
||||
for (const result of item["search_result"]) {
|
||||
parsedContent.push(
|
||||
<div className="w-full h-full bg-white">
|
||||
<span className="text-sm pl-2 mb-4">query response (dataset: {item["dataset_name"]})</span>
|
||||
<span className="block px-2 py-2">{result}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (typeof item === "object" && item["graph"] && typeof item["graph"] === "object") {
|
||||
parsedContent.push(
|
||||
<div className="w-full h-full bg-white">
|
||||
<span className="text-sm pl-2 mb-4">reasoning graph</span>
|
||||
<GraphVisualization
|
||||
data={transformToVisualizationData(item["graph"])}
|
||||
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
|
||||
graphControls={graphControls}
|
||||
className="min-h-48"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
parsedContent.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedContent.map((item, index) => (
|
||||
<div key={index} className="px-2 py-1">
|
||||
{item}
|
||||
{/* {typeof item === "object" && item["search_result"] && Array.isArray(item["search_result"]) && (
|
||||
(item["search_result"] as []).map((result: string) => (<pre key={result.slice(0, -10)}>{result}</pre>))
|
||||
)}
|
||||
{typeof item === "object" && item["graph"] && typeof item["graph"] === "object" && (
|
||||
(item["graph"])
|
||||
)} */}
|
||||
</div>
|
||||
));
|
||||
|
||||
};
|
||||
|
||||
function transformToVisualizationData(triplets) {
|
||||
// Implementation to transform triplet to visualization data
|
||||
|
||||
const nodes = {};
|
||||
const links = {};
|
||||
|
||||
for (const triplet of triplets) {
|
||||
nodes[triplet.source.id] = {
|
||||
id: triplet.source.id,
|
||||
label: triplet.source.attributes.name,
|
||||
type: triplet.source.attributes.type,
|
||||
attributes: triplet.source.attributes,
|
||||
};
|
||||
nodes[triplet.destination.id] = {
|
||||
id: triplet.destination.id,
|
||||
label: triplet.destination.attributes.name,
|
||||
type: triplet.destination.attributes.type,
|
||||
attributes: triplet.destination.attributes,
|
||||
};
|
||||
links[`${triplet.source.id}_${triplet.attributes.relationship_name}_${triplet.destination.id}`] = {
|
||||
source: triplet.source.id,
|
||||
target: triplet.destination.id,
|
||||
label: triplet.attributes.relationship_name,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: Object.values(nodes),
|
||||
links: Object.values(links),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import classNames from "classnames";
|
||||
|
||||
import { useBoolean } from "@/utils";
|
||||
import { LocalCogneeIcon, PlayIcon } from "@/ui/Icons";
|
||||
import { PopupMenu } from "@/ui/elements";
|
||||
import { LoadingIndicator } from "@/ui/App";
|
||||
|
||||
import { Cell } from "./types";
|
||||
import IconButton from "../IconButton";
|
||||
|
||||
interface NotebookCellHeaderProps {
|
||||
cell: Cell;
|
||||
runCell: (cell: Cell) => Promise<void>;
|
||||
renameCell: (cell: Cell) => void;
|
||||
removeCell: (cell: Cell) => void;
|
||||
moveCellUp: (cell: Cell) => void;
|
||||
moveCellDown: (cell: Cell) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function NotebookCellHeader({
|
||||
cell,
|
||||
runCell,
|
||||
renameCell,
|
||||
removeCell,
|
||||
moveCellUp,
|
||||
moveCellDown,
|
||||
className,
|
||||
}: NotebookCellHeaderProps) {
|
||||
const {
|
||||
value: isRunningCell,
|
||||
setTrue: setIsRunningCell,
|
||||
setFalse: setIsNotRunningCell,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleCellRun = () => {
|
||||
setIsRunningCell();
|
||||
runCell(cell)
|
||||
.then(() => {
|
||||
setIsNotRunningCell();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames("flex flex-row justify-between items-center h-9 bg-gray-100", className)}>
|
||||
<div className="flex flex-row items-center px-3.5">
|
||||
{isRunningCell ? <LoadingIndicator /> : <IconButton onClick={handleCellRun}><PlayIcon /></IconButton>}
|
||||
<span className="ml-4">{cell.name}</span>
|
||||
</div>
|
||||
<div className="pr-4 flex flex-row items-center gap-8">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<LocalCogneeIcon className="text-indigo-700" />
|
||||
<span className="text-xs">local cognee</span>
|
||||
</div>
|
||||
<PopupMenu>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<button onClick={() => moveCellUp(cell)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">move cell up</button>
|
||||
<button onClick={() => moveCellDown(cell)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">move cell down</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 items-start">
|
||||
<button onClick={() => renameCell(cell)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">rename</button>
|
||||
<button onClick={() => removeCell(cell)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete</button>
|
||||
</div>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
cognee-frontend/src/ui/elements/Notebook/index.ts
Normal file
1
cognee-frontend/src/ui/elements/Notebook/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./Notebook";
|
||||
15
cognee-frontend/src/ui/elements/Notebook/types.ts
Normal file
15
cognee-frontend/src/ui/elements/Notebook/types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export interface Cell {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "markdown" | "code";
|
||||
content: string;
|
||||
result?: [];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface Notebook {
|
||||
id: string;
|
||||
name: string;
|
||||
cells: Cell[];
|
||||
deletable?: boolean;
|
||||
}
|
||||
48
cognee-frontend/src/ui/elements/PopupMenu.tsx
Normal file
48
cognee-frontend/src/ui/elements/PopupMenu.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"use client";
|
||||
|
||||
import { useBoolean, useOutsideClick } from "@/utils";
|
||||
import { MenuIcon } from "@/ui/Icons";
|
||||
import { IconButton } from "@/ui/elements";
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface PopupMenuProps {
|
||||
children: React.ReactNode;
|
||||
triggerElement?: React.ReactNode;
|
||||
triggerClassName?: string;
|
||||
openToRight?: boolean;
|
||||
}
|
||||
|
||||
export default function PopupMenu({ triggerElement, triggerClassName, children, openToRight = false }: PopupMenuProps) {
|
||||
const {
|
||||
value: isMenuOpen,
|
||||
setFalse: closeMenu,
|
||||
toggle: toggleMenu,
|
||||
} = useBoolean(false);
|
||||
|
||||
const menuRootRef = useOutsideClick<HTMLDivElement>(closeMenu);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block" ref={menuRootRef}>
|
||||
<IconButton as="div" className={triggerClassName} onClick={toggleMenu}>
|
||||
{triggerElement || <MenuIcon />}
|
||||
</IconButton>
|
||||
|
||||
{isMenuOpen && (
|
||||
<div
|
||||
className={
|
||||
classNames(
|
||||
"absolute top-full flex flex-col gap-4 pl-1 py-3 pr-4",
|
||||
"whitespace-nowrap bg-white border-1 border-gray-100 z-10",
|
||||
{
|
||||
"left-0": openToRight,
|
||||
"right-0": !openToRight,
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@ export default function Select({ children, className, ...props }: SelectHTMLAttr
|
|||
<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",
|
||||
"block w-full appearance-none rounded-3xl bg-white pl-4 pr-8 h-8 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,
|
||||
)
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@ export default function Select({ children, className, ...props }: SelectHTMLAttr
|
|||
>
|
||||
{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 className="pointer-events-none absolute top-1/3 -mt-0.5 right-3 text-indigo-600 rotate-180">
|
||||
<CaretIcon />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,10 +24,8 @@ export default function TextArea({
|
|||
const fakeTextAreaElement = event.target as HTMLDivElement;
|
||||
const newValue = fakeTextAreaElement.innerText;
|
||||
|
||||
if (newValue !== value) {
|
||||
onChange?.(newValue);
|
||||
}
|
||||
}, [onChange, value]);
|
||||
onChange?.(newValue);
|
||||
}, [onChange]);
|
||||
|
||||
const handleKeyUp = useCallback((event: Event) => {
|
||||
if (onKeyUp) {
|
||||
|
|
@ -55,8 +53,15 @@ export default function TextArea({
|
|||
useLayoutEffect(() => {
|
||||
const fakeTextAreaElement = fakeTextAreaRef.current;
|
||||
|
||||
if (fakeTextAreaElement) {
|
||||
if (fakeTextAreaElement && fakeTextAreaElement.innerText.trim() !== "") {
|
||||
fakeTextAreaElement.innerText = placeholder;
|
||||
}
|
||||
}, [placeholder]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const fakeTextAreaElement = fakeTextAreaRef.current;
|
||||
|
||||
if (fakeTextAreaElement) {
|
||||
fakeTextAreaElement.addEventListener("input", handleTextChange);
|
||||
fakeTextAreaElement.addEventListener("keyup", handleKeyUp);
|
||||
}
|
||||
|
|
@ -67,15 +72,21 @@ export default function TextArea({
|
|||
fakeTextAreaElement.removeEventListener("keyup", handleKeyUp);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [handleKeyUp, handleTextChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const fakeTextAreaElement = fakeTextAreaRef.current;
|
||||
const textAreaText = fakeTextAreaElement?.innerText;
|
||||
if (fakeTextAreaElement && textAreaText !== value && textAreaText !== placeholder) {
|
||||
|
||||
if (fakeTextAreaElement && (value === "" || value === "\n")) {
|
||||
fakeTextAreaElement.innerText = placeholder;
|
||||
return;
|
||||
}
|
||||
|
||||
if (fakeTextAreaElement && textAreaText !== value) {
|
||||
fakeTextAreaElement.innerText = value;
|
||||
}
|
||||
}, [value]);
|
||||
}, [placeholder, value]);
|
||||
|
||||
return isAutoExpanding ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
export { default as Modal } from "./Modal";
|
||||
export { default as Modal } from "./Modal/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 PopupMenu } from "./PopupMenu";
|
||||
export { default as IconButton } from "./IconButton";
|
||||
export { default as GhostButton } from "./GhostButton";
|
||||
export { default as NeutralButton } from "./NeutralButton";
|
||||
export { default as StatusIndicator } from "./StatusIndicator";
|
||||
export { default as Accordion } from "./Accordion";
|
||||
export { default as Notebook } from "./Notebook";
|
||||
|
|
|
|||
|
|
@ -47,3 +47,7 @@ export default async function fetch(url: string, options: RequestInit = {}): Pro
|
|||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
fetch.checkHealth = () => {
|
||||
return global.fetch(`${backendApiUrl.replace("/api", "")}/health`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { default as fetch } from "./fetch";
|
||||
export { default as handleServerErrors } from "./handleServerErrors";
|
||||
export { default as useBoolean } from "./useBoolean";
|
||||
export { default as useOutsideClick } from "./useOutsideClick";
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ export default function useBoolean(initialValue: boolean) {
|
|||
|
||||
const setTrue = useCallback(() => setValue(true), []);
|
||||
const setFalse = useCallback(() => setValue(false), []);
|
||||
const toggle = useCallback(() => setValue((prevValue) => !prevValue), []);
|
||||
|
||||
return {
|
||||
value,
|
||||
setTrue,
|
||||
setFalse,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
25
cognee-frontend/src/utils/useOutsideClick.ts
Normal file
25
cognee-frontend/src/utils/useOutsideClick.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function useOutsideClick<ElementType extends HTMLElement>(callbackFn: () => void, isEnabled = true) {
|
||||
const rootElementRef = useRef<ElementType>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const clickedElement = event.target;
|
||||
|
||||
if (clickedElement && rootElementRef.current && !rootElementRef.current?.contains(clickedElement as Node)) {
|
||||
callbackFn();
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnabled) {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [callbackFn, isEnabled]);
|
||||
|
||||
return rootElementRef;
|
||||
}
|
||||
|
|
@ -1,124 +1,265 @@
|
|||
import os
|
||||
import uuid
|
||||
import json
|
||||
"""Cognee demo with simplified structure."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import pathlib
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, List, Mapping
|
||||
|
||||
from cognee import config, prune, search, SearchType, visualize_graph
|
||||
from cognee.low_level import setup, DataPoint
|
||||
from cognee.pipelines import run_tasks, Task
|
||||
from cognee.tasks.storage import add_data_points
|
||||
from cognee.tasks.storage.index_graph_edges import index_graph_edges
|
||||
from cognee.modules.users.methods import get_default_user
|
||||
from cognee.modules.data.methods import load_or_create_datasets
|
||||
|
||||
|
||||
class Person(DataPoint):
|
||||
"""Represent a person."""
|
||||
|
||||
name: str
|
||||
metadata: dict = {"index_fields": ["name"]}
|
||||
|
||||
|
||||
class Department(DataPoint):
|
||||
"""Represent a department."""
|
||||
|
||||
name: str
|
||||
employees: list[Person]
|
||||
metadata: dict = {"index_fields": ["name"]}
|
||||
|
||||
|
||||
class CompanyType(DataPoint):
|
||||
"""Represent a company type."""
|
||||
|
||||
name: str = "Company"
|
||||
|
||||
|
||||
class Company(DataPoint):
|
||||
"""Represent a company."""
|
||||
|
||||
name: str
|
||||
departments: list[Department]
|
||||
is_type: CompanyType
|
||||
metadata: dict = {"index_fields": ["name"]}
|
||||
|
||||
|
||||
def ingest_files():
|
||||
companies_file_path = os.path.join(os.path.dirname(__file__), "../data/companies.json")
|
||||
companies = json.loads(open(companies_file_path, "r").read())
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
DATA_DIR = ROOT.parent / "data"
|
||||
COGNEE_DIR = ROOT / ".cognee_system"
|
||||
ARTIFACTS_DIR = ROOT / ".artifacts"
|
||||
GRAPH_HTML = ARTIFACTS_DIR / "graph_visualization.html"
|
||||
COMPANIES_JSON = DATA_DIR / "companies.json"
|
||||
PEOPLE_JSON = DATA_DIR / "people.json"
|
||||
|
||||
people_file_path = os.path.join(os.path.dirname(__file__), "../data/people.json")
|
||||
people = json.loads(open(people_file_path, "r").read())
|
||||
|
||||
people_data_points = {}
|
||||
departments_data_points = {}
|
||||
def load_json_file(path: Path) -> Any:
|
||||
"""Load a JSON file."""
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Missing required file: {path}")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def remove_duplicates_preserve_order(seq: Iterable[Any]) -> list[Any]:
|
||||
"""Return list with duplicates removed while preserving order."""
|
||||
seen = set()
|
||||
out = []
|
||||
for x in seq:
|
||||
if x in seen:
|
||||
continue
|
||||
seen.add(x)
|
||||
out.append(x)
|
||||
return out
|
||||
|
||||
|
||||
def collect_people(payloads: Iterable[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
|
||||
"""Collect people from payloads."""
|
||||
people = [person for payload in payloads for person in payload.get("people", [])]
|
||||
return people
|
||||
|
||||
|
||||
def collect_companies(payloads: Iterable[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
|
||||
"""Collect companies from payloads."""
|
||||
companies = [company for payload in payloads for company in payload.get("companies", [])]
|
||||
return companies
|
||||
|
||||
|
||||
def build_people_nodes(people: Iterable[Mapping[str, Any]]) -> dict:
|
||||
"""Build person nodes keyed by name."""
|
||||
nodes = {p["name"]: Person(name=p["name"]) for p in people if p.get("name")}
|
||||
return nodes
|
||||
|
||||
|
||||
def group_people_by_department(people: Iterable[Mapping[str, Any]]) -> dict:
|
||||
"""Group person names by department."""
|
||||
groups = defaultdict(list)
|
||||
for person in people:
|
||||
new_person = Person(name=person["name"])
|
||||
people_data_points[person["name"]] = new_person
|
||||
name = person.get("name")
|
||||
if not name:
|
||||
continue
|
||||
dept = person.get("department", "Unknown")
|
||||
groups[dept].append(name)
|
||||
return groups
|
||||
|
||||
if person["department"] not in departments_data_points:
|
||||
departments_data_points[person["department"]] = Department(
|
||||
name=person["department"], employees=[new_person]
|
||||
)
|
||||
else:
|
||||
departments_data_points[person["department"]].employees.append(new_person)
|
||||
|
||||
companies_data_points = {}
|
||||
|
||||
# Create a single CompanyType node, so we connect all companies to it.
|
||||
companyType = CompanyType()
|
||||
|
||||
def collect_declared_departments(
|
||||
groups: Mapping[str, list[str]], companies: Iterable[Mapping[str, Any]]
|
||||
) -> set:
|
||||
"""Collect department names referenced anywhere."""
|
||||
names = set(groups)
|
||||
for company in companies:
|
||||
new_company = Company(name=company["name"], departments=[], is_type=companyType)
|
||||
companies_data_points[company["name"]] = new_company
|
||||
|
||||
for department_name in company["departments"]:
|
||||
if department_name not in departments_data_points:
|
||||
departments_data_points[department_name] = Department(
|
||||
name=department_name, employees=[]
|
||||
)
|
||||
|
||||
new_company.departments.append(departments_data_points[department_name])
|
||||
|
||||
return companies_data_points.values()
|
||||
for dept in company.get("departments", []):
|
||||
names.add(dept)
|
||||
return names
|
||||
|
||||
|
||||
async def main():
|
||||
cognee_directory_path = str(
|
||||
pathlib.Path(os.path.join(pathlib.Path(__file__).parent, ".cognee_system")).resolve()
|
||||
)
|
||||
# Set up the Cognee system directory. Cognee will store system files and databases here.
|
||||
config.system_root_directory(cognee_directory_path)
|
||||
def build_department_nodes(dept_names: Iterable[str]) -> dict:
|
||||
"""Build department nodes keyed by name."""
|
||||
nodes = {name: Department(name=name, employees=[]) for name in dept_names}
|
||||
return nodes
|
||||
|
||||
# Prune system metadata before running, only if we want "fresh" state.
|
||||
|
||||
def build_company_nodes(companies: Iterable[Mapping[str, Any]], company_type: CompanyType) -> dict:
|
||||
"""Build company nodes keyed by name."""
|
||||
nodes = {
|
||||
c["name"]: Company(name=c["name"], departments=[], is_type=company_type)
|
||||
for c in companies
|
||||
if c.get("name")
|
||||
}
|
||||
return nodes
|
||||
|
||||
|
||||
def iterate_company_department_pairs(companies: Iterable[Mapping[str, Any]]):
|
||||
"""Yield (company_name, department_name) pairs."""
|
||||
for company in companies:
|
||||
comp_name = company.get("name")
|
||||
if not comp_name:
|
||||
continue
|
||||
for dept in company.get("departments", []):
|
||||
yield comp_name, dept
|
||||
|
||||
|
||||
def attach_departments_to_companies(
|
||||
companies: Iterable[Mapping[str, Any]],
|
||||
dept_nodes: Mapping[str, Department],
|
||||
company_nodes: Mapping[str, Company],
|
||||
) -> None:
|
||||
"""Attach department nodes to companies."""
|
||||
for comp_name in company_nodes:
|
||||
company_nodes[comp_name].departments = []
|
||||
for comp_name, dept_name in iterate_company_department_pairs(companies):
|
||||
dept = dept_nodes.get(dept_name)
|
||||
company = company_nodes.get(comp_name)
|
||||
if not dept or not company:
|
||||
continue
|
||||
company.departments.append(dept)
|
||||
|
||||
|
||||
def attach_employees_to_departments(
|
||||
groups: Mapping[str, list[str]],
|
||||
people_nodes: Mapping[str, Person],
|
||||
dept_nodes: Mapping[str, Department],
|
||||
) -> None:
|
||||
"""Attach employees to departments."""
|
||||
for dept in dept_nodes.values():
|
||||
dept.employees = []
|
||||
for dept_name, names in groups.items():
|
||||
unique_names = remove_duplicates_preserve_order(names)
|
||||
target = dept_nodes.get(dept_name)
|
||||
if not target:
|
||||
continue
|
||||
employees = [people_nodes[n] for n in unique_names if n in people_nodes]
|
||||
target.employees = employees
|
||||
|
||||
|
||||
def build_companies(payloads: Iterable[Mapping[str, Any]]) -> list[Company]:
|
||||
"""Build company nodes from payloads."""
|
||||
people = collect_people(payloads)
|
||||
companies = collect_companies(payloads)
|
||||
people_nodes = build_people_nodes(people)
|
||||
groups = group_people_by_department(people)
|
||||
dept_names = collect_declared_departments(groups, companies)
|
||||
dept_nodes = build_department_nodes(dept_names)
|
||||
company_type = CompanyType()
|
||||
company_nodes = build_company_nodes(companies, company_type)
|
||||
attach_departments_to_companies(companies, dept_nodes, company_nodes)
|
||||
attach_employees_to_departments(groups, people_nodes, dept_nodes)
|
||||
result = list(company_nodes.values())
|
||||
return result
|
||||
|
||||
|
||||
def load_default_payload() -> list[Mapping[str, Any]]:
|
||||
"""Load the default payload from data files."""
|
||||
companies = load_json_file(COMPANIES_JSON)
|
||||
people = load_json_file(PEOPLE_JSON)
|
||||
payload = [{"companies": companies, "people": people}]
|
||||
return payload
|
||||
|
||||
|
||||
def ingest_payloads(data: List[Any] | None) -> list[Company]:
|
||||
"""Ingest payloads and build company nodes."""
|
||||
if not data or data == [None]:
|
||||
data = load_default_payload()
|
||||
companies = build_companies(data)
|
||||
return companies
|
||||
|
||||
|
||||
async def execute_pipeline() -> None:
|
||||
"""Execute Cognee pipeline."""
|
||||
|
||||
# Configure system paths
|
||||
logging.info("Configuring Cognee directories at %s", COGNEE_DIR)
|
||||
config.system_root_directory(str(COGNEE_DIR))
|
||||
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Reset state and initialize
|
||||
await prune.prune_system(metadata=True)
|
||||
|
||||
await setup()
|
||||
|
||||
# Generate a random dataset_id
|
||||
dataset_id = uuid.uuid4()
|
||||
# Get user and dataset
|
||||
user = await get_default_user()
|
||||
datasets = await load_or_create_datasets(["demo_dataset"], [], user)
|
||||
dataset_id = datasets[0].id
|
||||
|
||||
pipeline = run_tasks(
|
||||
[
|
||||
Task(ingest_files),
|
||||
Task(add_data_points),
|
||||
],
|
||||
dataset_id,
|
||||
None,
|
||||
user,
|
||||
"demo_pipeline",
|
||||
)
|
||||
|
||||
# Build and run pipeline
|
||||
tasks = [Task(ingest_payloads), Task(add_data_points)]
|
||||
pipeline = run_tasks(tasks, dataset_id, None, user, "demo_pipeline")
|
||||
async for status in pipeline:
|
||||
print(status)
|
||||
logging.info("Pipeline status: %s", status)
|
||||
|
||||
# Post-process: index graph edges and visualize
|
||||
await index_graph_edges()
|
||||
await visualize_graph(str(GRAPH_HTML))
|
||||
|
||||
# Or use our simple graph preview
|
||||
graph_file_path = str(
|
||||
os.path.join(os.path.dirname(__file__), ".artifacts/graph_visualization.html")
|
||||
)
|
||||
await visualize_graph(graph_file_path)
|
||||
|
||||
# Completion query that uses graph data to form context.
|
||||
# Run query against graph
|
||||
completion = await search(
|
||||
query_text="Who works for GreenFuture Solutions?",
|
||||
query_type=SearchType.GRAPH_COMPLETION,
|
||||
)
|
||||
print("Graph completion result is:")
|
||||
print(completion)
|
||||
result = completion
|
||||
logging.info("Graph completion result: %s", result)
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
"""Configure logging."""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s | %(levelname)s | %(message)s",
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run main function."""
|
||||
configure_logging()
|
||||
try:
|
||||
await execute_pipeline()
|
||||
except Exception:
|
||||
logging.exception("Run failed")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ logger = setup_logging()
|
|||
from .api.v1.add import add
|
||||
from .api.v1.delete import delete
|
||||
from .api.v1.cognify import cognify
|
||||
from .modules.memify import memify
|
||||
from .api.v1.config.config import config
|
||||
from .api.v1.datasets.datasets import datasets
|
||||
from .api.v1.prune import prune
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
|
|||
from fastapi import Request
|
||||
from fastapi import FastAPI, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
|
@ -16,14 +16,18 @@ from fastapi.openapi.utils import get_openapi
|
|||
from cognee.exceptions import CogneeApiError
|
||||
from cognee.shared.logging_utils import get_logger, setup_logging
|
||||
from cognee.api.health import health_checker, HealthStatus
|
||||
from cognee.api.v1.cloud.routers import get_checks_router
|
||||
from cognee.api.v1.notebooks.routers import get_notebooks_router
|
||||
from cognee.api.v1.permissions.routers import get_permissions_router
|
||||
from cognee.api.v1.settings.routers import get_settings_router
|
||||
from cognee.api.v1.datasets.routers import get_datasets_router
|
||||
from cognee.api.v1.cognify.routers import get_code_pipeline_router, get_cognify_router
|
||||
from cognee.api.v1.search.routers import get_search_router
|
||||
from cognee.api.v1.memify.routers import get_memify_router
|
||||
from cognee.api.v1.add.routers import get_add_router
|
||||
from cognee.api.v1.delete.routers import get_delete_router
|
||||
from cognee.api.v1.responses.routers import get_responses_router
|
||||
from cognee.api.v1.sync.routers import get_sync_router
|
||||
from cognee.api.v1.users.routers import (
|
||||
get_auth_router,
|
||||
get_register_router,
|
||||
|
|
@ -90,7 +94,7 @@ app.add_middleware(
|
|||
CORSMiddleware,
|
||||
allow_origins=allowed_origins, # Now controlled by env var
|
||||
allow_credentials=True,
|
||||
allow_methods=["OPTIONS", "GET", "POST", "DELETE"],
|
||||
allow_methods=["OPTIONS", "GET", "PUT", "POST", "DELETE"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
# To allow origins, set CORS_ALLOWED_ORIGINS env variable to a comma-separated list, e.g.:
|
||||
|
|
@ -241,6 +245,8 @@ app.include_router(get_add_router(), prefix="/api/v1/add", tags=["add"])
|
|||
|
||||
app.include_router(get_cognify_router(), prefix="/api/v1/cognify", tags=["cognify"])
|
||||
|
||||
app.include_router(get_memify_router(), prefix="/api/v1/memify", tags=["memify"])
|
||||
|
||||
app.include_router(get_search_router(), prefix="/api/v1/search", tags=["search"])
|
||||
|
||||
app.include_router(
|
||||
|
|
@ -259,6 +265,8 @@ app.include_router(get_delete_router(), prefix="/api/v1/delete", tags=["delete"]
|
|||
|
||||
app.include_router(get_responses_router(), prefix="/api/v1/responses", tags=["responses"])
|
||||
|
||||
app.include_router(get_sync_router(), prefix="/api/v1/sync", tags=["sync"])
|
||||
|
||||
codegraph_routes = get_code_pipeline_router()
|
||||
if codegraph_routes:
|
||||
app.include_router(codegraph_routes, prefix="/api/v1/code-pipeline", tags=["code-pipeline"])
|
||||
|
|
@ -269,6 +277,18 @@ app.include_router(
|
|||
tags=["users"],
|
||||
)
|
||||
|
||||
app.include_router(
|
||||
get_notebooks_router(),
|
||||
prefix="/api/v1/notebooks",
|
||||
tags=["notebooks"],
|
||||
)
|
||||
|
||||
app.include_router(
|
||||
get_checks_router(),
|
||||
prefix="/api/v1/checks",
|
||||
tags=["checks"],
|
||||
)
|
||||
|
||||
|
||||
def start_api_server(host: str = "0.0.0.0", port: int = 8000):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"""Health check system for cognee API."""
|
||||
|
||||
from io import BytesIO
|
||||
import time
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -117,12 +118,9 @@ class HealthChecker:
|
|||
engine = await get_graph_engine()
|
||||
|
||||
# Test basic operation with actual graph query
|
||||
if hasattr(engine, "execute"):
|
||||
# For SQL-like graph DBs (Neo4j, Memgraph)
|
||||
await engine.execute("MATCH () RETURN count(*) LIMIT 1")
|
||||
elif hasattr(engine, "query"):
|
||||
if hasattr(engine, "query"):
|
||||
# For other graph engines
|
||||
engine.query("MATCH () RETURN count(*) LIMIT 1", {})
|
||||
await engine.query("MATCH () RETURN count(*) LIMIT 1", {})
|
||||
# If engine exists but no test method, consider it healthy
|
||||
|
||||
response_time = int((time.time() - start_time) * 1000)
|
||||
|
|
@ -167,8 +165,8 @@ class HealthChecker:
|
|||
else:
|
||||
# For S3, test basic operations
|
||||
test_path = "health_check_test"
|
||||
await storage.store(test_path, b"test")
|
||||
await storage.delete(test_path)
|
||||
await storage.store(test_path, BytesIO(b"test"))
|
||||
await storage.remove(test_path)
|
||||
|
||||
response_time = int((time.time() - start_time) * 1000)
|
||||
return ComponentHealth(
|
||||
|
|
@ -190,8 +188,8 @@ class HealthChecker:
|
|||
"""Check LLM provider health (non-critical)."""
|
||||
start_time = time.time()
|
||||
try:
|
||||
from cognee.infrastructure.llm.LLMGateway import LLMGateway
|
||||
from cognee.infrastructure.llm.config import get_llm_config
|
||||
from cognee.infrastructure.llm import LLMGateway
|
||||
|
||||
config = get_llm_config()
|
||||
|
||||
|
|
@ -225,7 +223,7 @@ class HealthChecker:
|
|||
|
||||
# Test actual embedding generation with minimal text
|
||||
engine = get_embedding_engine()
|
||||
await engine.embed_text("test")
|
||||
await engine.embed_text(["test"])
|
||||
|
||||
response_time = int((time.time() - start_time) * 1000)
|
||||
return ComponentHealth(
|
||||
|
|
|
|||
|
|
@ -150,7 +150,9 @@ async def add(
|
|||
|
||||
user, authorized_dataset = await resolve_authorized_user_dataset(dataset_id, dataset_name, user)
|
||||
|
||||
await reset_dataset_pipeline_run_status(authorized_dataset.id, user)
|
||||
await reset_dataset_pipeline_run_status(
|
||||
authorized_dataset.id, user, pipeline_names=["add_pipeline", "cognify_pipeline"]
|
||||
)
|
||||
|
||||
pipeline_run_info = None
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import os
|
||||
import requests
|
||||
import subprocess
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
|
@ -24,6 +21,7 @@ def get_add_router() -> APIRouter:
|
|||
async def add(
|
||||
data: List[UploadFile] = File(default=None),
|
||||
datasetName: Optional[str] = Form(default=None),
|
||||
# Note: Literal is needed for Swagger use
|
||||
datasetId: Union[UUID, Literal[""], None] = Form(default=None, examples=[""]),
|
||||
node_set: Optional[List[str]] = Form(default=[""], example=[""]),
|
||||
user: User = Depends(get_authenticated_user),
|
||||
|
|
@ -60,9 +58,6 @@ def get_add_router() -> APIRouter:
|
|||
|
||||
## Notes
|
||||
- To add data to datasets not owned by the user, use dataset_id (when ENABLE_BACKEND_ACCESS_CONTROL is set to True)
|
||||
- GitHub repositories are cloned and all files are processed
|
||||
- HTTP URLs are fetched and their content is processed
|
||||
- The ALLOW_HTTP_REQUESTS environment variable controls URL processing
|
||||
- datasetId value can only be the UUID of an already existing dataset
|
||||
"""
|
||||
send_telemetry(
|
||||
|
|
|
|||
1
cognee/api/v1/cloud/routers/__init__.py
Normal file
1
cognee/api/v1/cloud/routers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .get_checks_router import get_checks_router
|
||||
23
cognee/api/v1/cloud/routers/get_checks_router.py
Normal file
23
cognee/api/v1/cloud/routers/get_checks_router.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from cognee.modules.users.models import User
|
||||
from cognee.modules.users.methods import get_authenticated_user
|
||||
from cognee.modules.cloud.operations import check_api_key
|
||||
from cognee.modules.cloud.exceptions import CloudApiKeyMissingError
|
||||
|
||||
|
||||
def get_checks_router():
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/connection")
|
||||
async def get_connection_check_endpoint(
|
||||
request: Request, user: User = Depends(get_authenticated_user)
|
||||
):
|
||||
api_token = request.headers.get("X-Api-Key")
|
||||
|
||||
if api_token is None:
|
||||
return CloudApiKeyMissingError()
|
||||
|
||||
return await check_api_key(api_token)
|
||||
|
||||
return router
|
||||
|
|
@ -5,6 +5,7 @@ from typing import List, Optional
|
|||
from typing_extensions import Annotated
|
||||
from fastapi import status
|
||||
from fastapi import APIRouter
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi import HTTPException, Query, Depends
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ class DataDTO(OutDTO):
|
|||
extension: str
|
||||
mime_type: str
|
||||
raw_data_location: str
|
||||
dataset_id: UUID
|
||||
|
||||
|
||||
class GraphNodeDTO(OutDTO):
|
||||
|
|
@ -328,7 +330,7 @@ def get_datasets_router() -> APIRouter:
|
|||
},
|
||||
)
|
||||
|
||||
from cognee.modules.data.methods import get_dataset_data, get_dataset
|
||||
from cognee.modules.data.methods import get_dataset_data
|
||||
|
||||
# Verify user has permission to read dataset
|
||||
dataset = await get_authorized_existing_datasets([dataset_id], "read", user)
|
||||
|
|
@ -339,12 +341,20 @@ def get_datasets_router() -> APIRouter:
|
|||
content=ErrorResponseDTO(f"Dataset ({str(dataset_id)}) not found."),
|
||||
)
|
||||
|
||||
dataset_data = await get_dataset_data(dataset_id=dataset[0].id)
|
||||
dataset_id = dataset[0].id
|
||||
|
||||
dataset_data = await get_dataset_data(dataset_id=dataset_id)
|
||||
|
||||
if dataset_data is None:
|
||||
return []
|
||||
|
||||
return dataset_data
|
||||
return [
|
||||
dict(
|
||||
**jsonable_encoder(data),
|
||||
dataset_id=dataset_id,
|
||||
)
|
||||
for data in dataset_data
|
||||
]
|
||||
|
||||
@router.get("/status", response_model=dict[str, PipelineRunStatus])
|
||||
async def get_dataset_status(
|
||||
|
|
|
|||
0
cognee/api/v1/memify/__init__.py
Normal file
0
cognee/api/v1/memify/__init__.py
Normal file
1
cognee/api/v1/memify/routers/__init__.py
Normal file
1
cognee/api/v1/memify/routers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .get_memify_router import get_memify_router
|
||||
100
cognee/api/v1/memify/routers/get_memify_router.py
Normal file
100
cognee/api/v1/memify/routers/get_memify_router.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import Depends
|
||||
from pydantic import Field
|
||||
from typing import List, Optional, Union, Literal
|
||||
|
||||
from cognee.api.DTO import InDTO
|
||||
from cognee.modules.users.models import User
|
||||
from cognee.modules.users.methods import get_authenticated_user
|
||||
from cognee.shared.utils import send_telemetry
|
||||
from cognee.modules.pipelines.models import PipelineRunErrored
|
||||
from cognee.shared.logging_utils import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class MemifyPayloadDTO(InDTO):
|
||||
extraction_tasks: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
examples=[[]],
|
||||
)
|
||||
enrichment_tasks: Optional[List[str]] = Field(default=None, examples=[[]])
|
||||
data: Optional[str] = Field(default="")
|
||||
dataset_name: Optional[str] = Field(default=None)
|
||||
# Note: Literal is needed for Swagger use
|
||||
dataset_id: Union[UUID, Literal[""], None] = Field(default=None, examples=[""])
|
||||
node_name: Optional[List[str]] = Field(default=None, examples=[[]])
|
||||
run_in_background: Optional[bool] = Field(default=False)
|
||||
|
||||
|
||||
def get_memify_router() -> APIRouter:
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
async def memify(payload: MemifyPayloadDTO, user: User = Depends(get_authenticated_user)):
|
||||
"""
|
||||
Enrichment pipeline in Cognee, can work with already built graphs. If no data is provided existing knowledge graph will be used as data,
|
||||
custom data can also be provided instead which can be processed with provided extraction and enrichment tasks.
|
||||
|
||||
Provided tasks and data will be arranged to run the Cognee pipeline and execute graph enrichment/creation.
|
||||
|
||||
## Request Parameters
|
||||
- **extractionTasks** Optional[List[str]]: List of available Cognee Tasks to execute for graph/data extraction.
|
||||
- **enrichmentTasks** Optional[List[str]]: List of available Cognee Tasks to handle enrichment of provided graph/data from extraction tasks.
|
||||
- **data** Optional[List[str]]: The data to ingest. Can be any text data when custom extraction and enrichment tasks are used.
|
||||
Data provided here will be forwarded to the first extraction task in the pipeline as input.
|
||||
If no data is provided the whole graph (or subgraph if node_name/node_type is specified) will be forwarded
|
||||
- **dataset_name** (Optional[str]): Name of the datasets to memify
|
||||
- **dataset_id** (Optional[UUID]): List of UUIDs of an already existing dataset
|
||||
- **node_name** (Optional[List[str]]): Filter graph to specific named entities (for targeted search). Used when no data is provided.
|
||||
- **run_in_background** (Optional[bool]): Whether to execute processing asynchronously. Defaults to False (blocking).
|
||||
|
||||
Either datasetName or datasetId must be provided.
|
||||
|
||||
## Response
|
||||
Returns information about the add operation containing:
|
||||
- Status of the operation
|
||||
- Details about the processed data
|
||||
- Any relevant metadata from the ingestion process
|
||||
|
||||
## Error Codes
|
||||
- **400 Bad Request**: Neither datasetId nor datasetName provided
|
||||
- **409 Conflict**: Error during memify operation
|
||||
- **403 Forbidden**: User doesn't have permission to use dataset
|
||||
|
||||
## Notes
|
||||
- To memify datasets not owned by the user, use dataset_id (when ENABLE_BACKEND_ACCESS_CONTROL is set to True)
|
||||
- datasetId value can only be the UUID of an already existing dataset
|
||||
"""
|
||||
|
||||
send_telemetry(
|
||||
"Memify API Endpoint Invoked",
|
||||
user.id,
|
||||
additional_properties={"endpoint": "POST /v1/memify"},
|
||||
)
|
||||
|
||||
if not payload.dataset_id and not payload.dataset_name:
|
||||
raise ValueError("Either datasetId or datasetName must be provided.")
|
||||
|
||||
try:
|
||||
from cognee.modules.memify import memify as cognee_memify
|
||||
|
||||
memify_run = await cognee_memify(
|
||||
extraction_tasks=payload.extraction_tasks,
|
||||
enrichment_tasks=payload.enrichment_tasks,
|
||||
data=payload.data,
|
||||
dataset=payload.dataset_id if payload.dataset_id else payload.dataset_name,
|
||||
node_name=payload.node_name,
|
||||
user=user,
|
||||
)
|
||||
|
||||
if isinstance(memify_run, PipelineRunErrored):
|
||||
return JSONResponse(status_code=420, content=memify_run)
|
||||
return memify_run
|
||||
except Exception as error:
|
||||
return JSONResponse(status_code=409, content={"error": str(error)})
|
||||
|
||||
return router
|
||||
1
cognee/api/v1/notebooks/routers/__init__.py
Normal file
1
cognee/api/v1/notebooks/routers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .get_notebooks_router import get_notebooks_router
|
||||
93
cognee/api/v1/notebooks/routers/get_notebooks_router.py
Normal file
93
cognee/api/v1/notebooks/routers/get_notebooks_router.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
from uuid import UUID
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import Field
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from cognee.api.DTO import InDTO
|
||||
from cognee.infrastructure.databases.relational import get_async_session
|
||||
from cognee.modules.notebooks.models import Notebook, NotebookCell
|
||||
from cognee.modules.notebooks.operations import run_in_local_sandbox
|
||||
from cognee.modules.users.models import User
|
||||
from cognee.modules.users.methods import get_authenticated_user
|
||||
from cognee.modules.notebooks.methods import (
|
||||
create_notebook,
|
||||
delete_notebook,
|
||||
get_notebook,
|
||||
get_notebooks,
|
||||
update_notebook,
|
||||
)
|
||||
|
||||
|
||||
class NotebookData(InDTO):
|
||||
name: Optional[str] = Field(...)
|
||||
cells: Optional[List[NotebookCell]] = Field(default=[])
|
||||
|
||||
|
||||
def get_notebooks_router():
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("")
|
||||
async def get_notebooks_endpoint(user: User = Depends(get_authenticated_user)):
|
||||
return await get_notebooks(user.id)
|
||||
|
||||
@router.post("")
|
||||
async def create_notebook_endpoint(
|
||||
notebook_data: NotebookData, user: User = Depends(get_authenticated_user)
|
||||
):
|
||||
return await create_notebook(user.id, notebook_data.name, notebook_data.cells)
|
||||
|
||||
@router.put("/{notebook_id}")
|
||||
async def update_notebook_endpoint(
|
||||
notebook_id: UUID, notebook_data: NotebookData, user: User = Depends(get_authenticated_user)
|
||||
):
|
||||
async with get_async_session(auto_commit=True) as session:
|
||||
notebook: Notebook = await get_notebook(notebook_id, user.id, session)
|
||||
|
||||
if notebook is None:
|
||||
return JSONResponse(status_code=404, content={"error": "Notebook not found"})
|
||||
|
||||
if notebook_data.name and notebook_data.name != notebook.name:
|
||||
notebook.name = notebook_data.name
|
||||
|
||||
if notebook_data.cells:
|
||||
notebook.cells = notebook_data.cells
|
||||
|
||||
return await update_notebook(notebook, session)
|
||||
|
||||
class RunCodeData(InDTO):
|
||||
content: str = Field(...)
|
||||
|
||||
@router.post("/{notebook_id}/{cell_id}/run")
|
||||
async def run_notebook_cell_endpoint(
|
||||
notebook_id: UUID,
|
||||
cell_id: UUID,
|
||||
run_code: RunCodeData,
|
||||
user: User = Depends(get_authenticated_user),
|
||||
):
|
||||
async with get_async_session() as session:
|
||||
notebook: Notebook = await get_notebook(notebook_id, user.id, session)
|
||||
|
||||
if notebook is None:
|
||||
return JSONResponse(status_code=404, content={"error": "Notebook not found"})
|
||||
|
||||
result, error = run_in_local_sandbox(run_code.content)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200, content={"result": jsonable_encoder(result), "error": error}
|
||||
)
|
||||
|
||||
@router.delete("/{notebook_id}")
|
||||
async def delete_notebook_endpoint(
|
||||
notebook_id: UUID, user: User = Depends(get_authenticated_user)
|
||||
):
|
||||
async with get_async_session(auto_commit=True) as session:
|
||||
notebook: Notebook = await get_notebook(notebook_id, user.id, session)
|
||||
|
||||
if notebook is None:
|
||||
return JSONResponse(status_code=404, content={"error": "Notebook not found"})
|
||||
|
||||
return await delete_notebook(notebook, session)
|
||||
|
||||
return router
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
from uuid import UUID
|
||||
import pathlib
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pydantic import Field
|
||||
|
|
@ -134,7 +133,7 @@ def get_search_router() -> APIRouter:
|
|||
only_context=payload.only_context,
|
||||
)
|
||||
|
||||
return results
|
||||
return JSONResponse(content=results)
|
||||
except PermissionDeniedError:
|
||||
return []
|
||||
except Exception as error:
|
||||
|
|
|
|||
17
cognee/api/v1/sync/__init__.py
Normal file
17
cognee/api/v1/sync/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from .sync import (
|
||||
sync,
|
||||
SyncResponse,
|
||||
LocalFileInfo,
|
||||
CheckMissingHashesRequest,
|
||||
CheckMissingHashesResponse,
|
||||
PruneDatasetRequest,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"sync",
|
||||
"SyncResponse",
|
||||
"LocalFileInfo",
|
||||
"CheckMissingHashesRequest",
|
||||
"CheckMissingHashesResponse",
|
||||
"PruneDatasetRequest",
|
||||
]
|
||||
3
cognee/api/v1/sync/routers/__init__.py
Normal file
3
cognee/api/v1/sync/routers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .get_sync_router import get_sync_router
|
||||
|
||||
__all__ = ["get_sync_router"]
|
||||
134
cognee/api/v1/sync/routers/get_sync_router.py
Normal file
134
cognee/api/v1/sync/routers/get_sync_router.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
from uuid import UUID
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
|
||||
from cognee.api.DTO import InDTO
|
||||
from cognee.modules.users.models import User
|
||||
from cognee.modules.users.methods import get_authenticated_user
|
||||
from cognee.modules.users.permissions.methods import get_specific_user_permission_datasets
|
||||
from cognee.shared.utils import send_telemetry
|
||||
from cognee.shared.logging_utils import get_logger
|
||||
from cognee.api.v1.sync import SyncResponse
|
||||
from cognee.context_global_variables import set_database_global_context_variables
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class SyncRequest(InDTO):
|
||||
"""Request model for sync operations."""
|
||||
|
||||
dataset_id: Optional[UUID] = None
|
||||
|
||||
|
||||
def get_sync_router() -> APIRouter:
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("", response_model=dict[str, SyncResponse])
|
||||
async def sync_to_cloud(
|
||||
request: SyncRequest,
|
||||
user: User = Depends(get_authenticated_user),
|
||||
):
|
||||
"""
|
||||
Sync local data to Cognee Cloud.
|
||||
|
||||
This endpoint triggers synchronization of local Cognee data to your cloud instance.
|
||||
It uploads your local datasets, knowledge graphs, and processed data to the cloud
|
||||
for backup, sharing, or cloud-based processing.
|
||||
|
||||
## Request Body (JSON)
|
||||
```json
|
||||
{
|
||||
"dataset_id": "123e4567-e89b-12d3-a456-426614174000"
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
Returns immediate response for the sync operation:
|
||||
- **run_id**: Unique identifier for tracking the background sync operation
|
||||
- **status**: Always "started" (operation runs in background)
|
||||
- **dataset_id**: ID of the dataset being synced
|
||||
- **dataset_name**: Name of the dataset being synced
|
||||
- **message**: Description of the background operation
|
||||
- **timestamp**: When the sync was initiated
|
||||
- **user_id**: User who initiated the sync
|
||||
|
||||
## Cloud Sync Features
|
||||
- **Automatic Authentication**: Uses your Cognee Cloud credentials
|
||||
- **Data Compression**: Optimizes transfer size for faster uploads
|
||||
- **Smart Sync**: Automatically handles data updates efficiently
|
||||
- **Progress Tracking**: Monitor sync status with sync_id
|
||||
- **Error Recovery**: Automatic retry for failed transfers
|
||||
- **Data Validation**: Ensures data integrity during transfer
|
||||
|
||||
## Example Usage
|
||||
```bash
|
||||
# Sync dataset to cloud by ID (JSON request)
|
||||
curl -X POST "http://localhost:8000/api/v1/sync" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Cookie: auth_token=your-token" \\
|
||||
-d '{"dataset_id": "123e4567-e89b-12d3-a456-426614174000"}'
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
- **400 Bad Request**: Invalid dataset_id format
|
||||
- **401 Unauthorized**: Invalid or missing authentication
|
||||
- **403 Forbidden**: User doesn't have permission to access dataset
|
||||
- **404 Not Found**: Dataset not found
|
||||
- **409 Conflict**: Sync operation conflict or cloud service unavailable
|
||||
- **413 Payload Too Large**: Dataset too large for current cloud plan
|
||||
- **429 Too Many Requests**: Rate limit exceeded
|
||||
|
||||
## Notes
|
||||
- Sync operations run in the background - you get an immediate response
|
||||
- Use the returned run_id to track progress (status API coming soon)
|
||||
- Large datasets are automatically chunked for efficient transfer
|
||||
- Cloud storage usage counts against your plan limits
|
||||
- The sync will continue even if you close your connection
|
||||
"""
|
||||
send_telemetry(
|
||||
"Cloud Sync API Endpoint Invoked",
|
||||
user.id,
|
||||
additional_properties={
|
||||
"endpoint": "POST /v1/sync",
|
||||
"dataset_id": str(request.dataset_id) if request.dataset_id else "*",
|
||||
},
|
||||
)
|
||||
|
||||
from cognee.api.v1.sync import sync as cognee_sync
|
||||
|
||||
try:
|
||||
# Retrieve existing dataset and check permissions
|
||||
datasets = await get_specific_user_permission_datasets(
|
||||
user.id, "write", [request.dataset_id] if request.dataset_id else None
|
||||
)
|
||||
|
||||
sync_results = {}
|
||||
|
||||
for dataset in datasets:
|
||||
await set_database_global_context_variables(dataset.id, dataset.owner_id)
|
||||
|
||||
# Execute cloud sync operation
|
||||
sync_result = await cognee_sync(
|
||||
dataset=dataset,
|
||||
user=user,
|
||||
)
|
||||
|
||||
sync_results[str(dataset.id)] = sync_result
|
||||
|
||||
return sync_results
|
||||
|
||||
except ValueError as e:
|
||||
return JSONResponse(status_code=400, content={"error": str(e)})
|
||||
except PermissionError as e:
|
||||
return JSONResponse(status_code=403, content={"error": str(e)})
|
||||
except ConnectionError as e:
|
||||
return JSONResponse(
|
||||
status_code=409, content={"error": f"Cloud service unavailable: {str(e)}"}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Cloud sync operation failed: {str(e)}")
|
||||
return JSONResponse(status_code=409, content={"error": "Cloud sync operation failed."})
|
||||
|
||||
return router
|
||||
548
cognee/api/v1/sync/sync.py
Normal file
548
cognee/api/v1/sync/sync.py
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
import os
|
||||
import uuid
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from cognee.infrastructure.files.storage import get_file_storage
|
||||
from cognee.shared.logging_utils import get_logger
|
||||
from cognee.modules.users.models import User
|
||||
from cognee.modules.data.models import Dataset
|
||||
from cognee.modules.data.methods import get_dataset_data
|
||||
from cognee.modules.sync.methods import (
|
||||
create_sync_operation,
|
||||
update_sync_operation,
|
||||
mark_sync_started,
|
||||
mark_sync_completed,
|
||||
mark_sync_failed,
|
||||
)
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class LocalFileInfo(BaseModel):
|
||||
"""Model for local file information with hash."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
mime_type: Optional[str]
|
||||
extension: Optional[str]
|
||||
raw_data_location: str
|
||||
content_hash: str # MD5 hash
|
||||
file_size: int
|
||||
node_set: Optional[str] = None
|
||||
|
||||
|
||||
class CheckMissingHashesRequest(BaseModel):
|
||||
"""Request model for checking missing hashes in a dataset"""
|
||||
|
||||
hashes: List[str]
|
||||
|
||||
|
||||
class CheckMissingHashesResponse(BaseModel):
|
||||
"""Response model for missing hashes check"""
|
||||
|
||||
missing: List[str]
|
||||
|
||||
|
||||
class PruneDatasetRequest(BaseModel):
|
||||
"""Request model for pruning dataset to specific hashes"""
|
||||
|
||||
items: List[str]
|
||||
|
||||
|
||||
class SyncResponse(BaseModel):
|
||||
"""Response model for sync operations."""
|
||||
|
||||
run_id: str
|
||||
status: str # "started" for immediate response
|
||||
dataset_id: str
|
||||
dataset_name: str
|
||||
message: str
|
||||
timestamp: str
|
||||
user_id: str
|
||||
|
||||
|
||||
async def sync(
|
||||
dataset: Dataset,
|
||||
user: User,
|
||||
) -> SyncResponse:
|
||||
"""
|
||||
Sync local Cognee data to Cognee Cloud.
|
||||
|
||||
This function handles synchronization of local datasets, knowledge graphs, and
|
||||
processed data to the Cognee Cloud infrastructure. It uploads local data for
|
||||
cloud-based processing, backup, and sharing.
|
||||
|
||||
Args:
|
||||
dataset: Dataset object to sync (permissions already verified)
|
||||
user: User object for authentication and permissions
|
||||
|
||||
Returns:
|
||||
SyncResponse model with immediate response:
|
||||
- run_id: Unique identifier for tracking this sync operation
|
||||
- status: Always "started" (sync runs in background)
|
||||
- dataset_id: ID of the dataset being synced
|
||||
- dataset_name: Name of the dataset being synced
|
||||
- message: Description of what's happening
|
||||
- timestamp: When the sync was initiated
|
||||
- user_id: User who initiated the sync
|
||||
|
||||
Raises:
|
||||
ConnectionError: If Cognee Cloud service is unreachable
|
||||
Exception: For other sync-related errors
|
||||
"""
|
||||
if not dataset:
|
||||
raise ValueError("Dataset must be provided for sync operation")
|
||||
|
||||
# Generate a unique run ID
|
||||
run_id = str(uuid.uuid4())
|
||||
|
||||
# Get current timestamp
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
logger.info(f"Starting cloud sync operation {run_id}: dataset {dataset.name} ({dataset.id})")
|
||||
|
||||
# Create sync operation record in database (total_records will be set during background sync)
|
||||
try:
|
||||
await create_sync_operation(
|
||||
run_id=run_id, dataset_id=dataset.id, dataset_name=dataset.name, user_id=user.id
|
||||
)
|
||||
logger.info(f"Created sync operation record for {run_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create sync operation record: {str(e)}")
|
||||
# Continue without database tracking if record creation fails
|
||||
|
||||
# Start the sync operation in the background
|
||||
asyncio.create_task(_perform_background_sync(run_id, dataset, user))
|
||||
|
||||
# Return immediately with run_id
|
||||
return SyncResponse(
|
||||
run_id=run_id,
|
||||
status="started",
|
||||
dataset_id=str(dataset.id),
|
||||
dataset_name=dataset.name,
|
||||
message=f"Sync operation started in background. Use run_id '{run_id}' to track progress.",
|
||||
timestamp=timestamp,
|
||||
user_id=str(user.id),
|
||||
)
|
||||
|
||||
|
||||
async def _perform_background_sync(run_id: str, dataset: Dataset, user: User) -> None:
|
||||
"""Perform the actual sync operation in the background."""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Background sync {run_id}: Starting sync for dataset {dataset.name} ({dataset.id})"
|
||||
)
|
||||
|
||||
# Mark sync as in progress
|
||||
await mark_sync_started(run_id)
|
||||
|
||||
# Perform the actual sync operation
|
||||
records_processed, bytes_transferred = await _sync_to_cognee_cloud(dataset, user, run_id)
|
||||
|
||||
end_time = datetime.now(timezone.utc)
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
|
||||
logger.info(
|
||||
f"Background sync {run_id}: Completed successfully. Records: {records_processed}, Bytes: {bytes_transferred}, Duration: {duration}s"
|
||||
)
|
||||
|
||||
# Mark sync as completed with final stats
|
||||
await mark_sync_completed(run_id, records_processed, bytes_transferred)
|
||||
|
||||
except Exception as e:
|
||||
end_time = datetime.now(timezone.utc)
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
|
||||
logger.error(f"Background sync {run_id}: Failed after {duration}s with error: {str(e)}")
|
||||
|
||||
# Mark sync as failed with error message
|
||||
await mark_sync_failed(run_id, str(e))
|
||||
|
||||
|
||||
async def _sync_to_cognee_cloud(dataset: Dataset, user: User, run_id: str) -> tuple[int, int]:
|
||||
"""
|
||||
Sync local data to Cognee Cloud using three-step idempotent process:
|
||||
1. Extract local files with stored MD5 hashes and check what's missing on cloud
|
||||
2. Upload missing files individually
|
||||
3. Prune cloud dataset to match local state
|
||||
"""
|
||||
logger.info(f"Starting sync to Cognee Cloud: dataset {dataset.name} ({dataset.id})")
|
||||
|
||||
try:
|
||||
# Get cloud configuration
|
||||
cloud_base_url = await _get_cloud_base_url()
|
||||
cloud_auth_token = await _get_cloud_auth_token(user)
|
||||
|
||||
logger.info(f"Cloud API URL: {cloud_base_url}")
|
||||
|
||||
# Step 1: Extract local file info with stored hashes
|
||||
local_files = await _extract_local_files_with_hashes(dataset, user, run_id)
|
||||
logger.info(f"Found {len(local_files)} local files to sync")
|
||||
|
||||
# Update sync operation with total file count
|
||||
try:
|
||||
await update_sync_operation(run_id, processed_records=0)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize sync progress: {str(e)}")
|
||||
|
||||
if not local_files:
|
||||
logger.info("No files to sync - dataset is empty")
|
||||
return 0, 0
|
||||
|
||||
# Step 2: Check what files are missing on cloud
|
||||
local_hashes = [f.content_hash for f in local_files]
|
||||
missing_hashes = await _check_missing_hashes(
|
||||
cloud_base_url, cloud_auth_token, dataset.id, local_hashes, run_id
|
||||
)
|
||||
logger.info(f"Cloud is missing {len(missing_hashes)} out of {len(local_hashes)} files")
|
||||
|
||||
# Update progress
|
||||
try:
|
||||
await update_sync_operation(run_id, progress_percentage=25)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update progress: {str(e)}")
|
||||
|
||||
# Step 3: Upload missing files
|
||||
bytes_uploaded = await _upload_missing_files(
|
||||
cloud_base_url, cloud_auth_token, dataset, local_files, missing_hashes, run_id
|
||||
)
|
||||
logger.info(f"Upload complete: {len(missing_hashes)} files, {bytes_uploaded} bytes")
|
||||
|
||||
# Update progress
|
||||
try:
|
||||
await update_sync_operation(run_id, progress_percentage=75)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update progress: {str(e)}")
|
||||
|
||||
# Step 4: Trigger cognify processing on cloud dataset (only if new files were uploaded)
|
||||
if missing_hashes:
|
||||
await _trigger_remote_cognify(cloud_base_url, cloud_auth_token, dataset.id, run_id)
|
||||
logger.info(f"Cognify processing triggered for dataset {dataset.id}")
|
||||
else:
|
||||
logger.info(
|
||||
f"Skipping cognify processing - no new files were uploaded for dataset {dataset.id}"
|
||||
)
|
||||
|
||||
# Final progress
|
||||
try:
|
||||
await update_sync_operation(run_id, progress_percentage=100)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update final progress: {str(e)}")
|
||||
|
||||
records_processed = len(local_files)
|
||||
|
||||
logger.info(
|
||||
f"Sync completed successfully: {records_processed} records, {bytes_uploaded} bytes uploaded"
|
||||
)
|
||||
|
||||
return records_processed, bytes_uploaded
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Sync failed: {str(e)}")
|
||||
raise ConnectionError(f"Cloud sync failed: {str(e)}")
|
||||
|
||||
|
||||
async def _extract_local_files_with_hashes(
|
||||
dataset: Dataset, user: User, run_id: str
|
||||
) -> List[LocalFileInfo]:
|
||||
"""
|
||||
Extract local dataset data with existing MD5 hashes from database.
|
||||
|
||||
Args:
|
||||
dataset: Dataset to extract files from
|
||||
user: User performing the sync
|
||||
run_id: Unique identifier for this sync operation
|
||||
|
||||
Returns:
|
||||
List[LocalFileInfo]: Information about each local file with stored hash
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Extracting files from dataset: {dataset.name} ({dataset.id})")
|
||||
|
||||
# Get all data entries linked to this dataset
|
||||
data_entries = await get_dataset_data(dataset.id)
|
||||
logger.info(f"Found {len(data_entries)} data entries in dataset")
|
||||
|
||||
# Process each data entry to get file info and hash
|
||||
local_files: List[LocalFileInfo] = []
|
||||
skipped_count = 0
|
||||
|
||||
for data_entry in data_entries:
|
||||
try:
|
||||
# Use existing content_hash from database
|
||||
content_hash = data_entry.raw_content_hash
|
||||
file_size = data_entry.data_size if data_entry.data_size else 0
|
||||
|
||||
# Skip entries without content hash (shouldn't happen in normal cases)
|
||||
if not content_hash:
|
||||
skipped_count += 1
|
||||
logger.warning(
|
||||
f"Skipping file {data_entry.name}: missing content_hash in database"
|
||||
)
|
||||
continue
|
||||
|
||||
if file_size == 0:
|
||||
# Get file size from filesystem if not stored
|
||||
file_size = await _get_file_size(data_entry.raw_data_location)
|
||||
|
||||
local_files.append(
|
||||
LocalFileInfo(
|
||||
id=str(data_entry.id),
|
||||
name=data_entry.name,
|
||||
mime_type=data_entry.mime_type,
|
||||
extension=data_entry.extension,
|
||||
raw_data_location=data_entry.raw_data_location,
|
||||
content_hash=content_hash,
|
||||
file_size=file_size,
|
||||
node_set=data_entry.node_set,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
skipped_count += 1
|
||||
logger.warning(f"Failed to process file {data_entry.name}: {str(e)}")
|
||||
# Continue with other entries even if one fails
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
f"File extraction complete: {len(local_files)} files processed, {skipped_count} skipped"
|
||||
)
|
||||
return local_files
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to extract files from dataset {dataset.name}: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
async def _get_file_size(file_path: str) -> int:
|
||||
"""Get file size in bytes."""
|
||||
try:
|
||||
file_dir = os.path.dirname(file_path)
|
||||
file_name = os.path.basename(file_path)
|
||||
file_storage = get_file_storage(file_dir)
|
||||
|
||||
return await file_storage.get_size(file_name)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
async def _get_cloud_base_url() -> str:
|
||||
"""Get Cognee Cloud API base URL."""
|
||||
# TODO: Make this configurable via environment variable or config
|
||||
return os.getenv("COGNEE_CLOUD_API_URL", "http://localhost:8001")
|
||||
|
||||
|
||||
async def _get_cloud_auth_token(user: User) -> str:
|
||||
"""Get authentication token for Cognee Cloud API."""
|
||||
# TODO: Implement proper authentication with Cognee Cloud
|
||||
# This should get or refresh an API token for the user
|
||||
return os.getenv("COGNEE_CLOUD_AUTH_TOKEN", "your-auth-token-here")
|
||||
|
||||
|
||||
async def _check_missing_hashes(
|
||||
cloud_base_url: str, auth_token: str, dataset_id: str, local_hashes: List[str], run_id: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
Step 1: Check which hashes are missing on cloud.
|
||||
|
||||
Returns:
|
||||
List[str]: MD5 hashes that need to be uploaded
|
||||
"""
|
||||
url = f"{cloud_base_url}/api/sync/{dataset_id}/diff"
|
||||
headers = {"X-Api-Key": auth_token, "Content-Type": "application/json"}
|
||||
|
||||
payload = CheckMissingHashesRequest(hashes=local_hashes)
|
||||
|
||||
logger.info(f"Checking missing hashes on cloud for dataset {dataset_id}")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, json=payload.dict(), headers=headers) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
missing_response = CheckMissingHashesResponse(**data)
|
||||
logger.info(
|
||||
f"Cloud reports {len(missing_response.missing)} missing files out of {len(local_hashes)} total"
|
||||
)
|
||||
return missing_response.missing
|
||||
else:
|
||||
error_text = await response.text()
|
||||
logger.error(
|
||||
f"Failed to check missing hashes: Status {response.status} - {error_text}"
|
||||
)
|
||||
raise ConnectionError(
|
||||
f"Failed to check missing hashes: {response.status} - {error_text}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking missing hashes: {str(e)}")
|
||||
raise ConnectionError(f"Failed to check missing hashes: {str(e)}")
|
||||
|
||||
|
||||
async def _upload_missing_files(
|
||||
cloud_base_url: str,
|
||||
auth_token: str,
|
||||
dataset: Dataset,
|
||||
local_files: List[LocalFileInfo],
|
||||
missing_hashes: List[str],
|
||||
run_id: str,
|
||||
) -> int:
|
||||
"""
|
||||
Step 2: Upload files that are missing on cloud.
|
||||
|
||||
Returns:
|
||||
int: Total bytes uploaded
|
||||
"""
|
||||
# Filter local files to only those with missing hashes
|
||||
files_to_upload = [f for f in local_files if f.content_hash in missing_hashes]
|
||||
|
||||
logger.info(f"Uploading {len(files_to_upload)} missing files to cloud")
|
||||
|
||||
if not files_to_upload:
|
||||
logger.info("No files need to be uploaded - all files already exist on cloud")
|
||||
return 0
|
||||
|
||||
total_bytes_uploaded = 0
|
||||
uploaded_count = 0
|
||||
|
||||
headers = {"X-Api-Key": auth_token}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for file_info in files_to_upload:
|
||||
try:
|
||||
file_dir = os.path.dirname(file_info.raw_data_location)
|
||||
file_name = os.path.basename(file_info.raw_data_location)
|
||||
file_storage = get_file_storage(file_dir)
|
||||
|
||||
async with file_storage.open(file_name, mode="rb") as file:
|
||||
file_content = file.read()
|
||||
|
||||
# Upload file
|
||||
url = f"{cloud_base_url}/api/sync/{dataset.id}/data/{file_info.id}"
|
||||
|
||||
request_data = aiohttp.FormData()
|
||||
|
||||
request_data.add_field(
|
||||
"file", file_content, content_type=file_info.mime_type, filename=file_info.name
|
||||
)
|
||||
request_data.add_field("dataset_id", str(dataset.id))
|
||||
request_data.add_field("dataset_name", dataset.name)
|
||||
request_data.add_field("data_id", str(file_info.id))
|
||||
request_data.add_field("mime_type", file_info.mime_type)
|
||||
request_data.add_field("extension", file_info.extension)
|
||||
request_data.add_field("md5", file_info.content_hash)
|
||||
|
||||
async with session.put(url, data=request_data, headers=headers) as response:
|
||||
if response.status in [200, 201]:
|
||||
total_bytes_uploaded += len(file_content)
|
||||
uploaded_count += 1
|
||||
|
||||
# Update progress periodically
|
||||
if uploaded_count % 10 == 0:
|
||||
progress = (
|
||||
25 + (uploaded_count / len(files_to_upload)) * 50
|
||||
) # 25-75% range
|
||||
await update_sync_operation(run_id, progress_percentage=int(progress))
|
||||
else:
|
||||
error_text = await response.text()
|
||||
logger.error(
|
||||
f"Failed to upload {file_info.name}: Status {response.status} - {error_text}"
|
||||
)
|
||||
raise ConnectionError(
|
||||
f"Upload failed for {file_info.name}: HTTP {response.status} - {error_text}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error uploading file {file_info.name}: {str(e)}")
|
||||
raise ConnectionError(f"Upload failed for {file_info.name}: {str(e)}")
|
||||
|
||||
logger.info(f"All {uploaded_count} files uploaded successfully: {total_bytes_uploaded} bytes")
|
||||
return total_bytes_uploaded
|
||||
|
||||
|
||||
async def _prune_cloud_dataset(
|
||||
cloud_base_url: str, auth_token: str, dataset_id: str, local_hashes: List[str], run_id: str
|
||||
) -> None:
|
||||
"""
|
||||
Step 3: Prune cloud dataset to match local state.
|
||||
"""
|
||||
url = f"{cloud_base_url}/api/sync/{dataset_id}?prune=true"
|
||||
headers = {"X-Api-Key": auth_token, "Content-Type": "application/json"}
|
||||
|
||||
payload = PruneDatasetRequest(items=local_hashes)
|
||||
|
||||
logger.info("Pruning cloud dataset to match local state")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.put(url, json=payload.dict(), headers=headers) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
deleted_entries = data.get("deleted_database_entries", 0)
|
||||
deleted_files = data.get("deleted_files_from_storage", 0)
|
||||
|
||||
logger.info(
|
||||
f"Cloud dataset pruned successfully: {deleted_entries} entries deleted, {deleted_files} files removed"
|
||||
)
|
||||
else:
|
||||
error_text = await response.text()
|
||||
logger.error(
|
||||
f"Failed to prune cloud dataset: Status {response.status} - {error_text}"
|
||||
)
|
||||
# Don't raise error for prune failures - sync partially succeeded
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error pruning cloud dataset: {str(e)}")
|
||||
# Don't raise error for prune failures - sync partially succeeded
|
||||
|
||||
|
||||
async def _trigger_remote_cognify(
|
||||
cloud_base_url: str, auth_token: str, dataset_id: str, run_id: str
|
||||
) -> None:
|
||||
"""
|
||||
Step 4: Trigger cognify processing on the cloud dataset.
|
||||
|
||||
This initiates knowledge graph processing on the synchronized dataset
|
||||
using the cloud infrastructure.
|
||||
"""
|
||||
url = f"{cloud_base_url}/api/cognify"
|
||||
headers = {"X-Api-Key": auth_token, "Content-Type": "application/json"}
|
||||
|
||||
payload = {
|
||||
"dataset_ids": [str(dataset_id)], # Convert UUID to string for JSON serialization
|
||||
"run_in_background": False,
|
||||
"custom_prompt": "",
|
||||
}
|
||||
|
||||
logger.info(f"Triggering cognify processing for dataset {dataset_id}")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, json=payload, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
logger.info(f"Cognify processing started successfully: {data}")
|
||||
|
||||
# Extract pipeline run IDs for monitoring if available
|
||||
if isinstance(data, dict):
|
||||
for dataset_key, run_info in data.items():
|
||||
if isinstance(run_info, dict) and "pipeline_run_id" in run_info:
|
||||
logger.info(
|
||||
f"Cognify pipeline run ID for dataset {dataset_key}: {run_info['pipeline_run_id']}"
|
||||
)
|
||||
else:
|
||||
error_text = await response.text()
|
||||
logger.warning(
|
||||
f"Failed to trigger cognify processing: Status {response.status} - {error_text}"
|
||||
)
|
||||
# TODO: consider adding retries
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error triggering cognify processing: {str(e)}")
|
||||
# TODO: consider adding retries
|
||||
|
|
@ -1,7 +1,19 @@
|
|||
from fastapi import Depends
|
||||
|
||||
from cognee.modules.users.get_fastapi_users import get_fastapi_users
|
||||
from cognee.modules.users.models import User
|
||||
from cognee.modules.users.methods import get_authenticated_user
|
||||
from cognee.modules.users.authentication.get_client_auth_backend import get_client_auth_backend
|
||||
|
||||
|
||||
def get_auth_router():
|
||||
auth_backend = get_client_auth_backend()
|
||||
return get_fastapi_users().get_auth_router(auth_backend)
|
||||
auth_router = get_fastapi_users().get_auth_router(auth_backend)
|
||||
|
||||
@auth_router.get("/me")
|
||||
async def get_me(user: User = Depends(get_authenticated_user)):
|
||||
return {
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
return auth_router
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from .ModelBase import Base
|
||||
from .config import get_relational_config
|
||||
from .config import get_migration_config
|
||||
from .get_async_session import get_async_session
|
||||
from .with_async_session import with_async_session
|
||||
from .create_db_and_tables import create_db_and_tables
|
||||
from .get_relational_engine import get_relational_engine
|
||||
from .get_migration_relational_engine import get_migration_relational_engine
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
from typing import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .get_relational_engine import get_relational_engine
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_async_session(auto_commit=False) -> AsyncGenerator[AsyncSession, None]:
|
||||
db_engine = get_relational_engine()
|
||||
async with db_engine.get_async_session() as session:
|
||||
yield session
|
||||
|
||||
if auto_commit:
|
||||
await session.commit()
|
||||
|
|
@ -12,6 +12,7 @@ from sqlalchemy import NullPool, text, select, MetaData, Table, delete, inspect
|
|||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
|
||||
from cognee.modules.data.models.Data import Data
|
||||
from cognee.modules.sync.models.SyncOperation import SyncOperation
|
||||
from cognee.shared.logging_utils import get_logger
|
||||
from cognee.infrastructure.utils.run_sync import run_sync
|
||||
from cognee.infrastructure.databases.exceptions import EntityNotFoundError
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
from typing import Any, Callable, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from .get_async_session import get_async_session
|
||||
|
||||
|
||||
def get_session_from_args(args):
|
||||
last_arg = args[-1]
|
||||
if isinstance(last_arg, AsyncSession):
|
||||
return last_arg
|
||||
return None
|
||||
|
||||
|
||||
def with_async_session(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
async def wrapper(*args, **kwargs):
|
||||
session = kwargs.get("session") or get_session_from_args(args) # type: Optional[AsyncSession]
|
||||
|
||||
if session is None:
|
||||
async with get_async_session() as session:
|
||||
result = await func(*args, **kwargs, session=session)
|
||||
await session.commit()
|
||||
return result
|
||||
else:
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
|
@ -189,6 +189,15 @@ class LocalFileStorage(Storage):
|
|||
|
||||
return os.path.isfile(os.path.join(parsed_storage_path, file_path))
|
||||
|
||||
def get_size(self, file_path: str) -> int:
|
||||
parsed_storage_path = get_parsed_path(self.storage_path)
|
||||
|
||||
return (
|
||||
os.path.getsize(os.path.join(parsed_storage_path, file_path))
|
||||
if self.file_exists(file_path)
|
||||
else 0
|
||||
)
|
||||
|
||||
def ensure_directory_exists(self, directory_path: str = ""):
|
||||
"""
|
||||
Ensure that the specified directory exists, creating it if necessary.
|
||||
|
|
|
|||
|
|
@ -146,6 +146,11 @@ class S3FileStorage(Storage):
|
|||
self.s3.isfile, os.path.join(self.storage_path.replace("s3://", ""), file_path)
|
||||
)
|
||||
|
||||
async def get_size(self, file_path: str) -> int:
|
||||
return await run_async(
|
||||
self.s3.size, os.path.join(self.storage_path.replace("s3://", ""), file_path)
|
||||
)
|
||||
|
||||
async def ensure_directory_exists(self, directory_path: str = ""):
|
||||
"""
|
||||
Ensure that the specified directory exists, creating it if necessary.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ class StorageManager:
|
|||
else:
|
||||
return self.storage.is_file(file_path)
|
||||
|
||||
async def get_size(self, file_path: str) -> int:
|
||||
if inspect.iscoroutinefunction(self.storage.get_size):
|
||||
return await self.storage.get_size(file_path)
|
||||
else:
|
||||
return self.storage.get_size(file_path)
|
||||
|
||||
async def store(self, file_path: str, data: BinaryIO, overwrite: bool = False) -> str:
|
||||
"""
|
||||
Store data at the specified file path.
|
||||
|
|
@ -84,7 +90,7 @@ class StorageManager:
|
|||
"""
|
||||
# Check the actual storage type by class name to determine if open() is async or sync
|
||||
|
||||
if self.storage.__class__.__name__ == "S3FileStorage" and file_path.startswith("s3://"):
|
||||
if self.storage.__class__.__name__ == "S3FileStorage":
|
||||
# S3FileStorage.open() is async
|
||||
async with self.storage.open(file_path, *args, **kwargs) as file:
|
||||
yield file
|
||||
|
|
|
|||
|
|
@ -40,6 +40,22 @@ class Storage(Protocol):
|
|||
"""
|
||||
pass
|
||||
|
||||
def get_size(self, file_path: str) -> int:
|
||||
"""
|
||||
Get the size of a specified file in bytes.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
|
||||
- file_path (str): The path of the file to get the size of.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
|
||||
- int: The size of the file in bytes.
|
||||
"""
|
||||
pass
|
||||
|
||||
def store(self, file_path: str, data: Union[BinaryIO, str], overwrite: bool):
|
||||
"""
|
||||
Store data at the specified file path.
|
||||
|
|
|
|||
15
cognee/modules/cloud/exceptions/CloudApiKeyMissingError.py
Normal file
15
cognee/modules/cloud/exceptions/CloudApiKeyMissingError.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from fastapi import status
|
||||
|
||||
from cognee.exceptions.exceptions import CogneeConfigurationError
|
||||
|
||||
|
||||
class CloudApiKeyMissingError(CogneeConfigurationError):
|
||||
"""Raised when the API key for the cloud service is not provided."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Failed to connect to the cloud service. Please add your API key to local instance.",
|
||||
name: str = "CloudApiKeyMissingError",
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
):
|
||||
super().__init__(message, name, status_code)
|
||||
15
cognee/modules/cloud/exceptions/CloudConnectionError.py
Normal file
15
cognee/modules/cloud/exceptions/CloudConnectionError.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from fastapi import status
|
||||
|
||||
from cognee.exceptions.exceptions import CogneeConfigurationError
|
||||
|
||||
|
||||
class CloudConnectionError(CogneeConfigurationError):
|
||||
"""Raised when the connection to the cloud service fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Failed to connect to the cloud service. Please check your cloud API key in local instance.",
|
||||
name: str = "CloudConnnectionError",
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
):
|
||||
super().__init__(message, name, status_code)
|
||||
2
cognee/modules/cloud/exceptions/__init__.py
Normal file
2
cognee/modules/cloud/exceptions/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .CloudConnectionError import CloudConnectionError
|
||||
from .CloudApiKeyMissingError import CloudApiKeyMissingError
|
||||
1
cognee/modules/cloud/operations/__init__.py
Normal file
1
cognee/modules/cloud/operations/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .check_api_key import check_api_key
|
||||
25
cognee/modules/cloud/operations/check_api_key.py
Normal file
25
cognee/modules/cloud/operations/check_api_key.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import aiohttp
|
||||
|
||||
from cognee.modules.cloud.exceptions import CloudConnectionError
|
||||
|
||||
|
||||
async def check_api_key(auth_token: str):
|
||||
cloud_base_url = "http://localhost:8001"
|
||||
|
||||
url = f"{cloud_base_url}/api/api-keys/check"
|
||||
headers = {"X-Api-Key": auth_token}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
return
|
||||
else:
|
||||
error_text = await response.text()
|
||||
|
||||
raise CloudConnectionError(
|
||||
f"Failed to connect to cloud instance: {response.status} - {error_text}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise CloudConnectionError(f"Failed to connect to cloud instance: {str(e)}")
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
def check_dataset_name(dataset_name: str):
|
||||
if "." in dataset_name or " " in dataset_name:
|
||||
raise ValueError("Dataset name cannot contain spaces or underscores")
|
||||
raise ValueError("Dataset name cannot contain spaces or dots.")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue