mirror of
https://github.com/Mintplex-Labs/vector-admin.git
synced 2026-07-19 21:23:38 -04:00
Merge pull request #85 from Mintplex-Labs/v2-workspace-document
v2 workspace document
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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,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 →
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user