Merge branch 'dev' into COG-2826

This commit is contained in:
Vasilije 2025-09-07 16:06:42 -07:00 committed by GitHub
commit 0b6db2c23f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
142 changed files with 4674 additions and 237 deletions

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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) {

View 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&apos;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>
</>
);
}

View file

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

View 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&apos;t have any, don&apos;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>
</>
);
}

View file

@ -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>
</>
);
}

View 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>
);
}

View 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 &quot;add data&quot; 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>
</>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View file

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

View 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>
</>
);
}

View file

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

View file

@ -0,0 +1,2 @@
export { default as useAuthenticatedUser } from "./useAuthenticatedUser";
export { type User } from "./types";

View file

@ -0,0 +1,6 @@
export interface User {
id: string;
name: string;
email: string;
avatarImagePath: string;
}

View 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 };
}

View 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,
},
});
}

View file

@ -0,0 +1,2 @@
export { default as syncData } from "./syncData";
export { default as checkCloudConnection } from "./checkCloudConnection";

View 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: "{}" }),
});
}

View file

@ -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: "",
}))
);
}, []);

View file

@ -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;

View 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;

View file

@ -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;

View file

@ -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"/>

View 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>
);
}

View file

@ -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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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";

View 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>
</>
);
}

View file

@ -1 +1,2 @@
export { default as Divider } from './Divider/Divider';
export { default as Divider } from "./Divider/Divider";
export { default as Header } from "./Header";

View 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>
);
}

View 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>
);
}

View 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-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>
);
}

View 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>
);
}

View file

@ -3,6 +3,6 @@ import { InputHTMLAttributes } from "react"
export default function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
return (
<input className={classNames("block w-full rounded-md bg-white px-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} />
)
}

View file

@ -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>
);

View file

@ -0,0 +1,3 @@
export { default as Modal } from "./Modal";
export { default as useModal } from "./useModal";

View 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,
};
}

View 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>) {
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>
);
}

View 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),
};
}

View file

@ -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>
);
}

View file

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

View 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;
}

View 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>
);
};

View file

@ -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>
);

View file

@ -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 ? (
<>

View file

@ -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";

View file

@ -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`);
};

View file

@ -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";

View file

@ -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,
};
}

View 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;
}

View file

@ -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__":

View file

@ -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

View file

@ -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):
"""

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -0,0 +1 @@
from .get_checks_router import get_checks_router

View 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

View file

@ -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(

View file

View file

@ -0,0 +1 @@
from .get_memify_router import get_memify_router

View 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

View file

@ -0,0 +1 @@
from .get_notebooks_router import get_notebooks_router

View 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

View file

@ -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:

View file

@ -0,0 +1,17 @@
from .sync import (
sync,
SyncResponse,
LocalFileInfo,
CheckMissingHashesRequest,
CheckMissingHashesResponse,
PruneDatasetRequest,
)
__all__ = [
"sync",
"SyncResponse",
"LocalFileInfo",
"CheckMissingHashesRequest",
"CheckMissingHashesResponse",
"PruneDatasetRequest",
]

View file

@ -0,0 +1,3 @@
from .get_sync_router import get_sync_router
__all__ = ["get_sync_router"]

View 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
View 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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

View 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.

View 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)

View 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)

View file

@ -0,0 +1,2 @@
from .CloudConnectionError import CloudConnectionError
from .CloudApiKeyMissingError import CloudApiKeyMissingError

View file

@ -0,0 +1 @@
from .check_api_key import check_api_key

View 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)}")

View file

@ -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