Merge pull request #85 from Mintplex-Labs/v2-workspace-document

v2 workspace document
This commit is contained in:
Timothy Carambat
2023-12-21 11:06:15 -08:00
committed by GitHub
18 changed files with 850 additions and 553 deletions
@@ -1,3 +1,4 @@
import { CaretDown } from '@phosphor-icons/react';
import { numberWithCommas } from '../../utils/numbers';
function generatePageItems(total: number, current: number) {
@@ -30,17 +31,37 @@ 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);
};
return (
<div className="my-4 flex justify-center">
<div className="my-4 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 +72,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>
);
}
+3 -101
View File
@@ -1,11 +1,4 @@
import { Link } from 'react-router-dom';
import Logo from '../../images/logo/logo-light.png';
import { CheckCircle, Copy } from 'react-feather';
import { useEffect, useState } from 'react';
import paths from '../../utils/paths';
import { STORE_TOKEN, STORE_USER } from '../../utils/constants';
import truncate from 'truncate';
import Notifications from './Notifications';
export default function Header(props: {
entity?: any | null;
@@ -17,12 +10,7 @@ export default function Header(props: {
}) {
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 +23,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>
);
}
@@ -82,7 +82,7 @@ export default function OrganizationTab({
to={paths.organization(organization)}
className={({ isActive: active }) => {
setIsActive(active);
return `group relative flex w-[13vw] 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 ${
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' : ''
}`;
}}
@@ -102,9 +102,18 @@ export default function OrganizationTab({
</div>
</div>
</NavLink>
{isActive && menuOpen && (
<>
<div className="mb-3.5 mt-4 flex items-center justify-between px-3">
{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"
@@ -150,7 +159,7 @@ export default function OrganizationTab({
{isSearching ? (
<LoadingWorkspaceItem />
) : searchTerm !== '' && searchResults.length > 0 ? (
<div className="mt-2">
<div className="mt-2 max-h-[150px] overflow-y-auto">
{searchResults.map((workspace, idx) => (
<WorkspaceItem key={idx} workspace={workspace} slug={slug} />
))}
@@ -195,7 +204,7 @@ export default function OrganizationTab({
</div>
)}
<CreateWorkspaceModal organization={organization} />
</>
</div>
)}
</li>
);
@@ -212,7 +221,7 @@ function WorkspaceItem({ workspace, slug }: any) {
}`;
}}
>
{truncate(workspace.name, 24)}
{truncate(workspace.name, 23)}
</NavLink>
</li>
);
+6 -10
View File
@@ -1,6 +1,6 @@
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 useUser from '../../hooks/useUser';
@@ -90,7 +90,7 @@ export default function Sidebar({
<>
<aside
ref={sidebar}
className={`max-w-72.5 absolute left-0 top-0 z-9999 flex h-screen min-w-[220px] flex-col overflow-y-hidden bg-main duration-300 ease-linear 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'
}`}
>
@@ -100,12 +100,8 @@ export default function Sidebar({
to={paths.dashboard()}
className="flex w-full shrink-0 justify-center"
>
<div className="flex w-full justify-center rounded-br-[14px] bg-main-bg">
<img
src={Logo}
alt="Logo"
className="w-full max-w-[180px] object-cover p-4"
/>
<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>
@@ -134,9 +130,9 @@ 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 --> */}
<div className="no-scrollbar m-4 h-screen flex-grow overflow-y-auto rounded-xl border-2 border-white/20">
<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>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

+34 -1
View File
@@ -26,7 +26,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 +34,7 @@ dialog {
dialog[open] {
opacity: 1;
display: flex;
pointer-events: inherit;
}
@@ -50,3 +51,35 @@ dialog::backdrop {
) !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;
}
+3 -3
View File
@@ -30,7 +30,7 @@ const AppLayout = ({
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 +42,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
@@ -56,7 +56,7 @@ const AppLayout = ({
</div>
)}
<main>
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
<div className="mx-auto rounded-tr-xl bg-main pr-6 pt-6">
{children}
</div>
</main>
+1 -2
View File
@@ -1,5 +1,4 @@
import { ISearchTypes } from '../pages/DocumentView/FragmentList/SearchView';
import { API_BASE } from '../utils/constants';
import { API_BASE, ISearchTypes } from '../utils/constants';
import { baseHeaders, getAPIUrlString } from '../utils/request';
const Document = {
+1 -2
View File
@@ -1,5 +1,4 @@
import { ISearchTypes } from '../pages/WorkspaceDashboard/DocumentsList/SearchView';
import { API_BASE } from '../utils/constants';
import { API_BASE, ISearchTypes } from '../utils/constants';
import { baseHeaders, getAPIUrlString } from '../utils/request';
const Workspace = {
@@ -230,95 +230,3 @@ export default function DocumentsList({
</>
);
}
// 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>
// );
// };
@@ -5,31 +5,9 @@ import {
useRef,
useState,
} from 'react';
import { ChevronDown, Search, Loader } from 'react-feather';
import { CaretDown, MagnifyingGlass, X } from '@phosphor-icons/react';
import Document from '../../../../models/document';
export type ISearchTypes = 'semantic' | 'exactText' | 'metadata' | 'vectorId';
const SEARCH_MODES = {
exactText: {
display: 'Fuzzy Text Search',
placeholder: 'Find embedding via a fuzzy text match on your query.',
},
semantic: {
display: 'Semantic Search',
placeholder:
'Search with natural language finding the most similar embedding by meaning. Use of this search will cost OpenAI credits to embed the query.',
},
metadata: {
display: 'Metadata',
placeholder:
'Find embedding by exact key:value pair. Formatted as key:value_to_look_for',
},
vectorId: {
display: 'Vector Id',
placeholder: 'Find by a specific vector ID',
},
};
import { SEARCH_MODES, ISearchTypes } from '../../../../utils/constants';
export default function SearchView({
searchMode,
@@ -37,28 +15,35 @@ export default function SearchView({
document,
FragmentItem,
canEdit,
setSearchFragments,
setSearching,
searching,
searchBy,
setSearchBy,
searchTerm,
setSearchTerm,
}: {
searchMode: boolean;
document: object;
setSearchMode: Dispatch<SetStateAction<boolean>>;
FragmentItem: (props: any) => JSX.Element;
canEdit: boolean;
setSearchFragments: Dispatch<SetStateAction<[]>>;
setSearching: Dispatch<SetStateAction<boolean>>;
searching: boolean;
searchBy: ISearchTypes;
setSearchBy: Dispatch<SetStateAction<ISearchTypes>>;
searchTerm: string;
setSearchTerm: Dispatch<SetStateAction<string>>;
}) {
const formEl = useRef<HTMLFormElement>(null);
const [searching, setSearching] = useState(false);
const [showSearchMethods, setShowSearchMethods] = useState(false);
const [searchBy, setSearchBy] = useState<ISearchTypes>('exactText');
const [searchTerm, setSearchTerm] = useState<string>('');
const [fragments, setFragments] = useState([]);
const [sourceDoc, setSourceDoc] = useState(null);
const clearSearch = () => {
const clearSearch = (e: SyntheticEvent<HTMLElement, SubmitEvent>) => {
e.preventDefault();
setSearchBy('exactText');
setSearchTerm('');
setFragments([]);
setSearching(false);
setSearchMode(false);
setSourceDoc(null);
(formEl.current as HTMLFormElement).reset();
};
const handleSearch = async (e: SyntheticEvent<HTMLElement, SubmitEvent>) => {
@@ -69,6 +54,7 @@ export default function SearchView({
setSearching(true);
setSearchTerm(query);
const matches = await Document.searchEmbeddings(
document.id,
searchBy,
@@ -76,160 +62,117 @@ export default function SearchView({
);
const vectorIds = matches.map((fragment) => fragment.vectorId);
const metadataForIds = await Document.metadatas(document.id, vectorIds);
setSourceDoc(metadataForIds);
setFragments(matches);
if (vectorIds.length === 0) {
setSearching(false);
setSearchFragments([]);
return;
}
setSearchFragments(matches);
setSearching(false);
};
return (
<div className="w-full flex-1 rounded-sm py-6">
<div className="flex items-center">
<form ref={formEl} onSubmit={handleSearch} className="w-full">
<div className="relative flex">
<button
onClick={() => setShowSearchMethods(!showSearchMethods)}
className="z-10 inline-flex flex-shrink-0 items-center rounded-l-lg border border-gray-300 bg-gray-100 px-4 py-2.5 text-center text-sm font-medium text-gray-900 hover:bg-gray-200 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-700"
type="button"
>
{SEARCH_MODES[searchBy].display}
<ChevronDown size={18} />
</button>
<div
hidden={!showSearchMethods}
className="absolute left-0 top-12 z-99 w-44 divide-y divide-gray-100 rounded-lg bg-white shadow dark:bg-gray-700"
>
<ul
className="py-2 text-sm text-gray-700 dark:text-gray-200"
aria-labelledby="dropdown-button"
>
{Object.keys(SEARCH_MODES).map((_key, i) => {
const method = _key as ISearchTypes;
return (
<li key={i}>
<button
onClick={() => {
setSearchBy(method);
setShowSearchMethods(false);
setFragments([]);
}}
type="button"
className="inline-flex w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
{SEARCH_MODES[method].display}
</button>
</li>
);
})}
</ul>
</div>
<div className="relative w-full">
<input
type="search"
name="query"
className="z-20 block w-full rounded-r-lg border border-l-2 border-gray-300 border-l-gray-50 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:border-l-gray-700 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500"
placeholder={SEARCH_MODES[searchBy].placeholder}
required
/>
<>
<div className="w-full flex-1">
<div className="flex items-center">
<form ref={formEl} onSubmit={handleSearch} className="w-full">
<div className="relative flex">
<button
type="submit"
disabled={searching}
className="absolute right-0 top-0 h-full rounded-r-lg border border-blue-700 bg-blue-700 p-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => setShowSearchMethods(!showSearchMethods)}
className="z-10 inline-flex h-9 flex-shrink-0 items-center rounded-[100px] bg-zinc-700 px-5 text-center text-sm font-medium text-white transition-all duration-300 hover:bg-zinc-800 focus:outline-none"
type="button"
>
{searching ? (
<Loader size={18} className="animate-spin" />
) : (
<Search size={18} />
)}
<span className="sr-only">Search</span>
{SEARCH_MODES[searchBy].display}
<div
className={`ml-2 transition-all duration-300 ${
showSearchMethods ? '' : 'rotate-180'
}`}
>
<CaretDown size={16} weight="bold" />
</div>
</button>
</div>
<button
onClick={clearSearch}
type="button"
className="ml-2 flex items-center rounded-lg px-4 py-2 text-center text-black hover:bg-gray-200"
>
X
</button>
</div>
</form>
</div>
<div hidden={!searchMode} className="h-auto w-auto">
{searching ? (
<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">
<Loader size={15} className="animate-spin rounded-sm" />
<p className="text-sm">
Running {SEARCH_MODES[searchBy].display} for{' '}
<code className="bg-gray-200 px-2">"{searchTerm}"</code>
</p>
</div>
</div>
</div>
) : (
<>
{fragments.length > 0 ? (
<table className="w-full text-left text-sm text-gray-500 dark:text-gray-400">
<thead className="bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-6 py-3">
#
</th>
<th scope="col" className="px-6 py-3">
Vector DB Id
</th>
<th scope="col" className="px-6 py-3">
Text Chunk
</th>
<th scope="col" className="px-6 py-3">
Last Updated
</th>
<th scope="col" className="px-6 py-3">
Actions
</th>
</tr>
</thead>
<tbody>
{fragments.map((fragment) => {
<div
className={`absolute left-0 top-12 z-99 w-44 divide-y divide-gray-100 rounded-lg bg-zinc-700 shadow ${
showSearchMethods ? 'slide-down' : 'slide-up'
}`}
style={{
animationDuration: '0.15s',
}}
>
<ul
className="py-2 text-sm text-white"
aria-labelledby="dropdown-button"
>
{Object.keys(SEARCH_MODES).map((_key, i) => {
const method = _key as ISearchTypes;
return (
<FragmentItem
key={fragment.id}
fragment={fragment}
sourceDoc={sourceDoc}
canEdit={canEdit}
/>
<li key={i}>
<button
onClick={() => {
setSearchBy(method);
setShowSearchMethods(false);
setSearchMode(false);
}}
type="button"
className="inline-flex w-full px-4 py-2 hover:bg-zinc-800"
>
{SEARCH_MODES[method].display}
</button>
</li>
);
})}
</tbody>
</table>
) : (
<>
<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">
{!!searchTerm ? (
<p className="text-sm">
No results on {SEARCH_MODES[searchBy].display} for{' '}
<code className="bg-gray-200 px-2">
"{searchTerm}"
</code>
</p>
) : (
<p className="text-sm">
Type in a query to search for an embedding
</p>
)}
</div>
</ul>
</div>
<div className="relative w-full">
<input
name="query"
className="z-20 -ml-4 block h-9 w-full rounded-r-[100px] bg-main-2 pl-8 text-sm text-white focus:outline-none"
placeholder={SEARCH_MODES[searchBy].placeholder}
required
/>
{searching ? (
<div className="absolute right-0 top-0 mr-4.5 flex h-full p-2.5 text-sm font-medium text-white focus:outline-none">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-white border-t-transparent" />
</div>
</div>
</>
)}
</>
)}
) : searchMode ? (
<button
onClick={clearSearch}
className="absolute right-0 top-0 mr-4.5 flex h-full items-center justify-center p-2.5 text-sm font-medium text-white focus:outline-none"
>
<X
size={16}
className="text-sky-400 transition-all duration-300 hover:text-sky-700"
weight="bold"
/>
</button>
) : (
<button
type="submit"
className="absolute -right-1 top-0 mr-4.5 flex h-full items-center justify-center rounded-r-[100px] bg-[#303237] p-2.5 text-sm font-medium text-white focus:outline-none"
>
<MagnifyingGlass
className="text-sky-400 transition-all duration-300 hover:text-sky-700"
size={18}
weight="bold"
/>
</button>
)}
</div>
<button
onClick={(e) => {
e.preventDefault();
window.document
?.getElementById('upload-document-modal')
?.showModal();
}}
className="flex w-18 items-center justify-center rounded-[100px] bg-sky-400 px-2.5 text-xs"
>
<div className="font-bold uppercase text-black">Upload</div>
</button>
</div>
</form>
</div>
</div>
</div>
</>
);
}
@@ -2,13 +2,12 @@ import { lazy, memo, useEffect, useState } from 'react';
import PreLoader from '../../../components/Preloader';
import Document from '../../../models/document';
import truncate from 'truncate';
import moment from 'moment';
import pluralize from 'pluralize';
import { useParams } from 'react-router-dom';
import paths from '../../../utils/paths';
import DocumentListPagination from '../../../components/DocumentPaginator';
import SearchView from './SearchView';
import MetadataEditor from './MetadataEditor';
import { Trash } from '@phosphor-icons/react';
import { ISearchTypes, SEARCH_MODES } from '../../../utils/constants';
const DeleteEmbeddingConfirmation = lazy(
() => import('./DeleteEmbeddingConfirmation')
);
@@ -26,32 +25,22 @@ export default function FragmentList({
document: any;
canEdit: boolean;
}) {
const { slug, workspaceSlug } = useParams();
const [loading, setLoading] = useState(true);
const [searchMode, setSearchMode] = useState(false);
const [fragments, setFragments] = useState([]);
const [sourceDoc, setSourceDoc] = useState(null);
const [totalFragments, setTotalFragments] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [searchFragments, setSearchFragments] = useState([]);
const [searching, setSearching] = useState(false);
const [searchBy, setSearchBy] = useState<ISearchTypes>('exactText');
const [searchTerm, setSearchTerm] = useState('');
const totalPages = Math.ceil(totalFragments / PAGE_SIZE);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const deleteDocument = async () => {
if (!document) return false;
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(document.id);
if (!success) return false;
window.location.replace(paths.workspace(slug, workspaceSlug));
};
const getFragments = async (page = 1) => {
if (!document?.id) return;
setLoading(true);
@@ -72,90 +61,117 @@ export default function FragmentList({
useEffect(() => {
getFragments(currentPage);
}, [document, currentPage]);
return (
<>
<div className="col-span-12 flex-1 rounded-sm dark:border-strokedark dark:bg-boxdark xl:col-span-4">
<div className="flex items-start justify-between">
<div className="mb-6 flex flex-col gap-y-1 px-7.5 ">
<div className="flex items-center gap-x-2">
<h4 className="text-3xl font-semibold text-black dark:text-white">
Embeddings Overview for Document #{document.id}
</h4>
<button
type="button"
onClick={() =>
window.document
.getElementById(`copy-document-${document.id}-modal`)
?.showModal()
}
className="rounded-lg px-4 py-2 text-sm text-blue-400 hover:bg-blue-50 hover:text-blue-600"
>
Clone Document
</button>
<button
onClick={deleteDocument}
className="rounded-lg px-4 py-2 text-sm text-slate-400 hover:bg-red-50 hover:text-red-600"
>
Delete Document
</button>
<div
className="flex h-screen flex-col overflow-hidden bg-main transition-all duration-300"
style={{ height: `calc(100vh - ${searchMode ? '130px' : '100px'})` }}
>
<div className="">
<div className="flex flex-col">
<div className="mb-6 flex w-full items-center justify-between gap-x-12">
<div className="ml-4 w-48 text-sm font-bold uppercase tracking-wide text-white">
embeddings overview
</div>
<SearchView
searchMode={searchMode}
setSearchMode={setSearchMode}
document={document}
FragmentItem={Fragment}
canEdit={canEdit}
setSearchFragments={setSearchFragments}
setSearching={setSearching}
searching={searching}
searchBy={searchBy}
setSearchBy={setSearchBy}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
/>
</div>
<p className="text-sm text-slate-500">{document?.name}</p>
</div>
</div>
<SearchView
searchMode={searchMode}
setSearchMode={setSearchMode}
document={document}
FragmentItem={Fragment}
canEdit={canEdit}
/>
<div hidden={searchMode} className="px-6">
{loading ? (
<div>
<PreLoader />
<div className="flex-grow overflow-y-auto rounded-xl border-2 border-white/20 bg-main">
{loading || searching ? (
<div className="flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-y-4 text-center">
<PreLoader />
{searching && (
<p className="text-white text-opacity-80">
Running {SEARCH_MODES[searchBy].display} for{' '}
<code className="px-2">"{searchTerm}"</code>
</p>
)}
</div>
</div>
) : (
<table className="w-full text-left text-sm text-gray-500 dark:text-gray-400">
<thead className="bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-6 py-3">
#
<table className="w-full rounded-xl text-left text-xs font-medium text-white text-opacity-80">
<thead className="sticky top-0 w-full 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"
>
Vector ID
</th>
<th scope="col" className="px-6 py-3">
Vector DB Id
</th>
<th scope="col" className="px-6 py-3">
<th
scope="col"
className="px-6 pb-2 pt-6 text-xs font-light text-white text-opacity-80"
>
Text Chunk
</th>
<th scope="col" className="px-6 py-3">
<th
scope="col"
className="px-6 pb-2 pt-6 text-xs font-light text-white text-opacity-80"
>
Metadata
</th>
<th scope="col" className="px-6 py-3">
Last Updated
</th>
<th scope="col" className="px-6 py-3">
Actions
<th
scope="col"
className="px-6 pb-2 pt-6 text-xs font-light text-white text-opacity-80"
>
{' '}
</th>
</tr>
</thead>
<tbody>
{fragments.map((fragment) => {
return (
<Fragment
key={fragment.id}
fragment={fragment}
sourceDoc={sourceDoc}
canEdit={canEdit}
connector={connector}
/>
);
})}
{(searchMode ? searchFragments : fragments).map(
(fragment, index) => {
return (
<Fragment
key={fragment.id}
index={index}
fragment={fragment}
sourceDoc={sourceDoc}
canEdit={canEdit}
connector={connector}
/>
);
}
)}
</tbody>
</table>
)}
{searchMode && searchFragments.length === 0 && !searching && (
<div className="flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-y-4 text-center">
<p className="text-white text-opacity-80">
No results found on {SEARCH_MODES[searchBy].display} for{' '}
<code className="px-2">"{searchTerm}"</code>
</p>
</div>
</div>
)}
{!searchMode && fragments.length === 0 && !searching && !loading && (
<div className="flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-y-4 text-center">
<p className="text-white text-opacity-80">
No vectors found for this document.
<code className="px-2">"{searchTerm}"</code>
</p>
</div>
</div>
)}
</div>
{!searchMode && (
<DocumentListPagination
@@ -174,11 +190,13 @@ const Fragment = ({
sourceDoc,
canEdit,
connector,
index,
}: {
fragment: any;
sourceDoc: any;
canEdit: boolean;
connector: any;
index: number;
}) => {
const [data, setData] = useState(null);
const [metadata, setMetadata] = useState({});
@@ -201,20 +219,18 @@ const Fragment = ({
<>
<tr
id={`embedding-row-${fragment.id}`}
className="border-b bg-white transition-all duration-300 dark:border-gray-700 dark:bg-gray-800"
className={`h-9 transition-all duration-300 ${
index % 2 === 0 ? 'bg-main-2' : 'bg-main'
}`}
>
<th
scope="row"
className="whitespace-nowrap px-6 py-4 font-medium text-gray-900 dark:text-white"
>
{fragment.id}
</th>
<td className="px-6 py-4">{fragment.vectorId}</td>
<td className="px-6 py-4">
{truncate(data?.metadata?.text, 40)}
<td className="px-6 text-sm font-bold text-white">
{fragment.vectorId}
</td>
<td className="px-6 ">
{truncate(data?.metadata?.text, 30)}
{!!data?.metadata?.text ? (
<button
className="text-blue-400"
className="rounded-lg px-2 py-1 text-sky-400 transition-all duration-300 hover:text-opacity-80"
onClick={() => {
document.getElementById(`${fragment.id}-text`)?.showModal();
}}
@@ -231,18 +247,21 @@ const Fragment = ({
</>
)}
</td>
<td className="px-6 py-4">
<td className="px-6 ">
{Object.keys(metadata)?.length > 0 ? (
<button
onClick={() => {
onClick={() =>
document
.getElementById(`${fragment.id}-metadata-editor`)
?.showModal();
}}
className="rounded-full bg-blue-200 px-2 py-[1px] text-center text-blue-700 hover:bg-blue-300"
?.showModal()
}
>
+{Object.keys(metadata).length} metadata{' '}
{pluralize('item', Object.keys(metadata).length)}
<div className="flex h-5 items-center justify-center rounded-[84px] bg-white bg-opacity-10 px-2 py-1 hover:opacity-75">
<div className="whitespace-nowrap text-[10px] text-white">
+{Object.keys(metadata).length}{' '}
{pluralize('item', Object.keys(metadata).length)}
</div>
</div>
</button>
) : (
<>
@@ -255,40 +274,39 @@ const Fragment = ({
.getElementById(`${fragment.id}-metadata-editor`)
?.showModal();
}}
className="rounded-full bg-blue-200 px-2 py-[1px] text-center text-blue-700 hover:bg-blue-300"
className="flex h-5 items-center justify-center rounded-[84px] bg-white bg-opacity-10 px-2 py-1"
>
<p>none</p>
<p className="text-[10px] text-white">none</p>
</button>
)}
</>
)}
</td>
<td className="px-6 py-4">
{moment(fragment.lastUpdatedAt).fromNow()}
</td>
<td className="flex items-center gap-x-4 px-6 py-4">
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-edit-embedding`)
?.showModal();
}}
className="rounded-lg px-2 py-1 text-blue-400 transition-all duration-300 hover:bg-blue-50 hover:text-blue-600"
>
Edit
</button>
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-delete-embedding`)
?.showModal();
}}
className="rounded-lg px-2 py-1 text-red-400 transition-all duration-300 hover:bg-red-50 hover:text-red-600"
>
Delete
</button>
<td>
<div className="flex items-center gap-x-4 px-4">
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-edit-embedding`)
?.showModal();
}}
className="rounded-lg px-2 py-1 text-sky-400 transition-all duration-300 hover:bg-blue-50"
>
Edit
</button>
<button
type="button"
onClick={() => {
document
.getElementById(`${fragment.id}-delete-embedding`)
?.showModal();
}}
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>
</td>
</tr>
{!!data && !!fragment && (
@@ -0,0 +1,84 @@
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);
@@ -0,0 +1,68 @@
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>
);
}
@@ -0,0 +1,213 @@
import { useCallback, useState, useEffect, ReactNode } from 'react';
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 FileUploadProgress from './FileUploadProgress';
import { useParams } from 'react-router-dom';
export default function UploadDocumentModal({
workspaces,
}: {
workspaces: any;
}) {
const { slug } = useParams();
const [targetWorkspace, setTargetWorkspace] = useState(null);
const [ready, setReady] = useState<boolean | null>(null);
const [files, setFiles] = useState([]);
const [fileTypes, setFileTypes] = useState({});
const onDrop = useCallback(
async (acceptedFiles: any[], rejections: any[]) => {
const newAccepted = acceptedFiles.map((file) => {
return {
uid: v4(),
workspaceId: targetWorkspace?.id,
file,
};
});
const newRejected = rejections.map((file) => {
return {
uid: v4(),
file: file.file,
rejected: true,
reason: file.errors[0].code,
};
});
setFiles([...files, ...newAccepted, ...newRejected]);
},
[]
);
useEffect(() => {
async function checkProcessorOnline() {
const online = await System.documentProcessorOnline();
const acceptedTypes = await System.acceptedDocumentTypes();
setFileTypes(acceptedTypes ?? {});
setReady(online);
}
checkProcessorOnline();
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: {
...fileTypes,
},
});
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-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">
this should only take a few moments.
</p>
</div>
</div>
</ModalWrapper>
);
}
if (ready === false) {
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" />
<p className="text-center text-xs text-red-800">
Document processor is offline.
</p>
<p className="text-center text-[10px] text-red-800 md:text-xs">
you cannot upload documents from the UI right now
</p>
</div>
</div>
</ModalWrapper>
);
}
if (ready === true && targetWorkspace === null) {
const saveWorkspace = (e: any) => {
e.preventDefault();
const form = new FormData(e.target);
const selectedWsId = form.get('workspaceId');
const workspace = workspaces.find(
(ws: any) => ws.id === Number(selectedWsId)
);
if (!workspace) return false;
setTargetWorkspace(workspace);
};
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-full w-full flex-col items-center justify-center gap-y-1">
<p className="text-sm text-slate-800">
Please select the workspace you wish to upload documents to.
</p>
<form onSubmit={saveWorkspace} className="flex flex-col gap-y-1">
<select
name="workspaceId"
className="rounded-lg px-4 py-2 outline-none"
>
{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">
Continue &rarr;
</button>
</form>
</div>
</div>
</ModalWrapper>
);
}
return (
<ModalWrapper>
<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"
>
<input {...getInputProps()} />
{files.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center">
<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"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
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
and drop
</p>
<p className="text-xs text-gray-600 dark:text-slate-300"></p>
</div>
</div>
) : (
<div className="flex w-full flex-col gap-y-2 p-4">
{files.map((file) => (
<FileUploadProgress
key={file.uid}
file={file.file}
slug={slug}
workspace={targetWorkspace}
rejected={file?.rejected}
reason={file?.reason}
/>
))}
</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">
{Object.values(fileTypes).flat().join(' ')}
</code>
</p>
</div>
</ModalWrapper>
);
}
const ModalWrapper = ({ children }: { children: ReactNode }) => {
return (
<dialog
id="upload-document-modal"
className="w-1/2 rounded-lg outline-none"
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">
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>
);
};
+89 -4
View File
@@ -11,6 +11,10 @@ import Document from '../../models/document';
import System from '../../models/system';
import Organization from '../../models/organization';
import { APP_NAME } from '../../utils/constants';
import { CaretDown } from '@phosphor-icons/react';
import truncate from 'truncate';
import UploadDocumentModal from './UploadModal';
import UploadModalNoKey from './UploadModal/UploadModalNoKey';
export default function DocumentView() {
const { user } = useUser();
@@ -109,9 +113,16 @@ export default function DocumentView() {
workspaces={workspaces}
hasMoreWorkspaces={hasMoreWorkspaces}
loadMoreWorkspaces={fetchWorkspaces}
headerExtendedItems={
<DocumentViewHeader
organization={organization}
workspace={workspace}
document={document}
/>
}
>
<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-12">
<div className="grid grid-cols-12">
<div className="col-span-12 h-screen bg-main xl:col-span-12">
<FragmentList
connector={connector}
document={document}
@@ -124,10 +135,84 @@ export default function DocumentView() {
workspace={workspace}
workspaces={workspaces}
/>
{canEdit ? (
<UploadDocumentModal workspaces={workspaces} />
) : (
<UploadModalNoKey />
)}
</AppLayout>
);
}
function DocumentViewHeader({ organization, workspace, document }: any) {
const { slug, workspaceSlug } = useParams();
const deleteDocument = async () => {
if (!document) return false;
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(document.id);
if (!success) return false;
window.location.replace(paths.workspace(slug, workspaceSlug));
};
const cloneDocument = async () => {
window.document
.getElementById(`copy-document-${document.id}-modal`)
?.showModal();
};
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">
<a
href={paths.organization(organization)}
className="text-sky-400 hover:cursor-pointer hover:underline"
>
{truncate(organization?.name, 20)}
</a>
<div className="text-sky-400" style={{ transform: 'rotate(270deg)' }}>
<CaretDown weight="bold" />
</div>
<a
href={paths.workspace(slug, workspaceSlug)}
className="text-sky-400 hover:cursor-pointer hover:underline"
>
{truncate(workspace?.name, 20)}
</a>
<div className="text-sky-400" style={{ transform: 'rotate(270deg)' }}>
<CaretDown weight="bold" />
</div>
<span className="text-white">{truncate(document?.name, 30)}</span>
</div>
</div>
<div className="flex gap-x-3">
<button
onClick={cloneDocument}
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="h-[25.53px] w-11 text-center font-['Satoshi'] text-base font-bold text-white">
Clone
</div>
</button>
<button
onClick={deleteDocument}
className="inline-flex h-11 w-[74px] flex-col items-center justify-center gap-2.5 rounded-lg border border-white border-opacity-20 px-3.5 py-2.5 transition-all duration-300 hover:bg-red-500"
>
<div className="h-[25.53px] w-[59px] text-center font-['Satoshi'] text-base font-bold text-white">
Delete
</div>
</button>
</div>
</>
);
}
const CopyDocToModal = memo(
({
document,
@@ -162,7 +247,7 @@ const CopyDocToModal = memo(
return (
<dialog
id={`copy-document-${document.id}-modal`}
id={`copy-document-${document?.id}-modal`}
className="w-1/2 rounded-lg outline-none"
onClick={(event) => {
event.target == event.currentTarget && event.currentTarget?.close();
@@ -198,7 +283,7 @@ const CopyDocToModal = memo(
<div className="my-2 flex w-full justify-center p-[20px]">
<form onSubmit={copyToWorkspace} className="flex flex-col gap-y-1">
<p className="my-2 text-sm text-gray-800">
Clone {document.name} and it's embeddings to...
Clone {document?.name} and it's embeddings to...
</p>
<select
name="workspaceId"
@@ -5,29 +5,7 @@ import truncate from 'truncate';
import moment from 'moment';
import paths from '../../../../utils/paths';
import Workspace from '../../../../models/workspace';
export type ISearchTypes = 'semantic' | 'exactText' | 'metadata' | 'vectorId';
const SEARCH_MODES = {
exactText: {
display: 'Fuzzy Text Search',
placeholder: 'Find documents via a fuzzy text match on your query.',
},
semantic: {
display: 'Semantic Search',
placeholder:
'Search with natural language finding the most similar text by meaning. Use of this search will cost OpenAI credits to embed the query.',
},
metadata: {
display: 'Metadata',
placeholder:
'Find documents by exact key:value pair. Formatted as key:value_to_look_for',
},
vectorId: {
display: 'Vector Id',
placeholder: 'Find a document which contains a specific vector ID',
},
};
import { SEARCH_MODES, ISearchTypes } from '../../../../utils/constants';
export default function SearchView({
organization,
+22
View File
@@ -9,3 +9,25 @@ export const SUPPORTED_VECTOR_DBS = [
'qdrant',
'weaviate',
];
export type ISearchTypes = 'semantic' | 'exactText' | 'metadata' | 'vectorId';
export const SEARCH_MODES = {
exactText: {
display: 'Fuzzy Text Search',
placeholder: 'Find embedding via a fuzzy text match on your query.',
},
semantic: {
display: 'Semantic Search',
placeholder:
'Search with natural language finding the most similar embedding by meaning. Use of this search will cost OpenAI credits to embed the query.',
},
metadata: {
display: 'Metadata',
placeholder:
'Find embedding by exact key:value pair. Formatted as key:value_to_look_for',
},
vectorId: {
display: 'Vector Id',
placeholder: 'Find by a specific vector ID',
},
};