diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx index 494d37e..44dc798 100644 --- a/frontend/src/components/Header/index.tsx +++ b/frontend/src/components/Header/index.tsx @@ -7,6 +7,7 @@ export default function Header(props: { sidebarOpen: string | boolean | undefined; setSidebarOpen: (arg0: boolean) => void; extendedItems?: any; + quickActions: boolean; }) { const [copied, setCopied] = useState(false); if (!props.entity) return null; diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx index 25d236c..2a29122 100644 --- a/frontend/src/layout/AppLayout.tsx +++ b/frontend/src/layout/AppLayout.tsx @@ -13,6 +13,7 @@ interface DefaultLayoutProps { children: ReactNode; hasMoreWorkspaces?: boolean; loadMoreWorkspaces?: VoidFunction; + hasQuickActions?: boolean; } const AppLayout = ({ @@ -26,6 +27,7 @@ const AppLayout = ({ children, hasMoreWorkspaces, loadMoreWorkspaces, + hasQuickActions = false, }: DefaultLayoutProps) => { const [sidebarOpen, setSidebarOpen] = useState(false); @@ -52,6 +54,7 @@ const AppLayout = ({ sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} extendedItems={headerExtendedItems} + quickActions={hasQuickActions} /> )} diff --git a/frontend/src/pages/Dashboard/DocumentsList/index.tsx b/frontend/src/pages/Dashboard/DocumentsList/index.tsx index 69e9fe0..cf15e94 100644 --- a/frontend/src/pages/Dashboard/DocumentsList/index.tsx +++ b/frontend/src/pages/Dashboard/DocumentsList/index.tsx @@ -1,8 +1,5 @@ -import { Link } from 'react-router-dom'; import paths from '../../../utils/paths'; import moment from 'moment'; -import { AlertOctagon, FileText } from 'react-feather'; -// import { CodeBlock, vs2015 } from 'react-code-blocks'; import { useEffect, useState } from 'react'; import Organization from '../../../models/organization'; import truncate from 'truncate'; @@ -11,6 +8,8 @@ import UploadDocumentModal from './UploadModal'; import UploadModalNoKey from './UploadModal/UploadModalNoKey'; import DocumentListPagination from '../../../components/DocumentPaginator'; import useQuery from '../../../hooks/useQuery'; +import Document from '../../../models/document'; +import { File, Trash } from '@phosphor-icons/react'; export default function DocumentsList({ organization, @@ -41,6 +40,18 @@ export default function DocumentsList({ setCurrentPage(setTo); } + const deleteDocument = async (documentId: number) => { + if ( + !confirm( + 'Are you sure you want to delete this document? This will remove the document from your vector database and remove it from the cache. This process cannot be undone.' + ) + ) + return false; + const success = await Document.delete(documentId); + if (!success) return false; + document.getElementById(`document-row-${documentId}`)?.remove(); + }; + useEffect(() => { async function getDocs(slug?: string) { if (!slug) return false; @@ -59,174 +70,245 @@ export default function DocumentsList({ if (loading) { return ( -
-
-
-

- Documents {totalDocuments! > 0 ? `(${totalDocuments})` : ''} -

-
-
-
-
-
-
+
); } return ( <> -
-
-
-

- Documents {totalDocuments! > 0 ? `(${totalDocuments})` : ''} -

-
- {workspaces.length > 0 ? ( - <> - {!!knownConnector ? ( +
+
+ + + + + + + + + + + + {documents?.length > 0 && ( + + {documents.map((document, index) => ( + + ))} + + )} +
+ Name + + Workspace + + Date + + Vectors + + {' '} +
+ + {!!!knownConnector && ( +
+
+
+ Connect a Vector Database to get started +
+
+ Begin by connecting a Vector Database to your organization +
- ) : ( +
+
+ )} + + {!!knownConnector && workspaces?.length === 0 && ( +
+
+
+ Create a workspace to get started +
+
+ Workspaces are used to organize your documents +
- )} - - ) : ( - +
+
+ )} + + {documents?.length === 0 && workspaces?.length > 0 && ( +
+
+
+ 0 Documents +
+
+ Upload documents to your workspace +
+ +
+
)}
- {documents.length > 0 ? ( -
-
-
-
- Document Name -
-
- Workspace -
-
- Created -
-
- Status -
-
- -
-
-
- <> - {documents.map((document) => { - return ( -
-
-
-
- - - {truncate(document.name, 20)} - -
-
- -
- - {moment(document.createdAt).format('lll')} - -
-
- - Cached - -
- -
-
- ); - })} - -
+
+ +
+ {canUpload ? ( + ) : ( -
-
-
-

You have no documents in any workspaces!

-

- Get started managing documents by adding them to workspaces - via the UI or code. -

- -
-
-
+ )}
- - {canUpload ? ( - - ) : ( - - )} ); } + +const Fragment = ({ + document, + index, + deleteDocument, + organization, +}: { + document: any; + index: number; + deleteDocument: any; + organization: any; +}) => { + return ( + <> + + + +

{truncate(document?.name, 35)}

+ + + + + {truncate(document.workspace.name, 20) || ''} + + + + {moment(document?.createdAt).format('lll')} + Cached + + + Details + + + +
+
+ +
+
+ + + + ); +}; diff --git a/frontend/src/pages/Dashboard/QuickActionSidebar/index.tsx b/frontend/src/pages/Dashboard/QuickActionSidebar/index.tsx new file mode 100644 index 0000000..eb6350f --- /dev/null +++ b/frontend/src/pages/Dashboard/QuickActionSidebar/index.tsx @@ -0,0 +1,84 @@ +import { + CaretDown, + Key, + SpinnerGap, + Toolbox, + User, +} from '@phosphor-icons/react'; +import { useState } from 'react'; +import { NavLink } from 'react-router-dom'; +import paths from '../../../utils/paths'; +import useUser from '../../../hooks/useUser'; + +export default function QuickActionsSidebar({ + organization, +}: { + organization: any; +}) { + const { user } = useUser(); + const [quickActionsOpen, setQuickActionsOpen] = useState(true); + + return ( +
+ + +
+ {user?.role === 'admin' && ( + <> + +
+ +
Tools
+
+
+ + +
+ +
Add User
+
+
+ + +
+ +
OpenAI Key
+
+
+ + )} + +
+ +
Background Jobs
+
+
+
+
+ ); +} diff --git a/frontend/src/pages/Dashboard/Statistics/index.tsx b/frontend/src/pages/Dashboard/Statistics/index.tsx index f3be2f7..e0c2577 100644 --- a/frontend/src/pages/Dashboard/Statistics/index.tsx +++ b/frontend/src/pages/Dashboard/Statistics/index.tsx @@ -1,11 +1,16 @@ import { memo, useState, useEffect } from 'react'; -import PreLoader from '../../../components/Preloader'; import { humanFileSize, nFormatter } from '../../../utils/numbers'; -import moment from 'moment'; import Organization from '../../../models/organization'; import pluralize from 'pluralize'; +import Workspace from '../../../models/workspace'; -const Statistics = ({ organization }: { organization: any }) => { +const Statistics = ({ + organization, + workspaces, +}: { + organization: any; + workspaces: any; +}) => { const [documents, setDocuments] = useState({ status: 'loading', value: 0, @@ -18,6 +23,10 @@ const Statistics = ({ organization }: { organization: any }) => { status: 'loading', value: 0, }); + const [dimensions, setDimensions] = useState({ + status: 'loading', + value: '-', + }); useEffect(() => { async function collectStats() { @@ -32,65 +41,49 @@ const Statistics = ({ organization }: { organization: any }) => { Organization.stats(organization.slug, 'cache-size').then((json) => { setCache({ status: 'complete', value: json.value }); }); + Workspace.stats(organization.slug, workspaces[0].slug, 'dimensions').then( + (json) => { + setDimensions({ status: 'complete', value: json.value }); + } + ); } collectStats(); - }, [organization?.slug]); + }, [organization?.slug, workspaces[0]?.slug]); return ( -
-
-
- {documents.status === 'loading' ? ( - - ) : ( -
-

- {nFormatter(documents.value)} -

-

- {pluralize('Document', documents.value)} -

-
- )} +
+
+
+ + Documents + + + {' '} + + + ({nFormatter(documents.value)}) +
+
-
- {vectors.status === 'loading' ? ( - - ) : ( -
-

- {nFormatter(vectors.value)} -

-

- {pluralize('Vector', vectors.value)} -

-
- )} -
- -
- {cache.status === 'loading' ? ( - - ) : ( -
-

- {humanFileSize(cache.value)} -

-

Vector Cache (MB)

-
- )} -
- -
-
-

- {organization?.lastUpdated - ? moment(organization.lastUpdated).fromNow() - : moment(organization.createdAt).fromNow()} -

-

Last Modified

-
+
+
+ + {pluralize('Vector', vectors.value)}:{' '} + + {nFormatter(vectors.value)} + + + + Vector Cache:{' '} + + {humanFileSize(cache.value)} + + + + Dimensions:{' '} + {dimensions.value} +
diff --git a/frontend/src/pages/Dashboard/WorkspacesList/index.tsx b/frontend/src/pages/Dashboard/WorkspacesList/index.tsx index 7ba65a9..a35bc64 100644 --- a/frontend/src/pages/Dashboard/WorkspacesList/index.tsx +++ b/frontend/src/pages/Dashboard/WorkspacesList/index.tsx @@ -20,7 +20,7 @@ export default function WorkspacesList({ totalWorkspaces?: number; }) { return ( -
+

diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index d3134b7..e01ae59 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -1,17 +1,25 @@ -import { FullScreenLoader } from '../../components/Preloader'; +import PreLoader, { FullScreenLoader } from '../../components/Preloader'; import useUser from '../../hooks/useUser'; import { useState, useEffect } from 'react'; import DefaultLayout from '../../layout/DefaultLayout'; import User from '../../models/user'; import paths from '../../utils/paths'; import AppLayout from '../../layout/AppLayout'; -import { useParams } from 'react-router-dom'; +import { NavLink, useParams } from 'react-router-dom'; import Statistics from './Statistics'; -import WorkspacesList from './WorkspacesList'; import DocumentsList from './DocumentsList'; import Organization from '../../models/organization'; -import ApiKeyCard from './ApiKey'; -import ConnectorCard from './Connector'; +import truncate from 'truncate'; + +import ChromaLogo from '../../images/vectordbs/chroma.png'; +import PineconeLogoInverted from '../../images/vectordbs/pinecone-inverted.png'; +import PineconeLogo from '../../images/vectordbs/pinecone.png'; +import qDrantLogo from '../../images/vectordbs/qdrant.png'; +import WeaviateLogo from '../../images/vectordbs/weaviate.png'; +import { GearSix, Prohibit } from '@phosphor-icons/react'; +import { titleCase } from 'title-case'; +import QuickActionsSidebar from './QuickActionSidebar'; +import { APP_NAME } from '../../utils/constants'; export default function Dashboard() { const { slug } = useParams(); @@ -25,7 +33,6 @@ export default function Dashboard() { const [workspaces, setWorkspaces] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [hasMoreWorkspaces, setHasMoreWorkspaces] = useState(true); - const [totalWorkspaces, setTotalWorkspaces] = useState(0); async function fetchWorkspaces(focusedOrg?: { slug: string }) { const org = focusedOrg || organization; @@ -44,11 +51,9 @@ export default function Dashboard() { setWorkspaces(uniques); setHasMoreWorkspaces(uniques.length < totalWorkspaces); - setTotalWorkspaces(totalWorkspaces); } else { setWorkspaces(_workspaces); setHasMoreWorkspaces(totalWorkspaces > Organization.workspacePageSize); - setTotalWorkspaces(totalWorkspaces); } setCurrentPage(currentPage + 1); return true; @@ -97,34 +102,1050 @@ export default function Dashboard() { workspaces={workspaces} hasMoreWorkspaces={hasMoreWorkspaces} loadMoreWorkspaces={fetchWorkspaces} + headerExtendedItems={ + {}} + /> + } + hasQuickActions={true} > - {!!organization && ( + {!!organization && !!connector && (
- setConnector(newConnector)} + /> + -
)} - - -
-
+ +
+
- +
); } + +function OrganizationHeader({ organization, connector }: any) { + let logo; + switch (connector?.type) { + case 'chroma': + logo = ChromaLogo; + break; + case 'qdrant': + logo = qDrantLogo; + break; + case 'weaviate': + logo = WeaviateLogo; + break; + case 'pinecone': + logo = PineconeLogoInverted; + break; + } + + return ( + <> +
+
+ + {truncate(organization?.name, 20)} + +
+
+
+ + + + + + + +
+ + ); +} + +const NewConnectorModal = ({ + organization, + onNew, +}: { + organization: any; + onNew: (newConnector: any) => void; +}) => { + const [loading, setLoading] = useState(false); + const [type, setType] = useState('chroma'); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const handleSubmit = async (e: any) => { + e.preventDefault(); + setError(null); + setLoading(true); + const data = { type }; + const form = new FormData(e.target); + for (var [_k, value] of form.entries()) { + if (_k.includes('::')) { + const [_key, __key] = _k.split('::'); + if (!data.hasOwnProperty(_key)) data[_key] = {}; + data[_key][__key] = value; + } else { + data[_k] = value; + } + } + + const { connector, error } = await Organization.addConnector( + organization.slug, + data + ); + + if (!connector) { + setLoading(false); + setError(error); + return false; + } + + setLoading(false); + setSuccess(true); + setTimeout(() => { + onNew(connector); + setSuccess(false); + }, 1500); + }; + + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
+
+

+ Connect to Vector Database +

+

+ {APP_NAME} is a tool to help you manage vectors in a vector + database, but without access to a valid vector database you will be + limited to read-only actions and limited functionality - you should + connect to a vector database provider to unlock full functionality. +

+
+ + + + +
+
+ ); +}; + +const UpdateConnectorModal = ({ + organization, + connector, + onUpdate, +}: { + organization: any; + connector: any; + onUpdate: (newConnector: any) => void; +}) => { + const [loading, setLoading] = useState(false); + const [type, setType] = useState(connector?.type); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const settings = JSON.parse(connector?.settings); + + const handleSubmit = async (e: any) => { + e.preventDefault(); + setError(null); + setLoading(true); + const data = { type }; + const form = new FormData(e.target); + for (var [_k, value] of form.entries()) { + if (_k.includes('::')) { + const [_key, __key] = _k.split('::'); + if (!data.hasOwnProperty(_key)) data[_key] = {}; + data[_key][__key] = value; + } else { + data[_k] = value; + } + } + + const { connector, error } = await Organization.updateConnector( + organization.slug, + data + ); + if (!connector) { + setLoading(false); + setError(error); + return false; + } + + setLoading(false); + setSuccess(true); + setTimeout(() => { + onUpdate(connector); + setSuccess(false); + }, 1500); + }; + + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
+
+

+ Update Vector Database Connection +

+

+ Currently connected to a {connector.type} vector database instance. + You can update your configuration settings here if they have + changed. +

+
+ {loading ? ( +
+
+ +
+
+ ) : ( +
+
    +
  • setType('chroma')} className="w-[250px]"> + + +
  • +
  • setType('pinecone')} className="w-[250px]"> + + +
  • +
  • setType('qdrant')} className="w-[250px]"> + + +
  • +
  • setType('weaviate')} className="w-[250px]"> + + +
  • +
+ + {type === 'chroma' && ( +
+
+
+ +

+ This is the URL your chroma instance is reachable at. +

+
+ +
+
+
+ +

+ If your hosted Chroma instance is protected by an API key + - enter the header and api key here. +

+
+
+ + +
+
+
+ )} + + {type === 'pinecone' && ( +
+
+
+ +

+ You can find this on your Pinecone index. +

+
+ +
+ +
+
+ +

+ You can find this on your Pinecone index. +

+
+ +
+ +
+
+ +

+ If your hosted Chroma instance is protected by an API key + - enter it here. +

+
+ +
+
+ )} + + {type === 'qdrant' && ( +
+
+
+ +

+ You can find this in your cloud hosted qDrant cluster or + just using the URL to your local docker container. +

+
+ +
+ +
+
+ +

+ (optional) If you are using qDrant cloud you will need an + API key. +

+
+ +
+
+ )} + + {type === 'weaviate' && ( +
+
+
+ +

+ You can find this in your cloud hosted Weaviate cluster or + just using the URL to your local docker container. +

+
+ +
+ +
+
+ +

+ (optional) If you are using Weaviate cloud you will need + an API key. +

+
+ +
+
+ )} + +
+ {error && ( +

+ {error} +

+ )} + {success && ( +

+ Connector changes saved +

+ )} + +
+
+ )} +
+
+ ); +}; + +const SyncConnectorModal = ({ + organization, + connector, +}: { + organization: any; + connector: any; +}) => { + const [synced, setSynced] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const sync = async () => { + setError(null); + setLoading(true); + const { job, error } = await Organization.syncConnector( + organization.slug, + connector.id + ); + + if (!job) { + setError(error); + setLoading(false); + setSynced(false); + return; + } + + setLoading(false); + setSynced(true); + }; + + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
+
+

+ Sync Vector Database Connection +

+

+ Automatically sync existing information in your{' '} + {titleCase(connector.type)}{' '} + {connector.type === 'chroma' ? 'collections' : 'namespaces'} so you + can manage it more easily. This process can take a long time to + complete depending on how much data you have embedded already. +
+
+ Once you start this process you can check on its progress in the{' '} + + job queue. + +

+
+
+ {error && ( +

+ {error} +

+ )} + {synced ? ( + + ) : ( + + )} +
+
+
+ ); +}; diff --git a/frontend/src/pages/DocumentView/index.tsx b/frontend/src/pages/DocumentView/index.tsx index 9ce0360..a4b935b 100644 --- a/frontend/src/pages/DocumentView/index.tsx +++ b/frontend/src/pages/DocumentView/index.tsx @@ -169,7 +169,7 @@ function DocumentViewHeader({ organization, workspace, document }: any) { return ( <>
diff --git a/frontend/src/pages/WorkspaceDashboard/index.tsx b/frontend/src/pages/WorkspaceDashboard/index.tsx index 7f4cbb3..014b1e6 100644 --- a/frontend/src/pages/WorkspaceDashboard/index.tsx +++ b/frontend/src/pages/WorkspaceDashboard/index.tsx @@ -7,7 +7,6 @@ import paths from '../../utils/paths'; import AppLayout from '../../layout/AppLayout'; import { useParams } from 'react-router-dom'; import Organization from '../../models/organization'; -import ApiKeyCard from './ApiKey'; import Statistics from './Statistics'; import DocumentsList from './DocumentsList'; import Workspace from '../../models/workspace'; @@ -17,7 +16,8 @@ import { CaretDown } from '@phosphor-icons/react'; import truncate from 'truncate'; import ChromaLogo from '../../images/vectordbs/chroma.png'; -import PineconeLogo from '../../images/vectordbs/pinecone-inverted.png'; +import PineconeLogoInverted from '../../images/vectordbs/pinecone-inverted.png'; +import PineconeLogo from '../../images/vectordbs/pinecone.png'; import qDrantLogo from '../../images/vectordbs/qdrant.png'; import WeaviateLogo from '../../images/vectordbs/weaviate.png'; @@ -795,13 +795,13 @@ function WorkspaceViewHeader({ logo = WeaviateLogo; break; default: - logo = PineconeLogo; + logo = PineconeLogoInverted; } return ( <>