@@ -6,4 +6,6 @@ node_modules
|
||||
__pycache__
|
||||
v-env
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
frontend/.env.development
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 →
|
||||
</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',
|
||||
// }}
|
||||
// />
|
||||
@@ -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 →
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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,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,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 = {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 →
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||