Merge pull request #112 from Mintplex-Labs/ui-v2

UI v2
This commit is contained in:
Timothy Carambat
2024-01-18 15:06:30 -08:00
committed by GitHub
146 changed files with 6572 additions and 4728 deletions
+3 -1
View File
@@ -6,4 +6,6 @@ node_modules
__pycache__
v-env
.DS_Store
yarn-error.log
yarn-error.log
yarn.lock
frontend/.env.development
+1 -1
View File
@@ -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
+24
View File
@@ -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 {
@@ -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();
@@ -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;
+1
View File
@@ -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",
+5 -2
View File
@@ -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={<PrivateRoute Component={DocumentView} />}
/>
<Route path="/onboarding-setup" element={<OnboardingFlow />} />
<Route
path="/onboarding"
element={<PrivateRoute Component={OnboardingHome} />}
@@ -103,7 +107,6 @@ function App() {
<Route path="/auth/sign-up" element={<SignUp />} />
<Route path="/auth/sign-in" element={<SignIn />} />
<Route path="/system-setup" element={<SystemSetup />} />
<Route
path="/system-settings"
element={<AdminRoute Component={SystemSettingsView} />}
@@ -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 <div className="mb-18"></div>;
return (
<div className="my-4 flex justify-center">
<div className="my-4 -mt-8 mb-8 flex justify-center">
{hasPrevious && (
<button
onClick={goToPrevious}
className="rotate-90 px-2 text-white/20 transition-all duration-300 hover:text-sky-400"
>
<CaretDown size={20} weight="bold" />
</button>
)}
<ul className="pagination pagination-sm">
{pageItems.map((item, i) =>
typeof item === 'number' ? (
<button
key={item}
className={`border px-3 py-2 text-sm ${
className={`border px-3 py-2 text-sm transition-all duration-300 hover:border-sky-400 hover:bg-sky-400/20 ${
currentPage === item
? 'border-blue-500 text-blue-500'
: 'border-gray-300 text-gray-500'
? 'border-sky-400 bg-sky-400 bg-opacity-20 text-white'
: 'border-white border-opacity-20 text-white text-opacity-60'
} ${i === 0 ? 'rounded-l-lg' : ''} ${
i === pageItems.length - 1 ? 'rounded-r-lg' : ''
}`}
@@ -51,13 +74,21 @@ export default function DocumentListPagination({
) : (
<button
key={item}
className={`border border-gray-300 px-3 py-2 text-sm text-gray-500`}
className={`border border-white border-opacity-20 px-3 py-2 text-sm text-gray-500`}
>
...
</button>
)
)}
</ul>
{hasNext && (
<button
onClick={goToNext}
className="-rotate-90 px-2 text-white/20 transition-all duration-300 hover:text-sky-400"
>
<CaretDown size={20} weight="bold" />
</button>
)}
</div>
);
}
@@ -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<INotification[]>([]);
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 (
<div className="relative">
<button
type="button"
onClick={handleClick}
className="group rounded-lg px-4 py-2 text-slate-800 hover:bg-slate-200"
>
<div className="relative">
<p
hidden={!hasUnseen}
className="absolute -top-[4px] right-0 h-[12px] w-[12px] rounded-full bg-red-600"
/>
<Bell
size={20}
className="group-hover:fill-blue-400 group-hover:stroke-blue-500"
/>
</div>
</button>
<div
hidden={!showNotifs}
className="absolute right-0 top-10 z-99 max-h-[50vh] w-[20rem] divide-y divide-gray-100 overflow-y-scroll rounded-lg bg-white shadow"
>
<div className="block rounded-t-lg bg-blue-100/50 bg-gray-50 px-4 py-2 text-center font-medium text-blue-700">
Recent Notifications
</div>
<div className="divide-y divide-gray-100">
{notifications.length === 0 ? (
<div className="flex px-4 py-3 hover:bg-gray-100">
<div className="w-full pl-3 text-center">
<div className="mb-1.5 text-xs text-gray-500">
no notifications
</div>
<div className="text-xs text-blue-600" />
</div>
</div>
) : (
<>
{notifications.map((notification) => (
<Notification
key={notification.id}
notification={notification}
/>
))}
</>
)}
</div>
</div>
</div>
);
}
function NotificationImage({ notification }: { notification: INotification }) {
switch (notification.symbol) {
case 'info':
return <Info className="text-blue-500" size={20} />;
case 'warning':
return <AlertTriangle className="text-orange-500" size={20} />;
case 'error':
return <AlertOctagon className="text-red-600" size={20} />;
case 'chroma':
return <img className="h-10 w-10 rounded-full" src={ChromaLogo} />;
case 'pinecone':
return <img className="h-8 w-8 rounded-full" src={PineconeLogo} />;
case 'qdrant':
return <img className="h-8 w-8 rounded-full" src={qDrantLogo} />;
case 'weaviate':
return <img className="h-8 w-8 rounded-full" src={WeaviateLogo} />;
default:
return <Info className="text-blue-500" size={20} />;
}
}
function NotificationWrapper({
notification,
children,
}: {
notification: INotification;
children: ReactNode;
}) {
if (!!notification.link) {
return (
<a
key={notification.id}
href={notification?.link || '#'}
target={notification?.target || 'self'}
className={`flex px-4 py-3 hover:bg-gray-100 ${
!notification.seen ? 'border-l-4 !border-l-blue-600' : ''
}`}
>
{children}
</a>
);
}
return (
<div
key={notification.id}
className={`flex px-4 py-3 hover:bg-gray-100 ${
!notification.seen ? 'border-l-4 !border-l-blue-600' : ''
}`}
>
{children}
</div>
);
}
function Notification({ notification }: { notification: INotification }) {
return (
<NotificationWrapper notification={notification}>
<div className="flex flex-shrink-0 items-center justify-center">
<NotificationImage notification={notification} />
</div>
<div className="w-full pl-3">
<div className="mb-1.5 text-sm text-gray-500">
{notification.textContent}
</div>
<div className="text-xs text-blue-600">
{databaseTimestampFromNow(notification.createdAt)}
</div>
</div>
</NotificationWrapper>
);
}
+4 -101
View File
@@ -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 (
<header className="sticky top-0 z-999 flex w-full bg-slate-900 drop-shadow-1 dark:bg-boxdark dark:drop-shadow-none md:bg-white">
<div className="flex flex-grow items-center justify-between px-4 py-4 shadow-2 md:px-6 2xl:px-11">
<div className="flex items-center gap-2 sm:gap-4 lg:hidden">
{/* <!-- Hamburger Toggle BTN --> */}
<button
aria-controls="sidebar"
onClick={(e) => {
e.stopPropagation();
props.setSidebarOpen(!props.sidebarOpen);
}}
className="z-99999 block rounded-sm border border-stroke bg-white p-1.5 shadow-sm dark:border-strokedark dark:bg-boxdark lg:hidden"
>
<span className="relative block h-5.5 w-5.5 cursor-pointer">
<span className="du-block absolute right-0 h-full w-full">
<span
className={`relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-[0] duration-200 ease-in-out dark:bg-white ${
!props.sidebarOpen && '!w-full delay-300'
}`}
></span>
<span
className={`relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-150 duration-200 ease-in-out dark:bg-white ${
!props.sidebarOpen && 'delay-400 !w-full'
}`}
></span>
<span
className={`relative left-0 top-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-200 duration-200 ease-in-out dark:bg-white ${
!props.sidebarOpen && '!w-full delay-500'
}`}
></span>
</span>
<span className="absolute right-0 h-full w-full rotate-45">
<span
className={`absolute left-2.5 top-0 block h-full w-0.5 rounded-sm bg-black delay-300 duration-200 ease-in-out dark:bg-white ${
!props.sidebarOpen && '!h-0 !delay-[0]'
}`}
></span>
<span
className={`delay-400 absolute left-0 top-2.5 block h-0.5 w-full rounded-sm bg-black duration-200 ease-in-out dark:bg-white ${
!props.sidebarOpen && '!h-0 !delay-200'
}`}
></span>
</span>
</span>
</button>
{/* <!-- Hamburger Toggle BTN --> */}
<Link className="flex w-full justify-center lg:hidden" to="/">
<img src={Logo} alt="Logo" className="h-14" />
</Link>
</div>
<div className="hidden w-full sm:block">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-x-4">
<p className="text-4xl font-semibold text-slate-800">
{truncate(entity[nameProp ?? 'name'], 20)}
</p>
<button
onClick={handleCopy}
disabled={copied}
className="transition-duration-300 font-mono flex items-center gap-x-2 rounded-md bg-slate-200 px-4 py-2 text-sm text-slate-700 transition disabled:bg-green-300"
>
<p className="">ID: {entity[property]}</p>
{copied ? (
<CheckCircle className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
{extendedItems}
</div>
<div className="flex w-fit items-center gap-x-2">
<Notifications />
<button
onClick={() => {
if (!window) return;
window.localStorage.removeItem(STORE_USER);
window.localStorage.removeItem(STORE_TOKEN);
window.location.replace(paths.home());
}}
className="rounded-lg px-4 py-2 text-slate-800 hover:bg-slate-200"
>
Logout
</button>
</div>
</div>
</div>
</div>
<header className="mr-26 flex h-[76px] w-full rounded-t-xl bg-main">
<div className="flex w-full justify-between p-4">{extendedItems}</div>
</header>
);
}
@@ -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 | string>(null);
const [success, setSuccess] = useState<null | boolean>(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 (
<dialog
id="new-connector-modal"
className="rounded-xl border-2 border-white/20 bg-main shadow"
onClick={(event) =>
event.target == event.currentTarget && event.currentTarget?.close()
}
>
<div className="rounded-sm p-[20px]">
<div className="px-6.5 py-4">
<h3 className="text-lg font-medium text-white">
Connect to Vector Database
</h3>
<p className="text-sm text-white/60">
{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.
</p>
</div>
{loading ? (
<div className="px-6.5">
<div className="mb-4.5 flex w-full justify-center">
<PreLoader />
</div>
</div>
) : (
<form onSubmit={handleSubmit}>
<ul className="mx-6 flex w-full flex-wrap gap-6">
<li onClick={() => setType('chroma')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="chroma"
className="peer hidden"
checked={type === 'chroma'}
formNoValidate={true}
/>
<label
style={{
background:
type === 'chroma'
? `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`
: `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)`,
}}
className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-xl border-2 border-transparent p-5 text-white shadow-md transition-all duration-300 hover:border-sky-400 hover:shadow-lg peer-checked:border-sky-400"
>
<div className="block">
<img
src={ChromaLogo}
className="mb-2 h-10 w-10 rounded-full"
alt="chroma logo"
/>
<div className="w-full text-lg font-semibold">Chroma</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-white/80">trychroma.com</p>
Open source vector database you can host yourself.
</div>
</div>
</label>
</li>
<li onClick={() => setType('pinecone')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="pinecone"
className="peer hidden"
checked={type === 'pinecone'}
formNoValidate={true}
/>
<label
style={{
background:
type === 'pinecone'
? `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`
: `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)`,
}}
className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-xl border-2 border-transparent p-5 text-white shadow-md transition-all duration-300 hover:border-sky-400 hover:shadow-lg peer-checked:border-sky-400"
>
<div className="block">
<img
src={PineconeLogoInverted}
className="mb-2 h-10 w-10 rounded-full"
alt="pinecone logo"
/>
<div className="w-full text-lg font-semibold">
Pinecone
</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-white/80">pinecone.io</p>
Cloud-hosted vector database.
</div>
</div>
</label>
</li>
<li onClick={() => setType('qdrant')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="qdrant"
className="peer hidden"
checked={type === 'qdrant'}
formNoValidate={true}
/>
<label
style={{
background:
type === 'qdrant'
? `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`
: `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)`,
}}
className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-xl border-2 border-transparent p-5 text-white shadow-md transition-all duration-300 hover:border-sky-400 hover:shadow-lg peer-checked:border-sky-400"
>
<div className="block">
<img
src={qDrantLogo}
className="mb-2 h-10 w-10 rounded-full"
alt="qdrant logo"
/>
<div className="w-full text-lg font-semibold">qDrant</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-white/80">qdrant.tech</p>
Open-source & hosted vector database.
</div>
</div>
</label>
</li>
<li onClick={() => setType('weaviate')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="weaviate"
className="peer hidden"
checked={type === 'weaviate'}
formNoValidate={true}
/>
<label
style={{
background:
type === 'weaviate'
? `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`
: `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)`,
}}
className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-xl border-2 border-transparent p-5 text-white shadow-md transition-all duration-300 hover:border-sky-400 hover:shadow-lg peer-checked:border-sky-400"
>
<div className="block">
<img
src={WeaviateLogo}
className="mb-2 h-10 w-10 rounded-full"
alt="weaviate logo"
/>
<div className="w-full text-lg font-semibold">
Weaviate
</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-white/80">weaviate.io</p>
Open-source & hosted vector database.
</div>
</div>
</label>
</li>
</ul>
{type === 'chroma' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::instanceURL"
className="block text-sm font-medium text-white"
>
Instance URL
</label>
<p className="text-sm text-white/60">
This is the URL your chroma instance is reachable at.
</p>
</div>
<input
name="settings::instanceURL"
autoComplete="off"
type="url"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="https://my-domain.com:8000"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::authToken"
className="block text-sm font-medium text-white"
>
API Header & Key
</label>
<p className="text-sm text-white/60">
If your hosted Chroma instance is protected by an API
key - enter the header and api key here.
</p>
</div>
<div className="flex w-full items-center gap-x-4">
<input
name="settings::authTokenHeader"
autoComplete="off"
type="text"
className="block h-11 w-[20%] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="X-Api-Key"
/>
<input
name="settings::authToken"
autoComplete="off"
type="password"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="sk-myApiKeyToAccessMyChromaInstance"
/>
</div>
</div>
</div>
)}
{type === 'pinecone' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::environment"
className="block text-sm font-medium text-white"
>
Pinecone Environment
</label>
<p className="text-sm text-white/60">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::environment"
autoComplete="off"
type="text"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="us-west4-gcp-free"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::index"
className="block text-sm font-medium text-white"
>
Pinecone Index
</label>
<p className="text-sm text-white/60">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::index"
autoComplete="off"
type="text"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="my-index"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-white"
>
API Key
</label>
<p className="text-sm text-white/60">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
{type === 'qdrant' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::clusterUrl"
className="block text-sm font-medium text-white"
>
qDrant Cluster URL
</label>
<p className="text-sm text-white/60">
You can find this in your cloud hosted qDrant cluster or
just using the URL to your local docker container.
</p>
</div>
<input
name="settings::clusterUrl"
autoComplete="off"
type="url"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="https://6b3a2d01-3b3f-4339-84e9-ead94f28a844.us-east-1-0.aws.cloud.qdrant.io"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-white"
>
API Key
</label>
<p className="text-sm text-white/60">
(optional) If you are using qDrant cloud you will need
an API key.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
{type === 'weaviate' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::clusterUrl"
className="block text-sm font-medium text-white"
>
Weaviate Cluster URL
</label>
<p className="text-sm text-white/60">
You can find this in your cloud hosted Weaviate cluster
or just using the URL to your local docker container.
</p>
</div>
<input
name="settings::clusterUrl"
autoComplete="off"
type="url"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="https://my-sandbox-b5vipdmw.weaviate.network"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-white"
>
API Key
</label>
<p className="text-sm text-white/60">
(optional) If you are using Weaviate cloud you will need
an API key.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
<div className="w-full px-6">
{error && (
<p className="my-2 w-full rounded-lg border-red-800 bg-red-50 px-4 py-2 text-red-800">
{error}
</p>
)}
{success && (
<p className="my-2 w-full rounded-lg border-green-800 bg-green-50 px-4 py-2 text-green-800">
Connector changes saved
</p>
)}
<div className="flex items-center justify-center">
<button
type="submit"
className="mb-4 mt-4 h-10 w-full items-center rounded-lg bg-white p-2 text-center text-sm font-bold text-neutral-700 shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
Connect to Vector Database
</button>
</div>
</div>
</form>
)}
</div>
</dialog>
);
}
);
export default NewConnectorModal;
@@ -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 | string>(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 (
<dialog
id="sync-connector-modal"
className="w-1/3 rounded-xl border-2 border-white/20 bg-main shadow"
onClick={(event) =>
event.target == event.currentTarget && event.currentTarget?.close()
}
>
<div className="overflow-y-scroll rounded-sm bg-main p-[20px]">
<div className="px-6.5 py-4">
<h3 className="text-lg font-medium text-white">
Sync Vector Database Connection
</h3>
<p className="mt-4 text-sm text-white/60">
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.
<br />
<br />
Once you start this process you can check on its progress in the{' '}
<a
href={paths.jobs(organization)}
className="font-semibold text-sky-400 hover:underline"
>
job queue.
</a>
</p>
</div>
<div className="w-full px-6">
{error && (
<p className="my-2 w-full rounded-lg border-red-800 bg-red-50 px-4 py-2 text-sm text-red-800">
{error}
</p>
)}
{synced ? (
<button
type="button"
onClick={() =>
window.location.replace(paths.jobs(organization))
}
className="w-full rounded-lg py-2 text-center text-gray-600 hover:bg-gray-400 hover:text-white"
>
Check progress
</button>
) : (
<button
type="button"
disabled={loading}
onClick={sync}
className="mb-4 h-11 w-full items-center rounded-lg bg-white p-2 text-center text-sm font-bold text-neutral-700 shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
{loading ? 'Synchronizing...' : 'Synchronize embeddings'}
</button>
)}
</div>
</div>
</dialog>
);
}
);
export default SyncConnectorModal;
@@ -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 | string>(null);
const [success, setSuccess] = useState<null | boolean>(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 (
<dialog
id="edit-connector-modal"
className="rounded-xl border-2 border-white/20 bg-main shadow"
onClick={(event) =>
event.target == event.currentTarget && event.currentTarget?.close()
}
>
<div className="rounded-sm p-[20px]">
<div className="px-6.5 py-4">
<h3 className="text-lg font-medium text-white">
Update Vector Database Connection
</h3>
<p className="text-sm text-white/60">
Currently connected to a {connector.type} vector database
instance. You can update your configuration settings here if they
have changed.
</p>
</div>
{loading ? (
<div className="px-6.5">
<div className="mb-4.5 flex w-full justify-center">
<PreLoader />
</div>
</div>
) : (
<form onSubmit={handleSubmit}>
<ul className="mx-6 flex w-full flex-wrap gap-6">
<li onClick={() => setType('chroma')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="chroma"
className="peer hidden"
checked={type === 'chroma'}
formNoValidate={true}
/>
<label
style={{
background:
type === 'chroma'
? `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`
: `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)`,
}}
className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-xl border-2 border-transparent p-5 text-white shadow-md transition-all duration-300 hover:border-sky-400 hover:shadow-lg peer-checked:border-sky-400"
>
<div className="block">
<img
src={ChromaLogo}
className="mb-2 h-10 w-10 rounded-full"
alt="chroma logo"
/>
<div className="w-full text-lg font-semibold">Chroma</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-white/80">trychroma.com</p>
Open source vector database you can host yourself.
</div>
</div>
</label>
</li>
<li onClick={() => setType('pinecone')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="pinecone"
className="peer hidden"
checked={type === 'pinecone'}
formNoValidate={true}
/>
<label
style={{
background:
type === 'pinecone'
? `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`
: `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)`,
}}
className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-xl border-2 border-transparent p-5 text-white shadow-md transition-all duration-300 hover:border-sky-400 hover:shadow-lg peer-checked:border-sky-400"
>
<div className="block">
<img
src={PineconeLogoInverted}
className="mb-2 h-10 w-10 rounded-full"
alt="pinecone logo"
/>
<div className="w-full text-lg font-semibold">
Pinecone
</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-white/80">pinecone.io</p>
Cloud-hosted vector database.
</div>
</div>
</label>
</li>
<li onClick={() => setType('qdrant')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="qdrant"
className="peer hidden"
checked={type === 'qdrant'}
formNoValidate={true}
/>
<label
style={{
background:
type === 'qdrant'
? `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`
: `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)`,
}}
className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-xl border-2 border-transparent p-5 text-white shadow-md transition-all duration-300 hover:border-sky-400 hover:shadow-lg peer-checked:border-sky-400"
>
<div className="block">
<img
src={qDrantLogo}
className="mb-2 h-10 w-10 rounded-full"
alt="qdrant logo"
/>
<div className="w-full text-lg font-semibold">qDrant</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-white/80">qdrant.tech</p>
Open-source & hosted vector database.
</div>
</div>
</label>
</li>
<li onClick={() => setType('weaviate')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="weaviate"
className="peer hidden"
checked={type === 'weaviate'}
formNoValidate={true}
/>
<label
style={{
background:
type === 'weaviate'
? `linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.06) 100%)`
: `linear-gradient(180deg, #313236 0%, rgba(63, 65, 70, 0.00) 100%)`,
}}
className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-xl border-2 border-transparent p-5 text-white shadow-md transition-all duration-300 hover:border-sky-400 hover:shadow-lg peer-checked:border-sky-400"
>
<div className="block">
<img
src={WeaviateLogo}
className="mb-2 h-10 w-10 rounded-full"
alt="weaviate logo"
/>
<div className="w-full text-lg font-semibold">
Weaviate
</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-white/80">weaviate.io</p>
Open-source & hosted vector database.
</div>
</div>
</label>
</li>
</ul>
{type === 'chroma' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::instanceURL"
className="block text-sm font-medium text-white"
>
Instance URL
</label>
<p className="text-sm text-white/60">
This is the URL your chroma instance is reachable at.
</p>
</div>
<input
name="settings::instanceURL"
autoComplete="off"
type="url"
defaultValue={settings.instanceURL}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="https://my-domain.com:8000"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::authToken"
className="block text-sm font-medium text-white"
>
API Header & Key
</label>
<p className="text-sm text-white/60">
If your hosted Chroma instance is protected by an API
key - enter the header and api key here.
</p>
</div>
<div className="flex w-full items-center gap-x-4">
<input
name="settings::authTokenHeader"
autoComplete="off"
type="text"
defaultValue={settings.authTokenHeader}
className="block h-11 w-[20%] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="X-Api-Key"
/>
<input
name="settings::authToken"
autoComplete="off"
type="password"
defaultValue={settings.authToken}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="sk-myApiKeyToAccessMyChromaInstance"
/>
</div>
</div>
</div>
)}
{type === 'pinecone' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::environment"
className="block text-sm font-medium text-white"
>
Pinecone Environment
</label>
<p className="text-sm text-white/60">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::environment"
autoComplete="off"
type="text"
defaultValue={settings.environment}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="us-west4-gcp-free"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::index"
className="block text-sm font-medium text-white"
>
Pinecone Index
</label>
<p className="text-sm text-white/60">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::index"
autoComplete="off"
type="text"
defaultValue={settings.index}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="my-index"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-white"
>
API Key
</label>
<p className="text-sm text-white/60">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
defaultValue={settings.apiKey}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
{type === 'qdrant' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::clusterUrl"
className="block text-sm font-medium text-white"
>
qDrant Cluster URL
</label>
<p className="text-sm text-white/60">
You can find this in your cloud hosted qDrant cluster or
just using the URL to your local docker container.
</p>
</div>
<input
name="settings::clusterUrl"
autoComplete="off"
type="url"
defaultValue={settings.clusterUrl}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="https://6b3a2d01-3b3f-4339-84e9-ead94f28a844.us-east-1-0.aws.cloud.qdrant.io"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-white"
>
API Key
</label>
<p className="text-sm text-white/60">
(optional) If you are using qDrant cloud you will need
an API key.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
defaultValue={settings.apiKey}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
{type === 'weaviate' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::clusterUrl"
className="block text-sm font-medium text-white"
>
Weaviate Cluster URL
</label>
<p className="text-sm text-white/60">
You can find this in your cloud hosted Weaviate cluster
or just using the URL to your local docker container.
</p>
</div>
<input
name="settings::clusterUrl"
autoComplete="off"
type="url"
defaultValue={settings.clusterUrl}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="https://my-sandbox-b5vipdmw.weaviate.network"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-white"
>
API Key
</label>
<p className="text-sm text-white/60">
(optional) If you are using Weaviate cloud you will need
an API key.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
defaultValue={settings.apiKey}
className="block h-11 w-full min-w-[350px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white placeholder:text-opacity-60"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
<div className="w-full px-6">
{error && (
<p className="my-2 w-full rounded-lg border-red-800 bg-red-50 px-4 py-2 text-red-800">
{error}
</p>
)}
{success && (
<p className="my-2 w-full rounded-lg border-green-800 bg-green-50 px-4 py-2 text-green-800">
Connector changes saved
</p>
)}
<div className="flex items-center justify-center">
<button
type="submit"
className="mb-4 mt-4 h-10 w-full items-center rounded-lg bg-white p-2 text-center text-sm font-bold text-neutral-700 shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
Connect to Vector Database
</button>
</div>
</div>
</form>
)}
</div>
</dialog>
);
}
);
export default UpdateConnectorModal;
@@ -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({
<XCircle className="h-6 h-full w-6 w-full rounded-full bg-red-500 stroke-white p-1" />
</div>
<div className="flex flex-col">
<p className="font-mono overflow-x-scroll text-sm text-black dark:text-stone-200">
<p className="overflow-x-scroll font-mono text-sm text-black dark:text-stone-200">
{truncate(file.name, 30)}
</p>
<p className="font-mono text-xs text-red-700 dark:text-red-400">
@@ -70,7 +70,7 @@ function FileUploadProgressComponent({
)}
</div>
<div className="flex flex-col">
<p className="font-mono overflow-x-scroll text-sm text-black dark:text-stone-200">
<p className="overflow-x-scroll font-mono text-sm text-black dark:text-stone-200">
{truncate(file.name, 30)}
</p>
<p className="font-mono text-xs text-gray-700 dark:text-stone-400">
@@ -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<boolean | null>(null);
const [files, setFiles] = useState([]);
const [fileTypes, setFileTypes] = useState({});
@@ -60,12 +64,12 @@ export default function UploadDocumentModal({
if (ready === null || !slug) {
return (
<ModalWrapper>
<div className="flex h-[20rem] w-full cursor-wait overflow-x-hidden overflow-y-scroll rounded-lg bg-stone-400 bg-opacity-20 outline-none transition-all duration-300">
<div className="flex h-[20rem] w-full cursor-wait overflow-x-hidden overflow-y-scroll rounded-xl border-2 border-white/20 bg-main shadow transition-all duration-300">
<div className="flex h-full w-full flex-col items-center justify-center gap-y-1">
<p className="text-xs text-slate-400">
Checking document processor is online - please wait.
</p>
<p className="text-xs text-slate-400">
<p className="text-xs text-white/60">
this should only take a few moments.
</p>
</div>
@@ -78,8 +82,8 @@ export default function UploadDocumentModal({
return (
<ModalWrapper>
<div className="flex h-[20rem] w-full overflow-x-hidden overflow-y-scroll rounded-lg bg-red-200 outline-none transition-all duration-300">
<div className="flex h-full w-full flex-col items-center justify-center gap-y-1 px-2 md:px-0">
<Frown className="h-8 w-8 text-red-800" />
<div className="flex h-full w-full flex-col items-center justify-center gap-y-1 px-2 text-red-800 md:px-0">
<SmileySad size={32} />
<p className="text-center text-xs text-red-800">
Document processor is offline.
</p>
@@ -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 (
<ModalWrapper>
<div className="flex h-[20rem] w-full overflow-x-hidden overflow-y-scroll rounded-lg bg-stone-400 bg-opacity-20 outline-none transition-all duration-300">
<div className="flex h-[20rem] w-full cursor-wait overflow-x-hidden overflow-y-scroll rounded-xl border border-white/5 bg-main-2 shadow transition-all duration-300">
<div className="flex h-full w-full flex-col items-center justify-center gap-y-1">
<p className="text-sm text-slate-800">
<p className="pb-2 text-sm text-white/60">
Please select the workspace you wish to upload documents to.
</p>
<form onSubmit={saveWorkspace} className="flex flex-col gap-y-1">
<form onSubmit={saveWorkspace} className="flex flex-col gap-y-4">
<select
name="workspaceId"
className="rounded-lg px-4 py-2 outline-none"
className="rounded-lg border border-white/10 bg-main-2 px-2 py-2 text-white/60"
>
{workspaces.map((ws: any) => {
return <option value={ws.id}>{ws.name}</option>;
})}
</select>
<button className="my-2 rounded-lg px-4 py-2 text-blue-800 hover:bg-blue-50">
<button className="w-full rounded-lg bg-white p-2 text-center text-sm font-bold text-neutral-700 shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90">
Continue &rarr;
</button>
</form>
@@ -135,7 +141,7 @@ export default function UploadDocumentModal({
<div className="flex w-full flex-col gap-y-1">
<div
{...getRootProps()}
className="flex h-[20rem] w-full cursor-pointer overflow-x-hidden overflow-y-scroll rounded-lg bg-stone-400 bg-opacity-20 outline-none transition-all duration-300 hover:bg-opacity-40"
className="flex h-[20rem] w-full cursor-pointer overflow-x-hidden overflow-y-scroll rounded-lg border-2 border-dashed border-white/20 bg-main-2 shadow outline-none transition-all duration-300 hover:bg-white/10"
>
<input {...getInputProps()} />
{files.length === 0 ? (
@@ -143,7 +149,7 @@ export default function UploadDocumentModal({
<div className="flex flex-col items-center justify-center pb-6 pt-5">
<svg
aria-hidden="true"
className="mb-3 h-10 w-10 text-gray-600 dark:text-slate-300"
className="mb-3 h-10 w-10 text-white/60"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -156,8 +162,8 @@ export default function UploadDocumentModal({
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
></path>
</svg>
<p className="mb-2 text-sm text-gray-600 dark:text-slate-300">
<span className="font-semibold">Click to upload</span> or drag
<p className="mb-2 text-sm text-white/60">
<span className="font-normal">Click to upload</span> or drag
and drop
</p>
<p className="text-xs text-gray-600 dark:text-slate-300"></p>
@@ -178,9 +184,9 @@ export default function UploadDocumentModal({
</div>
)}
</div>
<p className="text-xs text-gray-600 dark:text-stone-400 ">
supported file extensions are{' '}
<code className="font-mono rounded-sm bg-gray-200 px-1 text-xs text-gray-800 dark:bg-stone-800 dark:text-slate-400">
<p className="mt-2 text-xs text-white/60 ">
Supported file extensions are{' '}
<code className="rounded-md bg-white/80 px-1 font-mono text-xs text-main">
{Object.values(fileTypes).flat().join(' ')}
</code>
</p>
@@ -193,16 +199,14 @@ const ModalWrapper = ({ children }: { children: ReactNode }) => {
return (
<dialog
id="upload-document-modal"
className="w-1/2 rounded-lg outline-none"
className="w-1/2 rounded-xl border-2 border-white/20 bg-main shadow"
onClick={(event) => {
event.target == event.currentTarget && event.currentTarget?.close();
}}
>
<div className="my-4 flex w-full flex-col gap-y-1 p-[20px]">
<p className="text-lg font-semibold text-blue-600">
Upload new document
</p>
<p className="text-base text-slate-800">
<div className="flex w-full flex-col gap-y-1 p-[20px]">
<p className="text-lg font-medium text-white">Upload new document</p>
<p className="text-sm text-white/60">
Select a workspace and document you wish to upload and {APP_NAME} will
process, embed and store the data for you automatically.
</p>
@@ -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 (
<ModalWrapper>
<div className="flex flex w-full flex-col items-center gap-y-2 rounded-lg border border-orange-500 bg-transparent px-4 py-2 text-orange-500">
<div className="flex w-full items-center gap-x-2 text-lg">
<AlertTriangle /> You cannot upload and embed documents without an
OpenAI API Key.
</div>
<p>
{APP_NAME} will automatically upload and embed your documents for you,
but for this to happen we must have an OpenAI key set.
</p>
<form onSubmit={updateSystemSetting} className="w-full">
<div className="">
<div className="mb-4.5">
<label className="mb-2.5 block">Your OpenAI API Key</label>
<input
required={true}
type="password"
name="open_ai_api_key"
placeholder="sk-xxxxxxxxxx"
autoComplete="off"
className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
</div>
<div className="flex flex-col gap-y-2">
<button
type="submit"
className="flex w-full justify-center rounded bg-orange-500 p-3 font-medium text-white"
>
Add OpenAI API Key
</button>
</div>
</div>
<p className="my-2 p-2 text-center text-sm text-orange-500">
This key will only be used for the embedding of documents you upload
via {APP_NAME}.
</p>
</form>
</div>
</ModalWrapper>
);
}
const ModalWrapper = ({ children }: { children: ReactNode }) => {
return (
<dialog
id="upload-document-modal"
className="w-1/2 rounded-xl border-2 border-white/20 bg-main shadow"
onClick={(event) => {
event.target == event.currentTarget && event.currentTarget?.close();
}}
>
<div className="flex w-full flex-col gap-y-1 p-[20px]">
<p className="text-lg font-medium text-white">Upload new document</p>
<p className="text-sm text-white/60">
Select a workspace and document you wish to upload and {APP_NAME} will
process, embed and store the data for you automatically.
</p>
</div>
<div className="my-2 flex w-full p-[20px]">{children}</div>
</dialog>
);
};
@@ -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<INotification[]>([]);
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 (
<div className="relative">
<button
ref={bellButtonRef}
type="button"
onClick={handleClick}
className={`group rounded-lg p-2 hover:bg-main-2 ${
showNotifs && 'bg-main-2'
}`}
>
<div className="relative">
<p
hidden={!hasUnseen}
className="absolute -top-[4px] right-0 h-[12px] w-[12px] rounded-full bg-red-600"
/>
<div className="text-sky-400">
<Bell size={24} weight={showNotifs ? 'fill' : 'bold'} />
</div>
</div>
</button>
<div
hidden={!showNotifs}
ref={notificationRef}
className="absolute right-0 top-12 z-1 max-h-[50vh] w-[20rem] overflow-y-auto rounded-lg border border-neutral-600 bg-main shadow-2xl"
>
<div className="sticky left-0 top-0 z-10 block rounded-t-lg border-b border-neutral-600 bg-main px-5 py-2 text-lg text-white shadow-lg">
Notifications
</div>
<div className="divide-y divide-gray-100">
{notifications.length === 0 ? (
<div className="flex px-4 py-3 hover:bg-main-2">
<div className="w-full pl-3 text-center">
<div className="mb-1.5 text-xs text-white">
No notifications
</div>
</div>
</div>
) : (
<div>
{notifications.map((notification) => (
<Notification
key={notification.id}
notification={notification}
/>
))}
</div>
)}
</div>
</div>
</div>
);
}
function NotificationImage({ notification }: { notification: INotification }) {
switch (notification.symbol) {
case 'info':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-2 text-white">
<Info size={24} weight="bold" />
</div>
);
case 'warning':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-yellow-300 bg-opacity-20 p-2 text-yellow-300">
<WarningOctagon size={24} weight="bold" />
</div>
);
case 'error':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-red-700/20 p-2 text-red-600">
<Warning size={24} weight="bold" />
</div>
);
case 'chroma':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
<img alt="Chroma Logo" className="rounded-full" src={ChromaLogo} />
</div>
);
case 'pinecone':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
<img
alt="Pinecone Logo"
className="rounded-full"
src={PineconeLogo}
/>
</div>
);
case 'qdrant':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
<img alt="qDrant Logo" className="rounded-full" src={qDrantLogo} />
</div>
);
case 'weaviate':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
<img
alt="Weaviate Logo"
className="rounded-full"
src={WeaviateLogo}
/>
</div>
);
default:
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-2 text-white">
<Info size={24} weight="bold" />
</div>
);
}
}
function NotificationWrapper({
notification,
children,
}: {
notification: INotification;
children: ReactNode;
}) {
if (!!notification.link) {
return (
<a
key={notification.id}
href={notification?.link || '#'}
target={notification?.target || 'self'}
className={`flex px-4 py-3 transition-all duration-300 hover:bg-main-2 ${
!notification.seen
? 'border-l-2 !border-l-sky-400 bg-sky-400/10'
: 'border-l-2 !border-l-transparent'
}`}
>
{children}
</a>
);
}
return (
<div
key={notification.id}
className={`flex px-4 py-3 hover:bg-main-2 ${
!notification.seen
? 'border-l-2 !border-l-sky-400'
: 'border-l-2 !border-l-transparent'
}`}
>
{children}
</div>
);
}
function Notification({ notification }: { notification: INotification }) {
return (
<NotificationWrapper notification={notification}>
<div className="flex flex-shrink-0 items-center justify-center">
<NotificationImage notification={notification} />
</div>
<div className="w-full pl-3">
<div className="mb-1 text-sm font-medium leading-tight text-white">
{notification.textContent}
</div>
<div className="text-sm text-white text-opacity-60">
{databaseTimestampFromNow(notification.createdAt)}
</div>
</div>
</NotificationWrapper>
);
}
// <Notification
// key={'pinecone'}
// notification={{
// id: 1,
// organization_id: 1,
// seen: false,
// textContent: 'Pinecone is now available!',
// symbol: 'pinecone',
// link: 'https://pinecone.io',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'chroma'}
// notification={{
// id: 2,
// organization_id: 1,
// seen: false,
// textContent: 'Chroma is now available!',
// symbol: 'chroma',
// link: 'https://chroma.ml',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'qdrant'}
// notification={{
// id: 3,
// organization_id: 1,
// seen: false,
// textContent: 'qDrant is now available!',
// symbol: 'qdrant',
// link: 'https://qdrant.tech',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'weaviate'}
// notification={{
// id: 4,
// organization_id: 1,
// seen: false,
// textContent: 'Weaviate is now available!',
// symbol: 'weaviate',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'error'}
// notification={{
// id: 5,
// organization_id: 1,
// seen: true,
// textContent: 'Something went wrong!',
// symbol: 'error',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'warning'}
// notification={{
// id: 6,
// organization_id: 1,
// seen: true,
// textContent: 'Something went wrong!',
// symbol: 'warning',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'info'}
// notification={{
// id: 7,
// organization_id: 1,
// seen: false,
// textContent: 'Something went wrong!',
// symbol: 'info',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />{' '}
// <Notification
// key={'info'}
// notification={{
// id: 7,
// organization_id: 1,
// seen: true,
// textContent: 'Something went wrong!',
// symbol: 'info',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />{' '}
// <Notification
// key={'info'}
// notification={{
// id: 7,
// organization_id: 1,
// seen: false,
// textContent: 'Something went wrong!',
// symbol: 'info',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
+2 -2
View File
@@ -1,6 +1,6 @@
export default function PreLoader() {
return (
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent"></div>
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-white border-t-transparent"></div>
);
}
@@ -8,7 +8,7 @@ export function FullScreenLoader() {
return (
<div
id="preloader"
className="fixed left-0 top-0 z-999999 flex h-screen w-screen items-center justify-center bg-white"
className="fixed left-0 top-0 z-999999 flex h-screen w-screen items-center justify-center bg-main-bg"
>
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent"></div>
</div>
@@ -18,10 +18,13 @@ export default function CreateOrganizationModal() {
};
return (
<dialog id="organization-creation-modal" className="w-1/3 rounded-lg">
<div className="w-full overflow-y-scroll rounded-sm bg-white p-[20px]">
<dialog
id="organization-creation-modal"
className="w-1/3 rounded-xl border-2 border-white/20 bg-main shadow"
>
<div className="w-full overflow-y-scroll rounded-sm p-[20px]">
<div className="px-6.5 py-4">
<h3 className="font-medium text-black dark:text-white">
<h3 className="text-lg font-medium text-white">
Create a New Organization
</h3>
</div>
@@ -35,7 +38,7 @@ export default function CreateOrganizationModal() {
<form onSubmit={handleSubmit}>
<div className="px-6.5">
<div className="mb-4.5">
<label className="mb-2.5 block text-black dark:text-white">
<label className="mb-2.5 block text-sm font-medium text-white">
Organization Name
</label>
<input
@@ -44,15 +47,15 @@ export default function CreateOrganizationModal() {
name="name"
placeholder="My Organization"
autoComplete="off"
className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
className="placeholder-text-white/60 w-full rounded-lg border border-white/10 bg-main-2 px-2.5 py-2 text-sm text-white"
/>
</div>
<div className="flex flex-col gap-y-2">
<button
type="submit"
className="flex w-full justify-center rounded bg-blue-500 p-3 font-medium text-white"
className="w-full rounded-lg bg-white p-2 font-medium text-main shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
Create Organization
Create Organization &rarr;
</button>
<button
type="button"
@@ -61,12 +64,12 @@ export default function CreateOrganizationModal() {
.getElementById('organization-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
</button>
</div>
<p className="my-2 rounded-lg border border-orange-800 bg-orange-100 p-2 text-center text-sm text-orange-800">
<p className="my-2 rounded-lg border border-white/20 bg-main-2 p-2 text-center text-sm text-white">
Once your organization exists you can start workspaces and
documents.
</p>
@@ -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) => (
<WorkspaceItem key={workspace.id} workspace={workspace} slug={slug} />
);
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 (
<li key={i}>
<NavLink
key={organization.id}
reloadDocument={!isActive}
to={paths.organization(organization)}
className={({ isActive: active }) => {
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' : ''
}`;
}}
>
<div className="flex w-full flex-col" onClick={toggleMenu}>
<div className="flex w-full items-center justify-between">
<div className={`${isActive ? 'text-sky-400' : 'text-white/60'}`}>
{truncate(organization.name, 19)}
</div>
<div
className={`transition-all duration-300 ${
isActive && menuOpen ? 'text-sky-400' : 'rotate-180 '
}`}
>
<CaretDown weight="bold" />
</div>
</div>
</div>
</NavLink>
{isActive && (
<div
className={`${
menuOpen
? 'slide-down mb-4 mt-4 transition-all duration-300'
: 'slide-up'
}`}
style={{
animationDuration: '0.15s',
}}
>
<div className="mb-3.5 flex items-center justify-between px-3">
<div className="flex w-full items-center gap-x-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="12"
viewBox="0 0 16 12"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.20098 8.16845H13.601C14.0426 8.16845 14.401 8.52685 14.401 8.96845C14.401 9.41005 14.0426 9.76845 13.601 9.76845H7.20098C6.75938 9.76845 6.40098 9.41005 6.40098 8.96845C6.40098 8.52685 6.75938 8.16845 7.20098 8.16845ZM7.20098 2.56845H13.601C14.0426 2.56845 14.401 2.92685 14.401 3.36845C14.401 3.81005 14.0426 4.16845 13.601 4.16845H7.20098C6.75938 4.16845 6.40098 3.81005 6.40098 3.36845C6.40098 2.92685 6.75938 2.56845 7.20098 2.56845ZM4.80098 4.16845C4.80098 5.05245 5.51698 5.76845 6.40098 5.76845H14.401C15.285 5.76845 16.001 5.05245 16.001 4.16845V2.56845C16.001 1.68445 15.285 0.96845 14.401 0.96845H6.40098C5.51698 0.96845 4.80098 1.68445 4.80098 2.56845H1.60098L1.60098 0.854024H0.000976562V8.16767C0.000976562 9.05167 0.717782 9.76845 1.60178 9.76845H1.77617H4.80098C4.80098 10.6525 5.51698 11.3685 6.40098 11.3685H14.401C15.285 11.3685 16.001 10.6525 16.001 9.76845V8.16845C16.001 7.28445 15.285 6.56845 14.401 6.56845H6.40098C5.51698 6.56845 4.80098 7.28445 4.80098 8.16845H2.39778C1.95778 8.16845 1.60098 7.81158 1.60098 7.37158V4.16845H4.80098Z"
fill="#A8A9AB"
/>
</svg>
<div className="text-xs font-medium uppercase tracking-widest text-white/60">
Workspaces
</div>
</div>
<button
onClick={() => {
document
.getElementById('workspace-creation-modal')
?.showModal();
}}
>
<Plus className="text-sky-400" size={17} weight="bold" />
</button>
</div>
<div className="mx-3 flex items-center rounded-full bg-main-2 p-2">
<MagnifyingGlass
className="mx-1 h-4 w-4 text-white/60"
weight="bold"
/>
<input
type="text"
placeholder="Search"
onChange={(e) => setSearchTerm(e.target.value)}
className="border-none bg-transparent text-sm text-white/60 placeholder-white/60 focus:outline-none"
/>
</div>
{isSearching ? (
<LoadingWorkspaceItem />
) : searchTerm !== '' && searchResults.length > 0 ? (
<div className="mt-2 max-h-[150px] overflow-y-auto">
{searchResults.map((workspace, idx) => (
<WorkspaceItem key={idx} workspace={workspace} slug={slug} />
))}
</div>
) : searchTerm !== '' && searchResults.length === 0 ? (
<div className="mt-2">
<div className="flex w-full items-center justify-center rounded-sm text-xs text-white/60">
<p className="p-1">No results found.</p>
</div>
</div>
) : workspaces.length > 0 ? (
<div className="mt-2">
<InfiniteScroll
dataLength={workspaces.length}
scrollableTarget="organization-list"
height={workspaces.length > 5 ? 150 : workspaces.length * 30}
next={loadMoreWorkspacesAndScrollToBottom}
hasMore={hasMoreWorkspaces}
loader={<LoadingWorkspaceItem />}
>
{workspaces.map(renderWorkspaceItem)}
</InfiniteScroll>
</div>
) : (
<div className="mt-2">
<div className="flex w-48 items-center justify-center rounded-sm text-xs text-white/60">
<p className="p-1">
No workspaces,{' '}
<button
onClick={() => {
document
.getElementById('workspace-creation-modal')
?.showModal();
}}
className="italic underline hover:cursor-pointer"
>
create
</button>
.
</p>
</div>
</div>
)}
<CreateWorkspaceModal organization={organization} />
</div>
)}
</li>
);
}
function WorkspaceItem({ workspace, slug }: any) {
return (
<li className="mx-5 mt-1">
<NavLink
to={paths.workspace(slug, workspace.slug)}
className={({ isActive }) => {
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)}
</NavLink>
</li>
);
}
function LoadingWorkspaceItem() {
return (
<div className="mt-2">
<div className="flex w-full animate-pulse items-center justify-center rounded-sm text-xs text-white/60">
<p className="p-1">Loading...</p>
</div>
</div>
);
}
@@ -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)}
</NavLink>
</li>
);
+70 -264
View File
@@ -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({
<>
<aside
ref={sidebar}
className={`absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-slate-900 duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${
className={`max-w-72.5 absolute left-0 top-0 z-9999 flex h-screen min-w-[300px] flex-col overflow-y-hidden bg-main duration-300 ease-linear lg:static lg:translate-x-0 ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
{/* <!-- SIDEBAR HEADER --> */}
<div className="flex items-center justify-between gap-2 px-6 py-5.5 lg:py-6.5">
<NavLink to={paths.dashboard()}>
<img src={Logo} alt="Logo" />
<div className="flex">
<NavLink
to={paths.dashboard()}
className="flex w-full shrink-0 justify-center"
>
<div className="flex h-[62px] w-full items-center justify-center rounded-br-xl bg-main-bg">
<img src={LogoSky} alt="Logo" className="w-[170px]" />
</div>
</NavLink>
<button
@@ -130,271 +130,77 @@ export default function Sidebar({
</div>
{/* <!-- SIDEBAR HEADER --> */}
<div className="no-scrollbar flex flex-col overflow-y-auto duration-300 ease-linear">
<div className="no-scrollbar flex flex-col overflow-y-auto rounded-tl-xl bg-main duration-300 ease-linear">
{/* <!-- Sidebar Menu --> */}
<nav className="mt-5 px-4 py-4 lg:mt-9 lg:px-6">
{/* <!-- Menu Group --> */}
<div>
<div className="mb-4 ml-4 flex flex w-full items-center justify-between">
<h3 className="text-sm font-semibold text-bodydark2">MENU</h3>
<button
onClick={() => {
document
.getElementById('organization-creation-modal')
?.showModal();
}}
type="button"
className="rounded-lg px-4 px-4 py-1 py-1 text-sm font-semibold text-bodydark2 hover:bg-slate-800 hover:text-slate-200"
>
+ New Org
</button>
</div>
<ul className="mb-6 flex flex-col gap-1.5">
{/* <!-- Menu Item Dashboard --> */}
<SidebarLinkGroup
activeCondition={
pathname === '/' || pathname.includes('dashboard')
}
>
{(handleClick, open) => {
return (
<React.Fragment>
<NavLink
to="#"
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
(pathname === '/' ||
pathname.includes('dashboard')) &&
'bg-graydark dark:bg-meta-4'
}`}
onClick={(e) => {
e.preventDefault();
sidebarExpanded
? handleClick()
: setSidebarExpanded(true);
}}
>
<Command className="h-4 w-4" />
Organizations
<ChevronUp
className={`absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 fill-current ${
open && 'rotate-180'
}`}
/>
</NavLink>
{/* <!-- Dropdown Menu Start --> */}
<div
className={`translate transform overflow-hidden ${
!open && 'hidden'
}`}
>
<ul className="mb-5.5 mt-4 flex flex-col gap-2.5 pl-6">
{organizations.map((org: any, i: number) => {
return (
<li key={i}>
<NavLink
key={org.id}
reloadDocument={true}
to={paths.organization(org)}
className={({ isActive }) =>
'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +
(isActive && '!text-white')
}
>
{org.name}
</NavLink>
</li>
);
})}
</ul>
</div>
{/* <!-- Dropdown Menu End --> */}
</React.Fragment>
);
}}
</SidebarLinkGroup>
{/* <!-- Menu Item Dashboard --> */}
</ul>
</div>
{!!slug && workspaces?.length > 0 && (
<div className="no-scrollbar mx-4 mb-8 mt-4 h-screen flex-grow overflow-y-auto rounded-xl border-2 border-white/20">
<nav className="p-4 px-4 py-4 lg:px-6">
{/* <!-- Menu Group --> */}
<div>
<div className="mb-3.5 flex items-center justify-between">
<div className="flex w-full items-center gap-x-1">
<SquaresFour
className="text-white/60"
size={20}
weight="bold"
/>
<div className="text-xs font-medium uppercase tracking-widest text-white/60">
Organizations
</div>
</div>
<button
onClick={() => {
document
.getElementById('organization-creation-modal')
?.showModal();
}}
>
<Plus className="text-sky-400" size={17} weight="bold" />
</button>
</div>
<ul className="mb-6 flex flex-col gap-1.5">
{/* <!-- Menu Item Dashboard --> */}
<SidebarLinkGroup
activeCondition={pathname.includes('dashboard')}
activeCondition={
pathname === '/' || pathname.includes('dashboard')
}
>
{(handleClick, open) => {
{() => {
return (
<React.Fragment>
<NavLink
to="#"
className={`group relative flex items-center gap-2.5 rounded-t-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
(pathname === '/' ||
pathname.includes('dashboard')) &&
'bg-graydark dark:bg-meta-4'
}`}
onClick={(e) => {
e.preventDefault();
sidebarExpanded
? handleClick()
: setSidebarExpanded(true);
}}
>
<Box className="h-4 w-4" />
Workspaces
<ChevronUp
className={`absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 fill-current ${
open && 'rotate-180'
}`}
/>
</NavLink>
{/* <!-- Dropdown Menu Start --> */}
<div
className={`translate transform overflow-hidden ${
!open && 'hidden'
}`}
>
<WorkspaceSearch
RenderComponent={WorkspaceItem}
maxContainerHeight={150}
canSearch={
workspaces.length >=
Organization.workspacePageSize
}
<div>
<ul
className="mb-5.5 mt-3 flex flex-col gap-3"
id="organization-list"
>
<ul
id="workspaces-sidebar"
className="no-scrollbar mb-5.5 mt-4 flex flex-col gap-1 pl-6"
>
<InfiniteScroll
dataLength={workspaces.length}
next={continueLoadWorkspaces}
hasMore={hasMoreWorkspaces}
height={150}
scrollableTarget="workspaces-sidebar"
scrollThreshold={0.8}
loader={
<div className="ml-2 flex h-[30px] w-3/4 animate-pulse items-center justify-center rounded-sm bg-slate-800 px-4">
<p className="text-xs text-slate-500 ">
loading...
</p>
</div>
}
>
{workspaces?.map(
(workspace: any, i: number) => (
<WorkspaceItem
key={i}
workspace={workspace}
slug={slug}
/>
)
)}
</InfiniteScroll>
</ul>
</WorkspaceSearch>
{sortedOrganizations.map(
(org: any, i: number) => {
return (
<OrganizationTab
key={org.id}
i={i}
workspaces={workspaces}
organization={org}
hasMoreWorkspaces={hasMoreWorkspaces}
loadMoreWorkspaces={loadMoreWorkspaces}
/>
);
}
)}
</ul>
</div>
{/* <!-- Dropdown Menu End --> */}
</React.Fragment>
);
}}
</SidebarLinkGroup>
{/* <!-- Menu Item Dashboard --> */}
</ul>
</div>
)}
</nav>
</div>
<div>
{!!organization && (
<ul className="mb-6 flex flex-col gap-1.5">
{user?.role === 'admin' && (
<>
<li>
<div className={`translate transform overflow-hidden`}>
<NavLink
to={paths.toolsHome(organization)}
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
(pathname === '/' ||
pathname.includes('all-tools')) &&
'bg-graydark dark:bg-meta-4'
}`}
>
<Package className="h-4 w-4" />
Tools & More
</NavLink>
</div>
</li>
<li>
<div className={`translate transform overflow-hidden`}>
<NavLink
to={paths.users()}
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
(pathname === '/' ||
pathname.includes('users')) &&
'bg-graydark dark:bg-meta-4'
}`}
>
<Users className="h-4 w-4" />
User Management
</NavLink>
</div>
</li>
<li>
<div className={`translate transform overflow-hidden`}>
<NavLink
to={paths.organizationSettings(organization)}
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
(pathname === '/' ||
pathname.includes(
`${organization?.slug}/settings`
)) &&
'bg-graydark dark:bg-meta-4'
}`}
>
<Briefcase className="h-4 w-4" />
Organization Settings
</NavLink>
</div>
</li>
</>
)}
{user?.role === 'admin' && (
<li>
<div className={`translate transform overflow-hidden`}>
<NavLink
to={paths.settings()}
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
(pathname === '/' ||
pathname.includes('system-settings')) &&
'bg-graydark dark:bg-meta-4'
}`}
>
<Tool className="h-4 w-4" />
System Settings
</NavLink>
</div>
</li>
)}
<li>
<div className={`translate transform overflow-hidden`}>
<NavLink
to={paths.jobs(organization)}
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
(pathname === '/' || pathname.includes('jobs')) &&
'bg-graydark dark:bg-meta-4'
}`}
>
<Radio className="h-4 w-4" />
Background Jobs
</NavLink>
</div>
</li>
</ul>
)}
</div>
</nav>
{/* <!-- Sidebar Menu --> */}
</div>
</aside>
@@ -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 (
<div>
<button
ref={buttonRef}
onClick={() => setShowMenu(!showMenu)}
className="flex h-[29px] w-[29px] items-center justify-center rounded-full bg-sky-400 bg-opacity-20 text-sm font-medium text-sky-400"
>
{user?.email?.slice(0, 2).toUpperCase()}
</button>
{showMenu && (
<div
ref={menuRef}
className="items-center-justify-center absolute right-0 top-12 flex w-fit rounded-lg border border-white/20 bg-main p-4"
>
<div className="flex flex-col gap-y-2">
<button
onClick={() => {
if (!window) return;
window.localStorage.removeItem(STORE_USER);
window.localStorage.removeItem(STORE_TOKEN);
window.location.replace(paths.home());
}}
type="button"
className="w-full whitespace-nowrap rounded-md px-4 py-1.5 text-left text-white hover:bg-slate-200/20"
>
Sign out
</button>
</div>
</div>
)}
</div>
);
}
@@ -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 (
<div
onClick={() => 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`}
>
<input
type="checkbox"
value={value}
className="peer hidden"
checked={checked}
readOnly={true}
formNoValidate={true}
/>
<label className="flex h-full w-full cursor-pointer flex-col items-start justify-between">
<div className="flex items-center">
<img src={image} alt={name} className="h-10 w-10 rounded" />
<div className="ml-4 text-sm font-semibold">{name}</div>
</div>
<div className="font-base mt-2 text-xs tracking-wide text-white">
{description}
</div>
<a
href={`https://${link}`}
className="mt-2 text-xs font-medium text-white underline"
>
{link}
</a>
</label>
</div>
);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

+78 -1
View File
@@ -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;
}
+13 -3
View File
@@ -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 (
<div className="dark:bg-boxdark-2 dark:text-bodydark">
<div className="bg-main-bg px-4 pt-4">
<div className="flex h-screen overflow-hidden">
<Sidebar
workspaces={workspaces}
@@ -42,7 +46,7 @@ const AppLayout = ({
loadMoreWorkspaces={loadMoreWorkspaces}
/>
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<div className="no-scrollbar w-full overflow-x-hidden">
{!!headerEntity && (
<div className="flex w-full items-center">
<Header
@@ -52,11 +56,17 @@ const AppLayout = ({
sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen}
extendedItems={headerExtendedItems}
quickActions={hasQuickActions}
/>
</div>
)}
<div className="absolute right-0 top-0 mr-9 mt-7 flex items-center gap-x-2">
<Notifications />
<UserMenu />
</div>
<main>
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
<div className="mx-auto overflow-y-auto rounded-tr-xl bg-main pr-6 pt-6">
{children}
</div>
</main>
+1 -1
View File
@@ -6,7 +6,7 @@ interface DefaultLayoutProps {
const DefaultLayout = ({ children }: DefaultLayoutProps) => {
return (
<div className="dark:bg-boxdark-2 dark:text-bodydark">
<div className="z-1 bg-zinc-900">
<div className="flex h-screen overflow-hidden">
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<main>
-1
View File
@@ -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(
<Router>
+1 -2
View File
@@ -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 = {
+1 -1
View File
@@ -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';
+1 -2
View File
@@ -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 = {
+73 -83
View File
@@ -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 (
<DefaultLayout>
<div className="bg-white">
<div className="">
<div className="flex flex-wrap items-center">
<div className="hidden w-full xl:block xl:w-1/2">
<div className="px-26 py-17.5 text-center">
<Link className="mb-5.5 inline-block" to="/">
<img
className="hidden h-[50px] dark:block"
src={Logo}
alt="Logo"
/>
<img
className="h-[50px] dark:hidden"
src={LogoDark}
alt="Logo"
/>
</Link>
<p className="2xl:px-20">
Did you know using {APP_NAME} can save you 75% on embedding
costs?
</p>
<span className="mt-15 inline-block">
<img src={ManageSvg} />
</span>
<div>
<img src={SignInImg} alt="Sign In" />
</div>
</div>
<div className="w-full border-stroke dark:border-strokedark xl:w-1/2 xl:border-l-2">
<div className="w-full border-stroke xl:w-1/2">
<div className="w-full p-4 sm:p-12.5 xl:p-17.5">
{stage !== 'ready' ? (
<ShowStatus
@@ -175,65 +153,77 @@ function ShowStatus({
function LoginForm({ handleSubmit }: { handleSubmit: any }) {
return (
<>
<span className="mb-1.5 block text-2xl font-medium">Sign back in</span>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="mb-2.5 block font-medium text-black dark:text-white">
Email
</label>
<div className="relative">
<input
required={true}
type="email"
name="email"
placeholder="Enter your email"
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
<span className="absolute right-4 top-4">
<Mail className="h-[22px] w-[22px] text-gray-500" />
</span>
<div
style={{
background: `
radial-gradient(circle at center, transparent 40%, black 100%),
linear-gradient(180deg, #85F8FF 0%, #65A6F2 100%)
`,
width: '575px',
filter: 'blur(150px)',
opacity: '0.5',
}}
className="absolute right-0 top-0 z-0 h-full w-full"
/>
<div className="relative z-10 flex flex-col items-center">
<div className="mb-3 flex justify-center gap-x-2 text-center">
<span className="text-2xl font-bold text-white">Log in to</span>
<span className="text-2xl font-bold text-sky-300"> VectorAdmin</span>
</div>
<div className="mb-11 w-[308.65px] text-center">
<span className="mt-3 text-sm text-white text-opacity-90">
Welcome back, please log in to your account.
</span>
</div>
<form onSubmit={handleSubmit} className="z-10">
<div className="mb-3.5">
<div className="">
<input
required={true}
type="email"
name="email"
placeholder="Enter your email"
className="h-11 w-[300px] rounded-lg bg-neutral-800/60 p-2.5 text-white shadow-lg transition-all duration-300 focus:scale-105"
/>
</div>
</div>
</div>
<div className="mb-4">
<label className="mb-2.5 block font-medium text-black dark:text-white">
Password
</label>
<div className="relative">
<input
required={true}
type="password"
name="password"
min={8}
placeholder={`Your ${APP_NAME} account password`}
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
<span className="absolute right-4 top-4">
<Key className="h-[22px] w-[22px] text-gray-500" />
</span>
<div className="mb-9">
<div className="">
<input
required={true}
type="password"
name="password"
min={8}
placeholder={`Your ${APP_NAME} password`}
className="h-11 w-[300px] rounded-lg bg-neutral-800/60 p-2.5 text-white shadow-lg transition-all duration-300 focus:scale-105"
/>
</div>
</div>
</div>
<div className="mb-5">
<button
type="submit"
className="w-full cursor-pointer rounded-lg border border-primary bg-primary p-4 text-white transition hover:bg-opacity-90"
>
Sign In
</button>
</div>
<div className="mb-5">
<button
type="submit"
className="h-11
w-[300px] items-center rounded-lg bg-white p-2 text-center text-sm font-bold leading-tight text-neutral-700 shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
Sign In
</button>
</div>
<div className="mt-6 text-center">
<p>
Don't have a {APP_NAME} account?{' '}
<Link to={paths.signUp()} className="text-primary">
Sign Up
</Link>
</p>
</div>
</form>
<div className="mt-6 text-center text-sm text-white/90">
<p>
Don't have a {APP_NAME} account?{' '}
<Link
to={paths.signUp()}
className="font-semibold transition-all duration-300 hover:underline"
>
Sign Up
</Link>
</p>
</div>
</form>
</div>
</>
);
}
+88 -64
View File
@@ -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 (
<DefaultLayout>
<div className="bg-white">
<div className="">
<div className="flex flex-wrap items-center">
<div className="hidden w-full xl:block xl:w-1/2">
<div>
<img src={SignInImg} alt="Sign In" />
</div>
</div>
<div className="w-full border-stroke xl:w-1/2">
<div className="w-full p-4 sm:p-12.5 xl:p-17.5">
{stage !== 'ready' ? (
<ShowStatus
stage={stage}
results={results}
resetForm={resetStage}
/>
) : (
<LoginForm handleSubmit={handleSubmit} />
)}
</div>
</div>
</div>
</div>
{/* <div className="bg-white">
<div className="flex flex-wrap items-center">
<div className="hidden w-full xl:block xl:w-1/2">
<div className="px-26 py-17.5 text-center">
@@ -99,7 +120,7 @@ const SignUp = () => {
</div>
</div>
</div>
</div>
</div> */}
</DefaultLayout>
);
};
@@ -153,69 +174,72 @@ function ShowStatus({
function LoginForm({ handleSubmit }: { handleSubmit: any }) {
return (
<>
<span className="mb-1.5 block font-medium">New account</span>
<h2 className="mb-9 text-2xl font-bold text-black dark:text-white sm:text-title-xl2">
Sign Up for {APP_NAME}
</h2>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="mb-2.5 block font-medium text-black dark:text-white">
Email
</label>
<div className="relative">
<input
required={true}
type="email"
name="email"
placeholder="Enter your email"
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
<span className="absolute right-4 top-4">
<Mail className="h-[22px] w-[22px] text-gray-500" />
</span>
<div
style={{
background: `
radial-gradient(circle at center, transparent 40%, black 100%),
linear-gradient(180deg, #85F8FF 0%, #65A6F2 100%)
`,
width: '575px',
filter: 'blur(150px)',
opacity: '0.5',
}}
className="absolute right-0 top-0 z-0 h-full w-full"
/>
<div className="relative z-10 flex flex-col items-center">
<div className="mb-3 flex justify-center gap-x-2 text-center">
<span className="text-2xl font-bold text-white">Sign Up for</span>
<span className="text-2xl font-bold text-sky-300">{APP_NAME}</span>
</div>
<form onSubmit={handleSubmit} className="z-10">
<div className="mb-3.5">
<div className="">
<input
required={true}
type="email"
name="email"
placeholder="Enter your email"
className="h-11 w-[300px] rounded-lg bg-neutral-800/60 p-2.5 text-white shadow-lg transition-all duration-300 focus:scale-105"
/>
</div>
</div>
</div>
<div className="mb-4">
<label className="mb-2.5 block font-medium text-black dark:text-white">
Password
</label>
<div className="relative">
<input
required={true}
type="password"
name="password"
min={8}
placeholder={`Your ${APP_NAME} account password`}
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
<span className="absolute right-4 top-4">
<Key className="h-[22px] w-[22px] text-gray-500" />
</span>
<div className="mb-9">
<div className="">
<input
required={true}
type="password"
name="password"
min={8}
placeholder={`Your ${APP_NAME} password`}
className="h-11 w-[300px] rounded-lg bg-neutral-800/60 p-2.5 text-white shadow-lg transition-all duration-300 focus:scale-105"
/>
</div>
</div>
</div>
<div className="mb-5 flex flex-col gap-y-1">
<button
type="submit"
className="w-full cursor-pointer rounded-lg border border-primary bg-primary p-4 text-white transition hover:bg-opacity-90"
>
Create account
</button>
</div>
<div className="mb-5">
<button
type="submit"
className="h-11
w-[300px] items-center rounded-lg bg-white p-2 text-center text-sm font-bold leading-tight text-neutral-700 shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
Create account
</button>
</div>
<div className="mt-6 text-center">
<p>
Already have an account?{' '}
<Link to={paths.signIn()} className="text-primary">
Sign in
</Link>
</p>
</div>
</form>
<div className="mt-6 text-center text-sm text-white/90">
<p>
Already have an account?{' '}
<Link
to={paths.signIn()}
className="font-semibold transition-all duration-300 hover:underline"
>
Log In
</Link>
</p>
</div>
</form>
</div>
</>
);
}
@@ -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<IStages>('ready');
const [results, setResults] = useState<IResult>({
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 (
<DefaultLayout>
<div className="bg-white">
<div className="flex flex-wrap items-center">
<div className="hidden w-full xl:block xl:w-1/2">
<div className="px-26 py-17.5 text-center">
<Link className="mb-5.5 inline-block" to="/">
<img
className="hidden h-[50px] dark:block"
src={Logo}
alt="Logo"
/>
<img
className="h-[50px] dark:hidden"
src={LogoDark}
alt="Logo"
/>
</Link>
<p className="2xl:px-20">
Did you know using {APP_NAME} can save you 75% on embedding
costs?
</p>
<span className="mt-15 inline-block">
<img src={ManageSvg} />
</span>
</div>
</div>
<div className="w-full border-stroke dark:border-strokedark xl:w-1/2 xl:border-l-2">
<div className="w-full p-4 sm:p-12.5 xl:p-17.5">
{stage !== 'ready' ? (
<ShowStatus
stage={stage}
results={results}
resetForm={resetStage}
/>
) : (
<LoginForm handleSubmit={handleSubmit} />
)}
</div>
</div>
</div>
</div>
</DefaultLayout>
);
};
function ShowStatus({
stage,
results,
resetForm,
}: {
stage: IStages;
results: IResult;
resetForm: any;
}) {
if (stage === 'loading') {
return (
<div className="flex h-auto w-full flex-col items-center justify-center gap-y-2">
<PreLoader />
<p className="text-gray-500">making you the system admin...</p>
</div>
);
}
if (stage === 'failed') {
return (
<div className="flex h-auto w-full flex-col items-center justify-center gap-y-2">
<XCircle className="h-20 w-20 text-red-400" />
<p className="text-red-500">
We could not complete this process - check the system logs.
</p>
<p className="text-xs text-red-500">{results?.error}</p>
<button className="text-blue-400" onClick={resetForm}>
Try Again &rarr;
</button>
</div>
);
}
if (stage === 'success') {
return (
<div className="flex h-auto w-full flex-col items-center justify-center gap-y-2">
<CheckCircle className="h-20 w-20 text-green-400" />
<p className="text-center text-green-500">
Root account was deleted and you are now the system admin!
</p>
<p className="text-center text-xs text-gray-400">
Redirecting you to the right place!
</p>
</div>
);
}
return null;
}
function LoginForm({ handleSubmit }: { handleSubmit: any }) {
return (
<>
<span className="mb-1.5 block text-2xl font-medium">
Create the System Administrator
</span>
<p className="mb-1.5 text-sm text-gray-600 ">
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.
<br />
<br />
If you lose your password you will never be able to recover it - so keep
it safe.
</p>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="mb-2.5 block font-medium text-black dark:text-white">
Your Email
</label>
<div className="relative">
<input
required={true}
type="email"
name="email"
placeholder="Enter your email"
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
<span className="absolute right-4 top-4">
<Mail className="h-[22px] w-[22px] text-gray-500" />
</span>
</div>
</div>
<div className="mb-4">
<label className="mb-2.5 block font-medium text-black dark:text-white">
Your Password
</label>
<div className="relative">
<input
required={true}
type="password"
name="password"
min={8}
placeholder={`Your ${APP_NAME} system admin password`}
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
<span className="absolute right-4 top-4">
<Key className="h-[22px] w-[22px] text-gray-500" />
</span>
</div>
</div>
<div className="mb-5">
<button
type="submit"
className="w-full cursor-pointer rounded-lg border border-primary bg-primary p-4 text-white transition hover:bg-opacity-90"
>
Create System Admin
</button>
</div>
</form>
</>
);
}
export default SystemSetup;
@@ -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 | string>(null);
const [success, setSuccess] = useState<null | boolean>(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 (
<dialog
id="new-connector-modal"
className="w-1/2 rounded-lg"
onClick={(event) =>
event.target == event.currentTarget && event.currentTarget?.close()
}
>
<div className="rounded-sm bg-white p-[20px]">
<div className="px-6.5 py-4">
<h3 className="font-medium text-black dark:text-white">
Connect to Vector Database
</h3>
<p className="text-sm text-gray-500">
{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.
</p>
</div>
<div hidden={!loading} className="px-6.5">
<div className="mb-4.5 flex w-full justify-center">
<PreLoader />
</div>
</div>
<form hidden={loading} onSubmit={handleSubmit}>
<ul className="mx-6 flex w-full flex-wrap gap-6">
<li onClick={() => setType('chroma')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="chroma"
className="peer hidden"
checked={type === 'chroma'}
readOnly={true}
formNoValidate={true}
/>
<label className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white p-5 text-gray-500 hover:bg-gray-50 hover:text-gray-600 peer-checked:border-blue-600 peer-checked:bg-blue-50 peer-checked:text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:peer-checked:text-gray-300">
<div className="block">
<img
src={ChromaLogo}
className="mb-2 h-10 w-10 rounded-full"
/>
<div className="w-full text-lg font-semibold">Chroma</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-slate-400">trychroma.com</p>
Open source vector database you can host yourself.
</div>
</div>
</label>
</li>
<li onClick={() => setType('pinecone')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="pinecone"
className="peer hidden"
checked={type === 'pinecone'}
readOnly={true}
formNoValidate={true}
/>
<label className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white p-5 text-gray-500 hover:bg-gray-50 hover:text-gray-600 peer-checked:border-blue-600 peer-checked:bg-blue-50 peer-checked:text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:peer-checked:text-gray-300">
<div className="block">
<img
src={PineconeLogo}
className="mb-2 h-10 w-10 rounded-full"
/>
<div className="w-full text-lg font-semibold">Pinecone</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-slate-400">pinecone.io</p>
Cloud-hosted vector database.
</div>
</div>
</label>
</li>
<li onClick={() => setType('qdrant')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="qdrant"
className="peer hidden"
checked={type === 'qdrant'}
readOnly={true}
formNoValidate={true}
/>
<label className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white p-5 text-gray-500 hover:bg-gray-50 hover:text-gray-600 peer-checked:border-blue-600 peer-checked:bg-blue-50 peer-checked:text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:peer-checked:text-gray-300">
<div className="block">
<img
src={qDrantLogo}
className="mb-2 h-10 w-10 rounded-full"
/>
<div className="w-full text-lg font-semibold">qDrant</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-slate-400">qdrant.tech</p>
Open-source & hosted vector database.
</div>
</div>
</label>
</li>
<li onClick={() => setType('weaviate')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="weaviate"
className="peer hidden"
checked={type === 'weaviate'}
readOnly={true}
formNoValidate={true}
/>
<label className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white p-5 text-gray-500 hover:bg-gray-50 hover:text-gray-600 peer-checked:border-blue-600 peer-checked:bg-blue-50 peer-checked:text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:peer-checked:text-gray-300">
<div className="block">
<img
src={WeaviateLogo}
className="mb-2 h-10 w-10 rounded-full"
/>
<div className="w-full text-lg font-semibold">Weaviate</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-slate-400">weaviate.io</p>
Open-source & hosted vector database.
</div>
</div>
</label>
</li>
</ul>
{type === 'chroma' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::instanceURL"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Instance URL
</label>
<p className="text-xs text-gray-500">
This is the URL your chroma instance is reachable at.
</p>
</div>
<input
name="settings::instanceURL"
autoComplete="off"
type="url"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="https://my-domain.com:8000"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::authToken"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
API Header & Key
</label>
<p className="text-xs text-gray-500">
If your hosted Chroma instance is protected by an API key -
enter the header and api key here.
</p>
</div>
<div className="flex w-full items-center gap-x-4">
<input
name="settings::authTokenHeader"
autoComplete="off"
type="text"
className="block w-[20%] rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="X-Api-Key"
/>
<input
name="settings::authToken"
autoComplete="off"
type="password"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="sk-myApiKeyToAccessMyChromaInstance"
/>
</div>
</div>
</div>
)}
{type === 'pinecone' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::environment"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Pinecone Environment
</label>
<p className="text-xs text-gray-500">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::environment"
autoComplete="off"
type="text"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="us-west4-gcp-free"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::index"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Pinecone Index
</label>
<p className="text-xs text-gray-500">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::index"
autoComplete="off"
type="text"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="my-index"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
API Key
</label>
<p className="text-xs text-gray-500">
If your hosted Chroma instance is protected by an API key -
enter it here.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
{type === 'qdrant' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::clusterUrl"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
qDrant Cluster URL
</label>
<p className="text-xs text-gray-500">
You can find this in your cloud hosted qDrant cluster or
just using the URL to your local docker container.
</p>
</div>
<input
name="settings::clusterUrl"
autoComplete="off"
type="url"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="https://6b3a2d01-3b3f-4339-84e9-ead94f28a844.us-east-1-0.aws.cloud.qdrant.io"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
API Key
</label>
<p className="text-xs text-gray-500">
(optional) If you are using qDrant cloud you will need an
API key.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
{type === 'weaviate' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::clusterUrl"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Weaviate Cluster URL
</label>
<p className="text-xs text-gray-500">
You can find this in your cloud hosted Weaviate cluster or
just using the URL to your local docker container.
</p>
</div>
<input
name="settings::clusterUrl"
autoComplete="off"
type="url"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="https://my-sandbox-b5vipdmw.weaviate.network"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
API Key
</label>
<p className="text-xs text-gray-500">
(optional) If you are using Weaviate cloud may need an API
key.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
<div className="w-full px-6">
{error && (
<p className="my-2 w-full rounded-lg border-red-800 bg-red-50 px-4 py-2 text-red-800">
{error}
</p>
)}
{success && (
<p className="my-2 w-full rounded-lg border-green-800 bg-green-50 px-4 py-2 text-green-800">
Connector added to organization
</p>
)}
<button
type="submit"
className="w-full rounded-lg border border-blue-600 py-2 text-center text-blue-600 hover:bg-blue-600 hover:text-white"
>
Connect to Vector Database
</button>
</div>
</form>
</div>
</dialog>
);
};
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 | string>(null);
const [success, setSuccess] = useState<null | boolean>(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 (
<dialog
id="edit-connector-modal"
className="w-1/2 rounded-lg"
onClick={(event) =>
event.target == event.currentTarget && event.currentTarget?.close()
}
>
<div className="rounded-sm bg-white p-[20px]">
<div className="px-6.5 py-4">
<h3 className="font-medium text-black dark:text-white">
Update Vector Database Connection
</h3>
<p className="text-sm text-gray-500">
{APP_NAME} is currently connected to a {connector.type} vector
database instance. You can update your configuration settings here
if they have changed.
</p>
</div>
{loading ? (
<div className="px-6.5">
<div className="mb-4.5 flex w-full justify-center">
<PreLoader />
</div>
</div>
) : (
<form onSubmit={handleSubmit}>
<ul className="mx-6 flex w-full flex-wrap gap-6">
<li onClick={() => setType('chroma')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="chroma"
className="peer hidden"
checked={type === 'chroma'}
formNoValidate={true}
/>
<label className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white p-5 text-gray-500 hover:bg-gray-50 hover:text-gray-600 peer-checked:border-blue-600 peer-checked:bg-blue-50 peer-checked:text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:peer-checked:text-gray-300">
<div className="block">
<img
src={ChromaLogo}
className="mb-2 h-10 w-10 rounded-full"
/>
<div className="w-full text-lg font-semibold">Chroma</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-slate-400">trychroma.com</p>
Open source vector database you can host yourself.
</div>
</div>
</label>
</li>
<li onClick={() => setType('pinecone')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="pinecone"
className="peer hidden"
checked={type === 'pinecone'}
formNoValidate={true}
/>
<label className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white p-5 text-gray-500 hover:bg-gray-50 hover:text-gray-600 peer-checked:border-blue-600 peer-checked:bg-blue-50 peer-checked:text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:peer-checked:text-gray-300">
<div className="block">
<img
src={PineconeLogo}
className="mb-2 h-10 w-10 rounded-full"
/>
<div className="w-full text-lg font-semibold">Pinecone</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-slate-400">pinecone.io</p>
Cloud-hosted vector database.
</div>
</div>
</label>
</li>
<li onClick={() => setType('qdrant')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="qdrant"
className="peer hidden"
checked={type === 'qdrant'}
formNoValidate={true}
/>
<label className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white p-5 text-gray-500 hover:bg-gray-50 hover:text-gray-600 peer-checked:border-blue-600 peer-checked:bg-blue-50 peer-checked:text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:peer-checked:text-gray-300">
<div className="block">
<img
src={qDrantLogo}
className="mb-2 h-10 w-10 rounded-full"
/>
<div className="w-full text-lg font-semibold">qDrant</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-slate-400">qdrant.tech</p>
Open-source & hosted vector database.
</div>
</div>
</label>
</li>
<li onClick={() => setType('weaviate')} className="w-[250px]">
<input
name="type"
type="checkbox"
value="weaviate"
className="peer hidden"
checked={type === 'weaviate'}
formNoValidate={true}
/>
<label className="inline-flex h-full w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white p-5 text-gray-500 hover:bg-gray-50 hover:text-gray-600 peer-checked:border-blue-600 peer-checked:bg-blue-50 peer-checked:text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 dark:peer-checked:text-gray-300">
<div className="block">
<img
src={WeaviateLogo}
className="mb-2 h-10 w-10 rounded-full"
/>
<div className="w-full text-lg font-semibold">Weaviate</div>
<div className="flex w-full flex-col gap-y-1 text-sm">
<p className="text-xs text-slate-400">weaviate.io</p>
Open-source & hosted vector database.
</div>
</div>
</label>
</li>
</ul>
{type === 'chroma' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::instanceURL"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Instance URL
</label>
<p className="text-xs text-gray-500">
This is the URL your chroma instance is reachable at.
</p>
</div>
<input
name="settings::instanceURL"
autoComplete="off"
type="url"
defaultValue={settings.instanceURL}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="https://my-domain.com:8000"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::authToken"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
API Header & Key
</label>
<p className="text-xs text-gray-500">
If your hosted Chroma instance is protected by an API key
- enter the header and api key here.
</p>
</div>
<div className="flex w-full items-center gap-x-4">
<input
name="settings::authTokenHeader"
autoComplete="off"
type="text"
defaultValue={settings.authTokenHeader}
className="block w-[20%] rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="X-Api-Key"
/>
<input
name="settings::authToken"
autoComplete="off"
type="password"
defaultValue={settings.authToken}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="sk-myApiKeyToAccessMyChromaInstance"
/>
</div>
</div>
</div>
)}
{type === 'pinecone' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::environment"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Pinecone Environment
</label>
<p className="text-xs text-gray-500">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::environment"
autoComplete="off"
type="text"
defaultValue={settings.environment}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="us-west4-gcp-free"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::index"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Pinecone Index
</label>
<p className="text-xs text-gray-500">
You can find this on your Pinecone index.
</p>
</div>
<input
name="settings::index"
autoComplete="off"
type="text"
defaultValue={settings.index}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="my-index"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
API Key
</label>
<p className="text-xs text-gray-500">
If your hosted Chroma instance is protected by an API key
- enter it here.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
defaultValue={settings.apiKey}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
{type === 'qdrant' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::clusterUrl"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
qDrant Cluster URL
</label>
<p className="text-xs text-gray-500">
You can find this in your cloud hosted qDrant cluster or
just using the URL to your local docker container.
</p>
</div>
<input
name="settings::clusterUrl"
autoComplete="off"
type="url"
defaultValue={settings.clusterUrl}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="https://6b3a2d01-3b3f-4339-84e9-ead94f28a844.us-east-1-0.aws.cloud.qdrant.io"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
API Key
</label>
<p className="text-xs text-gray-500">
(optional) If you are using qDrant cloud you will need an
API key.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
defaultValue={settings.apiKey}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
{type === 'weaviate' && (
<div className="mx-6 my-4 flex flex-col gap-y-6">
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::clusterUrl"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Weaviate Cluster URL
</label>
<p className="text-xs text-gray-500">
You can find this in your cloud hosted Weaviate cluster or
just using the URL to your local docker container.
</p>
</div>
<input
name="settings::clusterUrl"
autoComplete="off"
type="url"
defaultValue={settings.clusterUrl}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="https://my-sandbox-b5vipdmw.weaviate.network"
required={true}
/>
</div>
<div className="">
<div className="mb-2 flex flex-col gap-y-1">
<label
htmlFor="settings::apiKey"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
API Key
</label>
<p className="text-xs text-gray-500">
(optional) If you are using Weaviate cloud you will need
an API key.
</p>
</div>
<input
name="settings::apiKey"
autoComplete="off"
type="password"
defaultValue={settings.apiKey}
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="ee1051-xxxx-xxxx-xxxx"
/>
</div>
</div>
)}
<div className="w-full px-6">
{error && (
<p className="my-2 w-full rounded-lg border-red-800 bg-red-50 px-4 py-2 text-red-800">
{error}
</p>
)}
{success && (
<p className="my-2 w-full rounded-lg border-green-800 bg-green-50 px-4 py-2 text-green-800">
Connector changes saved
</p>
)}
<button
type="submit"
className="w-full rounded-lg border border-blue-600 py-2 text-center text-blue-600 hover:bg-blue-600 hover:text-white"
>
Connect to Vector Database
</button>
</div>
</form>
)}
</div>
</dialog>
);
};
const SyncConnectorModal = ({
organization,
connector,
}: {
organization: any;
connector: any;
}) => {
const [synced, setSynced] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<null | string>(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 (
<dialog
id="sync-connector-modal"
className="w-1/3 rounded-lg"
onClick={(event) =>
event.target == event.currentTarget && event.currentTarget?.close()
}
>
<div className="overflow-y-scroll rounded-sm bg-white p-[20px]">
<div className="px-6.5 py-4">
<h3 className="font-medium text-black dark:text-white">
Sync Vector Database Connection
</h3>
<p className="text-sm text-gray-500">
{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.
<br />
<br />
Once you start this process you can check on its progress in the{' '}
<a
href={paths.jobs(organization)}
className="font-semibold text-blue-500"
>
job queue.
</a>
</p>
</div>
<div className="w-full px-6">
{error && (
<p className="my-2 w-full rounded-lg border-red-800 bg-red-50 px-4 py-2 text-red-800">
{error}
</p>
)}
{synced ? (
<button
type="button"
onClick={() => window.location.replace(paths.jobs(organization))}
className="w-full rounded-lg py-2 text-center text-gray-600 hover:bg-gray-400 hover:text-white"
>
Check progress
</button>
) : (
<button
type="button"
disabled={loading}
onClick={sync}
className="w-full rounded-lg bg-blue-600 py-2 text-center text-white"
>
{loading ? 'Synchronizing...' : 'Synchronize embeddings'}
</button>
)}
</div>
</div>
</dialog>
);
};
@@ -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 (
<div className="flex w-fit items-center gap-x-4 rounded-lg border border-blue-600 bg-blue-100 bg-opacity-50 px-2 py-2 dark:border-stone-600 dark:bg-stone-800">
<div className="h-6 w-6">
<XCircle className="h-6 h-full w-6 w-full rounded-full bg-red-500 stroke-white p-1" />
</div>
<div className="flex flex-col">
<p className="font-mono overflow-x-scroll text-sm text-black dark:text-stone-200">
{truncate(file.name, 30)}
</p>
<p className="font-mono text-xs text-red-700 dark:text-red-400">
{reason}
</p>
</div>
</div>
);
}
return (
<div className="flex w-fit items-center gap-x-4 rounded-lg border border-blue-600 bg-blue-100 bg-opacity-50 px-2 py-2 dark:border-stone-600 dark:bg-stone-800">
<div className="h-6 w-6">
{status !== 'complete' ? (
<Grid className="grid-loader h-6 w-6" />
) : (
<CheckCircle className="h-6 h-full w-6 w-full rounded-full bg-green-500 stroke-white p-1" />
)}
</div>
<div className="flex flex-col">
<p className="font-mono overflow-x-scroll text-sm text-black dark:text-stone-200">
{truncate(file.name, 30)}
</p>
<p className="font-mono text-xs text-gray-700 dark:text-stone-400">
{humanFileSize(file.size)} | {milliToHms(timerMs)}
</p>
</div>
</div>
);
}
export default memo(FileUploadProgressComponent);
@@ -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 (
<dialog
id="upload-document-modal"
className="w-1/2 rounded-lg"
onClick={(event) =>
event.target == event.currentTarget && event.currentTarget?.close()
}
>
<div className="my-4 flex w-full flex-col gap-y-1 p-[20px]">
<p className="text-lg font-semibold text-blue-600">
Upload new document
</p>
</div>
<div className="my-2 flex w-full p-[20px]">
<div className="flex flex w-full flex-col items-center gap-y-2 rounded-lg border border-orange-800 bg-orange-50 px-4 py-2 text-orange-800">
<div className="flex w-full items-center gap-x-2 text-lg">
<AlertTriangle /> You cannot upload and embed documents without an
OpenAI API Key.
</div>
<p>
{APP_NAME} will automatically upload and embed your documents for
you, but for this to happen we must have an OpenAI key set.
</p>
<form onSubmit={updateSystemSetting} className="w-full">
<div className="">
<div className="mb-4.5">
<label className="mb-2.5 block">Your OpenAI API Key</label>
<input
required={true}
type="password"
name="open_ai_api_key"
placeholder="sk-xxxxxxxxxx"
autoComplete="off"
className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
</div>
<div className="flex flex-col gap-y-2">
<button
type="submit"
className="flex w-full justify-center rounded bg-orange-500 p-3 font-medium text-white"
>
Add OpenAI API Key
</button>
</div>
</div>
<p className="my-2 p-2 text-center text-sm text-orange-800">
This key will only be used for the embedding of documents you
upload via {APP_NAME}.
</p>
</form>
</div>
</div>
</dialog>
);
}
@@ -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 (
<div className="col-span-12 flex-1 rounded-sm border border-stroke bg-white py-6 shadow-default dark:border-strokedark dark:bg-boxdark xl:col-span-4">
<div className="flex items-start justify-between px-4">
<div>
<h4 className="mb-6 px-4 text-xl font-semibold text-black dark:text-white">
Documents {totalDocuments! > 0 ? `(${totalDocuments})` : ''}
</h4>
</div>
</div>
<div className="flex h-60 w-full items-center justify-center px-7.5">
<div className="h-full w-full animate-pulse rounded-lg bg-slate-100" />
</div>
</div>
<div
className="mb-9 flex h-screen flex-col overflow-hidden bg-main py-6 transition-all duration-300"
style={{ height: `calc(100vh - 210px` }}
></div>
);
}
return (
<>
<div className="col-span-12 flex-1 rounded-sm border border-stroke bg-white py-6 shadow-default dark:border-strokedark dark:bg-boxdark xl:col-span-4">
<div className="flex items-start justify-between px-4">
<div>
<h4 className="mb-6 px-4 text-xl font-semibold text-black dark:text-white">
Documents {totalDocuments! > 0 ? `(${totalDocuments})` : ''}
</h4>
</div>
{workspaces.length > 0 ? (
<>
{!!knownConnector ? (
<div
className="mb-9 flex h-screen flex-col overflow-hidden bg-main transition-all duration-300"
style={{ height: `calc(100vh - 160px)` }}
>
<div className="flex-grow overflow-y-auto rounded-xl border-2 border-white/20 bg-main">
<table className="w-full rounded-xl text-left text-xs font-medium text-white text-opacity-80">
<thead className="sticky top-0 w-full border-b-2 border-white/20 bg-main ">
<tr className="mt-10">
<th
scope="col"
className="px-6 pb-2 pt-6 text-xs font-light text-white text-opacity-80"
>
Name
</th>
<th
scope="col"
className="px-6 pb-2 pt-6 text-xs font-light text-white text-opacity-80"
>
Workspace
</th>
<th
scope="col"
className="px-6 pb-2 pt-6 text-xs font-light text-white text-opacity-80"
>
Date
</th>
<th
scope="col"
className="px-6 pb-2 pt-6 text-xs font-light text-white text-opacity-80"
>
Vectors
</th>
<th
scope="col"
className="px-6 pb-2 pt-6 text-xs font-light text-white text-opacity-80"
>
{' '}
</th>
<th
hidden={workspaces.length === 0}
scope="col"
className="relative px-6 pb-2 pt-6"
>
<button
onClick={() => {
document
.getElementById('upload-document-modal')
?.showModal();
}}
className="inline-flex h-[26px] w-[98px] items-center justify-center gap-2.5 rounded-[100px] bg-white bg-opacity-10 px-2.5 py-1"
>
<div className="font-satoshi text-xs font-bold tracking-tight text-sky-400">
UPLOAD
</div>
</button>
</th>
</tr>
</thead>
{documents?.length > 0 && (
<tbody className="bg-main">
{documents.map((document, index) => (
<Fragment
key={document?.id}
document={document}
index={index}
deleteDocument={deleteDocument}
organization={organization}
/>
))}
</tbody>
)}
</table>
{!!!knownConnector && (
<div className="-mt-10 flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-y-4 text-center">
<div className="text-center font-medium text-white text-opacity-40">
Connect a Vector Database to get started
</div>
<div className="text-center text-sm font-light text-white text-opacity-80">
Begin by connecting a Vector Database to your organization
</div>
<button
onClick={() => {
document
.getElementById('upload-document-modal')
window.document
?.getElementById('new-connector-modal')
?.showModal();
}}
className="rounded-lg px-2 py-1 text-sm text-slate-800 hover:bg-slate-200"
className="mt-4 inline-flex items-center justify-center gap-2.5 rounded-lg bg-white p-2.5 px-36 shadow"
>
Add Document
<div className="text-center text-sm font-bold leading-tight text-zinc-900">
Connect Vector Database
</div>
</button>
) : (
</div>
</div>
)}
{!!knownConnector && workspaces?.length === 0 && (
<div className="-mt-10 flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-y-4 text-center">
<div className="text-center font-medium text-white text-opacity-40">
Create a workspace to get started
</div>
<div className="text-center text-sm font-light text-white text-opacity-80">
Workspaces are used to organize your documents
</div>
<button
type="button"
disabled={true}
className="flex items-center gap-x-1 rounded-lg bg-red-50 px-2 py-1 text-sm text-red-800"
onClick={() => {
window.document
?.getElementById('workspace-creation-modal')
?.showModal();
}}
className="mt-4 inline-flex items-center justify-center gap-2.5 rounded-lg bg-white p-2.5 px-36 shadow"
>
<AlertOctagon className="h4- w-4" /> Requires Vector Database
Connection
<div className="text-center text-sm font-bold leading-tight text-zinc-900">
Create Workspace
</div>
</button>
)}
</>
) : (
<button
type="button"
disabled={true}
className="flex items-center gap-x-1 rounded-lg bg-red-50 px-2 py-1 text-sm text-red-800"
>
<AlertOctagon className="h4- w-4" /> Requires Workspace
</button>
</div>
</div>
)}
{documents?.length === 0 && workspaces?.length > 0 && (
<div className="-mt-10 flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-y-4 text-center">
<div className="text-center font-medium text-white text-opacity-40">
0 Documents
</div>
<div className="text-center text-sm font-light text-white text-opacity-80">
Upload documents to your workspace
</div>
<button
onClick={() => {
window.document
?.getElementById('upload-document-modal')
?.showModal();
}}
className="mt-4 inline-flex items-center justify-center gap-2.5 rounded-lg bg-white p-2.5 px-36 shadow"
>
<div className="text-center text-sm font-bold leading-tight text-zinc-900">
Upload Documents
</div>
</button>
</div>
</div>
)}
</div>
{documents.length > 0 ? (
<div>
<div className="border-b border-stroke px-4 pb-5 dark:border-strokedark md:px-6 xl:px-7.5">
<div className="flex items-center gap-3">
<div className="w-2/12 xl:w-3/12">
<span className="font-medium">Document Name</span>
</div>
<div className="w-6/12 2xsm:w-5/12 md:w-3/12">
<span className="font-medium">Workspace</span>
</div>
<div className="hidden w-4/12 md:block xl:w-3/12">
<span className="font-medium">Created</span>
</div>
<div className="w-5/12 2xsm:w-4/12 md:w-3/12 xl:w-2/12">
<span className="font-medium">Status</span>
</div>
<div className="hidden w-2/12 text-center 2xsm:block md:w-1/12">
<span className="font-medium"></span>
</div>
</div>
</div>
<>
{documents.map((document) => {
return (
<div
id={`document-row-${document.id}`}
key={document.id}
className="flex w-full items-center gap-5 px-7.5 py-3 text-gray-600 hover:bg-gray-3 dark:hover:bg-meta-4"
>
<div className="flex w-full items-center gap-3">
<div className="w-2/12 xl:w-3/12">
<div className="flex items-center gap-x-1">
<FileText className="h-4 w-4" />
<span className="hidden font-medium xl:block">
{truncate(document.name, 20)}
</span>
</div>
</div>
<div className="w-6/12 2xsm:w-5/12 md:w-3/12">
<a
href={paths.workspace(
organization.slug,
document.workspace.slug
)}
className="hover:text-blue-500 hover:underline"
>
<span className="font-medium">
{truncate(document.workspace.name, 20) || ''}
</span>
</a>
</div>
<div className="hidden w-3/12 overflow-x-scroll md:block xl:w-3/12">
<span className="font-medium">
{moment(document.createdAt).format('lll')}
</span>
</div>
<div className="w-5/12 2xsm:w-4/12 md:w-3/12 xl:w-2/12">
<span className="inline-block rounded bg-green-500 bg-opacity-25 px-2.5 py-0.5 text-sm font-medium text-green-500">
Cached
</span>
</div>
<div className=" flex items-center gap-x-2">
<a
href={paths.document(
organization.slug,
document.workspace.slug,
document.id
)}
className="rounded-lg px-2 py-1 text-blue-400 transition-all duration-300 hover:bg-blue-50 hover:text-blue-600"
>
Details
</a>
</div>
</div>
</div>
);
})}
</>
</div>
<div className="pt-20">
<DocumentListPagination
pageCount={Math.ceil(
totalDocuments! / Organization.documentPageSize
)}
currentPage={currentPage}
gotoPage={updatePage}
/>
</div>
{canUpload ? (
<UploadDocumentModal workspaces={workspaces} />
) : (
<div>
<div className="flex min-h-[40vh] w-full px-8">
<div className="flex flex h-auto w-full flex-col items-center justify-center gap-y-2 rounded-lg bg-slate-50">
<p>You have no documents in any workspaces!</p>
<p>
Get started managing documents by adding them to workspaces
via the UI or code.
</p>
<button
type="button"
className="text-xl text-blue-500 underline"
>
Show code example (coming soon)
</button>
</div>
</div>
</div>
<UploadModalNoKey />
)}
</div>
<DocumentListPagination
pageCount={Math.ceil(totalDocuments! / Organization.documentPageSize)}
currentPage={currentPage}
gotoPage={updatePage}
/>
{canUpload ? (
<UploadDocumentModal workspaces={workspaces} />
) : (
<UploadModalNoKey />
)}
</>
);
}
// const CodeExampleModal = ({ organization }: { organization: any }) => {
// // Rework this to be an upload modal.
// return (
// <dialog id="document-code-modal" className="w-1/2 rounded-lg">
// <div className="rounded-sm bg-white dark:border-strokedark dark:bg-boxdark">
// <div className="px-6.5 py-4 dark:border-strokedark">
// <h3 className="font-medium text-black dark:text-white">
// Adding documents to Conifer
// </h3>
// <p className="text-sm text-gray-500">
// You can begin managing documents with the code you have already
// written. Our library currently only supports NodeJS environments.
// </p>
// </div>
// <p className="my-2 rounded-lg border border-orange-800 bg-orange-100 p-2 text-center text-sm text-orange-800">
// During the Pinecone Hackathon the library is a standalone fork of
// langchainJS, but ideally it would eventually be live in the main
// LangchainJS repo :)
// <br />
// 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.
// </p>
// <div className="max-h-[50vh] w-full overflow-y-scroll bg-slate-50">
// <CodeBlock
// theme={vs2015}
// text={`/* How to sync documents to Pinecone and Conifer with LangchainJS
// Be sure you have the correct packages installed!
// example: package.json
// {
// "dependencies": {
// "@mintplex-labs/langchain": "https://gitpkg.now.sh/Mintplex-Labs/langchainjs/langchain?conifer",
// ...other deps
// }
// */
// // Now write code as you usually would!
// import { PineconeClient } from "@pinecone-database/pinecone";
// import { PineconeStore } from "@mintplex-labs/langchain/dist/vectorstores/pinecone.js"
// import { ConiferVDBMS } from "@mintplex-labs/langchain/dist/vdbms/conifer.js"
// import { OpenAIEmbeddings } from "langchain/embeddings/openai";
// const client = new PineconeClient();
// await client.init({
// apiKey: 'my-pinecone-api-key',
// environment: 'us-central-gcp',
// });
// const pineconeIndex = client.Index('hackathon');
// const coniferInstance = new ConiferVDBMS({
// orgId: '${organization.orgId}',
// workspaceId: 'workspace-xxxx', // Get from workspace page.
// apiKey: 'ck-xxx' // Get from the api key at the top of the page.
// })
// // Split documents with LangChain text splitter as you normally would
// await PineconeStore.fromDocumentsVerbose(
// documents,
// new OpenAIEmbeddings({ openAIApiKey: 'sk-xxxxxxxx' }),
// {
// pineconeIndex,
// namespace:' testing-collection',
// },
// coniferInstance
// )
// // Documents will now exist in Conifer!
// // More CRUD methods available at ${window.location.origin}/api-docs
// `}
// language="javascript"
// showLineNumbers={false}
// />
// </div>
// <div className="mt-4 flex flex-col gap-y-2">
// <button
// type="button"
// onClick={() => {
// document.getElementById('document-code-modal')?.close();
// }}
// className="flex w-full justify-center rounded bg-transparent p-3 font-medium text-slate-500 hover:bg-slate-200"
// >
// Close Preview
// </button>
// </div>
// </div>
// </dialog>
// );
// };
const Fragment = ({
document,
index,
deleteDocument,
organization,
}: {
document: any;
index: number;
deleteDocument: any;
organization: any;
}) => {
return (
<>
<tr
key={document?.id}
id={`document-row-${document?.id}`}
className={`h-9 hover:bg-white/10 ${
index % 2 === 0 ? 'bg-main-2' : 'bg-main'
}`}
>
<td className="flex items-center gap-x-1 px-6 py-2 text-sm font-light text-white">
<File className="flex-shrink-0" size={16} weight="fill" />
<p>{truncate(document?.name, 35)}</p>
</td>
<td className="px-6 ">
<a
href={paths.workspace(organization.slug, document.workspace.slug)}
className="hover:text-sky-400 hover:underline"
>
<span className="font-medium">
{truncate(document.workspace.name, 20) || ''}
</span>
</a>
</td>
<td className="px-6 ">{moment(document?.createdAt).format('lll')}</td>
<td className="px-6 ">Cached</td>
<td className="px-6 ">
<a
href={paths.document(
organization.slug,
document.workspace.slug,
document.id
)}
className="rounded-lg px-2 py-1 text-sky-400 transition-all duration-300 hover:bg-blue-50"
>
Details
</a>
</td>
<td className="px-6">
<div className="flex items-center gap-x-4">
<div className=" flex items-center gap-x-6">
<button
type="button"
onClick={() => deleteDocument(document?.id)}
className="rounded-lg px-2 py-1 text-white transition-all duration-300 hover:bg-red-50 hover:text-red-600"
>
<Trash size={16} />
</button>
</div>
</div>
</td>
</tr>
</>
);
};
@@ -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 (
<div className="-mt-14 w-[217px]">
<button
onClick={() => setQuickActionsOpen(!quickActionsOpen)}
className="w-full text-white/80"
>
<div className="flex items-center justify-between">
<div className="font-['Plus Jakarta Sans'] whitespace-nowrap text-xs font-semibold uppercase leading-tight tracking-wide text-white text-opacity-80">
quick actions
</div>
<div
className={`${
quickActionsOpen ? '' : 'rotate-180'
} transition-all duration-300`}
>
<CaretDown size={18} weight="bold" />
</div>
</div>
</button>
<div
className={`${
quickActionsOpen
? 'slide-down mb-4 mt-4 transition-all duration-300'
: 'slide-up'
}`}
style={{
animationDuration: '0.15s',
}}
>
{user?.role === 'admin' && (
<>
<NavLink to={paths.settings()}>
<div className="mt-5 flex items-center gap-x-2 text-white hover:cursor-pointer hover:text-sky-400 hover:underline">
<ShieldCheckered size={18} weight="fill" />
<div className="text-sm font-medium">System Settings</div>
</div>
</NavLink>
<NavLink to={paths.toolsHome(organization)}>
<div className="mt-5 flex items-center gap-x-2 text-white hover:cursor-pointer hover:text-sky-400 hover:underline">
<Toolbox size={18} weight="bold" />
<div className="text-sm font-medium">Tools</div>
</div>
</NavLink>
<NavLink to={paths.users()}>
<div className="mt-5 flex items-center gap-x-2 text-white hover:cursor-pointer hover:text-sky-400 hover:underline">
<User size={18} weight="bold" />
<div className="text-sm font-medium">Add User</div>
</div>
</NavLink>
</>
)}
<NavLink to={paths.jobs(organization)}>
<div className="mt-5 flex items-center gap-x-2 text-white hover:cursor-pointer hover:text-sky-400 hover:underline">
<SpinnerGap size={18} weight="bold" />
<div className="text-sm font-medium">Background Jobs</div>
</div>
</NavLink>
</div>
</div>
);
}
@@ -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 (
<div className="col-span-12 rounded-md border border-stroke bg-white p-7.5 shadow-default dark:border-strokedark dark:bg-boxdark">
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4 xl:gap-0">
<div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark xl:border-b-0 xl:border-r xl:pb-0">
{documents.status === 'loading' ? (
<PreLoader />
) : (
<div className="flex flex-col items-center">
<h4 className="mb-0.5 text-xl font-bold text-black dark:text-white md:text-title-lg">
{nFormatter(documents.value)}
</h4>
<p className="text-sm font-medium">
{pluralize('Document', documents.value)}
</p>
</div>
)}
<div className="-mt-6 flex w-full items-center pr-7">
<div className=" ml-4 flex items-center gap-x-6">
<div className="flex items-center gap-x-1">
<span className="font-['Plus Jakarta Sans'] text-sm font-bold uppercase leading-[18px] tracking-wide text-white">
Documents
</span>
<span className="font-['JetBrains Mono'] text-sm font-bold uppercase leading-[18px] tracking-wide text-white">
{' '}
</span>
<span className="font-['JetBrains Mono'] text-sm font-extrabold uppercase leading-[18px] tracking-wide text-white">
({nFormatter(documents.value)})
</span>
</div>
</div>
<div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark xl:border-b-0 xl:border-r xl:pb-0">
{vectors.status === 'loading' ? (
<PreLoader />
) : (
<div className="flex flex-col items-center">
<h4 className="mb-0.5 text-xl font-bold text-black dark:text-white md:text-title-lg">
{nFormatter(vectors.value)}
</h4>
<p className="text-sm font-medium">
{pluralize('Vector', vectors.value)}
</p>
</div>
)}
</div>
<div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark sm:border-b-0 sm:pb-0 xl:border-r">
{cache.status === 'loading' ? (
<PreLoader />
) : (
<div className="flex flex-col items-center">
<h4 className="mb-0.5 text-xl font-bold text-black dark:text-white md:text-title-lg">
{humanFileSize(cache.value)}
</h4>
<p className="text-sm font-medium">Vector Cache (MB)</p>
</div>
)}
</div>
<div className="flex items-center justify-center gap-2">
<div className="flex flex-col items-center">
<h4 className="mb-0.5 text-xl font-bold text-black dark:text-white md:text-title-lg">
{organization?.lastUpdated
? moment(organization.lastUpdated).fromNow()
: moment(organization.createdAt).fromNow()}
</h4>
<p className="text-sm font-medium">Last Modified</p>
</div>
<div className="ml-4 mr-48 w-full rounded-xl border-2 border-white/20 px-5 py-2 text-sky-400">
<div className="flex items-center justify-between whitespace-nowrap">
<span className="font-jetbrains uppercase text-white">
{pluralize('Vector', vectors.value)}:{' '}
<span className=" font-jetbrainsbold">
{nFormatter(vectors.value)}
</span>
</span>
<span className="font-jetbrains uppercase text-white">
Vector Cache:{' '}
<span className=" font-jetbrainsbold">
{humanFileSize(cache.value)}
</span>
</span>
<span className="font-jetbrains uppercase text-white">
Dimensions:{' '}
<span className=" font-jetbrainsbold">{dimensions.value}</span>
</span>
</div>
</div>
</div>
@@ -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({
<div className="px-6.5">
<div className="mb-4.5">
<div className="mb-2.5 flex flex-col">
<label className="block text-black dark:text-white">
<label className="block text-sm font-medium text-white">
Workspace Name
</label>
<p className="text-xs text-gray-600">
{/* <p className="text-xs text-gray-600">
if a workspace with a matching name is found in your vector
database we will ask you to confirm before creating it in{' '}
{APP_NAME}.
</p>
</p> */}
</div>
<input
required={true}
@@ -119,10 +121,10 @@ export default function CreateWorkspaceModal({
placeholder="My workspace"
autoComplete="off"
onChange={debouncedOnChange}
className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
className="placeholder-text-white/60 w-full rounded-lg border border-white/10 bg-main-2 px-2.5 py-2 text-sm text-white"
/>
{error && (
<p className="my-1 rounded-lg border border-red-800 bg-red-600/10 p-1 px-2 text-sm text-red-600">
<p className="my-2 rounded-lg border border-red-800 bg-red-600/10 p-2 px-2 text-sm text-red-600">
Error: {error}
</p>
)}
@@ -133,7 +135,7 @@ export default function CreateWorkspaceModal({
<button
type="submit"
disabled={searching}
className="flex w-full justify-center rounded bg-blue-500 p-3 font-medium text-white disabled:bg-gray-400"
className="w-full rounded-lg bg-white p-2 font-medium text-main shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
Create new workspace
</button>
@@ -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
</button>
@@ -171,7 +173,7 @@ export default function CreateWorkspaceModal({
<button
type="submit"
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"
>
No, create new workspace
</button>
@@ -187,21 +189,22 @@ export default function CreateWorkspaceModal({
const ModalWrapper = ({ children }: { children: React.ReactElement }) => {
return (
<dialog id="workspace-creation-modal" className="w-1/2 rounded-lg">
<div className="overflow-y-scroll rounded-sm bg-white p-[20px]">
<dialog
id="workspace-creation-modal"
className="w-1/3 rounded-xl border-2 border-white/20 bg-main shadow"
>
<div className="w-full overflow-y-scroll rounded-sm p-[20px]">
<div className="px-6.5 py-4">
<h3 className="font-medium text-black dark:text-white">
<h3 className="text-lg font-medium text-white">
Create or find a new workspace
</h3>
<p className="text-sm text-gray-500">
Workspaces are collections of documents inside of your organization.
<p className="text-sm text-white/60">
{/* Workspaces are collections of documents inside of your organization.
They allow you to control permissions and documents with ultimate
visibility.
<br />
<b>
They should match with what you are calling your namespaces or
collections in your vector database.
</b>
<br /> */}
Workspaces should match with what you are calling your namespaces or
collections in your vector database.
</p>
</div>
{children}
@@ -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 (
<div className="col-span-12 flex-1 rounded-sm border border-stroke bg-white py-6 shadow-default dark:border-strokedark dark:bg-boxdark xl:col-span-4">
<div className="col-span-12 max-w-[217px] flex-1 rounded-sm border border-stroke bg-white py-6 shadow-default dark:border-strokedark dark:bg-boxdark xl:col-span-4">
<div className=" top-0 z-10 bg-white">
<div className="mb-6 flex w-full items-center justify-between px-7.5">
<h4 className="text-xl font-semibold text-black dark:text-white">
@@ -89,7 +88,6 @@ export default function WorkspacesList({
)}
</div>
</WorkspaceSearch>
<CreateWorkspaceModal organization={organization} />
</div>
);
}
+107 -22
View File
@@ -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<object[]>([]);
const [currentPage, setCurrentPage] = useState<number>(1);
const [hasMoreWorkspaces, setHasMoreWorkspaces] = useState<boolean>(true);
const [totalWorkspaces, setTotalWorkspaces] = useState<number>(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={
<OrganizationHeader
organization={organization}
workspace={workspaces?.[0]}
connector={connector}
deleteWorkspace={() => {}}
/>
}
hasQuickActions={true}
>
{!!organization && (
{!!organization && !!connector && (
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4 2xl:gap-7.5">
<ConnectorCard
knownConnector={connector}
<UpdateConnectorModal
organization={organization}
workspaces={workspaces}
connector={connector}
onUpdate={(newConnector: any) => setConnector(newConnector)}
/>
<SyncConnectorModal
organization={organization}
connector={connector}
/>
<ApiKeyCard organization={organization} />
</div>
)}
<Statistics organization={organization} />
<div className="mt-4 grid grid-cols-12 gap-4 md:mt-6 md:gap-6 2xl:mt-7.5 2xl:gap-7.5">
<div className="col-span-12 xl:col-span-8">
{!connector && (
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4 2xl:gap-7.5">
<NewConnectorModal
organization={organization}
onNew={() => window.location.reload()}
/>
</div>
)}
<Statistics organization={organization} workspaces={workspaces} />
<div className="mt-4 flex w-full">
<div className="mr-6.5 w-full">
<DocumentsList
knownConnector={connector}
organization={organization}
workspaces={workspaces}
/>
</div>
<WorkspacesList
knownConnector={connector}
organization={organization}
workspaces={workspaces}
totalWorkspaces={totalWorkspaces}
/>
<QuickActionsSidebar organization={organization} />
</div>
</AppLayout>
);
}
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 (
<>
<div className=" mr-10 w-full rounded-xl border-2 border-white/20 px-5 py-2 text-sky-400">
<div className="flex items-center gap-x-2">
<span className="text-lg font-medium text-white">
{truncate(organization?.name, 20)}
</span>
</div>
</div>
<div className="flex gap-x-3">
<button
onClick={() =>
window.document?.getElementById('edit-connector-modal')?.showModal()
}
className="flex h-11 w-11 items-center justify-center rounded-lg border-2 border-white border-opacity-20 transition-all duration-300 hover:bg-opacity-5"
>
{!!connector?.type ? (
<img src={logo} alt="Connector logo" className="h-full p-1" />
) : (
<>
<div className="text-white/60 hover:cursor-not-allowed">
<Prohibit size={28} />
</div>
</>
)}
</button>
<button
onClick={() =>
document?.getElementById('sync-connector-modal')?.showModal()
}
className="inline-flex h-11 w-[74px] flex-col items-center justify-center gap-2.5 rounded-lg bg-white bg-opacity-10 px-5 py-2.5 transition-all duration-300 hover:bg-opacity-5"
>
<div className="font-satoshi h-[25.53px] w-11 text-center text-base font-bold text-white">
Sync
</div>
</button>
<NavLink
className="flex h-11 w-11 items-center justify-center rounded-lg border-2 border-white border-opacity-20 text-white transition-all duration-300 hover:bg-opacity-5"
to={paths.organizationSettings(organization)}
>
<GearSix size={28} />
</NavLink>
</div>
</>
);
}
@@ -22,48 +22,49 @@ const DeleteEmbeddingConfirmation = memo(
return (
<dialog
id={`${fragment.id}-delete-embedding`}
className="w-1/2 rounded-lg"
className="w-1/3 rounded-xl border-2 border-white/20 bg-main shadow"
onClick={(event) =>
event.target === event.currentTarget && event.currentTarget?.close()
}
>
<div className="my-4 flex w-full flex-col gap-y-1 p-[20px]">
<p className="text-lg font-semibold text-red-600">
<div className="my-4 flex w-full flex-col justify-center px-8">
<h3 className="text-lg font-medium text-white">
Delete this embedding?
</p>
<p className="text-sm text-slate-800">
</h3>
<p className="mt-4 text-sm text-white/60">
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.
</p>
</div>
<div className="flex w-full flex-col overflow-y-scroll px-4">
<pre className="font-mono w-full whitespace-pre-line rounded-lg bg-slate-100 p-2">
<pre className="mt-4 whitespace-pre-line rounded-lg border-2 border-white/10 bg-main-2 p-4 font-mono text-white">
{data.metadata.text}
</pre>
<div className="mt-4 flex flex-col gap-y-2">
<button
type="button"
disabled={loading}
onClick={deleteEmbedding}
className="flex w-full justify-center rounded bg-transparent p-3 font-medium text-red-500 hover:bg-red-200 disabled:bg-red-200"
>
{loading ? (
<Loader className="h-6 w-6 animate-spin" />
) : (
'Yes, delete this embedding'
)}
</button>
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-delete-embedding`)
?.close();
}}
className="flex w-full justify-center rounded bg-transparent p-3 font-medium text-slate-500 hover:bg-slate-200"
>
Nevermind
</button>
</div>
</div>
<div className="w-full px-6">
<button
type="button"
disabled={loading}
onClick={deleteEmbedding}
className="mb-4 h-11 w-full items-center rounded-lg bg-white p-2 text-center text-sm font-bold text-neutral-700 shadow-lg transition-all duration-300 hover:bg-red-500 hover:text-white"
>
{loading ? (
<Loader className="animate-spin" />
) : (
'Yes, delete this embedding'
)}
</button>
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-delete-embedding`)
?.close();
}}
className="mb-4 h-11 w-full items-center rounded-lg bg-transparent p-2 text-center text-sm font-bold text-white transition-all duration-300 hover:bg-white hover:text-neutral-700"
>
Nevermind
</button>
</div>
</dialog>
);
@@ -69,132 +69,117 @@ const EditEmbeddingConfirmation = memo(
return (
<dialog
id={`${fragment.id}-edit-embedding`}
className="w-1/2 rounded-lg"
className="min-w-[200px] max-w-[65%] rounded-xl border-2 border-white/20 bg-main shadow"
onClick={(event) =>
event.target == event.currentTarget && event.currentTarget?.close()
}
>
<div className="my-4 flex w-full flex-col gap-y-1 p-[20px]">
<p className="text-lg font-semibold text-blue-600">
Edit embedding
</p>
<p className="text-sm text-slate-800">
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.
</p>
</div>
<div className="my-2 flex w-full p-[20px]">
<div className="flex flex w-full flex-col items-center gap-y-2 rounded-lg border border-orange-800 bg-orange-50 px-4 py-2 text-orange-800">
<div className="flex w-full items-center gap-x-2 text-lg">
<AlertTriangle /> You cannot edit embeddings without an OpenAI
API key set for your instance.
</div>
<p>
{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.
<div className="w-full overflow-y-scroll rounded-sm p-[20px]">
<div className="px-6.5 py-4">
<p className="text-lg font-medium text-white">Edit embedding</p>
<p className="text-sm text-white/60">
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.
</p>
<form onSubmit={updateSystemSetting} className="w-full">
<div className="">
</div>
<div className="flex w-full justify-center p-[20px]">
<div className="npr flex flex-col items-center gap-y-2 rounded-lg px-4 py-2 text-white">
<div className="my-4 flex flex-col items-center justify-center rounded-lg border border-red-500 p-4">
<AlertTriangle className="h-6 w-6 text-red-500" />
<p className="text-lg">
You cannot edit embeddings without an OpenAI API key set for
your instance.
</p>
</div>
<p>
{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.
</p>
<form onSubmit={updateSystemSetting} className="w-full">
<div className="mb-4.5">
<label className="mb-2.5 block">Your OpenAI API Key</label>
<label className="mb-2.5 block text-sm font-medium">
Your OpenAI API Key
</label>
<input
required={true}
type="password"
name="open_ai_api_key"
placeholder="sk-xxxxxxxxxx"
autoComplete="off"
className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
className="w-full rounded-lg border border-white/10 bg-main-2 px-2.5 py-2 text-sm text-white"
/>
</div>
<div className="flex flex-col gap-y-2">
<button
type="submit"
className="flex w-full justify-center rounded bg-orange-500 p-3 font-medium text-white"
>
Add OpenAI API Key
</button>
</div>
</div>
<p className="my-2 p-2 text-center text-sm text-orange-800">
This key will only be used for the embedding of changes or
additions you make via {APP_NAME}.
</p>
</form>
<button
type="submit"
className="w-full rounded-lg bg-white p-2 font-medium text-main shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
Add OpenAI API Key
</button>
</form>
</div>
</div>
</div>
<pre className="font-mono whitespace-pre-line rounded-lg bg-slate-100 p-2">
{data.metadata.text}
</pre>
</dialog>
);
}
const debouncedTokenLengthCheck = debounce(checkTokenSize, 500);
return (
<dialog id={`${fragment.id}-edit-embedding`} className="w-1/2 rounded-lg">
<div className="my-4 flex w-full flex-col gap-y-1 p-[20px]">
<p className="text-lg font-semibold text-blue-600">Edit embedding</p>
<p className="text-sm text-slate-800">
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.
</p>
</div>
<form
onSubmit={handleSubmit}
className="flex h-full w-full flex-col gap-y-1"
>
<div className="flex w-full items-center justify-between px-4">
<p className="font-semibold text-red-600">{error || ''}</p>
<p
className={`text-sm ${
error ? 'font-semibold text-red-600' : 'text-slate-600'
}`}
>
{numberWithCommas(tokenLength)}/
{numberWithCommas(MAX_TOKENS.cl100k_base)}{' '}
<dialog
id={`${fragment.id}-edit-embedding`}
className="min-w-[200px] max-w-[65%] rounded-xl border-2 border-white/20 bg-main shadow"
>
<div className="w-full overflow-y-scroll rounded-sm p-[20px]">
<div className="px-6.5 py-4">
<p className="text-lg font-medium text-white">Edit embedding</p>
<p className="text-sm text-white/60">
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.
</p>
</div>
<div className="flex max-h-[700px] w-full flex-col overflow-y-scroll px-4">
<textarea
ref={inputEl}
onChange={debouncedTokenLengthCheck}
name="embeddingText"
defaultValue={data.metadata.text}
spellCheck="true"
className="font-mono h-fit w-full overflow-y-scroll rounded-lg bg-slate-100 p-2"
/>
<div className="mt-4 flex flex-col gap-y-2">
<button
type="submit"
disabled={loading || !!error}
className="flex w-full justify-center rounded bg-transparent p-3 font-medium text-blue-500 hover:bg-blue-200 disabled:cursor-not-allowed disabled:bg-slate-200"
>
{loading ? (
<Loader className="h-6 w-6 animate-spin" />
) : !!error ? (
error
) : (
'Update embedding'
)}
</button>
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-edit-embedding`)
?.close();
}}
className="flex w-full justify-center rounded bg-transparent p-3 font-medium text-slate-500 hover:bg-slate-200"
>
Nevermind
</button>
<form onSubmit={handleSubmit} className="px-6.5">
<div className="mb-4.5">
<textarea
ref={inputEl}
onChange={debouncedTokenLengthCheck}
name="embeddingText"
defaultValue={data.metadata.text}
spellCheck="true"
className="w-full rounded-lg border border-white/10 bg-main-2 px-2.5 py-2 text-sm text-white"
/>
</div>
</div>
</form>
{loading ? (
<div className="flex w-full justify-center">
<Loader className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex flex-col gap-y-2">
<button
type="submit"
disabled={!!error}
className="w-full rounded-lg bg-white p-2 font-medium text-main shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
Update embedding
</button>
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-edit-embedding`)
?.close();
}}
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
</button>
</div>
)}
</form>
</div>
</dialog>
);
}
@@ -61,71 +61,63 @@ const MetadataEditor = memo(
<>
<dialog
id={`${fragment.id}-metadata-editor`}
className="w-1/2 rounded-lg"
className="max-w-180 rounded-xl border-2 border-white/20 bg-main shadow"
>
<div className="my-4 flex w-full flex-col gap-y-1 px-[20px]">
<p className="text-lg font-semibold text-blue-600">
Edit metadata for embedding
</p>
<p className="text-sm text-slate-800">
Listed below is all of the current metadata keys and values. You
can delete or create new key-value pairs. Metadata is useful if
your application of vector data requires filtering based on
non-embedded information.
</p>
{connector?.type === 'weaviate' && (
<div className="flex w-full items-center justify-center gap-x-2 rounded-lg border border-orange-600 bg-orange-50 px-4 py-2 text-lg text-orange-800">
<AlertTriangle size={18} />
<p>
Deletion of metadata keys is disabled for Weaviate databases.
</p>
</div>
)}
</div>
<div className="flex w-full flex-col overflow-y-scroll px-[20px] py-0">
{!!error && (
<p className="mb-4 w-full rounded-lg bg-red-200 px-4 py-2 text-lg text-red-600">
{error}
<div className="rounded-sm p-[20px]">
<div className="px-6.5 py-4">
<p className="text-lg font-medium text-white">
Edit metadata for embedding
</p>
)}
<JSONFormBuilder
fragment={fragment}
onSubmit={handleSubmit}
onChange={setHasChanges}
metadata={editableMetadata}
canEdit={canEdit}
canDelete={connector?.type !== 'weaviate'}
/>
<NewEntry addKeyPair={addNewKeyValue} />
<p className="text-sm text-white/60">
Delete or create new key-value pairs. Metadata is useful if your
application of vector data requires filtering based on
non-embedded information.
</p>
</div>
<div className="mt-4 flex flex-col gap-y-2">
<div hidden={!hasChanges || !canEdit}>
<div className="flex w-full flex-col overflow-y-scroll px-[20px] py-0">
{!!error && (
<p className="mb-4 w-full rounded-lg bg-red-600/10 px-4 py-2 text-lg text-red-600">
{error}
</p>
)}
<JSONFormBuilder
fragment={fragment}
onSubmit={handleSubmit}
onChange={setHasChanges}
metadata={editableMetadata}
canEdit={canEdit}
canDelete={connector?.type !== 'weaviate'}
/>
<NewEntry addKeyPair={addNewKeyValue} />
<div className="mt-4 flex flex-col gap-y-2">
<div hidden={!hasChanges || !canEdit}>
<button
type="button"
disabled={saving}
onClick={() =>
document
.getElementById(`${fragment.id}-submit-metadata`)
?.click()
}
className="flex w-full justify-center rounded-lg bg-white p-2 font-medium text-main shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
{saving ? 'Saving changes...' : 'Save changes'}
</button>
</div>
<button
type="button"
disabled={saving}
onClick={() =>
onClick={() => {
document
.getElementById(`${fragment.id}-submit-metadata`)
?.click()
}
className="flex w-full justify-center rounded border border-blue-100 bg-blue-50 bg-transparent p-3 font-medium text-blue-500 hover:bg-blue-600 hover:text-white"
.getElementById(`${fragment.id}-metadata-editor`)
?.close();
}}
className="flex w-full justify-center 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"
>
{saving ? 'Saving changes...' : 'Save changes'}
Close
</button>
</div>
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-metadata-editor`)
?.close();
}}
className="flex w-full justify-center rounded bg-transparent p-3 font-medium text-slate-500 hover:bg-slate-200"
>
Close
</button>
</div>
</div>
</dialog>
@@ -167,16 +159,16 @@ function JSONFormBuilder({
return (
<div key={key} id={containerId} className="mb-4">
<label className="mb-2 flex items-center gap-x-1 text-sm text-gray-700">
<label className="mb-2 flex items-center gap-x-1 text-sm text-white">
<p className="font-bold">{key}</p>
<i className="font-regular text-xs text-gray-600">
<i className="font-regular text-xs text-white/60">
({value === null ? 'string' : typeof value})
</i>
</label>
<div className="flex w-full items-center gap-x-2">
<input
disabled={!canEdit}
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
className="focus:shadow-outline w-full appearance-none rounded border border-white/20 bg-main-2 px-3 py-2 leading-tight text-white placeholder-white/60 shadow focus:outline-none"
name={name ? `${name}.${key}` : key}
type={typeof value === 'number' ? 'number' : 'text'}
data-output-type={value === null ? 'string' : typeof value}
@@ -204,7 +196,10 @@ function JSONFormBuilder({
const renderObject = (obj = {}, parentKey = '', name = '') => {
if (!obj) return null;
return (
<fieldset key={parentKey} className="mb-4 rounded border p-4">
<fieldset
key={parentKey}
className="mb-4 rounded-lg border border-white/20 bg-main-2 p-4"
>
<legend className="mb-2 text-sm font-bold text-gray-800">
{parentKey}
</legend>
@@ -263,10 +258,14 @@ const NewEntry = ({
<button
type="button"
onClick={() => setShowing(true)}
className="flex w-full items-center justify-center gap-x-2 rounded bg-transparent p-3 font-medium text-blue-500 hover:bg-blue-600 hover:text-white"
className="mb-4 mt-4 h-10 w-full items-center rounded-lg p-2 text-center text-sm font-bold text-white hover:bg-white hover:text-black"
>
<PlusCircle size={18} />
Add new metadata item
<div className="flex items-center justify-center gap-x-2">
<p className="inline">
<PlusCircle size={18} />
</p>
<p className="inline text-xs font-medium">Add new metadata item</p>
</div>
</button>
</div>
@@ -274,43 +273,43 @@ const NewEntry = ({
<form
onSubmit={handleSubmit}
id="newMetadata"
className="flex w-full flex-col gap-y-2 rounded-lg border border-gray-200 px-6 py-4"
className="flex w-full flex-col gap-y-2 rounded-xl border-2 border-white/20 bg-main p-[20px] shadow"
>
<div className="flex w-full items-center justify-between">
<p className="text-sm text-gray-700">
<p className="text-sm text-white/60">
Enter the information below to add a new metadata item to this
embedding.
</p>
<button
onClick={() => setShowing(false)}
type="button"
className="rounded-lg p-2 text-gray-700 hover:bg-gray-100 hover:text-red-500"
className="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"
>
<X size={18} />
</button>
</div>
<div className="flex items-center gap-x-2">
<div>
<label className="mb-2 block text-sm font-medium text-gray-900 dark:text-white">
<label className="mb-2 block text-sm font-medium text-white">
Metadata key
</label>
<input
pattern="[a-zA-Z0-9]*"
type="text"
name="metadata_key"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
className="block w-full rounded-lg border border-white/10 bg-main-2 px-2.5 py-2 text-sm text-white placeholder:text-white/60"
placeholder="MyKey"
required
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-900 dark:text-white">
<label className="mb-2 block text-sm font-medium text-white">
Value data type
</label>
<select
name="metadata_value_type"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
className="block w-full rounded-lg border border-white/10 bg-main-2 px-2.5 py-2 text-white placeholder:text-white/60"
>
<option value="string">String</option>
<option value="number">Number</option>
@@ -319,13 +318,13 @@ const NewEntry = ({
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-900 dark:text-white">
<label className="mb-2 block text-sm font-medium text-white">
Value
</label>
<input
type="text"
name="metadata_value"
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
className="block w-full rounded-lg border border-white/10 bg-main-2 px-2.5 py-2 text-sm text-white placeholder:text-white/60"
placeholder="My Special Value"
required
/>
@@ -335,7 +334,7 @@ const NewEntry = ({
<button
type="submit"
form="newMetadata"
className="flex w-fit items-center justify-center gap-x-2 rounded-lg bg-blue-50 px-4 py-2 text-blue-700 hover:bg-blue-600 hover:text-white"
className="flex w-fit items-center justify-center gap-x-2 rounded-lg bg-white p-2 font-medium text-main shadow-lg transition-all duration-300 hover:scale-105 hover:bg-opacity-90"
>
<PlusCircle size={18} />
<p>Add</p>

Some files were not shown because too many files have changed in this diff Show More