diff --git a/.gitignore b/.gitignore index 3c3bb305..3f76e4a5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ node_modules __pycache__ v-env .DS_Store -yarn-error.log \ No newline at end of file +yarn-error.log +yarn.lock +frontend/.env.development \ No newline at end of file diff --git a/backend/endpoints/v1/workspaces/index.js b/backend/endpoints/v1/workspaces/index.js index 1df3e38d..31ed492e 100644 --- a/backend/endpoints/v1/workspaces/index.js +++ b/backend/endpoints/v1/workspaces/index.js @@ -305,6 +305,7 @@ function workspaceEndpoints(app) { documents: "countForEntity", vectors: "calcVectors", "cache-size": "calcVectorCache", + dimensions: "calcDimensions", }; if (!Object.keys(methods).includes(statistic)) { @@ -314,7 +315,6 @@ function workspaceEndpoints(app) { return; } - console.log(workspace); const value = await WorkspaceDocument[methods[statistic]]( "workspace_id", workspace.id diff --git a/backend/models/workspaceDocument.js b/backend/models/workspaceDocument.js index a6852346..6e511ee7 100644 --- a/backend/models/workspaceDocument.js +++ b/backend/models/workspaceDocument.js @@ -159,6 +159,30 @@ const WorkspaceDocument = { return totalBytes; }, + + calcDimensions: async function (field = "workspace_id", value = null) { + try { + const { OrganizationConnection } = require("./organizationConnection"); + + const workspace = await prisma.organization_workspaces.findUnique({ + where: { id: value }, + include: { organization: true }, + }); + + const connector = await OrganizationConnection.get({ + organization_id: workspace.organization.id, + }); + + const vectorDb = selectConnector(connector); + const dimensions = await vectorDb.indexDimensions(workspace.fname); + + return dimensions; + } catch (e) { + console.error(e); + return 0; + } + }, + // Will get both the remote and local count of vectors to see if the numbers match. vectorCount: async function (field = "organization_id", value = null) { try { diff --git a/backend/utils/vectordatabases/providers/chroma/index.js b/backend/utils/vectordatabases/providers/chroma/index.js index f3ec4a87..4f77ff99 100644 --- a/backend/utils/vectordatabases/providers/chroma/index.js +++ b/backend/utils/vectordatabases/providers/chroma/index.js @@ -98,6 +98,12 @@ class Chroma { return { result: totalVectors, error: null }; } + // TODO: Solve this issue + async indexDimensions() { + // Chroma does not support this, defaulting to openai's 1536 + return 1536; + } + // Collections === namespaces for Chroma to normalize interfaces async collections() { return await this.namespaces(); diff --git a/backend/utils/vectordatabases/providers/weaviate/index.js b/backend/utils/vectordatabases/providers/weaviate/index.js index ebd85917..9e21a611 100644 --- a/backend/utils/vectordatabases/providers/weaviate/index.js +++ b/backend/utils/vectordatabases/providers/weaviate/index.js @@ -66,7 +66,6 @@ class Weaviate { var totalVectors = 0; for (const collection of collections) { if (!collection || !collection.name) continue; - console.log({ dim: await this.indexDimensions(collection.name) }); totalVectors += (await this.namespaceWithClient(client, collection.name)) ?.vectorCount || 0; diff --git a/frontend/package.json b/frontend/package.json index b3769cce..bbcdfa6f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "dependencies": { "@dqbd/tiktoken": "^1.0.7", "@metamask/jazzicon": "^2.0.0", + "@phosphor-icons/react": "^2.0.15", "jsvectormap": "^1.5.1", "lodash": "^4.17.21", "lodash.debounce": "^4.0.8", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 41d81db9..e5859255 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,10 +17,13 @@ const OrganizationSettingsView = lazy( const OrganizationDashboard = lazy(() => import('./pages/Dashboard')); const WorkspaceDashboard = lazy(() => import('./pages/WorkspaceDashboard')); const DocumentView = lazy(() => import('./pages/DocumentView')); -const SystemSetup = lazy(() => import('./pages/Authentication/SystemSetup')); const OnboardingSecuritySetup = lazy( () => import('./pages/Onboarding/security') ); + +// Onboarding v2 +const OnboardingFlow = lazy(() => import('./pages/OnboardingFlow')); + const OrganizationJobsView = lazy(() => import('./pages/Jobs')); const OrganizationToolsView = lazy(() => import('./pages/Tools')); const SystemSettingsView = lazy(() => import('./pages/SystemSettings')); @@ -75,6 +78,7 @@ function App() { element={} /> + } /> } @@ -103,7 +107,6 @@ function App() { } /> } /> - } /> } diff --git a/frontend/src/components/DocumentPaginator/index.tsx b/frontend/src/components/DocumentPaginator/index.tsx index 3d728897..9c9266c1 100644 --- a/frontend/src/components/DocumentPaginator/index.tsx +++ b/frontend/src/components/DocumentPaginator/index.tsx @@ -1,3 +1,4 @@ +import { CaretDown } from '@phosphor-icons/react'; import { numberWithCommas } from '../../utils/numbers'; function generatePageItems(total: number, current: number) { @@ -30,17 +31,39 @@ export default function DocumentListPagination({ gotoPage, }: IPaginationProps) { const pageItems = generatePageItems(pageCount, currentPage); + + const hasPrevious = currentPage > 1; + const hasNext = currentPage < pageCount; + + const goToPrevious = () => { + if (currentPage > 1) gotoPage(currentPage - 1); + }; + + const goToNext = () => { + if (currentPage < pageCount) gotoPage(currentPage + 1); + }; + + if (pageCount < 2) return
; + return ( -
+
+ {hasPrevious && ( + + )}
    {pageItems.map((item, i) => typeof item === 'number' ? ( ) )}
+ {hasNext && ( + + )}
); } diff --git a/frontend/src/components/Header/Notifications/index.tsx b/frontend/src/components/Header/Notifications/index.tsx deleted file mode 100644 index 9c10c212..00000000 --- a/frontend/src/components/Header/Notifications/index.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { ReactNode, useEffect, useState } from 'react'; -import { AlertOctagon, AlertTriangle, Bell, Info } from 'react-feather'; -import { useParams } from 'react-router-dom'; -import Organization from '../../../models/organization'; -import { databaseTimestampFromNow } from '../../../utils/data'; -import ChromaLogo from '../../../images/vectordbs/chroma.png'; -import PineconeLogo from '../../../images/vectordbs/pinecone.png'; -import qDrantLogo from '../../../images/vectordbs/qdrant.png'; -import WeaviateLogo from '../../../images/vectordbs/weaviate.png'; - -const POLLING_INTERVAL = 30_000; - -export type INotification = { - id: number; - organization_id: number; - seen: boolean; - textContent: string; - symbol?: - | 'info' - | 'warning' - | 'error' - | 'chroma' - | 'pinecone' - | 'weaviate' - | 'qdrant'; - link?: string; - target?: '_blank' | 'self'; - createdAt: string; - lastUpdatedAt: string; -}; - -export default function Notifications() { - const { slug } = useParams(); - const [loading, setLoading] = useState(true); - const [notifications, setNotifications] = useState([]); - const [hasUnseen, setHasUnseen] = useState(false); - const [showNotifs, setShowNotifs] = useState(false); - - async function handleClick() { - if (!showNotifs) { - !!slug && Organization.markNotificationsSeen(slug); - setShowNotifs(true); - setHasUnseen(false); - } else { - setShowNotifs(false); - } - } - - async function fetchNotifications() { - if (!slug) { - setLoading(false); - return; - } - - const { notifications: _notifications } = await Organization.notifications( - slug - ); - setNotifications(_notifications); - setHasUnseen(_notifications.some((notif) => notif.seen === false)); - setLoading(false); - } - - useEffect(() => { - if (!slug) return; - fetchNotifications(); - setInterval(() => { - fetchNotifications(); - }, POLLING_INTERVAL); - }, [slug]); - - if (loading) return null; - - return ( -
- - - - ); -} - -function NotificationImage({ notification }: { notification: INotification }) { - switch (notification.symbol) { - case 'info': - return ; - case 'warning': - return ; - case 'error': - return ; - case 'chroma': - return ; - case 'pinecone': - return ; - case 'qdrant': - return ; - case 'weaviate': - return ; - default: - return ; - } -} - -function NotificationWrapper({ - notification, - children, -}: { - notification: INotification; - children: ReactNode; -}) { - if (!!notification.link) { - return ( - - {children} - - ); - } - return ( -
- {children} -
- ); -} - -function Notification({ notification }: { notification: INotification }) { - return ( - -
- -
-
-
- {notification.textContent} -
-
- {databaseTimestampFromNow(notification.createdAt)} -
-
-
- ); -} diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx index b68848bf..44dc7989 100644 --- a/frontend/src/components/Header/index.tsx +++ b/frontend/src/components/Header/index.tsx @@ -1,11 +1,4 @@ -import { Link } from 'react-router-dom'; -import Logo from '../../images/logo/logo-light.png'; -import { CheckCircle, Copy } from 'react-feather'; import { useEffect, useState } from 'react'; -import paths from '../../utils/paths'; -import { STORE_TOKEN, STORE_USER } from '../../utils/constants'; -import truncate from 'truncate'; -import Notifications from './Notifications'; export default function Header(props: { entity?: any | null; @@ -14,15 +7,11 @@ 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; - const { entity, property, nameProp, extendedItems = <> } = props; - - const handleCopy = () => { - window.navigator.clipboard.writeText(entity[property]); - setCopied(true); - }; + const { extendedItems = <> } = props; useEffect(() => { function manageCopy() { @@ -35,94 +24,8 @@ export default function Header(props: { }, [copied]); return ( -
-
-
- {/* */} - - {/* */} - - - Logo - -
- -
-
-
-

- {truncate(entity[nameProp ?? 'name'], 20)} -

- - {extendedItems} -
-
- - -
-
-
-
+
+
{extendedItems}
); } diff --git a/frontend/src/components/Modals/NewConnectorModal.tsx b/frontend/src/components/Modals/NewConnectorModal.tsx new file mode 100644 index 00000000..8bc58097 --- /dev/null +++ b/frontend/src/components/Modals/NewConnectorModal.tsx @@ -0,0 +1,472 @@ +import { useState, memo } from 'react'; +import Organization from '../../models/organization'; +import PreLoader from '../Preloader'; + +import ChromaLogo from '../../images/vectordbs/chroma.png'; +import PineconeLogoInverted from '../../images/vectordbs/pinecone-inverted.png'; +import qDrantLogo from '../../images/vectordbs/qdrant.png'; +import WeaviateLogo from '../../images/vectordbs/weaviate.png'; +import { APP_NAME } from '../../utils/constants'; +const NewConnectorModal = memo( + ({ + 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. +

+
+ {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. +

+
+ +
+ +
+
+ +

+ You can find this on your Pinecone index. +

+
+ +
+
+ )} + + {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 +

+ )} +
+ +
+
+
+ )} +
+
+ ); + } +); + +export default NewConnectorModal; diff --git a/frontend/src/components/Modals/SyncConnectorModal.tsx b/frontend/src/components/Modals/SyncConnectorModal.tsx new file mode 100644 index 00000000..3b7cd0c5 --- /dev/null +++ b/frontend/src/components/Modals/SyncConnectorModal.tsx @@ -0,0 +1,93 @@ +import { useState, memo } from 'react'; +import Organization from '../../models/organization'; +import { titleCase } from 'title-case'; +import paths from '../../utils/paths'; + +const SyncConnectorModal = memo( + ({ 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 ? ( + + ) : ( + + )} +
+
+
+ ); + } +); + +export default SyncConnectorModal; diff --git a/frontend/src/components/Modals/UpdateConnectorModal.tsx b/frontend/src/components/Modals/UpdateConnectorModal.tsx new file mode 100644 index 00000000..fea0a1f8 --- /dev/null +++ b/frontend/src/components/Modals/UpdateConnectorModal.tsx @@ -0,0 +1,483 @@ +import { useState, memo } from 'react'; +import Organization from '../../models/organization'; +import PreLoader from '../Preloader'; + +import ChromaLogo from '../../images/vectordbs/chroma.png'; +import PineconeLogoInverted from '../../images/vectordbs/pinecone-inverted.png'; +import qDrantLogo from '../../images/vectordbs/qdrant.png'; +import WeaviateLogo from '../../images/vectordbs/weaviate.png'; + +const UpdateConnectorModal = memo( + ({ + 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. +

+
+ +
+ +
+
+ +

+ You can find this on your Pinecone index. +

+
+ +
+
+ )} + + {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 +

+ )} +
+ +
+
+
+ )} +
+
+ ); + } +); + +export default UpdateConnectorModal; diff --git a/frontend/src/pages/WorkspaceDashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx b/frontend/src/components/Modals/UploadDocumentModal/FileUploadProgress/index.tsx similarity index 90% rename from frontend/src/pages/WorkspaceDashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx rename to frontend/src/components/Modals/UploadDocumentModal/FileUploadProgress/index.tsx index 6af6c7ea..40879015 100644 --- a/frontend/src/pages/WorkspaceDashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx +++ b/frontend/src/components/Modals/UploadDocumentModal/FileUploadProgress/index.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, memo } from 'react'; -import Workspace from '../../../../../models/workspace'; +import Workspace from '../../../../models/workspace'; import truncate from 'truncate'; -import { humanFileSize, milliToHms } from '../../../../../utils/numbers'; +import { humanFileSize, milliToHms } from '../../../../utils/numbers'; import { CheckCircle, XCircle } from 'react-feather'; import { Grid } from 'react-loading-icons'; @@ -49,7 +49,7 @@ function FileUploadProgressComponent({
-

+

{truncate(file.name, 30)}

@@ -70,7 +70,7 @@ function FileUploadProgressComponent({ )}

-

+

{truncate(file.name, 30)}

diff --git a/frontend/src/pages/Dashboard/DocumentsList/UploadModal/index.tsx b/frontend/src/components/Modals/UploadDocumentModal/index.tsx similarity index 73% rename from frontend/src/pages/Dashboard/DocumentsList/UploadModal/index.tsx rename to frontend/src/components/Modals/UploadDocumentModal/index.tsx index aa37d1df..a36d05af 100644 --- a/frontend/src/pages/Dashboard/DocumentsList/UploadModal/index.tsx +++ b/frontend/src/components/Modals/UploadDocumentModal/index.tsx @@ -1,19 +1,23 @@ import { useCallback, useState, useEffect, ReactNode } from 'react'; -import { APP_NAME } from '../../../../utils/constants'; +import { APP_NAME } from '../../../utils/constants'; import { useDropzone } from 'react-dropzone'; import { v4 } from 'uuid'; -import System from '../../../../models/system'; -import { Frown } from 'react-feather'; +import System from '../../../models/system'; import FileUploadProgress from './FileUploadProgress'; import { useParams } from 'react-router-dom'; +import { SmileySad } from '@phosphor-icons/react'; export default function UploadDocumentModal({ workspaces, + workspace, }: { - workspaces: any; + workspaces?: any[]; + workspace?: any; }) { const { slug } = useParams(); - const [targetWorkspace, setTargetWorkspace] = useState(null); + const [targetWorkspace, setTargetWorkspace] = useState( + workspace ? { ...workspace } : null + ); const [ready, setReady] = useState(null); const [files, setFiles] = useState([]); const [fileTypes, setFileTypes] = useState({}); @@ -60,12 +64,12 @@ export default function UploadDocumentModal({ if (ready === null || !slug) { return ( -

+

Checking document processor is online - please wait.

-

+

this should only take a few moments.

@@ -78,8 +82,8 @@ export default function UploadDocumentModal({ return (
-
- +
+

Document processor is offline.

@@ -92,7 +96,9 @@ export default function UploadDocumentModal({ ); } - if (ready === true && targetWorkspace === null) { + // When workspace was not given as a prop we already know the workspace to + // target so do not allow a re-selection. + if (ready === true && targetWorkspace === null && !workspace) { const saveWorkspace = (e: any) => { e.preventDefault(); const form = new FormData(e.target); @@ -106,21 +112,21 @@ export default function UploadDocumentModal({ return ( -
+
-

+

Please select the workspace you wish to upload documents to.

-
+ -
@@ -135,7 +141,7 @@ export default function UploadDocumentModal({
{files.length === 0 ? ( @@ -143,7 +149,7 @@ export default function UploadDocumentModal({
-

- Click to upload or drag +

+ Click to upload or drag and drop

@@ -178,9 +184,9 @@ export default function UploadDocumentModal({
)}
-

- supported file extensions are{' '} - +

+ Supported file extensions are{' '} + {Object.values(fileTypes).flat().join(' ')}

@@ -193,16 +199,14 @@ const ModalWrapper = ({ children }: { children: ReactNode }) => { return ( { event.target == event.currentTarget && event.currentTarget?.close(); }} > -
-

- Upload new document -

-

+

+

Upload new document

+

Select a workspace and document you wish to upload and {APP_NAME} will process, embed and store the data for you automatically.

diff --git a/frontend/src/components/Modals/UploadModalNoKey.tsx b/frontend/src/components/Modals/UploadModalNoKey.tsx new file mode 100644 index 00000000..7fb910b9 --- /dev/null +++ b/frontend/src/components/Modals/UploadModalNoKey.tsx @@ -0,0 +1,77 @@ +import { AlertTriangle } from 'react-feather'; +import { APP_NAME } from '../../utils/constants'; +import System from '../../models/system'; +import { ReactNode } from 'react'; + +export default function UploadModalNoKey() { + const updateSystemSetting = async (e: any) => { + e.preventDefault(); + const form = new FormData(e.target); + const open_ai_api_key = form.get('open_ai_api_key'); + await System.updateSettings({ open_ai_api_key }); + window.location.reload(); + }; + + return ( + +
+
+ You cannot upload and embed documents without an + OpenAI API Key. +
+

+ {APP_NAME} will automatically upload and embed your documents for you, + but for this to happen we must have an OpenAI key set. +

+
+
+
+ + +
+
+ +
+
+

+ This key will only be used for the embedding of documents you upload + via {APP_NAME}. +

+
+
+
+ ); +} + +const ModalWrapper = ({ children }: { children: ReactNode }) => { + return ( + { + event.target == event.currentTarget && event.currentTarget?.close(); + }} + > +
+

Upload new document

+

+ Select a workspace and document you wish to upload and {APP_NAME} will + process, embed and store the data for you automatically. +

+
+
{children}
+
+ ); +}; diff --git a/frontend/src/components/Notifications/index.tsx b/frontend/src/components/Notifications/index.tsx new file mode 100644 index 00000000..342627a7 --- /dev/null +++ b/frontend/src/components/Notifications/index.tsx @@ -0,0 +1,391 @@ +import { ReactNode, useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import Organization from '../../models/organization'; +import { databaseTimestampFromNow } from '../../utils/data'; +import ChromaLogo from '../../images/vectordbs/chroma.png'; +import PineconeLogo from '../../images/vectordbs/pinecone-inverted.png'; +import qDrantLogo from '../../images/vectordbs/qdrant.png'; +import WeaviateLogo from '../../images/vectordbs/weaviate.png'; +import { Bell, Info, Warning, WarningOctagon } from '@phosphor-icons/react'; + +const POLLING_INTERVAL = 30_000; + +export type INotification = { + id: number; + organization_id: number; + seen: boolean; + textContent: string; + symbol?: + | 'info' + | 'warning' + | 'error' + | 'chroma' + | 'pinecone' + | 'weaviate' + | 'qdrant'; + link?: string; + target?: '_blank' | 'self'; + createdAt: string; + lastUpdatedAt: string; +}; + +export default function Notifications() { + const { slug } = useParams(); + const [loading, setLoading] = useState(true); + const [notifications, setNotifications] = useState([]); + const [hasUnseen, setHasUnseen] = useState(false); + const [showNotifs, setShowNotifs] = useState(false); + const notificationRef = useRef(null); + const bellButtonRef = useRef(null); + + async function handleClick() { + if (!showNotifs) { + !!slug && Organization.markNotificationsSeen(slug); + setShowNotifs(true); + setHasUnseen(false); + } else { + setShowNotifs(false); + } + } + useEffect(() => { + function handleClickOutside(event: any) { + if ( + bellButtonRef.current && + bellButtonRef.current.contains(event.target) + ) { + return; + } + if ( + notificationRef.current && + !notificationRef.current.contains(event.target) + ) { + setShowNotifs(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + async function fetchNotifications() { + if (!slug) { + setLoading(false); + return; + } + + const { notifications: _notifications } = await Organization.notifications( + slug + ); + setNotifications(_notifications); + setHasUnseen(_notifications.some((notif) => notif.seen === false)); + setLoading(false); + } + + useEffect(() => { + if (!slug) return; + fetchNotifications(); + setInterval(() => { + fetchNotifications(); + }, POLLING_INTERVAL); + }, [slug]); + + if (loading) return null; + + return ( +
+ + + +
+ ); +} + +function NotificationImage({ notification }: { notification: INotification }) { + switch (notification.symbol) { + case 'info': + return ( +
+ +
+ ); + case 'warning': + return ( +
+ +
+ ); + case 'error': + return ( +
+ +
+ ); + case 'chroma': + return ( +
+ Chroma Logo +
+ ); + case 'pinecone': + return ( +
+ Pinecone Logo +
+ ); + case 'qdrant': + return ( +
+ qDrant Logo +
+ ); + case 'weaviate': + return ( +
+ Weaviate Logo +
+ ); + default: + return ( +
+ +
+ ); + } +} + +function NotificationWrapper({ + notification, + children, +}: { + notification: INotification; + children: ReactNode; +}) { + if (!!notification.link) { + return ( + + {children} + + ); + } + return ( +
+ {children} +
+ ); +} + +function Notification({ notification }: { notification: INotification }) { + return ( + +
+ +
+
+
+ {notification.textContent} +
+
+ {databaseTimestampFromNow(notification.createdAt)} +
+
+
+ ); +} + +// +// +// +// +// +// +// {' '} +// {' '} +// diff --git a/frontend/src/components/Preloader.tsx b/frontend/src/components/Preloader.tsx index 6201d919..e03cc12c 100644 --- a/frontend/src/components/Preloader.tsx +++ b/frontend/src/components/Preloader.tsx @@ -1,6 +1,6 @@ export default function PreLoader() { return ( -
+
); } @@ -8,7 +8,7 @@ export function FullScreenLoader() { return (
diff --git a/frontend/src/components/Sidebar/CreateOrganizationModal/index.tsx b/frontend/src/components/Sidebar/CreateOrganizationModal/index.tsx index 4ae4bdc3..76bda003 100644 --- a/frontend/src/components/Sidebar/CreateOrganizationModal/index.tsx +++ b/frontend/src/components/Sidebar/CreateOrganizationModal/index.tsx @@ -18,10 +18,13 @@ export default function CreateOrganizationModal() { }; return ( - -
+ +
-

+

Create a New Organization

@@ -35,7 +38,7 @@ export default function CreateOrganizationModal() {
-
-

+

Once your organization exists you can start workspaces and documents.

diff --git a/frontend/src/components/Sidebar/OrganizationTab/index.tsx b/frontend/src/components/Sidebar/OrganizationTab/index.tsx new file mode 100644 index 00000000..9b8161d3 --- /dev/null +++ b/frontend/src/components/Sidebar/OrganizationTab/index.tsx @@ -0,0 +1,238 @@ +import { NavLink, useParams } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import paths from '../../../utils/paths'; +import { CaretDown, Plus, MagnifyingGlass } from '@phosphor-icons/react'; +import truncate from 'truncate'; +import Organization from '../../../models/organization'; +import { debounce } from 'lodash'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import CreateWorkspaceModal from '../../../pages/Dashboard/WorkspacesList/CreateWorkspaceModal'; + +type OrganizationTabProps = { + organization: any; + i: number; + workspaces: any; + hasMoreWorkspaces: boolean; + loadMoreWorkspaces?: VoidFunction; +}; + +const debouncedSearch = debounce( + async (searchTerm, setResults, setIsSearching, slug) => { + if (!slug) return; + setIsSearching(true); + + const { workspacesResults = [] } = await Organization.searchWorkspaces( + slug, + 1, // Page 1 + 30, // 30 results per page + searchTerm + ); + + setResults(workspacesResults); + setIsSearching(false); + }, + 500 +); + +export default function OrganizationTab({ + organization, + workspaces, + i, + hasMoreWorkspaces, + loadMoreWorkspaces, +}: OrganizationTabProps) { + const { slug } = useParams(); + const [isActive, setIsActive] = useState(false); + const [menuOpen, setMenuOpen] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + + const toggleMenu = () => { + setMenuOpen(!menuOpen); + }; + + const renderWorkspaceItem = (workspace: any) => ( + + ); + + const loadMoreWorkspacesAndScrollToBottom = async () => { + loadMoreWorkspaces?.(); + const organizationList = document.getElementById('organization-list'); + if (organizationList) { + organizationList.scrollTop = organizationList.scrollHeight; + } + }; + + useEffect(() => { + if (searchTerm !== '') { + setIsSearching(true); + debouncedSearch(searchTerm, setSearchResults, setIsSearching, slug); + } else { + setSearchResults(workspaces); + setIsSearching(false); + } + }, [searchTerm, slug]); + + return ( +
  • + { + setIsActive(active); + return `group relative flex w-full items-center justify-between rounded-lg border border-transparent bg-main-2 px-4 py-3 font-medium text-white duration-300 ease-in-out hover:border-sky-400 hover:text-white ${ + active ? 'border-sky-400 !text-white' : '' + }`; + }} + > +
    +
    +
    + {truncate(organization.name, 19)} +
    +
    + +
    +
    +
    +
    + {isActive && ( +
    +
    +
    + + + +
    + Workspaces +
    +
    + +
    +
    + + setSearchTerm(e.target.value)} + className="border-none bg-transparent text-sm text-white/60 placeholder-white/60 focus:outline-none" + /> +
    + + {isSearching ? ( + + ) : searchTerm !== '' && searchResults.length > 0 ? ( +
    + {searchResults.map((workspace, idx) => ( + + ))} +
    + ) : searchTerm !== '' && searchResults.length === 0 ? ( +
    +
    +

    No results found.

    +
    +
    + ) : workspaces.length > 0 ? ( +
    + 5 ? 150 : workspaces.length * 30} + next={loadMoreWorkspacesAndScrollToBottom} + hasMore={hasMoreWorkspaces} + loader={} + > + {workspaces.map(renderWorkspaceItem)} + +
    + ) : ( +
    +
    +

    + No workspaces,{' '} + + . +

    +
    +
    + )} + +
    + )} +
  • + ); +} + +function WorkspaceItem({ workspace, slug }: any) { + return ( +
  • + { + return `text-sm font-normal leading-tight text-sky-400 hover:cursor-pointer hover:text-sky-400 hover:underline ${ + isActive ? 'text-sky-400' : 'text-white/60' + }`; + }} + > + {truncate(workspace.name, 23)} + +
  • + ); +} + +function LoadingWorkspaceItem() { + return ( +
    +
    +

    Loading...

    +
    +
    + ); +} diff --git a/frontend/src/components/Sidebar/WorkspaceSearch/index.tsx b/frontend/src/components/Sidebar/WorkspaceSearch/index.tsx index 76635da0..bf46dc80 100644 --- a/frontend/src/components/Sidebar/WorkspaceSearch/index.tsx +++ b/frontend/src/components/Sidebar/WorkspaceSearch/index.tsx @@ -3,6 +3,7 @@ import { NavLink, useParams } from 'react-router-dom'; import paths from '../../../utils/paths'; import Organization from '../../../models/organization'; import { debounce } from 'lodash'; +import truncate from 'truncate'; interface IWorkspaceItem { workspace: { @@ -127,7 +128,7 @@ export function WorkspaceItem({ workspace, slug }: IWorkspaceItem) { (isActive && '!text-white') } > - {workspace.name} + {truncate(workspace.name, 10)} ); diff --git a/frontend/src/components/Sidebar/index.tsx b/frontend/src/components/Sidebar/index.tsx index b40cf0b1..fd7c5e0a 100644 --- a/frontend/src/components/Sidebar/index.tsx +++ b/frontend/src/components/Sidebar/index.tsx @@ -1,23 +1,12 @@ import React, { useEffect, useRef, useState } from 'react'; import { NavLink, useLocation, useParams } from 'react-router-dom'; -import Logo from '../../images/logo/logo-light.png'; +import LogoSky from '../../images/logo/logo-sky.svg'; import SidebarLinkGroup from '../SidebarLinkGroup'; import paths from '../../utils/paths'; -import { - Box, - Briefcase, - ChevronUp, - Command, - Package, - Radio, - Tool, - Users, -} from 'react-feather'; -import Organization from '../../models/organization'; import useUser from '../../hooks/useUser'; -import InfiniteScroll from 'react-infinite-scroll-component'; -import WorkspaceSearch, { WorkspaceItem } from './WorkspaceSearch'; import CreateOrganizationModal from './CreateOrganizationModal'; +import OrganizationTab from './OrganizationTab'; +import { SquaresFour, Plus } from '@phosphor-icons/react'; interface SidebarProps { organization: any; @@ -56,6 +45,12 @@ export default function Sidebar({ return true; } + const sortedOrganizations = organizations.sort((a, b) => { + if (a.slug === slug) return -1; + if (b.slug === slug) return 1; + return 0; + }); + // close on click outside useEffect(() => { const clickHandler = ({ target }: MouseEvent) => { @@ -95,14 +90,19 @@ export default function Sidebar({ <>
    - {/* */}
    diff --git a/frontend/src/components/UserMenu/index.tsx b/frontend/src/components/UserMenu/index.tsx new file mode 100644 index 00000000..a42a01e3 --- /dev/null +++ b/frontend/src/components/UserMenu/index.tsx @@ -0,0 +1,60 @@ +import { useState, useRef, useEffect } from 'react'; +import useUser from '../../hooks/useUser'; +import paths from '../../utils/paths'; +import { STORE_TOKEN, STORE_USER } from '../../utils/constants'; + +export default function UserMenu() { + const { user } = useUser(); + const [showMenu, setShowMenu] = useState(false); + const menuRef = useRef(null); + const buttonRef = useRef(); + const handleClose = (event: any) => { + if ( + menuRef.current && + !menuRef.current.contains(event.target) && + !buttonRef.current.contains(event.target) + ) { + setShowMenu(false); + } + }; + + useEffect(() => { + if (showMenu) { + document.addEventListener('mousedown', handleClose); + } + return () => document.removeEventListener('mousedown', handleClose); + }, [showMenu]); + + return ( +
    + + {showMenu && ( +
    +
    + +
    +
    + )} +
    + ); +} diff --git a/frontend/src/components/VectorDBOption/index.tsx b/frontend/src/components/VectorDBOption/index.tsx new file mode 100644 index 00000000..464e5b0b --- /dev/null +++ b/frontend/src/components/VectorDBOption/index.tsx @@ -0,0 +1,57 @@ +type VectorDBOptionProps = { + name: string; + link: string; + description: string; + value: string; + image: string; + checked?: boolean; + onClick: (value: string) => void; +}; + +export default function VectorDBOption({ + name, + link, + description, + value, + image, + checked = false, + onClick, +}: VectorDBOptionProps) { + return ( +
    onClick(value)} + style={{ + background: checked + ? `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)` + : `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`, + }} + className={`flex h-full w-60 cursor-pointer flex-col items-start justify-between rounded-2xl border-2 border-transparent px-5 py-4 text-white shadow-md transition-all duration-300 ${ + checked ? 'border-white/60' : '' + } hover:border-white/60 hover:shadow-lg md:max-w-sm`} + > + + +
    + ); +} diff --git a/frontend/src/fonts/Satoshi-Black.eot b/frontend/src/fonts/Satoshi-Black.eot deleted file mode 100644 index 11747f36..00000000 Binary files a/frontend/src/fonts/Satoshi-Black.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Black.ttf b/frontend/src/fonts/Satoshi-Black.ttf deleted file mode 100644 index 62015aca..00000000 Binary files a/frontend/src/fonts/Satoshi-Black.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Black.woff b/frontend/src/fonts/Satoshi-Black.woff deleted file mode 100644 index a6bee36d..00000000 Binary files a/frontend/src/fonts/Satoshi-Black.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Black.woff2 b/frontend/src/fonts/Satoshi-Black.woff2 deleted file mode 100644 index 64492d52..00000000 Binary files a/frontend/src/fonts/Satoshi-Black.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-BlackItalic.eot b/frontend/src/fonts/Satoshi-BlackItalic.eot deleted file mode 100644 index de2edbbc..00000000 Binary files a/frontend/src/fonts/Satoshi-BlackItalic.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-BlackItalic.ttf b/frontend/src/fonts/Satoshi-BlackItalic.ttf deleted file mode 100644 index 74410b97..00000000 Binary files a/frontend/src/fonts/Satoshi-BlackItalic.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-BlackItalic.woff b/frontend/src/fonts/Satoshi-BlackItalic.woff deleted file mode 100644 index 0e07e1c5..00000000 Binary files a/frontend/src/fonts/Satoshi-BlackItalic.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-BlackItalic.woff2 b/frontend/src/fonts/Satoshi-BlackItalic.woff2 deleted file mode 100644 index 9d5c911d..00000000 Binary files a/frontend/src/fonts/Satoshi-BlackItalic.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Bold.eot b/frontend/src/fonts/Satoshi-Bold.eot deleted file mode 100644 index 390ae252..00000000 Binary files a/frontend/src/fonts/Satoshi-Bold.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Bold.woff b/frontend/src/fonts/Satoshi-Bold.woff deleted file mode 100644 index bba8257f..00000000 Binary files a/frontend/src/fonts/Satoshi-Bold.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Bold.woff2 b/frontend/src/fonts/Satoshi-Bold.woff2 deleted file mode 100644 index 0a8db7a4..00000000 Binary files a/frontend/src/fonts/Satoshi-Bold.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-BoldItalic.eot b/frontend/src/fonts/Satoshi-BoldItalic.eot deleted file mode 100644 index 426be2ac..00000000 Binary files a/frontend/src/fonts/Satoshi-BoldItalic.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-BoldItalic.ttf b/frontend/src/fonts/Satoshi-BoldItalic.ttf deleted file mode 100644 index 24f012cb..00000000 Binary files a/frontend/src/fonts/Satoshi-BoldItalic.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-BoldItalic.woff b/frontend/src/fonts/Satoshi-BoldItalic.woff deleted file mode 100644 index 8bcb7a6e..00000000 Binary files a/frontend/src/fonts/Satoshi-BoldItalic.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-BoldItalic.woff2 b/frontend/src/fonts/Satoshi-BoldItalic.woff2 deleted file mode 100644 index 225527f7..00000000 Binary files a/frontend/src/fonts/Satoshi-BoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Italic.eot b/frontend/src/fonts/Satoshi-Italic.eot deleted file mode 100644 index 64039a84..00000000 Binary files a/frontend/src/fonts/Satoshi-Italic.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Italic.ttf b/frontend/src/fonts/Satoshi-Italic.ttf deleted file mode 100644 index c214f4fe..00000000 Binary files a/frontend/src/fonts/Satoshi-Italic.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Italic.woff b/frontend/src/fonts/Satoshi-Italic.woff deleted file mode 100644 index edd4d932..00000000 Binary files a/frontend/src/fonts/Satoshi-Italic.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Italic.woff2 b/frontend/src/fonts/Satoshi-Italic.woff2 deleted file mode 100644 index 8b98599d..00000000 Binary files a/frontend/src/fonts/Satoshi-Italic.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Light.eot b/frontend/src/fonts/Satoshi-Light.eot deleted file mode 100644 index d8fcaccd..00000000 Binary files a/frontend/src/fonts/Satoshi-Light.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Light.ttf b/frontend/src/fonts/Satoshi-Light.ttf deleted file mode 100644 index b41a2d4a..00000000 Binary files a/frontend/src/fonts/Satoshi-Light.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Light.woff b/frontend/src/fonts/Satoshi-Light.woff deleted file mode 100644 index 8f05e4e9..00000000 Binary files a/frontend/src/fonts/Satoshi-Light.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Light.woff2 b/frontend/src/fonts/Satoshi-Light.woff2 deleted file mode 100644 index cf18cd4c..00000000 Binary files a/frontend/src/fonts/Satoshi-Light.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-LightItalic.eot b/frontend/src/fonts/Satoshi-LightItalic.eot deleted file mode 100644 index e34a0df4..00000000 Binary files a/frontend/src/fonts/Satoshi-LightItalic.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-LightItalic.ttf b/frontend/src/fonts/Satoshi-LightItalic.ttf deleted file mode 100644 index 08f5db57..00000000 Binary files a/frontend/src/fonts/Satoshi-LightItalic.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-LightItalic.woff b/frontend/src/fonts/Satoshi-LightItalic.woff deleted file mode 100644 index a03a50d7..00000000 Binary files a/frontend/src/fonts/Satoshi-LightItalic.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-LightItalic.woff2 b/frontend/src/fonts/Satoshi-LightItalic.woff2 deleted file mode 100644 index 6bd15ad5..00000000 Binary files a/frontend/src/fonts/Satoshi-LightItalic.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Medium.eot b/frontend/src/fonts/Satoshi-Medium.eot deleted file mode 100644 index 83caceca..00000000 Binary files a/frontend/src/fonts/Satoshi-Medium.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Medium.ttf b/frontend/src/fonts/Satoshi-Medium.ttf deleted file mode 100644 index ab149b71..00000000 Binary files a/frontend/src/fonts/Satoshi-Medium.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Medium.woff b/frontend/src/fonts/Satoshi-Medium.woff deleted file mode 100644 index cef3226e..00000000 Binary files a/frontend/src/fonts/Satoshi-Medium.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Medium.woff2 b/frontend/src/fonts/Satoshi-Medium.woff2 deleted file mode 100644 index ffd0ac96..00000000 Binary files a/frontend/src/fonts/Satoshi-Medium.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-MediumItalic.eot b/frontend/src/fonts/Satoshi-MediumItalic.eot deleted file mode 100644 index 25d229a5..00000000 Binary files a/frontend/src/fonts/Satoshi-MediumItalic.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-MediumItalic.ttf b/frontend/src/fonts/Satoshi-MediumItalic.ttf deleted file mode 100644 index 387f278e..00000000 Binary files a/frontend/src/fonts/Satoshi-MediumItalic.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-MediumItalic.woff b/frontend/src/fonts/Satoshi-MediumItalic.woff deleted file mode 100644 index 46d8995a..00000000 Binary files a/frontend/src/fonts/Satoshi-MediumItalic.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-MediumItalic.woff2 b/frontend/src/fonts/Satoshi-MediumItalic.woff2 deleted file mode 100644 index 212adc92..00000000 Binary files a/frontend/src/fonts/Satoshi-MediumItalic.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Regular.eot b/frontend/src/fonts/Satoshi-Regular.eot deleted file mode 100644 index 452666f4..00000000 Binary files a/frontend/src/fonts/Satoshi-Regular.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Regular.woff b/frontend/src/fonts/Satoshi-Regular.woff deleted file mode 100644 index 03ac1952..00000000 Binary files a/frontend/src/fonts/Satoshi-Regular.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Regular.woff2 b/frontend/src/fonts/Satoshi-Regular.woff2 deleted file mode 100644 index 81c40ab0..00000000 Binary files a/frontend/src/fonts/Satoshi-Regular.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Variable.eot b/frontend/src/fonts/Satoshi-Variable.eot deleted file mode 100644 index f42624e1..00000000 Binary files a/frontend/src/fonts/Satoshi-Variable.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Variable.ttf b/frontend/src/fonts/Satoshi-Variable.ttf deleted file mode 100644 index 976e85cb..00000000 Binary files a/frontend/src/fonts/Satoshi-Variable.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Variable.woff b/frontend/src/fonts/Satoshi-Variable.woff deleted file mode 100644 index f8dcd1d6..00000000 Binary files a/frontend/src/fonts/Satoshi-Variable.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-Variable.woff2 b/frontend/src/fonts/Satoshi-Variable.woff2 deleted file mode 100644 index b00e833e..00000000 Binary files a/frontend/src/fonts/Satoshi-Variable.woff2 and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-VariableItalic.eot b/frontend/src/fonts/Satoshi-VariableItalic.eot deleted file mode 100644 index 5f4554af..00000000 Binary files a/frontend/src/fonts/Satoshi-VariableItalic.eot and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-VariableItalic.ttf b/frontend/src/fonts/Satoshi-VariableItalic.ttf deleted file mode 100644 index 4c2677c6..00000000 Binary files a/frontend/src/fonts/Satoshi-VariableItalic.ttf and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-VariableItalic.woff b/frontend/src/fonts/Satoshi-VariableItalic.woff deleted file mode 100644 index 3fe029e2..00000000 Binary files a/frontend/src/fonts/Satoshi-VariableItalic.woff and /dev/null differ diff --git a/frontend/src/fonts/Satoshi-VariableItalic.woff2 b/frontend/src/fonts/Satoshi-VariableItalic.woff2 deleted file mode 100644 index e7ab3a09..00000000 Binary files a/frontend/src/fonts/Satoshi-VariableItalic.woff2 and /dev/null differ diff --git a/frontend/src/images/logo/logo-sky.svg b/frontend/src/images/logo/logo-sky.svg new file mode 100644 index 00000000..2e765371 --- /dev/null +++ b/frontend/src/images/logo/logo-sky.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/images/undraws/onboarding.png b/frontend/src/images/undraws/onboarding.png new file mode 100644 index 00000000..837a8b08 Binary files /dev/null and b/frontend/src/images/undraws/onboarding.png differ diff --git a/frontend/src/images/undraws/sign-in.png b/frontend/src/images/undraws/sign-in.png new file mode 100644 index 00000000..190d4b80 Binary files /dev/null and b/frontend/src/images/undraws/sign-in.png differ diff --git a/frontend/src/images/vectordbs/chroma.png b/frontend/src/images/vectordbs/chroma.png index fde25384..f937631f 100644 Binary files a/frontend/src/images/vectordbs/chroma.png and b/frontend/src/images/vectordbs/chroma.png differ diff --git a/frontend/src/images/vectordbs/pinecone-inverted.png b/frontend/src/images/vectordbs/pinecone-inverted.png new file mode 100644 index 00000000..591c3b65 Binary files /dev/null and b/frontend/src/images/vectordbs/pinecone-inverted.png differ diff --git a/frontend/src/images/vectordbs/pinecone.png b/frontend/src/images/vectordbs/pinecone.png index 9f20d7f7..9b9a9fe0 100644 Binary files a/frontend/src/images/vectordbs/pinecone.png and b/frontend/src/images/vectordbs/pinecone.png differ diff --git a/frontend/src/images/vectordbs/qdrant.png b/frontend/src/images/vectordbs/qdrant.png index 138c88cd..3d3d8d2f 100644 Binary files a/frontend/src/images/vectordbs/qdrant.png and b/frontend/src/images/vectordbs/qdrant.png differ diff --git a/frontend/src/images/vectordbs/weaviate.png b/frontend/src/images/vectordbs/weaviate.png index d7980bf6..f93b1c9b 100644 Binary files a/frontend/src/images/vectordbs/weaviate.png and b/frontend/src/images/vectordbs/weaviate.png differ diff --git a/frontend/src/index.css b/frontend/src/index.css index 772ec930..4e8ee615 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,7 +1,42 @@ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;700&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; +@font-face { + font-family: 'Satoshi-Bold'; + src: url('./fonts/Satoshi-Bold.ttf') format('truetype'); + font-weight: 700; + font-display: swap; + font-style: normal; +} + +@font-face { + font-family: 'Satoshi'; + src: url('./fonts/Satoshi-Regular.ttf') format('truetype'); + font-weight: 300; + font-display: swap; + font-style: normal; +} + +.font-satoshi.font-bold { + font-family: 'Satoshi-Bold', sans-serif; +} + +.font-satoshi { + font-family: 'Satoshi', sans-serif; +} + +.font-jetbrains { + font-family: 'JetBrains Mono', monospace; + font-weight: 300; +} + +.font-jetbrainsbold { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; +} + /* Chrome, Safari and Opera */ .no-scrollbar::-webkit-scrollbar { display: none; @@ -26,7 +61,7 @@ dialog { pointer-events: none; opacity: 0; transition: opacity 0.2s; - display: flex; + display: none; flex-direction: column; align-items: center; justify-content: center; @@ -34,6 +69,7 @@ dialog { dialog[open] { opacity: 1; + display: flex; pointer-events: inherit; } @@ -41,3 +77,44 @@ dialog::backdrop { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(2px); } + +.login-input-gradient { + background: linear-gradient( + 180deg, + rgba(61, 65, 71, 0.3) 0%, + rgba(44, 47, 53, 0.3) 100% + ) !important; + box-shadow: 0px 4px 30px rgba(0, 0, 0, 0.25); +} + +@keyframes slideDown { + from { + max-height: 0; + opacity: 0; + } + + to { + max-height: 200px; + opacity: 1; + } +} + +.slide-down { + animation: slideDown 0.3s ease-out forwards; +} + +@keyframes slideUp { + from { + max-height: 200px; + opacity: 1; + } + + to { + max-height: 0; + opacity: 0; + } +} + +.slide-up { + animation: slideUp 0.3s ease-out forwards; +} diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx index b0b81f96..62068875 100644 --- a/frontend/src/layout/AppLayout.tsx +++ b/frontend/src/layout/AppLayout.tsx @@ -1,6 +1,8 @@ import { ReactNode, useState } from 'react'; import Header from '../components/Header'; import Sidebar from '../components/Sidebar'; +import Notifications from '../components/Notifications'; +import UserMenu from '../components/UserMenu'; interface DefaultLayoutProps { headerEntity: any; @@ -13,6 +15,7 @@ interface DefaultLayoutProps { children: ReactNode; hasMoreWorkspaces?: boolean; loadMoreWorkspaces?: VoidFunction; + hasQuickActions?: boolean; } const AppLayout = ({ @@ -26,11 +29,12 @@ const AppLayout = ({ children, hasMoreWorkspaces, loadMoreWorkspaces, + hasQuickActions = false, }: DefaultLayoutProps) => { const [sidebarOpen, setSidebarOpen] = useState(false); return ( -
    +
    -
    +
    {!!headerEntity && (
    )} + +
    + + +
    -
    +
    {children}
    diff --git a/frontend/src/layout/DefaultLayout.tsx b/frontend/src/layout/DefaultLayout.tsx index ee405759..1843a116 100644 --- a/frontend/src/layout/DefaultLayout.tsx +++ b/frontend/src/layout/DefaultLayout.tsx @@ -6,7 +6,7 @@ interface DefaultLayoutProps { const DefaultLayout = ({ children }: DefaultLayoutProps) => { return ( -
    +
    diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 9772ace0..b6e8033e 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,7 +2,6 @@ import ReactDOM from 'react-dom/client'; import { BrowserRouter as Router } from 'react-router-dom'; import App from './App'; import './index.css'; -import './satoshi.css'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/frontend/src/models/document.ts b/frontend/src/models/document.ts index 84146144..3583465f 100644 --- a/frontend/src/models/document.ts +++ b/frontend/src/models/document.ts @@ -1,5 +1,4 @@ -import { ISearchTypes } from '../pages/DocumentView/FragmentList/SearchView'; -import { API_BASE } from '../utils/constants'; +import { API_BASE, ISearchTypes } from '../utils/constants'; import { baseHeaders, getAPIUrlString } from '../utils/request'; const Document = { diff --git a/frontend/src/models/organization.ts b/frontend/src/models/organization.ts index 0b577457..73486944 100644 --- a/frontend/src/models/organization.ts +++ b/frontend/src/models/organization.ts @@ -1,4 +1,4 @@ -import { INotification } from '../components/Header/Notifications'; +import { INotification } from '../components/Notifications'; import { API_BASE } from '../utils/constants'; import { baseHeaders, getAPIUrlString } from '../utils/request'; diff --git a/frontend/src/models/workspace.ts b/frontend/src/models/workspace.ts index a0799114..59deb6b3 100644 --- a/frontend/src/models/workspace.ts +++ b/frontend/src/models/workspace.ts @@ -1,5 +1,4 @@ -import { ISearchTypes } from '../pages/WorkspaceDashboard/DocumentsList/SearchView'; -import { API_BASE } from '../utils/constants'; +import { API_BASE, ISearchTypes } from '../utils/constants'; import { baseHeaders, getAPIUrlString } from '../utils/request'; const Workspace = { diff --git a/frontend/src/pages/Authentication/SignIn.tsx b/frontend/src/pages/Authentication/SignIn.tsx index bb91ded5..2fe7f379 100644 --- a/frontend/src/pages/Authentication/SignIn.tsx +++ b/frontend/src/pages/Authentication/SignIn.tsx @@ -1,11 +1,9 @@ import { Link } from 'react-router-dom'; -import LogoDark from '../../images/logo/logo-dark.png'; -import Logo from '../../images/logo/logo-light.png'; import DefaultLayout from '../../layout/DefaultLayout'; -import ManageSvg from '../../images/undraws/manage.svg'; +import SignInImg from '../../images/undraws/sign-in.png'; import PreLoader from '../../components/Preloader'; import { useEffect, useState } from 'react'; -import { CheckCircle, Key, Mail, XCircle } from 'react-feather'; +import { CheckCircle, XCircle } from 'react-feather'; import User from '../../models/user'; import { APP_NAME, STORE_TOKEN, STORE_USER } from '../../utils/constants'; import paths from '../../utils/paths'; @@ -55,7 +53,7 @@ const SignIn = () => { window.localStorage.setItem(STORE_USER, JSON.stringify(user)); window.localStorage.setItem(STORE_TOKEN, token); window.location.replace( - user.role === 'root' ? paths.systemSetup() : paths.dashboard() + user.role === 'root' ? paths.onboardingSetup() : paths.dashboard() ); } }; @@ -77,35 +75,15 @@ const SignIn = () => { return ( -
    +
    -
    - - Logo - Logo - - -

    - Did you know using {APP_NAME} can save you 75% on embedding - costs? -

    - - - - +
    + Sign In
    -
    +
    {stage !== 'ready' ? ( - Sign back in - -
    - -
    - - - - - -
    +
    +
    +
    + Log in to + VectorAdmin +
    +
    + + Welcome back, please log in to your account. +
    + +
    +
    + +
    +
    -
    - -
    - - - - - +
    +
    + +
    -
    -
    - -
    +
    + +
    -
    -

    - Don't have a {APP_NAME} account?{' '} - - Sign Up - -

    -
    - +
    +

    + Don't have a {APP_NAME} account?{' '} + + Sign Up + +

    +
    + +
    ); } diff --git a/frontend/src/pages/Authentication/SignUp.tsx b/frontend/src/pages/Authentication/SignUp.tsx index 0d0c7990..0106ec50 100644 --- a/frontend/src/pages/Authentication/SignUp.tsx +++ b/frontend/src/pages/Authentication/SignUp.tsx @@ -1,11 +1,9 @@ import { Link } from 'react-router-dom'; -import LogoDark from '../../images/logo/logo-dark.png'; -import Logo from '../../images/logo/logo-light.png'; -import ManageSvg from '../../images/undraws/manage.svg'; +import SignInImg from '../../images/undraws/sign-in.png'; import DefaultLayout from '../../layout/DefaultLayout'; import { useState } from 'react'; import PreLoader from '../../components/Preloader'; -import { CheckCircle, Key, Mail, XCircle } from 'react-feather'; +import { CheckCircle, XCircle } from 'react-feather'; import User from '../../models/user'; import { APP_NAME, STORE_TOKEN, STORE_USER } from '../../utils/constants'; import paths from '../../utils/paths'; @@ -59,7 +57,30 @@ const SignUp = () => { return ( -
    +
    +
    +
    +
    + Sign In +
    +
    + +
    +
    + {stage !== 'ready' ? ( + + ) : ( + + )} +
    +
    +
    +
    + {/*
    @@ -99,7 +120,7 @@ const SignUp = () => {
    -
    +
    */}
    ); }; @@ -153,69 +174,72 @@ function ShowStatus({ function LoginForm({ handleSubmit }: { handleSubmit: any }) { return ( <> - New account -

    - Sign Up for {APP_NAME} -

    - -
    -
    - -
    - - - - - -
    +
    +
    +
    + Sign Up for + {APP_NAME}
    + +
    +
    + +
    +
    -
    - -
    - - - - - +
    +
    + +
    -
    -
    - -
    +
    + +
    -
    -

    - Already have an account?{' '} - - Sign in - -

    -
    - +
    +

    + Already have an account?{' '} + + Log In + +

    +
    + +
    ); } diff --git a/frontend/src/pages/Authentication/SystemSetup.tsx b/frontend/src/pages/Authentication/SystemSetup.tsx deleted file mode 100644 index dcce7847..00000000 --- a/frontend/src/pages/Authentication/SystemSetup.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { Link } from 'react-router-dom'; -import LogoDark from '../../images/logo/logo-dark.png'; -import Logo from '../../images/logo/logo-light.png'; -import DefaultLayout from '../../layout/DefaultLayout'; -import ManageSvg from '../../images/undraws/manage.svg'; -import PreLoader from '../../components/Preloader'; -import { useState } from 'react'; -import { CheckCircle, Key, Mail, XCircle } from 'react-feather'; -import User from '../../models/user'; -import { APP_NAME, STORE_TOKEN, STORE_USER } from '../../utils/constants'; -import paths from '../../utils/paths'; - -type IStages = 'loading' | 'failed' | 'success' | 'ready'; -type FormTypes = { - target: { - email: { - value: string; - }; - password: { - value: string; - }; - }; -}; - -type IResult = { - user: any; - token: string | null; - error?: string | null; -}; - -const SystemSetup = () => { - const [stage, setStage] = useState('ready'); - const [results, setResults] = useState({ - user: null, - token: null, - error: null, - }); - const resetStage = () => { - setResults({ user: null, token: null, error: null }); - setStage('ready'); - }; - const handleSubmit = async (e: React.FormEvent & FormTypes) => { - e.preventDefault(); - setStage('loading'); - const { user, token, error } = await User.transferRootOwnership( - e.target.email.value, - e.target.password.value - ); - if (!token) setStage('failed'); - if (!!token) setStage('success'); - setResults({ user, token, error }); - - if (!!token) { - window.localStorage.setItem(STORE_USER, JSON.stringify(user)); - window.localStorage.setItem(STORE_TOKEN, token); - window.location.replace(paths.onboarding.orgName()); - } - }; - - return ( - -
    -
    -
    -
    - - Logo - Logo - - -

    - Did you know using {APP_NAME} can save you 75% on embedding - costs? -

    - - - - -
    -
    - -
    -
    - {stage !== 'ready' ? ( - - ) : ( - - )} -
    -
    -
    -
    -
    - ); -}; - -function ShowStatus({ - stage, - results, - resetForm, -}: { - stage: IStages; - results: IResult; - resetForm: any; -}) { - if (stage === 'loading') { - return ( -
    - -

    making you the system admin...

    -
    - ); - } - - if (stage === 'failed') { - return ( -
    - -

    - We could not complete this process - check the system logs. -

    -

    {results?.error}

    - -
    - ); - } - - if (stage === 'success') { - return ( -
    - -

    - Root account was deleted and you are now the system admin! -

    -

    - Redirecting you to the right place! -

    -
    - ); - } - - return null; -} - -function LoginForm({ handleSubmit }: { handleSubmit: any }) { - return ( - <> - - Create the System Administrator - -

    - By default {APP_NAME} creates a temporary root account so you can set up - a system admin account. After creation of this account the root account - will no longer be accessible and you will use these credentials to login - going forward. -
    -
    - If you lose your password you will never be able to recover it - so keep - it safe. -

    -
    -
    - -
    - - - - - -
    -
    - -
    - -
    - - - - - -
    -
    - -
    - -
    -
    - - ); -} - -export default SystemSetup; diff --git a/frontend/src/pages/Dashboard/Connector/index.tsx b/frontend/src/pages/Dashboard/Connector/index.tsx index aa4c1945..56a75b5b 100644 --- a/frontend/src/pages/Dashboard/Connector/index.tsx +++ b/frontend/src/pages/Dashboard/Connector/index.tsx @@ -1,14 +1,11 @@ import { useEffect, useState } from 'react'; import { CheckCircle, Circle, XCircle } from 'react-feather'; -import PreLoader from '../../../components/Preloader'; import Organization from '../../../models/organization'; -import { APP_NAME, SUPPORTED_VECTOR_DBS } from '../../../utils/constants'; -import ChromaLogo from '../../../images/vectordbs/chroma.png'; -import PineconeLogo from '../../../images/vectordbs/pinecone.png'; -import qDrantLogo from '../../../images/vectordbs/qdrant.png'; -import WeaviateLogo from '../../../images/vectordbs/weaviate.png'; -import paths from '../../../utils/paths'; +import { SUPPORTED_VECTOR_DBS } from '../../../utils/constants'; import { titleCase } from 'title-case'; +import SyncConnectorModal from '../../../components/Modals/SyncConnectorModal'; +import UpdateConnectorModal from '../../../components/Modals/UpdateConnectorModal'; +import NewConnectorModal from '../../../components/Modals/NewConnectorModal'; export default function ConnectorCard({ knownConnector, @@ -184,943 +181,3 @@ export default function ConnectorCard({ ); } - -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 -

    -

    - {APP_NAME} is 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 -

    -

    - {APP_NAME} can 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/Dashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx b/frontend/src/pages/Dashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx deleted file mode 100644 index 6af6c7ea..00000000 --- a/frontend/src/pages/Dashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useState, useEffect, memo } from 'react'; -import Workspace from '../../../../../models/workspace'; -import truncate from 'truncate'; -import { humanFileSize, milliToHms } from '../../../../../utils/numbers'; -import { CheckCircle, XCircle } from 'react-feather'; -import { Grid } from 'react-loading-icons'; - -function FileUploadProgressComponent({ - slug, - workspace, - file, - rejected = false, - reason = null, -}: { - workspace: any; - slug: string; - file: any; - rejected: any; - reason: any; -}) { - const [timerMs, setTimerMs] = useState(10); - const [status, setStatus] = useState(file?.rejected ? 'uploading' : 'failed'); - - useEffect(() => { - async function uploadFile() { - const start = Number(new Date()); - const formData = new FormData(); - formData.append('file', file, file.name); - const timer = setInterval(() => { - setTimerMs(Number(new Date()) - start); - }, 100); - - // Chunk streaming not working in production so we just sit and wait - const { success } = await Workspace.uploadFile( - slug, - workspace.slug, - formData - ); - setStatus(success ? 'complete' : 'failed'); - clearInterval(timer); - } - !!file && !rejected && uploadFile(); - }, []); - - if (rejected) { - return ( -
    -
    - -
    -
    -

    - {truncate(file.name, 30)} -

    -

    - {reason} -

    -
    -
    - ); - } - - return ( -
    -
    - {status !== 'complete' ? ( - - ) : ( - - )} -
    -
    -

    - {truncate(file.name, 30)} -

    -

    - {humanFileSize(file.size)} | {milliToHms(timerMs)} -

    -
    -
    - ); -} - -export default memo(FileUploadProgressComponent); diff --git a/frontend/src/pages/Dashboard/DocumentsList/UploadModal/UploadModalNoKey.tsx b/frontend/src/pages/Dashboard/DocumentsList/UploadModal/UploadModalNoKey.tsx deleted file mode 100644 index 8f74e7ff..00000000 --- a/frontend/src/pages/Dashboard/DocumentsList/UploadModal/UploadModalNoKey.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { AlertTriangle } from 'react-feather'; -import { APP_NAME } from '../../../../utils/constants'; -import System from '../../../../models/system'; - -export default function UploadModalNoKey() { - const updateSystemSetting = async (e: any) => { - e.preventDefault(); - const form = new FormData(e.target); - const open_ai_api_key = form.get('open_ai_api_key'); - await System.updateSettings({ open_ai_api_key }); - window.location.reload(); - }; - - return ( - - event.target == event.currentTarget && event.currentTarget?.close() - } - > -
    -

    - Upload new document -

    -
    -
    -
    -
    - You cannot upload and embed documents without an - OpenAI API Key. -
    -

    - {APP_NAME} will automatically upload and embed your documents for - you, but for this to happen we must have an OpenAI key set. -

    -
    -
    -
    - - -
    -
    - -
    -
    -

    - This key will only be used for the embedding of documents you - upload via {APP_NAME}. -

    -
    -
    -
    -
    - ); -} diff --git a/frontend/src/pages/Dashboard/DocumentsList/index.tsx b/frontend/src/pages/Dashboard/DocumentsList/index.tsx index e9a8640d..bad073be 100644 --- a/frontend/src/pages/Dashboard/DocumentsList/index.tsx +++ b/frontend/src/pages/Dashboard/DocumentsList/index.tsx @@ -1,16 +1,15 @@ -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'; import System from '../../../models/system'; -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'; +import UploadModalNoKey from '../../../components/Modals/UploadModalNoKey'; +import UploadDocumentModal from '../../../components/Modals/UploadDocumentModal'; 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,266 +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 +
    - ) : ( - - )} - - ) : ( - +
    +
    )} -
    - {documents.length > 0 ? ( -
    -
    -
    -
    - Document Name -
    -
    - Workspace -
    -
    - Created -
    -
    - Status + + {!!knownConnector && workspaces?.length === 0 && ( +
    +
    +
    + Create a workspace to get started
    -
    - +
    + Workspaces are used to organize your documents
    +
    - <> - {documents.map((document) => { - return ( -
    -
    -
    -
    - - - {truncate(document.name, 20)} - -
    -
    - -
    - - {moment(document.createdAt).format('lll')} - -
    -
    - - Cached - -
    + )} - -
    -
    - ); - })} - -
    - ) : ( -
    -
    -
    -

    You have no documents in any workspaces!

    -

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

    + {documents?.length === 0 && workspaces?.length > 0 && ( +
    +
    +
    + 0 Documents +
    +
    + Upload documents to your workspace +
    -
    + )} +
    + +
    + +
    + {canUpload ? ( + + ) : ( + )}
    - - {canUpload ? ( - - ) : ( - - )} ); } -// const CodeExampleModal = ({ organization }: { organization: any }) => { -// // Rework this to be an upload modal. -// return ( -// -//
    -//
    -//

    -// Adding documents to Conifer -//

    -//

    -// You can begin managing documents with the code you have already -// written. Our library currently only supports NodeJS environments. -//

    -//
    - -//

    -// During the Pinecone Hackathon the library is a standalone fork of -// langchainJS, but ideally it would eventually be live in the main -// LangchainJS repo :) -//
    -// We werent able to add uploading or deleting docs via the UI but how -// cool would that be. It can be done via the library though. -//

    - -//
    -// -//
    - -//
    -// -//
    -//
    -//
    -// ); -// }; +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 00000000..369d2a6a --- /dev/null +++ b/frontend/src/pages/Dashboard/QuickActionSidebar/index.tsx @@ -0,0 +1,84 @@ +import { + CaretDown, + ShieldCheckered, + 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' && ( + <> + +
    + +
    System Settings
    +
    +
    + + +
    + +
    Tools
    +
    +
    + + +
    + +
    Add User
    +
    +
    + + )} + +
    + +
    Background Jobs
    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/Dashboard/Statistics/index.tsx b/frontend/src/pages/Dashboard/Statistics/index.tsx index f3be2f70..e0c25770 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)} -

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

    - {nFormatter(vectors.value)} -

    -

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

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

    - {humanFileSize(cache.value)} -

    -

    Vector Cache (MB)

    -
    - )} +
    +
    +
    + + Documents + + + {' '} + + + ({nFormatter(documents.value)}) +
    +
    -
    -
    -

    - {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/CreateWorkspaceModal/index.tsx b/frontend/src/pages/Dashboard/WorkspacesList/CreateWorkspaceModal/index.tsx index b071a83d..2dfb3c99 100644 --- a/frontend/src/pages/Dashboard/WorkspacesList/CreateWorkspaceModal/index.tsx +++ b/frontend/src/pages/Dashboard/WorkspacesList/CreateWorkspaceModal/index.tsx @@ -46,6 +46,8 @@ export default function CreateWorkspaceModal({ } setImported(true); + + window.location.replace(paths.workspace(organization.slug, workspace.slug)); }; const handleSubmit = async (e: any) => { @@ -62,7 +64,7 @@ export default function CreateWorkspaceModal({ return false; } - window.location.reload(); + window.location.replace(paths.workspace(organization.slug, workspace.slug)); }; if (imported) { @@ -103,14 +105,14 @@ export default function CreateWorkspaceModal({
    -
    {error && ( -

    +

    Error: {error}

    )} @@ -133,7 +135,7 @@ export default function CreateWorkspaceModal({ @@ -145,7 +147,7 @@ export default function CreateWorkspaceModal({ .getElementById('workspace-creation-modal') ?.close(); }} - className="flex w-full justify-center rounded bg-transparent p-3 font-medium text-slate-500 hover:bg-slate-200" + className="w-full rounded-lg bg-transparent p-2 font-medium text-white transition-all duration-300 hover:bg-red-500/80 hover:bg-opacity-90 hover:text-white" > Cancel @@ -171,7 +173,7 @@ export default function CreateWorkspaceModal({ @@ -187,21 +189,22 @@ export default function CreateWorkspaceModal({ const ModalWrapper = ({ children }: { children: React.ReactElement }) => { return ( - -
    + +
    -

    +

    Create or find a new workspace

    -

    - Workspaces are collections of documents inside of your organization. +

    + {/* Workspaces are collections of documents inside of your organization. They allow you to control permissions and documents with ultimate visibility. -
    - - They should match with what you are calling your namespaces or - collections in your vector database. - +
    */} + Workspaces should match with what you are calling your namespaces or + collections in your vector database.

    {children} diff --git a/frontend/src/pages/Dashboard/WorkspacesList/index.tsx b/frontend/src/pages/Dashboard/WorkspacesList/index.tsx index 741319c5..a35bc647 100644 --- a/frontend/src/pages/Dashboard/WorkspacesList/index.tsx +++ b/frontend/src/pages/Dashboard/WorkspacesList/index.tsx @@ -5,7 +5,6 @@ import moment from 'moment'; import { nFormatter } from '../../../utils/numbers'; import { FileText } from 'react-feather'; import truncate from 'truncate'; -import CreateWorkspaceModal from './CreateWorkspaceModal'; import Organization from '../../../models/organization'; import WorkspaceSearch from '../../../components/Sidebar/WorkspaceSearch'; @@ -21,7 +20,7 @@ export default function WorkspacesList({ totalWorkspaces?: number; }) { return ( -
    +

    @@ -89,7 +88,6 @@ export default function WorkspacesList({ )}

    -
    ); } diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index d3134b7e..c9cd3ee5 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -5,13 +5,21 @@ 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 qDrantLogo from '../../images/vectordbs/qdrant.png'; +import WeaviateLogo from '../../images/vectordbs/weaviate.png'; +import { GearSix, Prohibit } from '@phosphor-icons/react'; +import QuickActionsSidebar from './QuickActionSidebar'; +import SyncConnectorModal from '../../components/Modals/SyncConnectorModal'; +import UpdateConnectorModal from '../../components/Modals/UpdateConnectorModal'; +import NewConnectorModal from '../../components/Modals/NewConnectorModal'; 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,114 @@ export default function Dashboard() { workspaces={workspaces} hasMoreWorkspaces={hasMoreWorkspaces} loadMoreWorkspaces={fetchWorkspaces} + headerExtendedItems={ + {}} + /> + } + hasQuickActions={true} > - {!!organization && ( + {!!organization && !!connector && (
    - setConnector(newConnector)} + /> + -
    )} - - -
    -
    + {!connector && ( +
    + window.location.reload()} + /> +
    + )} + +
    +
    - +
    ); } + +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)} + +
    +
    +
    + + + + + + + +
    + + ); +} diff --git a/frontend/src/pages/DocumentView/FragmentList/DeleteEmbeddingConfirmation/index.tsx b/frontend/src/pages/DocumentView/FragmentList/DeleteEmbeddingConfirmation/index.tsx index 54f2a1cb..608237d9 100644 --- a/frontend/src/pages/DocumentView/FragmentList/DeleteEmbeddingConfirmation/index.tsx +++ b/frontend/src/pages/DocumentView/FragmentList/DeleteEmbeddingConfirmation/index.tsx @@ -22,48 +22,49 @@ const DeleteEmbeddingConfirmation = memo( return ( + event.target === event.currentTarget && event.currentTarget?.close() + } > -
    -

    +

    +

    Delete this embedding? -

    -

    +

    +

    Once you delete this embedding it will remove it from your connected Vector Database as well. This process is non-reversible and if you want to add it back will require you to manually insert it or re-embed the document.

    -
    -
    -
    +          
                 {data.metadata.text}
               
    -
    - - -
    +
    +
    + +
    ); diff --git a/frontend/src/pages/DocumentView/FragmentList/EditEmbeddingConfirmation/index.tsx b/frontend/src/pages/DocumentView/FragmentList/EditEmbeddingConfirmation/index.tsx index e5bbbff4..61d6cbde 100644 --- a/frontend/src/pages/DocumentView/FragmentList/EditEmbeddingConfirmation/index.tsx +++ b/frontend/src/pages/DocumentView/FragmentList/EditEmbeddingConfirmation/index.tsx @@ -69,132 +69,117 @@ const EditEmbeddingConfirmation = memo( return ( event.target == event.currentTarget && event.currentTarget?.close() } > -
    -

    - Edit embedding -

    -

    - You can edit your embedding chunk to be more inclusive of a chunk - of text if it was split incorrectly or simply just to provide more - context. -

    -
    -
    -
    -
    - You cannot edit embeddings without an OpenAI - API key set for your instance. -
    -

    - {APP_NAME} currently only supports editing and changes of - embeddings using OpenAI text embedding. If you did not embed - this data originally using OpenAI you will be unable to use this - feature. +

    +
    +

    Edit embedding

    +

    + You can edit your embedding chunk to be more inclusive of a + chunk of text if it was split incorrectly or simply just to + provide more context.

    -
    -
    +
    +
    +
    +
    + +

    + You cannot edit embeddings without an OpenAI API key set for + your instance. +

    +
    +

    + {APP_NAME} currently only supports editing and changes of + embeddings using OpenAI text embedding. If you did not embed + this data originally using OpenAI you will be unable to use + this feature. +

    +
    - +
    -
    - -
    -
    -

    - This key will only be used for the embedding of changes or - additions you make via {APP_NAME}. -

    - + + +
    -
    -            {data.metadata.text}
    -          
    ); } const debouncedTokenLengthCheck = debounce(checkTokenSize, 500); return ( - -
    -

    Edit embedding

    -

    - You can edit your embedding chunk to be more inclusive of a chunk of - text if it was split incorrectly or simply just to provide more - context. -

    -
    -
    -
    -

    {error || ''}

    -

    - {numberWithCommas(tokenLength)}/ - {numberWithCommas(MAX_TOKENS.cl100k_base)}{' '} +

    +
    +
    +

    Edit embedding

    +

    + You can edit your embedding chunk to be more inclusive of a chunk + of text if it was split incorrectly or simply just to provide more + context.

    -
    - {!!error && ( @@ -126,9 +126,9 @@ function CurrentSimilaritySearch({ organization, prompt, formData }) { if (!formData || !prompt.input || !workspaceId) return null; if (loading) { return ( -
    +

    - finding top {topK} similar embeddings for {workspaceName}. + Finding top {topK} similar embeddings for {workspaceName}.

    ); @@ -152,7 +152,7 @@ function CurrentSimilaritySearch({ organization, prompt, formData }) { return (
    -

    +

    If these results look okay to you - click "Create test" to save this test.

    @@ -165,14 +165,14 @@ function CurrentSimilaritySearch({ organization, prompt, formData }) { value={JSON.stringify(embedding)} type="hidden" /> -
    -
    -

    {embedding.vectorId}

    -

    +

    +
    +

    {embedding.vectorId}

    +

    Similarity {(embedding.score * 100.0).toFixed(2)}%

    -
    +                
                       {JSON.stringify(embedding.metadata || {}, null, 2)}
                     
    @@ -182,7 +182,7 @@ function CurrentSimilaritySearch({ organization, prompt, formData }) {
    diff --git a/frontend/src/pages/Tools/RAGTesting/NewTestForm/WorkspaceSearch/index.tsx b/frontend/src/pages/Tools/RAGTesting/NewTestForm/WorkspaceSearch/index.tsx index 1861766a..48e7c70c 100644 --- a/frontend/src/pages/Tools/RAGTesting/NewTestForm/WorkspaceSearch/index.tsx +++ b/frontend/src/pages/Tools/RAGTesting/NewTestForm/WorkspaceSearch/index.tsx @@ -54,10 +54,10 @@ export default function WorkspaceSearch({ return (
    -
    -
    +
    {workspaces.map((workspace) => ( @@ -106,12 +106,13 @@ export default function WorkspaceSearch({ type="text" disabled={true} value={selectedWorkspace.name} - className="block w-fit rounded-lg p-2.5 text-lg text-gray-900" + // mt-2 block w-full rounded-lg border border-white/10 bg-main-2/10 px-2 py-2 text-sm text-white outline-none + className="block w-fit rounded-lg border border-white/10 bg-main-2/10 px-2 py-2 text-sm text-white outline-none" /> diff --git a/frontend/src/pages/Tools/RAGTesting/NewTestForm/index.tsx b/frontend/src/pages/Tools/RAGTesting/NewTestForm/index.tsx index 75ec6e85..78365716 100644 --- a/frontend/src/pages/Tools/RAGTesting/NewTestForm/index.tsx +++ b/frontend/src/pages/Tools/RAGTesting/NewTestForm/index.tsx @@ -4,6 +4,7 @@ import { debounce } from 'lodash'; import WorkspaceSearch from './WorkspaceSearch'; import PromptInputAndSearchSubmission from './PromptInputAndSearchSubmission'; import { IOrganization } from '../../../../models/organization'; +import { Loader } from 'react-feather'; export default function NewTestForm({ title, @@ -63,22 +64,22 @@ export default function NewTestForm({
    -

    +

    {title || 'Create your first Context Drift test'}

    {error && ( -
    -

    {error}

    +
    +

    {error}

    )} -