diff --git a/cognee-frontend/public/next.svg b/cognee-frontend/public/next.svg deleted file mode 100644 index 5174b28c5..000000000 --- a/cognee-frontend/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cognee-frontend/public/vercel.svg b/cognee-frontend/public/vercel.svg deleted file mode 100644 index d2f842227..000000000 --- a/cognee-frontend/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cognee-frontend/src/app/(graph)/GraphVisualization.tsx b/cognee-frontend/src/app/(graph)/GraphVisualization.tsx index 67d6458f8..4e2d1e642 100644 --- a/cognee-frontend/src/app/(graph)/GraphVisualization.tsx +++ b/cognee-frontend/src/app/(graph)/GraphVisualization.tsx @@ -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; data?: GraphData; graphControls: MutableRefObject; + 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 ( -
+
{(data && typeof window !== "undefined") ? ( +
+
+
+
+
+
+
+ +
+ +
+ + + back + +
+
+
Account
+
Manage your account's settings.
+
{account.name}
+
+
+
Plan
+
You are using open-source version. Subscribe to get access to hosted cognee with your data!
+ + Select a plan + +
+
+
+
+
+
+
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/account/page.tsx b/cognee-frontend/src/app/account/page.tsx new file mode 100644 index 000000000..f6323c313 --- /dev/null +++ b/cognee-frontend/src/app/account/page.tsx @@ -0,0 +1 @@ +export { default } from "./Account"; diff --git a/cognee-frontend/src/app/dashboard/AddDataToCognee.tsx b/cognee-frontend/src/app/dashboard/AddDataToCognee.tsx new file mode 100644 index 000000000..e5f4bb932 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/AddDataToCognee.tsx @@ -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(null); + + const prepareFiles = useCallback((event: FormEvent) => { + const formElements = event.currentTarget; + const files = formElements.files; + + setFilesForUpload(files); + }, []); + + const processDataWithCognee = useCallback((state: object, event?: FormEvent) => { + 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 ( + <> + + + Add data to cognee + + + +
+
+ Add new data to a dataset? + +
+
Please select a dataset to add data in.
If you don't have any, don't worry, we will create one for you.
+
+
+ + + + + select files + + + {filesForUpload?.length && ( +
+
selected files:
+ {Array.from(filesForUpload || []).map((file) => ( +
+ {file.name} +
+ ))} +
+ )} +
+
+ closeAddDataModal()}>cancel + + {isProcessingDataWithCognee ? "processing..." : "add"} + +
+
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/dashboard/CogneeInstancesAccordion.tsx b/cognee-frontend/src/app/dashboard/CogneeInstancesAccordion.tsx new file mode 100644 index 000000000..037c9e828 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/CogneeInstancesAccordion.tsx @@ -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 ( + <> + Cognee Instances} + isOpen={isInstancesPanelOpen} + openAccordion={openInstancesPanel} + closeAccordion={closeInstancesPanel} + > + {children} + + + ); +} diff --git a/cognee-frontend/src/app/dashboard/Dashboard.tsx b/cognee-frontend/src/app/dashboard/Dashboard.tsx new file mode 100644 index 000000000..c5980fb29 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/Dashboard.tsx @@ -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(null); + + const handleNotebookRemove = useCallback((notebookId: string) => { + setSelectedNotebookId((currentSelectedNotebookId) => ( + currentSelectedNotebookId === notebookId ? null : currentSelectedNotebookId + )); + return removeNotebook(notebookId); + }, [removeNotebook]); + + const saveNotebookTimeoutRef = useRef(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([]); + const refreshDatasetsRef = useRef(() => {}); + + const handleDatasetsChange = useCallback((payload: { datasets: Dataset[], refreshDatasets: () => void }) => { + const { + datasets, + refreshDatasets, + } = payload; + + refreshDatasetsRef.current = refreshDatasets; + setDatasets(datasets); + }, []); + + return ( +
+
+ +
+
+
+ + +
+ + + + + +
+ + + +
+
+ +
+ {selectedNotebook && ( + + )} +
+
+
+ ); +} diff --git a/cognee-frontend/src/app/dashboard/DatasetsAccordion.tsx b/cognee-frontend/src/app/dashboard/DatasetsAccordion.tsx new file mode 100644 index 000000000..55ce23dfa --- /dev/null +++ b/cognee-frontend/src/app/dashboard/DatasetsAccordion.tsx @@ -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 { + 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>(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) => { + 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(null); + + const handleDatasetRemove = (dataset: Dataset) => { + setDatasetToRemove(dataset); + openRemoveDatasetModal(); + }; + + const handleDatasetRemoveCancel = () => { + setDatasetToRemove(null); + closeRemoveDatasetModal(); + }; + + const handleRemoveDatasetConfirm = (event: React.FormEvent) => { + 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) => { + 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(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) => { + event.preventDefault(); + + if (dataToRemove) { + removeDatasetData(dataToRemove.datasetId, dataToRemove.id) + .then(() => { + closeRemoveDataModal(); + setDataToRemove(null); + refreshDatasetsAndData(); + }); + } + } + + return ( + <> + Datasets} + isOpen={isDatasetsPanelOpen} + openAccordion={openDatasetsPanel} + closeAccordion={closeDatasetsPanel} + tools={tools || } + switchCaretPosition={switchCaretPosition} + className={className} + contentClassName={contentClassName} + > +
+ {datasets.length === 0 && ( +
+ No datasets here, add one by clicking + +
+ )} + {datasets.map((dataset) => { + return ( + + {isProcessingFiles ? : } + {dataset.name} +
+ )} + isOpen={openDatasets.has(dataset.id)} + openAccordion={() => toggleDataset(dataset.id)} + closeAccordion={() => toggleDataset(dataset.id)} + tools={( + + + +
+
+ + add data +
+
+
+
handleDatasetRemove(dataset)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete
+
+
+
+ )} + className="first:pt-1.5" + switchCaretPosition={true} + > + <> + {dataset.data?.length === 0 && ( +
+ No data in a dataset, add by clicking "add data" in a dropdown menu +
+ )} + {dataset.data?.map((data) => ( +
+ {data.name} +
+ handleDataRemove(data)}> +
+
+ ))} + +
+ ); + })} +
+ + + +
+
+ Create a new dataset? + +
+
Please provide a name for the dataset being created.
+
+
+ + {newDatasetError && {newDatasetError}} +
+
+ closeNewDatasetModal()}>cancel + create +
+
+
+
+ + +
+
+ Delete {datasetToRemove?.name} dataset? + +
+
Are you sure you want to delete {datasetToRemove?.name}? This action cannot be undone.
+
+ cancel + delete +
+
+
+ + +
+
+ Delete {dataToRemove?.name} data? + +
+
Are you sure you want to delete {dataToRemove?.name}? This action cannot be undone.
+
+ cancel + delete +
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/dashboard/InstanceDatasetsAccordion.tsx b/cognee-frontend/src/app/dashboard/InstanceDatasetsAccordion.tsx new file mode 100644 index 000000000..fd0605349 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/InstanceDatasetsAccordion.tsx @@ -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; + +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) => { + event.preventDefault(); + + const apiKeyValue = event.currentTarget.apiKey.value; + + checkConnectionToCloudCognee(apiKeyValue) + .then(() => { + closeCloudConnectionModal(); + }); + }; + + return ( + <> + +
+ + local cognee +
+
+ )} + tools={isLocalCogneeConnected ? Connected : Not connected} + switchCaretPosition={true} + className="pt-3 pb-1.5" + contentClassName="pl-4" + onDatasetsChange={onDatasetsChange} + /> + + + + +
+
+ Connect to cloud? + +
+
Please provide your API key. You can find it on our platform.
+
+
+ +
+
+ closeCloudConnectionModal()}>cancel + connect +
+
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/dashboard/NotebooksAccordion.tsx b/cognee-frontend/src/app/dashboard/NotebooksAccordion.tsx new file mode 100644 index 000000000..174efaa9e --- /dev/null +++ b/cognee-frontend/src/app/dashboard/NotebooksAccordion.tsx @@ -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; + removeNotebook: (id: string) => Promise; + 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(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) => { + 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(false, handleNotebookAdd); + + return ( + <> + Notebooks} + isOpen={isNotebookPanelOpen} + openAccordion={openNotebookPanel} + closeAccordion={closeNotebookPanel} + tools={isNewDatasetLoading ? ( + + ) : ( + + )} + > + {notebooks.length === 0 && ( +
+ No notebooks here, add one by clicking + +
+ )} + {notebooks.map((notebook: Notebook) => ( +
+ +
+ {notebook.deletable && handleNotebookRemove(notebook)}>} +
+
+ ))} +
+ + +
+
+ Create a new notebook? + +
+
Please provide a name for the notebook being created.
+
+
+ + {/* {newDatasetError && {newDatasetError}} */} +
+
+ closeNewNotebookModal()}>cancel + create +
+
+
+
+ + +
+
+ Delete {notebookToRemove?.name} notebook? + +
+
Are you sure you want to delete {notebookToRemove?.name}? This action cannot be undone.
+
+ cancel + delete +
+
+
+ + ); +} diff --git a/cognee-frontend/src/app/dashboard/page.tsx b/cognee-frontend/src/app/dashboard/page.tsx new file mode 100644 index 000000000..2ab67cdd6 --- /dev/null +++ b/cognee-frontend/src/app/dashboard/page.tsx @@ -0,0 +1 @@ +export { default } from "./Dashboard"; diff --git a/cognee-frontend/src/app/plan/Plan.tsx b/cognee-frontend/src/app/plan/Plan.tsx new file mode 100644 index 000000000..fcca31566 --- /dev/null +++ b/cognee-frontend/src/app/plan/Plan.tsx @@ -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 ( + <> +
+
+
+
+
+
+
+
+
+ +
+ +
+
+ + + back + +
+ +
+
+
+
Basic
+
Free
+
+ +
+
On-prem Subscription
+
$2470 /per month
+
Save 20% yearly
+
+ +
+
Cloud Subscription
+
$25 /per month
+
(beta pricing)
+
+ +
+
Everything in the free plan, plus...
+
+
License to use Cognee open source
+
Cognee tasks and pipelines
+
Custom schema and ontology generation
+
Integrated evaluations
+
More than 28 data sources supported
+
+
+ +
+
Everything in the free plan, plus...
+
+
License to use Cognee open source and Cognee Platform
+
1 day SLA
+
On-prem deployment
+
Hands-on support
+
Architecture review
+
Roadmap prioritization
+
Knowledge transfer
+
+
+ +
+
Everything in the free plan, plus...
+
+
Fully hosted cloud platform
+
Multi-tenant architecture
+
Comprehensive API endpoints
+
Automated scaling and parallel processing
+
Ability to group memories per user and domain
+
Automatic updates and priority support
+
1 GB ingestion + 10,000 API calls
+
+
+ +
+ Try for free +
+ +
+ Talk to us +
+ +
+ Sign up for Cogwit Beta +
+
+ +
+
Feature Comparison
+
Basic
+
On-prem
+
Cloud
+ +
Data Sources
+
28+
+
28+
+
28+
+ +
Deployment
+
Self-hosted
+
On-premise
+
Cloud
+ +
API Calls
+
Limited
+
Unlimited
+
10,000
+ +
Support
+
Community
+
Hands-on
+
Priority
+ +
SLA
+
+
1 day
+
Standard
+
+ +
+
+
Can I change my plan anytime?
+
Yes, you can upgrade or downgrade your plan at any time. Changes take effect immediately.
+
+
+
What happens to my data if I downgrade?
+
Your data is preserved, but features may be limited based on your new plan constraints.
+
+
+
Do you offer educational discounts?
+
Yes, we offer special pricing for educational institutions and students. Contact us for details.
+
+
+
Is there a free trial for paid plans?
+
All new accounts start with a 14-day free trial of our Pro plan features.
+
+
+
+ +
+ Need a custom solution? + Contact us +
+
+ + ); +} diff --git a/cognee-frontend/src/app/plan/page.tsx b/cognee-frontend/src/app/plan/page.tsx new file mode 100644 index 000000000..a1352fe8e --- /dev/null +++ b/cognee-frontend/src/app/plan/page.tsx @@ -0,0 +1 @@ +export { default } from "./Plan"; diff --git a/cognee-frontend/src/modules/auth/index.ts b/cognee-frontend/src/modules/auth/index.ts new file mode 100644 index 000000000..ea21cce2b --- /dev/null +++ b/cognee-frontend/src/modules/auth/index.ts @@ -0,0 +1,2 @@ +export { default as useAuthenticatedUser } from "./useAuthenticatedUser"; +export { type User } from "./types"; diff --git a/cognee-frontend/src/modules/auth/types.ts b/cognee-frontend/src/modules/auth/types.ts new file mode 100644 index 000000000..3441b4149 --- /dev/null +++ b/cognee-frontend/src/modules/auth/types.ts @@ -0,0 +1,6 @@ +export interface User { + id: string; + name: string; + email: string; + avatarImagePath: string; +} diff --git a/cognee-frontend/src/modules/auth/useAuthenticatedUser.ts b/cognee-frontend/src/modules/auth/useAuthenticatedUser.ts new file mode 100644 index 000000000..f789f3de4 --- /dev/null +++ b/cognee-frontend/src/modules/auth/useAuthenticatedUser.ts @@ -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(null); + + useEffect(() => { + if (!user) { + fetch("/v1/auth/me") + .then((response) => response.json()) + .then((data) => setUser(data)); + } + }, [user]); + + return { user }; +} diff --git a/cognee-frontend/src/modules/cloud/checkCloudConnection.ts b/cognee-frontend/src/modules/cloud/checkCloudConnection.ts new file mode 100644 index 000000000..dfc40767d --- /dev/null +++ b/cognee-frontend/src/modules/cloud/checkCloudConnection.ts @@ -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, + }, + }); +} diff --git a/cognee-frontend/src/modules/cloud/index.ts b/cognee-frontend/src/modules/cloud/index.ts new file mode 100644 index 000000000..409f803c3 --- /dev/null +++ b/cognee-frontend/src/modules/cloud/index.ts @@ -0,0 +1,2 @@ +export { default as syncData } from "./syncData"; +export { default as checkCloudConnection } from "./checkCloudConnection"; diff --git a/cognee-frontend/src/modules/cloud/syncData.ts b/cognee-frontend/src/modules/cloud/syncData.ts new file mode 100644 index 000000000..dc4360a27 --- /dev/null +++ b/cognee-frontend/src/modules/cloud/syncData.ts @@ -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: "{}" }), + }); +} diff --git a/cognee-frontend/src/modules/ingestion/useData.ts b/cognee-frontend/src/modules/ingestion/useData.ts index 4368e296e..941521135 100644 --- a/cognee-frontend/src/modules/ingestion/useData.ts +++ b/cognee-frontend/src/modules/ingestion/useData.ts @@ -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: "", })) ); }, []); diff --git a/cognee-frontend/src/modules/ingestion/useDatasets.ts b/cognee-frontend/src/modules/ingestion/useDatasets.ts index 9ae4ddcb2..7ef2b6b79 100644 --- a/cognee-frontend/src/modules/ingestion/useDatasets.ts +++ b/cognee-frontend/src/modules/ingestion/useDatasets.ts @@ -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; diff --git a/cognee-frontend/src/modules/notebooks/useNotebooks.ts b/cognee-frontend/src/modules/notebooks/useNotebooks.ts new file mode 100644 index 000000000..f36f97448 --- /dev/null +++ b/cognee-frontend/src/modules/notebooks/useNotebooks.ts @@ -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([]); + + 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; diff --git a/cognee-frontend/src/ui/App/Loading/DefaultLoadingIndicator/LoadingIndicator.module.css b/cognee-frontend/src/ui/App/Loading/DefaultLoadingIndicator/LoadingIndicator.module.css index 472081d57..d66b1e7f8 100644 --- a/cognee-frontend/src/ui/App/Loading/DefaultLoadingIndicator/LoadingIndicator.module.css +++ b/cognee-frontend/src/ui/App/Loading/DefaultLoadingIndicator/LoadingIndicator.module.css @@ -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; diff --git a/cognee-frontend/src/ui/Icons/AddIcon.tsx b/cognee-frontend/src/ui/Icons/AddIcon.tsx index b9092feec..da150c8a3 100644 --- a/cognee-frontend/src/ui/Icons/AddIcon.tsx +++ b/cognee-frontend/src/ui/Icons/AddIcon.tsx @@ -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 ( diff --git a/cognee-frontend/src/ui/Icons/BackIcon.tsx b/cognee-frontend/src/ui/Icons/BackIcon.tsx new file mode 100644 index 000000000..796fb923b --- /dev/null +++ b/cognee-frontend/src/ui/Icons/BackIcon.tsx @@ -0,0 +1,8 @@ +export default function BackIcon({ width = 16, height = 16, color = "#17191C", className = "" }) { + return ( + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/CaretIcon.tsx b/cognee-frontend/src/ui/Icons/CaretIcon.tsx index 29a5eca62..cef9b9a9a 100644 --- a/cognee-frontend/src/ui/Icons/CaretIcon.tsx +++ b/cognee-frontend/src/ui/Icons/CaretIcon.tsx @@ -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 ( - - - + + ); } diff --git a/cognee-frontend/src/ui/Icons/CheckIcon.tsx b/cognee-frontend/src/ui/Icons/CheckIcon.tsx new file mode 100644 index 000000000..68610b1eb --- /dev/null +++ b/cognee-frontend/src/ui/Icons/CheckIcon.tsx @@ -0,0 +1,7 @@ +export default function CheckIcon({ width = 17, height = 18, color = "#5C10F4", className = "" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/CloseIcon.tsx b/cognee-frontend/src/ui/Icons/CloseIcon.tsx new file mode 100644 index 000000000..7ea30123d --- /dev/null +++ b/cognee-frontend/src/ui/Icons/CloseIcon.tsx @@ -0,0 +1,8 @@ +export default function CloseIcon({ width = 29, height = 29, color = "#000000", className = "" }) { + return ( + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/CloudIcon.tsx b/cognee-frontend/src/ui/Icons/CloudIcon.tsx new file mode 100644 index 000000000..9578c24d4 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/CloudIcon.tsx @@ -0,0 +1,7 @@ +export default function CloudIcon({ width = 16, height = 12, color = "#5C10F4", className = "" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/CogneeIcon.tsx b/cognee-frontend/src/ui/Icons/CogneeIcon.tsx new file mode 100644 index 000000000..d9f95e0f2 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/CogneeIcon.tsx @@ -0,0 +1,7 @@ +export default function CogneeIcon({ width = 21, height = 24, color="#6510F4", className="" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/DatasetIcon.tsx b/cognee-frontend/src/ui/Icons/DatasetIcon.tsx new file mode 100644 index 000000000..d17ff0470 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/DatasetIcon.tsx @@ -0,0 +1,9 @@ +export default function DatasetIcon({ width = 16, height = 16, color = "#000000", className = '' }) { + return ( + + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/LocalCogneeIcon.tsx b/cognee-frontend/src/ui/Icons/LocalCogneeIcon.tsx new file mode 100644 index 000000000..37f6016fc --- /dev/null +++ b/cognee-frontend/src/ui/Icons/LocalCogneeIcon.tsx @@ -0,0 +1,10 @@ +export default function LocalCogneeIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/MenuIcon.tsx b/cognee-frontend/src/ui/Icons/MenuIcon.tsx new file mode 100644 index 000000000..666b3293d --- /dev/null +++ b/cognee-frontend/src/ui/Icons/MenuIcon.tsx @@ -0,0 +1,9 @@ +export default function AddIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/MinusIcon.tsx b/cognee-frontend/src/ui/Icons/MinusIcon.tsx new file mode 100644 index 000000000..7757d81a6 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/MinusIcon.tsx @@ -0,0 +1,7 @@ +export default function MinusIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/NotebookIcon.tsx b/cognee-frontend/src/ui/Icons/NotebookIcon.tsx new file mode 100644 index 000000000..a46228d80 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/NotebookIcon.tsx @@ -0,0 +1,8 @@ +export default function NotebookIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/PlayIcon.tsx b/cognee-frontend/src/ui/Icons/PlayIcon.tsx new file mode 100644 index 000000000..865f103b0 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/PlayIcon.tsx @@ -0,0 +1,7 @@ +export default function PlayIcon({ width = 11, height = 14, color = "#000000", className = "" }) { + return ( + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/PlusIcon.tsx b/cognee-frontend/src/ui/Icons/PlusIcon.tsx new file mode 100644 index 000000000..69a760e20 --- /dev/null +++ b/cognee-frontend/src/ui/Icons/PlusIcon.tsx @@ -0,0 +1,8 @@ +export default function PlusIcon({ width = 16, height = 16, color = "#000000", className = "" }) { + return ( + + + + + ); +} diff --git a/cognee-frontend/src/ui/Icons/SearchIcon.tsx b/cognee-frontend/src/ui/Icons/SearchIcon.tsx index 3a3baac33..56cddf4c2 100644 --- a/cognee-frontend/src/ui/Icons/SearchIcon.tsx +++ b/cognee-frontend/src/ui/Icons/SearchIcon.tsx @@ -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 ( - - - - + + + ); } diff --git a/cognee-frontend/src/ui/Icons/SettingsIcon.tsx b/cognee-frontend/src/ui/Icons/SettingsIcon.tsx index ce006f49b..87e4c9872 100644 --- a/cognee-frontend/src/ui/Icons/SettingsIcon.tsx +++ b/cognee-frontend/src/ui/Icons/SettingsIcon.tsx @@ -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 ( - - + + + ); } diff --git a/cognee-frontend/src/ui/Icons/index.ts b/cognee-frontend/src/ui/Icons/index.ts index 0adaa8fd1..3d3f8124f 100644 --- a/cognee-frontend/src/ui/Icons/index.ts +++ b/cognee-frontend/src/ui/Icons/index.ts @@ -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"; diff --git a/cognee-frontend/src/ui/Layout/Header.tsx b/cognee-frontend/src/ui/Layout/Header.tsx new file mode 100644 index 000000000..465153e1a --- /dev/null +++ b/cognee-frontend/src/ui/Layout/Header.tsx @@ -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 ( + <> +
+
+ +
Cognee Graph Interface
+
+ +
+ + +
Sync
+
+ + Premium + + {/*
+ +
*/} + + {user?.avatarImagePath ? ( + Name of the user + ) : ( +
+ {user?.email?.charAt(0) || "C"} +
+ )} + +
+
+ + +
+
+ Sync local datasets with cloud datasets? + +
+
Are you sure you want to sync local datasets to cloud?
+
+ cancel + confirm +
+
+
+ + ); +} diff --git a/cognee-frontend/src/ui/Layout/index.ts b/cognee-frontend/src/ui/Layout/index.ts index 54938ca4d..af5a67ac5 100644 --- a/cognee-frontend/src/ui/Layout/index.ts +++ b/cognee-frontend/src/ui/Layout/index.ts @@ -1 +1,2 @@ -export { default as Divider } from './Divider/Divider'; +export { default as Divider } from "./Divider/Divider"; +export { default as Header } from "./Header"; diff --git a/cognee-frontend/src/ui/elements/Accordion.tsx b/cognee-frontend/src/ui/elements/Accordion.tsx new file mode 100644 index 000000000..8779d6d36 --- /dev/null +++ b/cognee-frontend/src/ui/elements/Accordion.tsx @@ -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 ( +
+
+ + {tools} +
+ + {isOpen && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/cognee-frontend/src/ui/elements/AvatarImage.tsx b/cognee-frontend/src/ui/elements/AvatarImage.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/cognee-frontend/src/ui/elements/CTAButton.tsx b/cognee-frontend/src/ui/elements/CTAButton.tsx index c38384cdd..02f1871fc 100644 --- a/cognee-frontend/src/ui/elements/CTAButton.tsx +++ b/cognee-frontend/src/ui/elements/CTAButton.tsx @@ -1,8 +1,8 @@ -import classNames from 'classnames'; +import classNames from "classnames"; import { ButtonHTMLAttributes } from "react"; export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes) { return ( - + ); } diff --git a/cognee-frontend/src/ui/elements/GhostButton.tsx b/cognee-frontend/src/ui/elements/GhostButton.tsx index 333dcc394..a27a0ff94 100644 --- a/cognee-frontend/src/ui/elements/GhostButton.tsx +++ b/cognee-frontend/src/ui/elements/GhostButton.tsx @@ -1,8 +1,8 @@ -import classNames from 'classnames'; +import classNames from "classnames"; import { ButtonHTMLAttributes } from "react"; export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes) { return ( - + ); } diff --git a/cognee-frontend/src/ui/elements/IconButton.tsx b/cognee-frontend/src/ui/elements/IconButton.tsx new file mode 100644 index 000000000..cbc35df5b --- /dev/null +++ b/cognee-frontend/src/ui/elements/IconButton.tsx @@ -0,0 +1,14 @@ +import classNames from "classnames"; +import { ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + as?: React.ElementType; +} + +export default function IconButton({ as, children, className, ...props }: ButtonProps) { + const Element = as || "button"; + + return ( + {children} + ); +} diff --git a/cognee-frontend/src/ui/elements/Input.tsx b/cognee-frontend/src/ui/elements/Input.tsx index 904658eba..76451f9fa 100644 --- a/cognee-frontend/src/ui/elements/Input.tsx +++ b/cognee-frontend/src/ui/elements/Input.tsx @@ -3,6 +3,6 @@ import { InputHTMLAttributes } from "react" export default function Input({ className, ...props }: InputHTMLAttributes) { return ( - + ) } diff --git a/cognee-frontend/src/ui/elements/Modal.tsx b/cognee-frontend/src/ui/elements/Modal/Modal.tsx similarity index 84% rename from cognee-frontend/src/ui/elements/Modal.tsx rename to cognee-frontend/src/ui/elements/Modal/Modal.tsx index fd1db3c32..9d559a7ac 100644 --- a/cognee-frontend/src/ui/elements/Modal.tsx +++ b/cognee-frontend/src/ui/elements/Modal/Modal.tsx @@ -5,7 +5,7 @@ interface ModalProps { export default function Modal({ isOpen, children }: ModalProps) { return isOpen && ( -
+
{children}
); diff --git a/cognee-frontend/src/ui/elements/Modal/index.ts b/cognee-frontend/src/ui/elements/Modal/index.ts new file mode 100644 index 000000000..6386401d6 --- /dev/null +++ b/cognee-frontend/src/ui/elements/Modal/index.ts @@ -0,0 +1,3 @@ +export { default as Modal } from "./Modal"; +export { default as useModal } from "./useModal"; + diff --git a/cognee-frontend/src/ui/elements/Modal/useModal.ts b/cognee-frontend/src/ui/elements/Modal/useModal.ts new file mode 100644 index 000000000..4947d32ca --- /dev/null +++ b/cognee-frontend/src/ui/elements/Modal/useModal.ts @@ -0,0 +1,49 @@ +import { FormEvent, useCallback, useState } from "react"; +import { useBoolean } from "@/utils"; + +export default function useModal(initiallyOpen?: boolean, confirmCallback?: (state: object, event?: FormEvent) => Promise | ConfirmActionReturnType) { + const [modalState, setModalState] = useState({}); + 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) => { + 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, + }; +} diff --git a/cognee-frontend/src/ui/elements/NeutralButton.tsx b/cognee-frontend/src/ui/elements/NeutralButton.tsx index 5b274ad65..7b991fcb8 100644 --- a/cognee-frontend/src/ui/elements/NeutralButton.tsx +++ b/cognee-frontend/src/ui/elements/NeutralButton.tsx @@ -1,8 +1,8 @@ -import classNames from 'classnames'; +import classNames from "classnames"; import { ButtonHTMLAttributes } from "react"; -export default function CTAButton({ children, className, ...props }: ButtonHTMLAttributes) { +export default function NeutralButton({ children, className, ...props }: ButtonHTMLAttributes) { return ( - + ); } diff --git a/cognee-frontend/src/ui/elements/Notebook/Notebook.tsx b/cognee-frontend/src/ui/elements/Notebook/Notebook.tsx new file mode 100644 index 000000000..0e037890a --- /dev/null +++ b/cognee-frontend/src/ui/elements/Notebook/Notebook.tsx @@ -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; + 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 ( +
+
{notebook.name}
+ + {notebook.cells.map((cell: Cell, index) => ( + +
+
+ {cell.type === "code" ? ( + <> +
+ + + +
+ + + + {openCells.has(cell.id) && ( + <> +