mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-10-02 08:50:11 +02:00
AnythingLLM UI overhaul (#278)
* v2 Login screen (#254) * adding gradients for modal and sidebar * adding font setup * redesigned login screen for MultiUserAuth * completed multi user mode login screen * linting * login screen for single user auth redesign complete * created reusable gradient for login screen --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> * v2 sidebar (#262) * adding gradients for modal and sidebar * adding font setup * redesigned login screen for MultiUserAuth * completed multi user mode login screen * linting * login screen for single user auth redesign complete * WIP sidebar redesign * created reusable gradient for login screen * remove dark mode items * update new workspace button * completed sidebar for desktop view * add interactivity states --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> * remove duplicated pkg * v2 settings (#264) * adding gradients for modal and sidebar * adding font setup * redesigned login screen for MultiUserAuth * completed multi user mode login screen * linting * login screen for single user auth redesign complete * WIP sidebar redesign * created reusable gradient for login screen * remove dark mode items * update new workspace button * completed sidebar for desktop view * WIP added colors/gradients to admin settings * WIP fix discord logo import * WIP settings redesign - added routes for general settings and restyled components * WIP settings for LLM Preference, VectorDB, ExportImport * settings menu UI complete WIP functionality * settings fully functional/removed dark mode logo * linting * removing unneeded dependency * Fix admin sidebar visibility Fix API Keys location and work with single/mum Fix Appearance location - WIP on funcitonality * update api key page * fix permissions for appearance * Single user mode fixes * fix multi user mode enabled * fix import export * Rename AdminSidebar to SettingsSidebar * Fix mobile sidebar links --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> * V2 user logout (#265) * Add user logout button * hide other 3 dot button * wrap admin routes * V2 workspace modal (#267) Update new workspace modal remove duplicate tailwind colors * v2 Settings modal styles (#266) * EditUserModal styles complete * workspaces modals styles complete * create invite link modal styles complete * create new api key modal styles complete --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> * v2 Chats Redesign (#270) * fix default message for new workspace * prompt input box ui redesign complete * ui tweak to prompt input * WIP chat msg redesign * chat container and historical chat messages redesign * manage workspace modal appears when clicking upload a document on empty workspace * fixed loading skeleton styles * citations redesign complete * restyle pending chat and prompt reply components * default chat messages styles updated * linting * update how chats are returned --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> * Onboarding modal flow for first time setup (#274) * WIP onboarding modal flow * onboarding flow complete and private route redirection for onboarding setep * redirect to home on onboarding complete * add onboarding redirect using paths.onboarding() * Apply changes to auth flow, onboarding determination, and flows * remove formref --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> * v2 document picker (#275) * remove unneeded comments * WIP document picker UI * WIP basic UI complete for document picker tab and settings tab * linting * settings menu complete, document row WIP * WIP document picker loading from localFiles * WIP file picker logic * refactoring document picker to work with backend * WIP refactoring document picker * WIP refactor document picker to work with backend * file uploading with dropzone working * WIP deleting file when not embedded * WIP embeddings * WIP embedding with temp button and hardcoded paths * WIP placeholder for WorkspaceDirectory component * WIP WorkspaceDirectory * WIP * sort workspaceDocs and availibleDocs complete * added directories util * add and remove document from ws working * v2 document picker complete * reference modal ui bug fixes * truncate function bug fix * ManageWorkspace modal bug fixes * blocking mobile users modal for workspace settings * mobile ui fixes * linting * ui padding fixes * citation bug fixes * code review changes * debounce handlers * change tempFile object to array * selection count fix * Convert workspace modal to div Memo workspace settings update conditional rendering of workspace settings * Show no documents --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> * mobile sidebar styles * padding on Mobile view mobile sidebar items * UI touchup * suggestion implementations * CSS fixes and animation perfomance change to GPU accelerated and 60fps * change will-change * remove transitions from onboarding modals, simplify on-change handlers * Swap onboarding to memoized components and debounce onchange handlers * remove console log * remove Avenir font --------- Co-authored-by: Sean Hatfield <seanhatfield5@gmail.com>
This commit is contained in:
parent
d1fbe94a33
commit
708068a09e
@ -12,8 +12,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||||
"@metamask/jazzicon": "^2.0.0",
|
"@metamask/jazzicon": "^2.0.0",
|
||||||
|
"@phosphor-icons/react": "^2.0.13",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
Binary file not shown.
BIN
frontend/public/fonts/PlusJakartaSans.ttf
Normal file
BIN
frontend/public/fonts/PlusJakartaSans.ttf
Normal file
Binary file not shown.
@ -4,6 +4,7 @@ import { ContextWrapper } from "./AuthContext";
|
|||||||
import PrivateRoute, { AdminRoute } from "./components/PrivateRoute";
|
import PrivateRoute, { AdminRoute } from "./components/PrivateRoute";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
import Login from "./pages/Login";
|
||||||
|
|
||||||
const Main = lazy(() => import("./pages/Main"));
|
const Main = lazy(() => import("./pages/Main"));
|
||||||
const InvitePage = lazy(() => import("./pages/Invite"));
|
const InvitePage = lazy(() => import("./pages/Invite"));
|
||||||
@ -13,21 +14,63 @@ const AdminInvites = lazy(() => import("./pages/Admin/Invitations"));
|
|||||||
const AdminWorkspaces = lazy(() => import("./pages/Admin/Workspaces"));
|
const AdminWorkspaces = lazy(() => import("./pages/Admin/Workspaces"));
|
||||||
const AdminChats = lazy(() => import("./pages/Admin/Chats"));
|
const AdminChats = lazy(() => import("./pages/Admin/Chats"));
|
||||||
const AdminSystem = lazy(() => import("./pages/Admin/System"));
|
const AdminSystem = lazy(() => import("./pages/Admin/System"));
|
||||||
const AdminAppearance = lazy(() => import("./pages/Admin/Appearance"));
|
const GeneralAppearance = lazy(() =>
|
||||||
const AdminApiKeys = lazy(() => import("./pages/Admin/ApiKeys"));
|
import("./pages/GeneralSettings/Appearance")
|
||||||
|
);
|
||||||
|
const GeneralApiKeys = lazy(() => import("./pages/GeneralSettings/ApiKeys"));
|
||||||
|
|
||||||
|
const GeneralLLMPreference = lazy(() =>
|
||||||
|
import("./pages/GeneralSettings/LLMPreference")
|
||||||
|
);
|
||||||
|
const GeneralVectorDatabase = lazy(() =>
|
||||||
|
import("./pages/GeneralSettings/VectorDatabase")
|
||||||
|
);
|
||||||
|
const GeneralExportImport = lazy(() =>
|
||||||
|
import("./pages/GeneralSettings/ExportImport")
|
||||||
|
);
|
||||||
|
const GeneralSecurity = lazy(() => import("./pages/GeneralSettings/Security"));
|
||||||
|
|
||||||
|
const OnboardingFlow = lazy(() => import("./pages/OnboardingFlow"));
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div />}>
|
<Suspense fallback={<div />}>
|
||||||
<ContextWrapper>
|
<ContextWrapper>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Main />} />
|
<Route path="/" element={<PrivateRoute Component={Main} />} />
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
<Route
|
<Route
|
||||||
path="/workspace/:slug"
|
path="/workspace/:slug"
|
||||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||||
/>
|
/>
|
||||||
<Route path="/accept-invite/:code" element={<InvitePage />} />
|
<Route path="/accept-invite/:code" element={<InvitePage />} />
|
||||||
|
|
||||||
|
{/* General Routes */}
|
||||||
|
<Route
|
||||||
|
path="/general/llm-preference"
|
||||||
|
element={<PrivateRoute Component={GeneralLLMPreference} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/general/vector-database"
|
||||||
|
element={<PrivateRoute Component={GeneralVectorDatabase} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/general/export-import"
|
||||||
|
element={<PrivateRoute Component={GeneralExportImport} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/general/security"
|
||||||
|
element={<PrivateRoute Component={GeneralSecurity} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/general/appearance"
|
||||||
|
element={<PrivateRoute Component={GeneralAppearance} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/general/api-keys"
|
||||||
|
element={<PrivateRoute Component={GeneralApiKeys} />}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Admin Routes */}
|
{/* Admin Routes */}
|
||||||
<Route
|
<Route
|
||||||
path="/admin/system-preferences"
|
path="/admin/system-preferences"
|
||||||
@ -49,14 +92,9 @@ export default function App() {
|
|||||||
path="/admin/workspace-chats"
|
path="/admin/workspace-chats"
|
||||||
element={<AdminRoute Component={AdminChats} />}
|
element={<AdminRoute Component={AdminChats} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/admin/appearance"
|
{/* Onboarding Flow */}
|
||||||
element={<AdminRoute Component={AdminAppearance} />}
|
<Route path="/onboarding" element={<OnboardingFlow />} />
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/admin/api-keys"
|
|
||||||
element={<AdminRoute Component={AdminApiKeys} />}
|
|
||||||
/>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</ContextWrapper>
|
</ContextWrapper>
|
||||||
|
@ -1,323 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import {
|
|
||||||
BookOpen,
|
|
||||||
Eye,
|
|
||||||
GitHub,
|
|
||||||
Key,
|
|
||||||
Mail,
|
|
||||||
Menu,
|
|
||||||
MessageSquare,
|
|
||||||
Settings,
|
|
||||||
Users,
|
|
||||||
X,
|
|
||||||
} from "react-feather";
|
|
||||||
import IndexCount from "../Sidebar/IndexCount";
|
|
||||||
import LLMStatus from "../Sidebar/LLMStatus";
|
|
||||||
import paths from "../../utils/paths";
|
|
||||||
import Discord from "../Icons/Discord";
|
|
||||||
import useLogo from "../../hooks/useLogo";
|
|
||||||
|
|
||||||
export default function AdminSidebar() {
|
|
||||||
const { logo } = useLogo();
|
|
||||||
const sidebarRef = useRef(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
ref={sidebarRef}
|
|
||||||
style={{ height: "calc(100% - 32px)" }}
|
|
||||||
className="transition-all duration-500 relative m-[16px] rounded-[26px] bg-white dark:bg-black-900 min-w-[15.5%] p-[18px] "
|
|
||||||
>
|
|
||||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
|
||||||
{/* Header Information */}
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex shrink-0 max-w-[50%] items-center justify-start">
|
|
||||||
<img
|
|
||||||
src={logo}
|
|
||||||
alt="Logo"
|
|
||||||
className="rounded max-h-[40px]"
|
|
||||||
style={{ objectFit: "contain" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-x-2 items-center text-slate-500">
|
|
||||||
<a
|
|
||||||
href={paths.home()}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-stone-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Primary Body */}
|
|
||||||
<div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
|
|
||||||
<div className="h-auto sidebar-items dark:sidebar-items">
|
|
||||||
<div className="flex flex-col gap-y-4 h-[65vh] pb-8 overflow-y-scroll no-scroll">
|
|
||||||
<Option
|
|
||||||
href={paths.admin.system()}
|
|
||||||
btnText="System Preferences"
|
|
||||||
icon={<Settings className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.invites()}
|
|
||||||
btnText="Invitation Management"
|
|
||||||
icon={<Mail className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.users()}
|
|
||||||
btnText="User Management"
|
|
||||||
icon={<Users className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.workspaces()}
|
|
||||||
btnText="Workspace Management"
|
|
||||||
icon={<BookOpen className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.chats()}
|
|
||||||
btnText="Workspace Chat Management"
|
|
||||||
icon={<MessageSquare className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.appearance()}
|
|
||||||
btnText="Appearance"
|
|
||||||
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.apiKeys()}
|
|
||||||
btnText="API Keys"
|
|
||||||
icon={<Key className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<div className="w-full flex items-center justify-between">
|
|
||||||
<LLMStatus />
|
|
||||||
<IndexCount />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-end justify-between mt-2">
|
|
||||||
<div className="flex gap-x-1 items-center">
|
|
||||||
<a
|
|
||||||
href={paths.github()}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<GitHub className="h-4 w-4 " />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={paths.docs()}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<BookOpen className="h-4 w-4 " />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={paths.discord()}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 dark:bg-slate-800 hover:bg-slate-800 group"
|
|
||||||
>
|
|
||||||
<Discord className="h-4 w-4 stroke-slate-400 group-hover:stroke-slate-200 dark:group-hover:stroke-slate-200" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={paths.mailToMintplex()}
|
|
||||||
className="transition-all duration-300 text-xs text-slate-200 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400"
|
|
||||||
>
|
|
||||||
@MintplexLabs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SidebarMobileHeader() {
|
|
||||||
const { logo } = useLogo();
|
|
||||||
const sidebarRef = useRef(null);
|
|
||||||
const [showSidebar, setShowSidebar] = useState(false);
|
|
||||||
const [showBgOverlay, setShowBgOverlay] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleBg() {
|
|
||||||
if (showSidebar) {
|
|
||||||
setTimeout(() => {
|
|
||||||
setShowBgOverlay(true);
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
setShowBgOverlay(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleBg();
|
|
||||||
}, [showSidebar]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex justify-between relative top-0 left-0 w-full rounded-b-lg px-2 pb-4 bg-white dark:bg-black-900 text-slate-800 dark:text-slate-200">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowSidebar(true)}
|
|
||||||
className="rounded-md bg-stone-200 p-2 flex items-center justify-center text-slate-800 hover:bg-stone-300 group dark:bg-stone-800 dark:text-slate-200 dark:hover:bg-stone-900 dark:border dark:border-stone-800"
|
|
||||||
>
|
|
||||||
<Menu className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
<div className="flex shrink-0 w-fit items-center justify-start">
|
|
||||||
<img
|
|
||||||
src={logo}
|
|
||||||
alt="Logo"
|
|
||||||
className="rounded w-full max-h-[40px]"
|
|
||||||
style={{ objectFit: "contain" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
transform: showSidebar ? `translateX(0vw)` : `translateX(-100vw)`,
|
|
||||||
}}
|
|
||||||
className={`z-99 fixed top-0 left-0 transition-all duration-500 w-[100vw] h-[100vh]`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
showBgOverlay
|
|
||||||
? "transition-all opacity-1"
|
|
||||||
: "transition-none opacity-0"
|
|
||||||
} duration-500 fixed top-0 left-0 bg-black-900 bg-opacity-75 w-screen h-screen`}
|
|
||||||
onClick={() => setShowSidebar(false)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={sidebarRef}
|
|
||||||
className="h-[100vh] fixed top-0 left-0 rounded-r-[26px] bg-white dark:bg-black-900 w-[80%] p-[18px] "
|
|
||||||
>
|
|
||||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
|
||||||
{/* Header Information */}
|
|
||||||
<div className="flex w-full items-center justify-between gap-x-4">
|
|
||||||
<div className="flex shrink-1 w-fit items-center justify-start">
|
|
||||||
<img
|
|
||||||
src={logo}
|
|
||||||
alt="Logo"
|
|
||||||
className="rounded w-full max-h-[40px]"
|
|
||||||
style={{ objectFit: "contain" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-x-2 items-center text-slate-500 shrink-0">
|
|
||||||
<a
|
|
||||||
href={paths.home()}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-stone-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Primary Body */}
|
|
||||||
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-hidden ">
|
|
||||||
<div className="h-auto md:sidebar-items md:dark:sidebar-items">
|
|
||||||
<div
|
|
||||||
style={{ height: "calc(100vw - -3rem)" }}
|
|
||||||
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
|
|
||||||
>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.system()}
|
|
||||||
btnText="System Preferences"
|
|
||||||
icon={<Settings className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.invites()}
|
|
||||||
btnText="Invitation Management"
|
|
||||||
icon={<Mail className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.users()}
|
|
||||||
btnText="User Management"
|
|
||||||
icon={<Users className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.workspaces()}
|
|
||||||
btnText="Workspace Management"
|
|
||||||
icon={<BookOpen className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.chats()}
|
|
||||||
btnText="Workspace Chat Management"
|
|
||||||
icon={<MessageSquare className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.appearance()}
|
|
||||||
btnText="Appearance"
|
|
||||||
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
href={paths.admin.apiKeys()}
|
|
||||||
btnText="API Keys"
|
|
||||||
icon={<Key className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<div className="w-full flex items-center justify-between">
|
|
||||||
<LLMStatus />
|
|
||||||
<IndexCount />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-end justify-between mt-2">
|
|
||||||
<div className="flex gap-x-1 items-center">
|
|
||||||
<a
|
|
||||||
href={paths.github()}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<GitHub className="h-4 w-4 " />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={paths.docs()}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<BookOpen className="h-4 w-4 " />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={paths.discord()}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 dark:bg-slate-800 hover:bg-slate-800 group"
|
|
||||||
>
|
|
||||||
<Discord className="h-4 w-4 stroke-slate-400 group-hover:stroke-slate-200 dark:group-hover:stroke-slate-200" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={paths.mailToMintplex()}
|
|
||||||
className="transition-all duration-300 text-xs text-slate-200 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400"
|
|
||||||
>
|
|
||||||
@MintplexLabs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Option = ({ btnText, icon, href }) => {
|
|
||||||
const isActive = window.location.pathname === href;
|
|
||||||
return (
|
|
||||||
<div className="flex gap-x-2 items-center justify-between">
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
className={`flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center ${
|
|
||||||
isActive
|
|
||||||
? "bg-gray-100 dark:bg-stone-600"
|
|
||||||
: "hover:bg-slate-100 dark:hover:bg-stone-900 "
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold whitespace-nowrap overflow-hidden ">
|
|
||||||
{btnText}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,28 +1,33 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Jazzicon from "../UserIcon";
|
||||||
|
import { userFromStorage } from "../../utils/request";
|
||||||
|
import {
|
||||||
|
AI_BACKGROUND_COLOR,
|
||||||
|
USER_BACKGROUND_COLOR,
|
||||||
|
} from "../../utils/constants";
|
||||||
|
|
||||||
export default function ChatBubble({ message, type, popMsg }) {
|
export default function ChatBubble({ message, type, popMsg }) {
|
||||||
const isUser = type === "user";
|
const isUser = type === "user";
|
||||||
|
const backgroundColor = isUser ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={`flex justify-center items-end w-full ${backgroundColor}`}>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 items-center ${
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
popMsg ? "chat__message" : ""
|
|
||||||
} ${isUser ? "justify-end" : "justify-start"}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex gap-x-5">
|
||||||
className={`p-4 max-w-full md:max-w-[75%] ${
|
<Jazzicon
|
||||||
isUser
|
size={36}
|
||||||
? "bg-slate-200 dark:bg-amber-800"
|
user={{ uid: isUser ? userFromStorage()?.username : "system" }}
|
||||||
: "bg-orange-100 dark:bg-stone-700"
|
role={type}
|
||||||
} rounded-b-2xl ${isUser ? "rounded-tl-2xl" : "rounded-tr-2xl"} ${
|
/>
|
||||||
isUser ? "rounded-tr-sm" : "rounded-tl-sm"
|
|
||||||
}`}
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
{message && (
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
{message}
|
{message}
|
||||||
</p>
|
</span>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,6 +8,12 @@ import { isMobile } from "react-device-detect";
|
|||||||
import { SidebarMobileHeader } from "../Sidebar";
|
import { SidebarMobileHeader } from "../Sidebar";
|
||||||
import ChatBubble from "../ChatBubble";
|
import ChatBubble from "../ChatBubble";
|
||||||
import System from "../../models/system";
|
import System from "../../models/system";
|
||||||
|
import Jazzicon from "../UserIcon";
|
||||||
|
import { userFromStorage } from "../../utils/request";
|
||||||
|
import {
|
||||||
|
AI_BACKGROUND_COLOR,
|
||||||
|
USER_BACKGROUND_COLOR,
|
||||||
|
} from "../../utils/constants";
|
||||||
|
|
||||||
export default function DefaultChatContainer() {
|
export default function DefaultChatContainer() {
|
||||||
const [mockMsgs, setMockMessages] = useState([]);
|
const [mockMsgs, setMockMessages] = useState([]);
|
||||||
@ -30,134 +36,182 @@ export default function DefaultChatContainer() {
|
|||||||
const MESSAGES = [
|
const MESSAGES = [
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 justify-start ${
|
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR} md:mt-0 mt-[40px]`}
|
||||||
popMsg ? "chat__message" : ""
|
>
|
||||||
}`}
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
Welcome to AnythingLLM, AnythingLLM is an open-source AI tool by
|
Welcome to AnythingLLM, AnythingLLM is an open-source AI tool by
|
||||||
Mintplex Labs that turns <i>anything</i> into a trained chatbot you
|
Mintplex Labs that turns anything into a trained chatbot you can
|
||||||
can query and chat with. AnythingLLM is a BYOK (bring-your-own-keys)
|
query and chat with. AnythingLLM is a BYOK (bring-your-own-keys)
|
||||||
software so there is no subscription, fee, or charges for this
|
software so there is no subscription, fee, or charges for this
|
||||||
software outside of the services you want to use with it.
|
software outside of the services you want to use with it.
|
||||||
</p>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 justify-start ${
|
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
|
||||||
popMsg ? "chat__message" : ""
|
>
|
||||||
}`}
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
AnythingLLM is the easiest way to put powerful AI products like
|
AnythingLLM is the easiest way to put powerful AI products like
|
||||||
OpenAi, GPT-4, LangChain, PineconeDB, ChromaDB, and other services
|
OpenAi, GPT-4, LangChain, PineconeDB, ChromaDB, and other services
|
||||||
together in a neat package with no fuss to increase your
|
together in a neat package with no fuss to increase your
|
||||||
productivity by 100x.
|
productivity by 100x.
|
||||||
</p>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 justify-start ${
|
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
|
||||||
popMsg ? "chat__message" : ""
|
>
|
||||||
}`}
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
AnythingLLM can run totally locally on your machine with little
|
AnythingLLM can run totally locally on your machine with little
|
||||||
overhead you wont even notice it's there! No GPU needed. Cloud and
|
overhead you wont even notice it's there! No GPU needed. Cloud
|
||||||
on-premises installation is available as well.
|
and on-premises installation is available as well.
|
||||||
<br />
|
<br />
|
||||||
The AI tooling ecosystem gets more powerful everyday. AnythingLLM
|
The AI tooling ecosystem gets more powerful everyday.
|
||||||
makes it easy to use.
|
AnythingLLM makes it easy to use.
|
||||||
</p>
|
</span>
|
||||||
<a
|
<a
|
||||||
href={paths.github()}
|
href={paths.github()}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="mt-4 w-fit flex flex-grow gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900 dark:bg-stone-900"
|
className="mt-5 w-fit transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
<GitMerge className="h-4 w-4" />
|
<GitMerge className="h-4 w-4" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
|
<p>Create an issue on Github</p>
|
||||||
Create an issue on Github
|
|
||||||
</p>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 justify-end ${
|
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
|
||||||
popMsg ? "chat__message" : ""
|
>
|
||||||
}`}
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon
|
||||||
|
size={36}
|
||||||
|
user={{ uid: userFromStorage()?.username }}
|
||||||
|
role={"user"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
How do I get started?!
|
How do I get started?!
|
||||||
</p>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 justify-start ${
|
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
|
||||||
popMsg ? "chat__message" : ""
|
>
|
||||||
}`}
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
It's simple. All collections are organized into buckets we call{" "}
|
It's simple. All collections are organized into buckets we call{" "}
|
||||||
<b>"Workspaces"</b>. Workspaces are buckets of files, documents,
|
"Workspaces". Workspaces are buckets of files, documents,
|
||||||
images, PDFs, and other files which will be transformed into
|
images, PDFs, and other files which will be transformed into
|
||||||
something LLM's can understand and use in conversation.
|
something LLM's can understand and use in conversation.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
You can add and remove files at anytime.
|
You can add and remove files at anytime.
|
||||||
</p>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={showNewWsModal}
|
onClick={showNewWsModal}
|
||||||
className="mt-4 w-fit flex flex-grow gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900 dark:bg-stone-900"
|
className="mt-5 w-fit transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
|
<p>Create your first workspace</p>
|
||||||
Create your first workspace
|
|
||||||
</p>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>,
|
|
||||||
|
|
||||||
<React.Fragment>
|
|
||||||
<div
|
|
||||||
className={`flex w-full mt-2 justify-end ${
|
|
||||||
popMsg ? "chat__message" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
Is this like an AI dropbox or something? What about chatting? It is
|
|
||||||
a chatbot isn't it?
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 justify-start ${
|
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
|
||||||
popMsg ? "chat__message" : ""
|
>
|
||||||
}`}
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon
|
||||||
|
size={36}
|
||||||
|
user={{ uid: userFromStorage()?.username }}
|
||||||
|
role={"user"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
|
>
|
||||||
|
Is this like an AI dropbox or something? What about chatting? It
|
||||||
|
is a chatbot isn't it?
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>,
|
||||||
|
|
||||||
|
<React.Fragment>
|
||||||
|
<div
|
||||||
|
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
AnythingLLM is more than a smarter Dropbox.
|
AnythingLLM is more than a smarter Dropbox.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@ -175,59 +229,75 @@ export default function DefaultChatContainer() {
|
|||||||
misunderstandings the LLM might have.
|
misunderstandings the LLM might have.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
You can toggle between either mode <i>in the middle of chatting!</i>
|
You can toggle between either mode{" "}
|
||||||
</p>
|
<i>in the middle of chatting!</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 justify-end ${
|
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
|
||||||
popMsg ? "chat__message" : ""
|
>
|
||||||
}`}
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon
|
||||||
|
size={36}
|
||||||
|
user={{ uid: userFromStorage()?.username }}
|
||||||
|
role={"user"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
Wow, this sounds amazing, let me try it out already!
|
Wow, this sounds amazing, let me try it out already!
|
||||||
</p>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 justify-start ${
|
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
|
||||||
popMsg ? "chat__message" : ""
|
>
|
||||||
}`}
|
<div
|
||||||
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
>
|
>
|
||||||
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
|
||||||
Have Fun!
|
Have Fun!
|
||||||
</p>
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-1 md:gap-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center gap-1 md:gap-4">
|
||||||
<a
|
<a
|
||||||
href={paths.github()}
|
href={paths.github()}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="mt-4 w-fit flex flex-grow gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900 dark:bg-stone-900"
|
className="mt-5 w-fit transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
<GitHub className="h-4 w-4" />
|
<GitHub className="h-4 w-4" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
|
<p>Star on GitHub</p>
|
||||||
Star on GitHub
|
|
||||||
</p>
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={paths.mailToMintplex()}
|
href={paths.mailToMintplex()}
|
||||||
className="mt-4 w-fit flex flex-grow gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900 dark:bg-stone-900"
|
className="mt-5 w-fit transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
<Mail className="h-4 w-4" />
|
<Mail className="h-4 w-4" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
|
<p>Contact Mintplex Labs</p>
|
||||||
Contact Mintplex Labs
|
|
||||||
</p>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -259,7 +329,7 @@ export default function DefaultChatContainer() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full md:min-w-[82%] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
{fetchedMessages.length === 0
|
{fetchedMessages.length === 0
|
||||||
|
@ -14,25 +14,27 @@ export default function EditingChatBubble({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex w-full mt-2 items-center ${
|
className={`relative flex w-full mt-2 items-start ${
|
||||||
isUser ? "justify-end" : "justify-start"
|
isUser ? "justify-end" : "justify-start"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isUser && (
|
|
||||||
<button
|
<button
|
||||||
className="flex items-center text-red-500 hover:text-red-700 transition mr-2"
|
className={`transition-all duration-300 absolute z-10 text-white bg-neutral-700 rounded-full hover:bg-selected-preference-gradient hover:border-white border-transparent border shadow-lg ${
|
||||||
|
isUser ? "right-0 mr-2" : "ml-2"
|
||||||
|
}`}
|
||||||
|
style={{ top: "-8px", [isUser ? "right" : "left"]: "255px" }}
|
||||||
onClick={() => removeMessage(index)}
|
onClick={() => removeMessage(index)}
|
||||||
>
|
>
|
||||||
<X className="mr-2" size={20} />
|
<X className="m-0.5" size={20} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={`p-4 max-w-full md:max-w-[75%] ${
|
className={`p-4 max-w-full md:w-[290px] ${
|
||||||
|
isUser ? "bg-sky-400 text-black" : "bg-white text-black"
|
||||||
|
} ${
|
||||||
isUser
|
isUser
|
||||||
? "bg-slate-200 dark:bg-amber-800"
|
? "rounded-tr-[40px] rounded-tl-[40px] rounded-bl-[40px]"
|
||||||
: "bg-orange-100 dark:bg-stone-700"
|
: "rounded-br-[40px] rounded-tl-[40px] rounded-tr-[40px]"
|
||||||
} rounded-b-2xl ${isUser ? "rounded-tl-2xl" : "rounded-tr-2xl"} ${
|
}
|
||||||
isUser ? "rounded-tr-sm" : "rounded-tl-sm"
|
|
||||||
}`}
|
}`}
|
||||||
onDoubleClick={() => setIsEditing(true)}
|
onDoubleClick={() => setIsEditing(true)}
|
||||||
>
|
>
|
||||||
@ -45,23 +47,16 @@ export default function EditingChatBubble({
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
tempMessage && (
|
tempMessage && (
|
||||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
<p className="text-black font-[500] md:font-semibold text-sm md:text-base break-words">
|
||||||
{tempMessage}
|
{tempMessage}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isUser && (
|
|
||||||
<button
|
|
||||||
className="flex items-center text-red-500 hover:text-red-700 transition ml-2"
|
|
||||||
onClick={() => removeMessage(index)}
|
|
||||||
>
|
|
||||||
<X className="mr-2" size={20} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
export default function Discord({ className = "" }) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
className={className}
|
|
||||||
style={{ strokeWidth: 4, transform: "scale(1.15)" }}
|
|
||||||
viewBox="0 0 128 128"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title />
|
|
||||||
<path d="M45.23,57.2c-6.16,0-11.17,5.6-11.17,12.48s5,12.47,11.17,12.47,11.16-5.59,11.16-12.47S51.38,57.2,45.23,57.2Zm0,21c-4,0-7.17-3.8-7.17-8.47s3.21-8.48,7.17-8.48,7.16,3.8,7.16,8.48S49.18,78.15,45.23,78.15Z" />
|
|
||||||
<path d="M121.83,59.58a156.78,156.78,0,0,0-11.52-31,2.1,2.1,0,0,0-.71-.77,87.08,87.08,0,0,0-15.23-7.17C84.55,17.07,79.91,17,79.72,17a2,2,0,0,0-2,1.72l-.6,4.17a133.14,133.14,0,0,0-26.28,0l-.6-4.17a2,2,0,0,0-2-1.72c-.19,0-4.83,0-14.65,3.61A87.08,87.08,0,0,0,18.4,27.81a2.1,2.1,0,0,0-.71.77,156.72,156.72,0,0,0-11.52,31C1,80.46,0,90.91,0,91.34a2,2,0,0,0,.49,1.5,55.2,55.2,0,0,0,18.2,12.74A76.32,76.32,0,0,0,38.48,111a2,2,0,0,0,1.92-1l5.4-9.25A105.08,105.08,0,0,0,64,102.24a105.08,105.08,0,0,0,18.2-1.51L87.6,110a2,2,0,0,0,1.72,1h.2a76.32,76.32,0,0,0,19.78-5.38,55.2,55.2,0,0,0,18.2-12.74,2,2,0,0,0,.49-1.5C128,90.91,127.05,80.46,121.83,59.58Zm-14.06,42.31a76.76,76.76,0,0,1-17.39,4.92l-4.08-7c4.68-1.24,14.42-4.46,21.83-11.2a2,2,0,1,0-2.69-3c-9,8.23-22.46,10.84-22.6,10.87h-.06A96.59,96.59,0,0,1,64,98.24a96.59,96.59,0,0,1-18.78-1.7h-.06c-.14,0-13.55-2.64-22.6-10.87a2,2,0,1,0-2.69,3c7.41,6.74,17.15,10,21.83,11.2l-4.08,7a76.08,76.08,0,0,1-17.39-4.92A52.24,52.24,0,0,1,4.08,90.8c.33-2.91,1.68-13.07,6-30.24A156.25,156.25,0,0,1,21,30.92,88.17,88.17,0,0,1,35,24.4a61.35,61.35,0,0,1,11.58-3.19l.35,2.39c-4,1-13.85,3.86-21.65,9.53a2,2,0,1,0,2.36,3.23c8.82-6.41,21-9.06,21.86-9.25A118.4,118.4,0,0,1,64,26.27a117.64,117.64,0,0,1,14.51.84c.91.19,13,2.83,21.86,9.25a2,2,0,1,0,2.36-3.23c-7.8-5.67-17.61-8.52-21.65-9.53l.35-2.39A61.75,61.75,0,0,1,93,24.4a88.17,88.17,0,0,1,14,6.52A156.25,156.25,0,0,1,118,60.56c4.29,17.17,5.64,27.33,6,30.24A52.24,52.24,0,0,1,107.77,101.89Z" />
|
|
||||||
<path d="M82.77,57.2c-6.15,0-11.16,5.6-11.16,12.48s5,12.47,11.16,12.47,11.17-5.59,11.17-12.47S88.93,57.2,82.77,57.2Zm0,21c-4,0-7.16-3.8-7.16-8.47s3.21-8.48,7.16-8.48,7.17,3.8,7.17,8.48S86.73,78.15,82.77,78.15Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
37
frontend/src/components/LLMProviderOption/index.jsx
Normal file
37
frontend/src/components/LLMProviderOption/index.jsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export default function LLMProviderOption({
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
image,
|
||||||
|
checked = false,
|
||||||
|
onClick,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div onClick={() => onClick(value)}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={value}
|
||||||
|
className="peer hidden"
|
||||||
|
checked={checked}
|
||||||
|
readOnly={true}
|
||||||
|
formNoValidate={true}
|
||||||
|
/>
|
||||||
|
<label className="transition-all duration-300 inline-flex flex-col h-full w-60 cursor-pointer items-start justify-between rounded-2xl bg-preference-gradient border-2 border-transparent shadow-md px-5 py-4 text-white hover:bg-selected-preference-gradient hover:border-white/60 peer-checked:border-white peer-checked:border-opacity-90 peer-checked:bg-selected-preference-gradient">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img src={image} alt={name} className="h-10 w-10 rounded" />
|
||||||
|
<div className="ml-4 text-sm font-semibold">{name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs font-base text-white tracking-wide">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={`https://${link}`}
|
||||||
|
className="mt-2 text-xs text-white font-medium underline"
|
||||||
|
>
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -3,8 +3,7 @@ import useLogo from "../../../../hooks/useLogo";
|
|||||||
import usePrefersDarkMode from "../../../../hooks/usePrefersDarkMode";
|
import usePrefersDarkMode from "../../../../hooks/usePrefersDarkMode";
|
||||||
import System from "../../../../models/system";
|
import System from "../../../../models/system";
|
||||||
import EditingChatBubble from "../../../EditingChatBubble";
|
import EditingChatBubble from "../../../EditingChatBubble";
|
||||||
import AnythingLLMLight from "../../../../media/logo/anything-llm-light.png";
|
import AnythingLLM from "../../../../media/logo/anything-llm.png";
|
||||||
import AnythingLLMDark from "../../../../media/logo/anything-llm-dark.png";
|
|
||||||
import showToast from "../../../../utils/toast";
|
import showToast from "../../../../utils/toast";
|
||||||
|
|
||||||
export default function Appearance() {
|
export default function Appearance() {
|
||||||
@ -120,11 +119,7 @@ export default function Appearance() {
|
|||||||
src={logo}
|
src={logo}
|
||||||
alt="Uploaded Logo"
|
alt="Uploaded Logo"
|
||||||
className="w-48 h-48 object-contain mr-6"
|
className="w-48 h-48 object-contain mr-6"
|
||||||
onError={(e) =>
|
onError={(e) => (e.target.src = AnythingLLM)}
|
||||||
(e.target.src = prefersDarkMode
|
|
||||||
? AnythingLLMLight
|
|
||||||
: AnythingLLMDark)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
@ -115,10 +115,7 @@ export default function LLMSelection({
|
|||||||
required={true}
|
required={true}
|
||||||
className="bg-gray-50 border border-gray-500 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-stone-700 dark:border-slate-200 dark:placeholder-stone-500 dark:text-slate-200"
|
className="bg-gray-50 border border-gray-500 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-stone-700 dark:border-slate-200 dark:placeholder-stone-500 dark:text-slate-200"
|
||||||
>
|
>
|
||||||
{[
|
{["gpt-3.5-turbo", "gpt-4"].map((model) => {
|
||||||
"gpt-3.5-turbo",
|
|
||||||
"gpt-4",
|
|
||||||
].map((model) => {
|
|
||||||
return (
|
return (
|
||||||
<option key={model} value={model}>
|
<option key={model} value={model}>
|
||||||
{model}
|
{model}
|
@ -1,89 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { dollarFormat } from "../../../../../utils/numbers";
|
|
||||||
|
|
||||||
export default function ConfirmationModal({
|
|
||||||
directories,
|
|
||||||
hideConfirm,
|
|
||||||
additions,
|
|
||||||
updateWorkspace,
|
|
||||||
}) {
|
|
||||||
function estimateCosts() {
|
|
||||||
const cachedTokens = additions.map((filepath) => {
|
|
||||||
const [parent, filename] = filepath.split("/");
|
|
||||||
const details = directories.items
|
|
||||||
.find((folder) => folder.name === parent)
|
|
||||||
.items.find((file) => file.name === filename);
|
|
||||||
|
|
||||||
const { token_count_estimate = 0, cached = false } = details;
|
|
||||||
return cached ? token_count_estimate : 0;
|
|
||||||
});
|
|
||||||
const tokenEstimates = additions.map((filepath) => {
|
|
||||||
const [parent, filename] = filepath.split("/");
|
|
||||||
const details = directories.items
|
|
||||||
.find((folder) => folder.name === parent)
|
|
||||||
.items.find((file) => file.name === filename);
|
|
||||||
|
|
||||||
const { token_count_estimate = 0 } = details;
|
|
||||||
return token_count_estimate;
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalTokens = tokenEstimates.reduce((a, b) => a + b, 0);
|
|
||||||
const cachedTotal = cachedTokens.reduce((a, b) => a + b, 0);
|
|
||||||
const dollarValue = 0.0004 * ((totalTokens - cachedTotal) / 1_000);
|
|
||||||
|
|
||||||
return {
|
|
||||||
dollarValue,
|
|
||||||
dollarText:
|
|
||||||
dollarValue < 0.01 ? "< $0.01" : `about ${dollarFormat(dollarValue)}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { dollarValue, dollarText } = estimateCosts();
|
|
||||||
return (
|
|
||||||
<dialog
|
|
||||||
open={true}
|
|
||||||
style={{ zIndex: 100 }}
|
|
||||||
className="fixed top-0 flex bg-black bg-opacity-50 w-[100vw] h-full items-center justify-center "
|
|
||||||
>
|
|
||||||
<div className="w-fit px-10 p-4 min-w-1/2 rounded-lg bg-white shadow dark:bg-stone-700 text-black dark:text-slate-200">
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<p className="font-semibold">
|
|
||||||
Are you sure you want to embed these documents?
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-1">
|
|
||||||
{dollarValue <= 0 ? (
|
|
||||||
<p className="text-base mt-4">
|
|
||||||
You will be embedding {additions.length} new documents into this
|
|
||||||
workspace.
|
|
||||||
<br />
|
|
||||||
This will not incur any costs for OpenAI credits.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="text-base mt-4">
|
|
||||||
You will be embedding {additions.length} new documents into this
|
|
||||||
workspace. <br />
|
|
||||||
This will cost {dollarText} in OpenAI credits.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full justify-between items-center mt-4">
|
|
||||||
<button
|
|
||||||
onClick={hideConfirm}
|
|
||||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={updateWorkspace}
|
|
||||||
className="border border-gray-800 text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:border-slate-200 dark:hover:bg-stone-900"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,106 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
formatDate,
|
||||||
|
getFileExtension,
|
||||||
|
truncate,
|
||||||
|
} from "../../../../../../utils/directories";
|
||||||
|
import { File, Trash } from "@phosphor-icons/react";
|
||||||
|
import System from "../../../../../../models/system";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
|
export default function FileRow({
|
||||||
|
item,
|
||||||
|
folderName,
|
||||||
|
selected,
|
||||||
|
toggleSelection,
|
||||||
|
expanded,
|
||||||
|
fetchKeys,
|
||||||
|
setLoading,
|
||||||
|
setLoadingMessage,
|
||||||
|
}) {
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
|
const onTrashClick = async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"Are you sure you want to delete this document?\nThis will require you to re-upload and re-embed it.\nThis document will be removed from any workspace that is currently referencing it.\nThis action is not reversible."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setLoadingMessage("This may take a while for large documents");
|
||||||
|
await System.deleteDocument(`${folderName}/${item.name}`, item);
|
||||||
|
await fetchKeys(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete the document:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected) toggleSelection(item);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowTooltip = () => {
|
||||||
|
setShowTooltip(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHideTooltip = () => {
|
||||||
|
setShowTooltip(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = debounce(handleShowTooltip, 500);
|
||||||
|
const handleMouseLeave = debounce(handleHideTooltip, 500);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => toggleSelection(item)}
|
||||||
|
className={`transition-all duration-200 text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 border-b border-white/20 hover:bg-sky-500/20 cursor-pointer ${`${
|
||||||
|
selected ? "bg-sky-500/20" : ""
|
||||||
|
} ${expanded ? "bg-sky-500/10" : ""}`}`}
|
||||||
|
>
|
||||||
|
<div className="col-span-4 flex gap-x-[4px] items-center">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={selected}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{selected && <div className="w-2 h-2 bg-white rounded-[2px]" />}
|
||||||
|
</div>
|
||||||
|
<File className="text-base font-bold w-4 h-4 mr-[3px]" weight="fill" />
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<p className="whitespace-nowrap overflow-hidden">
|
||||||
|
{truncate(item.title, 17)}
|
||||||
|
</p>
|
||||||
|
{showTooltip && (
|
||||||
|
<div className="absolute left-0 bg-white text-black p-1.5 rounded shadow-lg whitespace-nowrap">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="col-span-2 pl-3.5 whitespace-nowrap">
|
||||||
|
{formatDate(item?.published)}
|
||||||
|
</p>
|
||||||
|
<p className="col-span-2 pl-3">{item?.size || "---"}</p>
|
||||||
|
<p className="col-span-2 pl-2 uppercase">{getFileExtension(item.url)}</p>
|
||||||
|
<div className="col-span-2 flex justify-end items-center">
|
||||||
|
{item?.cached && (
|
||||||
|
<div className="bg-white/10 rounded-3xl">
|
||||||
|
<p className="text-xs px-2 py-0.5">Cached</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Trash
|
||||||
|
onClick={onTrashClick}
|
||||||
|
className="text-base font-bold w-4 h-4 ml-2 flex-shrink-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import FileRow from "../FileRow";
|
||||||
|
import { CaretDown, FolderNotch } from "@phosphor-icons/react";
|
||||||
|
import { truncate } from "../../../../../../utils/directories";
|
||||||
|
|
||||||
|
export default function FolderRow({
|
||||||
|
item,
|
||||||
|
selected,
|
||||||
|
onRowClick,
|
||||||
|
toggleSelection,
|
||||||
|
isSelected,
|
||||||
|
fetchKeys,
|
||||||
|
setLoading,
|
||||||
|
setLoadingMessage,
|
||||||
|
}) {
|
||||||
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
|
||||||
|
const handleExpandClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setExpanded(!expanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onClick={onRowClick}
|
||||||
|
className={`transition-all duration-200 text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 border-b border-white/20 hover:bg-sky-500/20 cursor-pointer w-full ${
|
||||||
|
selected ? "bg-sky-500/20" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="col-span-4 flex gap-x-[4px] items-center">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={selected}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{selected && <div className="w-2 h-2 bg-white rounded-[2px]" />}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={handleExpandClick}
|
||||||
|
className={`transform transition-transform duration-200 ${
|
||||||
|
expanded ? "rotate-360" : " rotate-270"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CaretDown className="text-base font-bold w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<FolderNotch
|
||||||
|
className="text-base font-bold w-4 h-4 mr-[3px]"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
<p className="whitespace-nowrap overflow-show">
|
||||||
|
{truncate(item.name, 40)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="col-span-2 pl-3.5" />
|
||||||
|
<p className="col-span-2 pl-3" />
|
||||||
|
<p className="col-span-2 pl-2" />
|
||||||
|
<div className="col-span-2 flex justify-end items-center" />
|
||||||
|
</div>
|
||||||
|
{expanded && (
|
||||||
|
<div className="col-span-full">
|
||||||
|
{item.items.map((fileItem) => (
|
||||||
|
<FileRow
|
||||||
|
key={fileItem.id}
|
||||||
|
item={fileItem}
|
||||||
|
folderName={item.name}
|
||||||
|
selected={isSelected(fileItem.id)}
|
||||||
|
expanded={expanded}
|
||||||
|
toggleSelection={toggleSelection}
|
||||||
|
fetchKeys={fetchKeys}
|
||||||
|
setLoading={setLoading}
|
||||||
|
setLoadingMessage={setLoadingMessage}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,171 +1,146 @@
|
|||||||
import React, { useState } from "react";
|
import UploadFile from "../UploadFile";
|
||||||
import {
|
import PreLoader from "../../../../Preloader";
|
||||||
FileMinus,
|
import { useEffect, useState } from "react";
|
||||||
FilePlus,
|
import FolderRow from "./FolderRow";
|
||||||
Folder,
|
import pluralize from "pluralize";
|
||||||
FolderMinus,
|
|
||||||
FolderPlus,
|
|
||||||
Zap,
|
|
||||||
} from "react-feather";
|
|
||||||
import { nFormatter } from "../../../../../utils/numbers";
|
|
||||||
import System from "../../../../../models/system";
|
|
||||||
|
|
||||||
export default function Directory({
|
export default function Directory({
|
||||||
files,
|
files,
|
||||||
parent = null,
|
loading,
|
||||||
nested = 0,
|
setLoading,
|
||||||
toggleSelection,
|
fileTypes,
|
||||||
isSelected,
|
workspace,
|
||||||
|
fetchKeys,
|
||||||
|
selectedItems,
|
||||||
|
setSelectedItems,
|
||||||
|
setHighlightWorkspace,
|
||||||
|
moveToWorkspace,
|
||||||
|
setLoadingMessage,
|
||||||
|
loadingMessage,
|
||||||
}) {
|
}) {
|
||||||
const [isExpanded, toggleExpanded] = useState(false);
|
const [amountSelected, setAmountSelected] = useState(0);
|
||||||
const [showDetails, toggleDetails] = useState(false);
|
|
||||||
const [showZap, setShowZap] = useState(false);
|
|
||||||
const handleDelete = async (name, meta) => {
|
|
||||||
if (
|
|
||||||
!window.confirm(
|
|
||||||
"Are you sure you want to delete this document?\nThis will require you to re-upload and re-embed it.\nThis document will be removed from any workspace that is currently referencing it.\nThis action is not reversible."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
document?.getElementById(meta?.id)?.remove();
|
|
||||||
await System.deleteDocument(name, meta);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (files.type === "folder") {
|
const toggleSelection = (item) => {
|
||||||
return (
|
setSelectedItems((prevSelectedItems) => {
|
||||||
<div style={{ marginLeft: nested }} className="mb-2">
|
const newSelectedItems = { ...prevSelectedItems };
|
||||||
<div
|
|
||||||
className={`flex items-center hover:bg-gray-100 gap-x-2 text-gray-800 dark:text-stone-200 dark:hover:bg-stone-800 px-2 rounded-lg`}
|
|
||||||
>
|
|
||||||
{files.items.some((files) => files.type === "folder") ? (
|
|
||||||
<Folder className="w-6 h-6" />
|
|
||||||
) : (
|
|
||||||
<button onClick={() => toggleSelection(files.name)}>
|
|
||||||
{isSelected(files.name) ? (
|
|
||||||
<FolderMinus className="w-6 h-6 stroke-red-800 hover:fill-red-500" />
|
|
||||||
) : (
|
|
||||||
<FolderPlus className="w-6 h-6 hover:stroke-green-800 hover:fill-green-500" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
if (item.type === "folder") {
|
||||||
className="flex gap-x-2 items-center cursor-pointer w-full"
|
const isCurrentlySelected = isFolderCompletelySelected(item);
|
||||||
onClick={() => toggleExpanded(!isExpanded)}
|
if (isCurrentlySelected) {
|
||||||
>
|
item.items.forEach((file) => delete newSelectedItems[file.id]);
|
||||||
<h2 className="text-base md:text-2xl">{files.name}</h2>
|
} else {
|
||||||
{files.items.some((files) => files.type === "folder") ? (
|
item.items.forEach((file) => (newSelectedItems[file.id] = true));
|
||||||
<p className="text-xs italic">{files.items.length} folders</p>
|
}
|
||||||
) : (
|
} else {
|
||||||
<p className="text-xs italic">
|
if (newSelectedItems[item.id]) {
|
||||||
{files.items.length} documents |{" "}
|
delete newSelectedItems[item.id];
|
||||||
{nFormatter(
|
} else {
|
||||||
files.items.reduce((a, b) => a + b.token_count_estimate, 0)
|
newSelectedItems[item.id] = true;
|
||||||
)}{" "}
|
}
|
||||||
tokens
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isExpanded &&
|
|
||||||
files.items.map((item) => (
|
|
||||||
<Directory
|
|
||||||
key={item.name}
|
|
||||||
parent={files.name}
|
|
||||||
files={item}
|
|
||||||
nested={nested + 20}
|
|
||||||
toggleSelection={toggleSelection}
|
|
||||||
isSelected={isSelected}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, type: _type, ...meta } = files;
|
return newSelectedItems;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFolderCompletelySelected = (folder) => {
|
||||||
|
if (folder.items.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return folder.items.every((file) => selectedItems[file.id]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelected = (id, item) => {
|
||||||
|
if (item && item.type === "folder") {
|
||||||
|
return isFolderCompletelySelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!selectedItems[id];
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAmountSelected(Object.keys(selectedItems).length);
|
||||||
|
}, [selectedItems]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ml-[20px] my-2" id={meta.id}>
|
<div className="px-8 pb-8">
|
||||||
<div className="flex items-center">
|
<div className="flex flex-col gap-y-6">
|
||||||
{meta?.cached && (
|
<div className="flex items-center justify-between w-[560px] px-5">
|
||||||
<button
|
<h3 className="text-white text-base font-bold">My Documents</h3>
|
||||||
type="button"
|
|
||||||
onClick={() => setShowZap(true)}
|
|
||||||
className="rounded-full p-1 hover:bg-stone-500 hover:bg-opacity-75"
|
|
||||||
>
|
|
||||||
<Zap className="h-4 w-4 stroke-yellow-500 fill-yellow-400" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{showZap && (
|
|
||||||
<dialog
|
|
||||||
open={true}
|
|
||||||
style={{ zIndex: 100 }}
|
|
||||||
className="fixed top-0 flex bg-black bg-opacity-50 w-[100vw] h-full items-center justify-center "
|
|
||||||
>
|
|
||||||
<div className="w-fit px-10 py-4 w-[25%] rounded-lg bg-white shadow dark:bg-stone-700 text-black dark:text-slate-200">
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<p className="font-semibold text-xl flex items-center gap-x-1 justify-left">
|
|
||||||
What does{" "}
|
|
||||||
<Zap className="h-4 w-4 stroke-yellow-500 fill-yellow-400" />{" "}
|
|
||||||
mean?
|
|
||||||
</p>
|
|
||||||
<p className="text-base mt-4">
|
|
||||||
This symbol indicates that you have embed this document before
|
|
||||||
and will not have to pay to re-embed this document.
|
|
||||||
</p>
|
|
||||||
<div className="flex w-full justify-center items-center mt-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowZap(false)}
|
|
||||||
className="border border-gray-800 text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:border-slate-200 dark:hover:bg-stone-900"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-[560px] h-[310px] bg-zinc-900 rounded-2xl">
|
||||||
|
<div className="rounded-t-2xl text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 shadow-lg bg-zinc-900 sticky top-0 z-10">
|
||||||
|
<p className="col-span-4">Name</p>
|
||||||
|
<p className="col-span-2">Date</p>
|
||||||
|
<p className="col-span-2">Size</p>
|
||||||
|
<p className="col-span-2">Kind</p>
|
||||||
|
<p className="col-span-2">Cached</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-x-2 text-gray-800 dark:text-stone-200 hover:bg-gray-100 dark:hover:bg-stone-800 px-2 rounded-lg`}
|
className="overflow-y-auto pb-9"
|
||||||
|
style={{ height: "calc(100% - 40px)" }}
|
||||||
>
|
>
|
||||||
<button onClick={() => toggleSelection(`${parent}/${name}`)}>
|
{loading ? (
|
||||||
{isSelected(`${parent}/${name}`) ? (
|
<div className="w-full h-full flex items-center justify-center flex-col gap-y-5">
|
||||||
<FileMinus className="w-6 h-6 stroke-red-800 hover:fill-red-500" />
|
<PreLoader />
|
||||||
) : (
|
<p className="text-white/80 text-sm font-semibold animate-pulse text-center w-1/3">
|
||||||
<FilePlus className="w-6 h-6 hover:stroke-green-800 hover:fill-green-500" />
|
{loadingMessage}
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
className="w-full items-center flex cursor-pointer"
|
|
||||||
onClick={() => toggleDetails(!showDetails)}
|
|
||||||
>
|
|
||||||
<h3 className="text-sm">{name}</h3>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{showDetails && (
|
|
||||||
<div className="w-full flex flex-col">
|
|
||||||
<div className="ml-[20px] flex flex-col gap-y-1 my-1 p-2 rounded-md bg-slate-200 font-mono text-sm overflow-x-scroll">
|
|
||||||
{Object.entries(meta).map(([key, value], i) => {
|
|
||||||
if (key === "cached") return null;
|
|
||||||
return (
|
|
||||||
<p key={i} className="whitespace-pre">
|
|
||||||
{key}: {value}
|
|
||||||
</p>
|
</p>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : !!files.items ? (
|
||||||
|
files.items.map(
|
||||||
|
(item, index) =>
|
||||||
|
item.type === "folder" && (
|
||||||
|
<FolderRow
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
selected={isSelected(
|
||||||
|
item.id,
|
||||||
|
item.type === "folder" ? item : null
|
||||||
|
)}
|
||||||
|
fetchKeys={fetchKeys}
|
||||||
|
onRowClick={() => toggleSelection(item)}
|
||||||
|
toggleSelection={toggleSelection}
|
||||||
|
isSelected={isSelected}
|
||||||
|
setLoading={setLoading}
|
||||||
|
setLoadingMessage={setLoadingMessage}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<p className="text-white text-opacity-40 text-sm font-medium">
|
||||||
|
No Documents
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{amountSelected !== 0 && (
|
||||||
|
<div className="absolute bottom-0 left-0 w-full flex justify-center items-center h-9 bg-white rounded-b-2xl">
|
||||||
|
<div className="flex gap-x-5">
|
||||||
<div
|
<div
|
||||||
onClick={() => handleDelete(`${parent}/${name}`, meta)}
|
onMouseEnter={() => setHighlightWorkspace(true)}
|
||||||
className="flex items-center justify-end w-full"
|
onMouseLeave={() => setHighlightWorkspace(false)}
|
||||||
|
onClick={moveToWorkspace}
|
||||||
|
className="text-sm font-semibold h-7 px-2.5 rounded-lg transition-all duration-300 hover:text-white hover:bg-neutral-800/80 cursor-pointer flex items-center"
|
||||||
>
|
>
|
||||||
<button className="text-sm text-slate-400 dark:text-stone-500 hover:text-red-500">
|
Move {amountSelected} {pluralize("file", amountSelected)} to
|
||||||
Purge Document
|
workspace
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<UploadFile
|
||||||
|
fileTypes={fileTypes}
|
||||||
|
workspace={workspace}
|
||||||
|
fetchKeys={fetchKeys}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useEffect, memo } from "react";
|
import React, { useState, useEffect, memo } from "react";
|
||||||
import Workspace from "../../../../../models/workspace";
|
|
||||||
import truncate from "truncate";
|
import truncate from "truncate";
|
||||||
import { humanFileSize, milliToHms } from "../../../../../utils/numbers";
|
|
||||||
import { CheckCircle, XCircle } from "react-feather";
|
import { CheckCircle, XCircle } from "react-feather";
|
||||||
import { Grid } from "react-loading-icons";
|
import Workspace from "../../../../../../models/workspace";
|
||||||
|
import { humanFileSize, milliToHms } from "../../../../../../utils/numbers";
|
||||||
|
import PreLoader from "../../../../../Preloader";
|
||||||
|
|
||||||
function FileUploadProgressComponent({
|
function FileUploadProgressComponent({
|
||||||
slug,
|
slug,
|
||||||
@ -44,17 +44,15 @@ function FileUploadProgressComponent({
|
|||||||
|
|
||||||
if (rejected) {
|
if (rejected) {
|
||||||
return (
|
return (
|
||||||
<div className="w-fit px-2 py-2 flex items-center gap-x-4 rounded-lg bg-blue-100 border-blue-600 dark:bg-stone-800 bg-opacity-50 border dark:border-stone-600">
|
<div className="h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40">
|
||||||
<div className="w-6 h-6">
|
<div className="w-6 h-6">
|
||||||
<XCircle className="w-6 h-6 stroke-white bg-red-500 rounded-full p-1 w-full h-full" />
|
<XCircle className="w-6 h-6 stroke-white bg-red-500 rounded-full p-1 w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-black dark:text-stone-200 text-sm font-mono overflow-x-scroll">
|
<p className="text-white text-xs font-medium">
|
||||||
{truncate(file.name, 30)}
|
{truncate(file.name, 30)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-red-700 dark:text-red-400 text-xs font-mono">
|
<p className="text-red-400 text-xs font-medium">{reason}</p>
|
||||||
{reason}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -62,43 +60,41 @@ function FileUploadProgressComponent({
|
|||||||
|
|
||||||
if (status === "failed") {
|
if (status === "failed") {
|
||||||
return (
|
return (
|
||||||
<div className="w-fit px-2 py-2 flex items-center gap-x-4 rounded-lg bg-blue-100 border-blue-600 dark:bg-stone-800 bg-opacity-50 border dark:border-stone-600">
|
<div className="h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40 overflow-y-auto">
|
||||||
<div className="w-6 h-6">
|
<div className="w-6 h-6">
|
||||||
<XCircle className="w-6 h-6 stroke-white bg-red-500 rounded-full p-1 w-full h-full" />
|
<XCircle className="w-6 h-6 stroke-white bg-red-500 rounded-full p-1 w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-black dark:text-stone-200 text-sm font-mono overflow-x-scroll">
|
<p className="text-white text-xs font-medium">
|
||||||
{truncate(file.name, 30)}
|
{truncate(file.name, 30)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-red-700 dark:text-red-400 text-xs font-mono">
|
<p className="text-red-400 text-xs font-medium">{error}</p>
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-fit px-2 py-2 flex items-center gap-x-4 rounded-lg bg-blue-100 border-blue-600 dark:bg-stone-800 bg-opacity-50 border dark:border-stone-600">
|
<div className="h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40">
|
||||||
<div className="w-6 h-6">
|
<div className="w-6 h-6">
|
||||||
{status !== "complete" ? (
|
{status !== "complete" ? (
|
||||||
<Grid className="w-6 h-6 grid-loader" />
|
<div className="flex items-center justify-center">
|
||||||
|
<PreLoader size="6" />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<CheckCircle className="w-6 h-6 stroke-white bg-green-500 rounded-full p-1 w-full h-full" />
|
<CheckCircle className="w-6 h-6 stroke-white bg-green-500 rounded-full p-1 w-full h-full" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-black dark:text-stone-200 text-sm font-mono overflow-x-scroll">
|
<p className="text-white text-xs font-medium">
|
||||||
{truncate(file.name, 30)}
|
{truncate(file.name, 30)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 dark:text-stone-400 text-xs font-mono">
|
<p className="text-white/60 text-xs font-medium">
|
||||||
{humanFileSize(file.size)} | {milliToHms(timerMs)}
|
{humanFileSize(file.size)} | {milliToHms(timerMs)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(FileUploadProgressComponent);
|
export default memo(FileUploadProgressComponent);
|
@ -0,0 +1,111 @@
|
|||||||
|
import { CloudArrowUp } from "@phosphor-icons/react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import showToast from "../../../../../utils/toast";
|
||||||
|
import System from "../../../../../models/system";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import FileUploadProgress from "./FileUploadProgress";
|
||||||
|
|
||||||
|
export default function UploadFile({ workspace, fileTypes, fetchKeys }) {
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
|
|
||||||
|
const handleUploadSuccess = () => {
|
||||||
|
fetchKeys(true);
|
||||||
|
showToast("File uploaded successfully", "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadError = (message) => {
|
||||||
|
showToast(`Error uploading file: ${message}`, "error");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = async (acceptedFiles, rejections) => {
|
||||||
|
const newAccepted = acceptedFiles.map((file) => {
|
||||||
|
return {
|
||||||
|
uid: v4(),
|
||||||
|
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.checkDocumentProcessorOnline();
|
||||||
|
setReady(online);
|
||||||
|
}
|
||||||
|
checkProcessorOnline();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept: {
|
||||||
|
...fileTypes,
|
||||||
|
},
|
||||||
|
disabled: !ready,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={`transition-all duration-300 w-[560px] border-2 border-dashed rounded-2xl bg-zinc-900/50 p-3 ${
|
||||||
|
ready ? "cursor-pointer" : "cursor-not-allowed"
|
||||||
|
} hover:bg-zinc-900/90`}
|
||||||
|
{...getRootProps()}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{ready === false ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<CloudArrowUp className="w-8 h-8 text-white/80" />
|
||||||
|
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
|
||||||
|
Document Processor Unavailable
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-60 text-xs font-medium py-1 px-20 text-center">
|
||||||
|
We can't upload your files right now because the document
|
||||||
|
processor is offline. Please try again later.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<CloudArrowUp className="w-8 h-8 text-white/80" />
|
||||||
|
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
|
||||||
|
Click to upload or drag and drop
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-60 text-xs font-medium py-1">
|
||||||
|
Supported file extensions are{" "}
|
||||||
|
{Object.values(fileTypes).flat().join(" ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[400px] p-1 overflow-y-auto">
|
||||||
|
{files.map((file) => (
|
||||||
|
<FileUploadProgress
|
||||||
|
key={file.uid}
|
||||||
|
file={file.file}
|
||||||
|
slug={workspace.slug}
|
||||||
|
rejected={file?.rejected}
|
||||||
|
reason={file?.reason}
|
||||||
|
onUploadSuccess={handleUploadSuccess}
|
||||||
|
onUploadError={handleUploadError}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 text-center text-white text-opacity-80 text-xs font-medium w-[560px]">
|
||||||
|
These files will be uploaded to the document processor running on this
|
||||||
|
AnythingLLM instance. These files are not sent or shared with a third
|
||||||
|
party.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
formatDate,
|
||||||
|
getFileExtension,
|
||||||
|
truncate,
|
||||||
|
} from "../../../../../../utils/directories";
|
||||||
|
import { ArrowUUpLeft, File } from "@phosphor-icons/react";
|
||||||
|
import Workspace from "../../../../../../models/workspace";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
|
export default function WorkspaceFileRow({
|
||||||
|
item,
|
||||||
|
folderName,
|
||||||
|
workspace,
|
||||||
|
setLoading,
|
||||||
|
setLoadingMessage,
|
||||||
|
fetchKeys,
|
||||||
|
hasChanges,
|
||||||
|
movedItems,
|
||||||
|
}) {
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
|
const onRemoveClick = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingMessage(`Removing file from workspace`);
|
||||||
|
await Workspace.modifyEmbeddings(workspace.slug, {
|
||||||
|
adds: [],
|
||||||
|
deletes: [`${folderName}/${item.name}`],
|
||||||
|
});
|
||||||
|
await fetchKeys(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to remove document:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingMessage("");
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowTooltip = () => {
|
||||||
|
setShowTooltip(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHideTooltip = () => {
|
||||||
|
setShowTooltip(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMovedItem = movedItems?.some((movedItem) => movedItem.id === item.id);
|
||||||
|
const handleMouseEnter = debounce(handleShowTooltip, 500);
|
||||||
|
const handleMouseLeave = debounce(handleHideTooltip, 500);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`items-center transition-all duration-200 text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 border-b border-white/20 hover:bg-sky-500/20 cursor-pointer
|
||||||
|
${isMovedItem ? "bg-green-800/40" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="col-span-4 flex gap-x-[4px] items-center">
|
||||||
|
<File
|
||||||
|
className="text-base font-bold w-4 h-4 ml-3 mr-[3px]"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<p className="whitespace-nowrap overflow-hidden">
|
||||||
|
{truncate(item.title, 17)}
|
||||||
|
</p>
|
||||||
|
{showTooltip && (
|
||||||
|
<div className="absolute left-0 bg-white text-black p-1.5 rounded shadow-lg whitespace-nowrap">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="col-span-2 pl-3.5 whitespace-nowrap">
|
||||||
|
{formatDate(item?.published)}
|
||||||
|
</p>
|
||||||
|
<p className="col-span-2 pl-3">{item?.size || "---"}</p>
|
||||||
|
<p className="col-span-2 pl-2 uppercase">{getFileExtension(item.url)}</p>
|
||||||
|
<div className="col-span-2 flex justify-end items-center">
|
||||||
|
{item?.cached && (
|
||||||
|
<div className="bg-white/10 rounded-3xl">
|
||||||
|
<p className="text-xs px-2 py-0.5">Cached</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasChanges ? (
|
||||||
|
<div className="w-4 h-4 ml-2 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ArrowUUpLeft
|
||||||
|
onClick={onRemoveClick}
|
||||||
|
className="text-base font-bold w-4 h-4 ml-2 flex-shrink-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
import PreLoader from "../../../../Preloader";
|
||||||
|
import { dollarFormat } from "../../../../../utils/numbers";
|
||||||
|
import WorkspaceFileRow from "./WorkspaceFileRow";
|
||||||
|
|
||||||
|
export default function WorkspaceDirectory({
|
||||||
|
workspace,
|
||||||
|
files,
|
||||||
|
highlightWorkspace,
|
||||||
|
loading,
|
||||||
|
loadingMessage,
|
||||||
|
setLoadingMessage,
|
||||||
|
setLoading,
|
||||||
|
fetchKeys,
|
||||||
|
hasChanges,
|
||||||
|
saveChanges,
|
||||||
|
embeddingCosts,
|
||||||
|
movedItems,
|
||||||
|
}) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="px-8">
|
||||||
|
<div className="flex items-center justify-start w-[560px]">
|
||||||
|
<h3 className="text-white text-base font-bold ml-5">
|
||||||
|
{workspace.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5">
|
||||||
|
<div className="text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20">
|
||||||
|
<p className="col-span-4">Name</p>
|
||||||
|
<p className="col-span-2">Date</p>
|
||||||
|
<p className="col-span-2">Size</p>
|
||||||
|
<p className="col-span-2">Kind</p>
|
||||||
|
<p className="col-span-2">Cached</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full flex items-center justify-center flex-col gap-y-5">
|
||||||
|
<PreLoader />
|
||||||
|
<p className="text-white/80 text-sm font-semibold animate-pulse text-center w-1/3">
|
||||||
|
{loadingMessage}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-8">
|
||||||
|
<div className="flex items-center justify-start w-[560px]">
|
||||||
|
<h3 className="text-white text-base font-bold ml-5">
|
||||||
|
{workspace.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5 overflow-y-auto border-4 transition-all duration-300 ${
|
||||||
|
highlightWorkspace ? "border-cyan-300/80" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 bg-zinc-900 sticky top-0 z-10">
|
||||||
|
<p className="col-span-4">Name</p>
|
||||||
|
<p className="col-span-2">Date</p>
|
||||||
|
<p className="col-span-2">Size</p>
|
||||||
|
<p className="col-span-2">Kind</p>
|
||||||
|
<p className="col-span-2">Cached</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full flex flex-col z-0">
|
||||||
|
{Object.values(files.items).some(
|
||||||
|
(folder) => folder.items.length > 0
|
||||||
|
) || movedItems.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{files.items.map((folder) =>
|
||||||
|
folder.items.map((item, index) => (
|
||||||
|
<WorkspaceFileRow
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
folderName={folder.name}
|
||||||
|
workspace={workspace}
|
||||||
|
setLoading={setLoading}
|
||||||
|
setLoadingMessage={setLoadingMessage}
|
||||||
|
fetchKeys={fetchKeys}
|
||||||
|
hasChanges={hasChanges}
|
||||||
|
movedItems={movedItems}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<p className="text-white text-opacity-40 text-sm font-medium">
|
||||||
|
No Documents
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{hasChanges && (
|
||||||
|
<div className="flex items-center justify-between py-6 transition-all duration-300">
|
||||||
|
<div className="text-white/80">
|
||||||
|
<p className="text-sm font-semibold">
|
||||||
|
{embeddingCosts === 0
|
||||||
|
? ""
|
||||||
|
: `Estimated Cost: ${
|
||||||
|
embeddingCosts < 0.01
|
||||||
|
? `< $0.01`
|
||||||
|
: dollarFormat(embeddingCosts)
|
||||||
|
}`}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs italic" hidden={embeddingCosts === 0}>
|
||||||
|
*One time cost for embeddings
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={saveChanges}
|
||||||
|
className="transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
|
>
|
||||||
|
Save and Embed
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,41 +1,74 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import { ArrowsDownUp } from "@phosphor-icons/react";
|
||||||
import System from "../../../../models/system";
|
import { useEffect, useState } from "react";
|
||||||
import Workspace from "../../../../models/workspace";
|
import Workspace from "../../../../models/workspace";
|
||||||
import paths from "../../../../utils/paths";
|
import System from "../../../../models/system";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import Directory from "./Directory";
|
import Directory from "./Directory";
|
||||||
import ConfirmationModal from "./ConfirmationModal";
|
|
||||||
import { AlertTriangle } from "react-feather";
|
|
||||||
import showToast from "../../../../utils/toast";
|
import showToast from "../../../../utils/toast";
|
||||||
|
import WorkspaceDirectory from "./WorkspaceDirectory";
|
||||||
|
|
||||||
export default function DocumentSettings({ workspace }) {
|
const COST_PER_TOKEN = 0.0004;
|
||||||
const { slug } = useParams();
|
|
||||||
|
export default function DocumentSettings({ workspace, fileTypes }) {
|
||||||
|
const [highlightWorkspace, setHighlightWorkspace] = useState(false);
|
||||||
|
const [availableDocs, setAvailableDocs] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [workspaceDocs, setWorkspaceDocs] = useState([]);
|
||||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
const [selectedItems, setSelectedItems] = useState({});
|
||||||
const [directories, setDirectories] = useState(null);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [originalDocuments, setOriginalDocuments] = useState([]);
|
const [movedItems, setMovedItems] = useState([]);
|
||||||
const [selectedFiles, setSelectFiles] = useState([]);
|
const [embeddingsCost, setEmbeddingsCost] = useState(0);
|
||||||
const [hasFiles, setHasFiles] = useState(true);
|
const [loadingMessage, setLoadingMessage] = useState("");
|
||||||
const [canDelete, setCanDelete] = useState(false);
|
|
||||||
|
|
||||||
async function fetchKeys(refetchWorkspace = false) {
|
async function fetchKeys(refetchWorkspace = false) {
|
||||||
|
setLoading(true);
|
||||||
const localFiles = await System.localFiles();
|
const localFiles = await System.localFiles();
|
||||||
const currentWorkspace = refetchWorkspace
|
const currentWorkspace = refetchWorkspace
|
||||||
? await Workspace.bySlug(slug ?? workspace.slug)
|
? await Workspace.bySlug(workspace.slug)
|
||||||
: workspace;
|
: workspace;
|
||||||
const originalDocs =
|
|
||||||
currentWorkspace.documents.map((doc) => doc.docpath) || [];
|
|
||||||
const hasAnyFiles = localFiles.items.some(
|
|
||||||
(folder) => folder?.items?.length > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
const canDelete = await System.getCanDeleteWorkspaces();
|
const documentsInWorkspace =
|
||||||
setCanDelete(canDelete);
|
currentWorkspace.documents.map((doc) => doc.docpath) || [];
|
||||||
setDirectories(localFiles);
|
|
||||||
setOriginalDocuments([...originalDocs]);
|
// Documents that are not in the workspace
|
||||||
setSelectFiles([...originalDocs]);
|
const availableDocs = {
|
||||||
setHasFiles(hasAnyFiles);
|
...localFiles,
|
||||||
|
items: localFiles.items.map((folder) => {
|
||||||
|
if (folder.items && folder.type === "folder") {
|
||||||
|
return {
|
||||||
|
...folder,
|
||||||
|
items: folder.items.filter(
|
||||||
|
(file) =>
|
||||||
|
file.type === "file" &&
|
||||||
|
!documentsInWorkspace.includes(`${folder.name}/${file.name}`)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Documents that are already in the workspace
|
||||||
|
const workspaceDocs = {
|
||||||
|
...localFiles,
|
||||||
|
items: localFiles.items.map((folder) => {
|
||||||
|
if (folder.items && folder.type === "folder") {
|
||||||
|
return {
|
||||||
|
...folder,
|
||||||
|
items: folder.items.filter(
|
||||||
|
(file) =>
|
||||||
|
file.type === "file" &&
|
||||||
|
documentsInWorkspace.includes(`${folder.name}/${file.name}`)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
setAvailableDocs(availableDocs);
|
||||||
|
setWorkspaceDocs(workspaceDocs);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,56 +76,20 @@ export default function DocumentSettings({ workspace }) {
|
|||||||
fetchKeys();
|
fetchKeys();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const deleteWorkspace = async () => {
|
|
||||||
if (
|
|
||||||
!window.confirm(
|
|
||||||
`You are about to delete your entire ${workspace.name} workspace. This will remove all vector embeddings on your vector database.\n\nThe original source files will remain untouched. This action is irreversible.`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
await Workspace.delete(workspace.slug);
|
|
||||||
workspace.slug === slug
|
|
||||||
? (window.location = paths.home())
|
|
||||||
: window.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const docChanges = () => {
|
|
||||||
const changes = {
|
|
||||||
adds: [],
|
|
||||||
deletes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
selectedFiles.map((doc) => {
|
|
||||||
const inOriginal = !!originalDocuments.find((oDoc) => oDoc === doc);
|
|
||||||
if (!inOriginal) {
|
|
||||||
changes.adds.push(doc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
originalDocuments.map((doc) => {
|
|
||||||
const selected = !!selectedFiles.find((oDoc) => oDoc === doc);
|
|
||||||
if (!selected) {
|
|
||||||
changes.deletes.push(doc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return changes;
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmChanges = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const changes = docChanges();
|
|
||||||
changes.adds.length > 0 ? setShowConfirmation(true) : updateWorkspace(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateWorkspace = async (e) => {
|
const updateWorkspace = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSaving(true);
|
setLoading(true);
|
||||||
showToast("Updating workspace...", "info", { autoClose: false });
|
showToast("Updating workspace...", "info", { autoClose: false });
|
||||||
setShowConfirmation(false);
|
setLoadingMessage("This may take a while for large documents");
|
||||||
|
|
||||||
const changes = docChanges();
|
const changesToSend = {
|
||||||
await Workspace.modifyEmbeddings(workspace.slug, changes)
|
adds: movedItems.map((item) => `${item.folderName}/${item.name}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedItems({});
|
||||||
|
setHasChanges(false);
|
||||||
|
setHighlightWorkspace(false);
|
||||||
|
await Workspace.modifyEmbeddings(workspace.slug, changesToSend)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res && res.workspace) {
|
if (res && res.workspace) {
|
||||||
showToast("Workspace updated successfully.", "success", {
|
showToast("Workspace updated successfully.", "success", {
|
||||||
@ -108,122 +105,110 @@ export default function DocumentSettings({ workspace }) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setSaving(false);
|
setMovedItems([]);
|
||||||
await fetchKeys(true);
|
await fetchKeys(true);
|
||||||
|
setLoading(false);
|
||||||
|
setLoadingMessage("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSelected = (filepath) => {
|
const moveSelectedItemsToWorkspace = () => {
|
||||||
const isFolder = !filepath.includes("/");
|
setHighlightWorkspace(false);
|
||||||
return isFolder
|
setHasChanges(true);
|
||||||
? selectedFiles.some((doc) => doc.includes(filepath.split("/")[0]))
|
|
||||||
: selectedFiles.some((doc) => doc.includes(filepath));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSelection = (filepath) => {
|
const newMovedItems = [];
|
||||||
const isFolder = !filepath.includes("/");
|
|
||||||
const parent = isFolder ? filepath : filepath.split("/")[0];
|
|
||||||
|
|
||||||
if (isSelected(filepath)) {
|
for (const itemId of Object.keys(selectedItems)) {
|
||||||
const updatedDocs = isFolder
|
for (const folder of availableDocs.items) {
|
||||||
? selectedFiles.filter((doc) => !doc.includes(parent))
|
const foundItem = folder.items.find((file) => file.id === itemId);
|
||||||
: selectedFiles.filter((doc) => !doc.includes(filepath));
|
if (foundItem) {
|
||||||
setSelectFiles([...new Set(updatedDocs)]);
|
newMovedItems.push({ ...foundItem, folderName: folder.name });
|
||||||
} else {
|
break;
|
||||||
var newDocs = [];
|
}
|
||||||
var parentDirs = directories.items.find((item) => item.name === parent);
|
}
|
||||||
if (isFolder && parentDirs) {
|
|
||||||
const folderItems = parentDirs.items;
|
|
||||||
newDocs = folderItems.map((item) => parent + "/" + item.name);
|
|
||||||
} else {
|
|
||||||
newDocs = [filepath];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const combined = [...selectedFiles, ...newDocs];
|
let totalTokenCount = 0;
|
||||||
setSelectFiles([...new Set(combined)]);
|
newMovedItems.forEach((item) => {
|
||||||
|
const { cached, token_count_estimate } = item;
|
||||||
|
if (!cached) {
|
||||||
|
totalTokenCount += token_count_estimate;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
if (loading) {
|
const dollarAmount = (totalTokenCount / 1000) * COST_PER_TOKEN;
|
||||||
return (
|
setEmbeddingsCost(dollarAmount);
|
||||||
<>
|
setMovedItems([...movedItems, ...newMovedItems]);
|
||||||
<div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll">
|
|
||||||
<div className="flex flex-col gap-y-1 w-full">
|
let newAvailableDocs = JSON.parse(JSON.stringify(availableDocs));
|
||||||
<p className="text-slate-200 dark:text-stone-300 text-center">
|
let newWorkspaceDocs = JSON.parse(JSON.stringify(workspaceDocs));
|
||||||
loading workspace files
|
|
||||||
</p>
|
for (const itemId of Object.keys(selectedItems)) {
|
||||||
</div>
|
let foundItem = null;
|
||||||
</div>
|
let foundFolderIndex = null;
|
||||||
<div className="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"></div>
|
|
||||||
</>
|
newAvailableDocs.items = newAvailableDocs.items.map(
|
||||||
|
(folder, folderIndex) => {
|
||||||
|
const remainingItems = folder.items.filter((file) => {
|
||||||
|
const match = file.id === itemId;
|
||||||
|
if (match) {
|
||||||
|
foundItem = { ...file };
|
||||||
|
foundFolderIndex = folderIndex;
|
||||||
|
}
|
||||||
|
return !match;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...folder,
|
||||||
|
items: remainingItems,
|
||||||
|
};
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (foundItem) {
|
||||||
|
newWorkspaceDocs.items[foundFolderIndex].items.push(foundItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAvailableDocs(newAvailableDocs);
|
||||||
|
setWorkspaceDocs(newWorkspaceDocs);
|
||||||
|
setSelectedItems({});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex gap-x-6 justify-center">
|
||||||
{showConfirmation && (
|
|
||||||
<ConfirmationModal
|
|
||||||
directories={directories}
|
|
||||||
hideConfirm={() => setShowConfirmation(false)}
|
|
||||||
additions={docChanges().adds}
|
|
||||||
updateWorkspace={updateWorkspace}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll">
|
|
||||||
<div className="flex flex-col gap-y-1 w-full">
|
|
||||||
{!hasFiles && (
|
|
||||||
<div className="mb-4 w-full gap-x-2 rounded-lg h-10 border bg-orange-200 border-orange-800 dark:bg-orange-300 text-orange-800 flex items-center justify-center">
|
|
||||||
<AlertTriangle className="h-6 w-6" />
|
|
||||||
<p className="text-sm">
|
|
||||||
You don't have any files uploaded. Upload a file via the "Upload
|
|
||||||
Docs" tab.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col mb-2">
|
|
||||||
<p className="text-gray-800 dark:text-stone-200 text-base ">
|
|
||||||
Select folders to add or remove from workspace.
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-800 dark:text-stone-400 text-xs italic">
|
|
||||||
{selectedFiles.length} documents in workspace selected.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-auto border border-slate-200 dark:border-stone-600 rounded-lg px-4 py-2">
|
|
||||||
{!!directories && (
|
|
||||||
<Directory
|
<Directory
|
||||||
files={directories}
|
files={availableDocs}
|
||||||
toggleSelection={toggleSelection}
|
loading={loading}
|
||||||
isSelected={isSelected}
|
loadingMessage={loadingMessage}
|
||||||
|
setLoading={setLoading}
|
||||||
|
fileTypes={fileTypes}
|
||||||
|
workspace={workspace}
|
||||||
|
fetchKeys={fetchKeys}
|
||||||
|
selectedItems={selectedItems}
|
||||||
|
setSelectedItems={setSelectedItems}
|
||||||
|
updateWorkspace={updateWorkspace}
|
||||||
|
highlightWorkspace={highlightWorkspace}
|
||||||
|
setHighlightWorkspace={setHighlightWorkspace}
|
||||||
|
moveToWorkspace={moveSelectedItemsToWorkspace}
|
||||||
|
setLoadingMessage={setLoadingMessage}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`flex items-center ${
|
|
||||||
canDelete ? "justify-between" : "justify-end"
|
|
||||||
} p-4 md:p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
hidden={!canDelete}
|
|
||||||
onClick={deleteWorkspace}
|
|
||||||
type="button"
|
|
||||||
className="border border-transparent text-gray-500 bg-white hover:bg-red-100 rounded-lg whitespace-nowrap text-sm font-medium px-5 py-2.5 hover:text-red-900 focus:z-10 dark:bg-transparent dark:text-gray-300 dark:hover:text-white dark:hover:bg-red-600"
|
|
||||||
>
|
|
||||||
Delete Workspace
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<ArrowsDownUp className="text-white text-base font-bold rotate-90 w-11 h-11" />
|
||||||
disabled={saving}
|
|
||||||
onClick={confirmChanges}
|
|
||||||
type="submit"
|
|
||||||
className="text-slate-200 bg-black-900 px-4 py-2 rounded-lg hover:bg-gray-900 whitespace-nowrap text-sm"
|
|
||||||
>
|
|
||||||
{saving ? "Saving..." : "Confirm Changes"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<WorkspaceDirectory
|
||||||
|
workspace={workspace}
|
||||||
|
files={workspaceDocs}
|
||||||
|
highlightWorkspace={highlightWorkspace}
|
||||||
|
loading={loading}
|
||||||
|
loadingMessage={loadingMessage}
|
||||||
|
setLoadingMessage={setLoadingMessage}
|
||||||
|
setLoading={setLoading}
|
||||||
|
fetchKeys={fetchKeys}
|
||||||
|
hasChanges={hasChanges}
|
||||||
|
saveChanges={updateWorkspace}
|
||||||
|
embeddingCosts={embeddingsCost}
|
||||||
|
movedItems={movedItems}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,9 @@ import React, { useState, useRef, useEffect } from "react";
|
|||||||
import Workspace from "../../../../models/workspace";
|
import Workspace from "../../../../models/workspace";
|
||||||
import paths from "../../../../utils/paths";
|
import paths from "../../../../utils/paths";
|
||||||
import { chatPrompt } from "../../../../utils/chat";
|
import { chatPrompt } from "../../../../utils/chat";
|
||||||
|
import System from "../../../../models/system";
|
||||||
|
import PreLoader from "../../../Preloader";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
// Ensure that a type is correct before sending the body
|
// Ensure that a type is correct before sending the body
|
||||||
// to the backend.
|
// to the backend.
|
||||||
@ -20,11 +23,14 @@ function castToType(key, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function WorkspaceSettings({ workspace }) {
|
export default function WorkspaceSettings({ workspace }) {
|
||||||
|
const { slug } = useParams();
|
||||||
const formEl = useRef(null);
|
const formEl = useRef(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [success, setSuccess] = useState(null);
|
const [success, setSuccess] = useState(null);
|
||||||
|
const [totalVectors, setTotalVectors] = useState(null);
|
||||||
|
const [canDelete, setCanDelete] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function setTimer() {
|
function setTimer() {
|
||||||
@ -43,6 +49,17 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
setTimer();
|
setTimer();
|
||||||
}, [success, error]);
|
}, [success, error]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchKeys() {
|
||||||
|
const canDelete = await System.getCanDeleteWorkspaces();
|
||||||
|
setCanDelete(canDelete);
|
||||||
|
|
||||||
|
const totalVectors = await System.totalIndexes();
|
||||||
|
setTotalVectors(totalVectors);
|
||||||
|
}
|
||||||
|
fetchKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleUpdate = async (e) => {
|
const handleUpdate = async (e) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuccess(null);
|
setSuccess(null);
|
||||||
@ -61,6 +78,7 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
setError(message);
|
setError(message);
|
||||||
}
|
}
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
|
setHasChanges(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteWorkspace = async () => {
|
const deleteWorkspace = async () => {
|
||||||
@ -78,35 +96,51 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form ref={formEl} onSubmit={handleUpdate}>
|
<form ref={formEl} onSubmit={handleUpdate}>
|
||||||
<div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll">
|
<div className="-mt-12 px-12 pb-6 flex flex-col h-full w-full max-h-[80vh] overflow-y-scroll">
|
||||||
<div className="flex flex-col gap-y-1 w-full">
|
<div className="flex flex-col gap-y-1 min-w-[900px]">
|
||||||
<div className="flex flex-col mb-2">
|
<div className="text-white text-opacity-60 text-sm font-bold uppercase py-6 border-b-2 border-white/10">
|
||||||
<p className="text-gray-800 dark:text-stone-200 text-base ">
|
Workspace Settings
|
||||||
Edit your workspace's settings
|
</div>
|
||||||
|
<div className="flex flex-row w-full py-6 border-b-2 border-white/10">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<h3 className="text-white text-sm font-semibold">
|
||||||
|
Vector database identifier
|
||||||
|
</h3>
|
||||||
|
<p className="text-white text-opacity-60 text-sm font-medium">
|
||||||
|
{workspace?.slug}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-y-4">
|
<div className="w-1/2">
|
||||||
<div>
|
<h3 className="text-white text-sm font-semibold">
|
||||||
<input
|
Number of vectors
|
||||||
type="text"
|
</h3>
|
||||||
disabled={true}
|
<p className="text-white text-opacity-60 text-xs font-medium my-[2px]">
|
||||||
defaultValue={workspace?.slug}
|
Total number of vectors in your vector database.
|
||||||
className="bg-gray-50 border disabled:bg-gray-400 disabled:text-gray-700 disabled:border-gray-400 disabled:dark:bg-stone-800 disabled:dark:border-stone-900 disabled:dark:text-stone-600 disabled:cursor-not-allowed border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
</p>
|
||||||
required={true}
|
{totalVectors !== null ? (
|
||||||
autoComplete="off"
|
<p className="text-white text-opacity-60 text-sm font-medium">
|
||||||
/>
|
{totalVectors}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<PreLoader size="4" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-y-1 w-full mt-7">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex flex-col gap-y-4 w-1/2">
|
||||||
|
<div className="w-3/4 flex flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col gap-y-1 mb-4">
|
<div className="flex flex-col">
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
className="block text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
Workspace Name
|
Workspace Name
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-gray-600 dark:text-stone-400">
|
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||||
This will only change the display name of your workspace.
|
This will only change the display name of your workspace.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -116,7 +150,7 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
minLength={2}
|
minLength={2}
|
||||||
maxLength={80}
|
maxLength={80}
|
||||||
defaultValue={workspace?.name}
|
defaultValue={workspace?.name}
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder="My Workspace"
|
placeholder="My Workspace"
|
||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -125,21 +159,21 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col gap-y-1 mb-4">
|
<div className="flex flex-col">
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
className="block text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
LLM Temperature
|
LLM Temperature
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-gray-600 dark:text-stone-400">
|
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||||
This setting controls how "random" or dynamic your chat
|
This setting controls how "random" or dynamic your chat
|
||||||
responses will be.
|
responses will be.
|
||||||
<br />
|
<br />
|
||||||
The higher the number (2.0 maximum) the more random and
|
The higher the number (2.0 maximum) the more random and
|
||||||
incoherent.
|
incoherent.
|
||||||
<br />
|
<br />
|
||||||
Recommended: 0.7
|
<i>Recommended: 0.7</i>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@ -150,7 +184,7 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
onWheel={(e) => e.target.blur()}
|
onWheel={(e) => e.target.blur()}
|
||||||
defaultValue={workspace?.openAiTemp ?? 0.7}
|
defaultValue={workspace?.openAiTemp ?? 0.7}
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder="0.7"
|
placeholder="0.7"
|
||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -162,45 +196,16 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
<div className="flex flex-col gap-y-1 mb-4">
|
<div className="flex flex-col gap-y-1 mb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
>
|
|
||||||
Prompt
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-gray-600 dark:text-stone-400">
|
|
||||||
The prompt that will be used on this workspace. Define the
|
|
||||||
context and instructions for the AI to generate a response.
|
|
||||||
You should to provide a carefully crafted prompt so the AI can
|
|
||||||
generate a relevant and accurate response.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
name="openAiPrompt"
|
|
||||||
maxLength={500}
|
|
||||||
rows={5}
|
|
||||||
defaultValue={chatPrompt(workspace)}
|
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
||||||
placeholder="Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed."
|
|
||||||
required={true}
|
|
||||||
wrap="soft"
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={() => setHasChanges(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col gap-y-1 mb-4">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>
|
>
|
||||||
Chat History
|
Chat History
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-gray-600 dark:text-stone-400">
|
<p className="text-white text-opacity-60 text-xs font-medium">
|
||||||
The number of previous chats that will be included in the
|
The number of previous chats that will be included in the
|
||||||
response's short-term memory.
|
response's short-term memory.
|
||||||
<br />
|
<i>Recommend 20. </i>
|
||||||
Recommend 20. Anything more than 45 is likely to lead to
|
Anything more than 45 is likely to lead to continuous chat
|
||||||
continuous chat failures depending on message size.
|
failures depending on message size.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@ -211,7 +216,7 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
step={1}
|
step={1}
|
||||||
onWheel={(e) => e.target.blur()}
|
onWheel={(e) => e.target.blur()}
|
||||||
defaultValue={workspace?.openAiHistory ?? 20}
|
defaultValue={workspace?.openAiHistory ?? 20}
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder="20"
|
placeholder="20"
|
||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -219,31 +224,61 @@ export default function WorkspaceSettings({ workspace }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
<div className="w-1/2">
|
||||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
<div className="w-3/4">
|
||||||
Error: {error}
|
<div className="flex flex-col">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Prompt
|
||||||
|
</label>
|
||||||
|
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||||
|
The prompt that will be used on this workspace. Define the
|
||||||
|
context and instructions for the AI to generate a response.
|
||||||
|
You should to provide a carefully crafted prompt so the AI
|
||||||
|
can generate a relevant and accurate response.
|
||||||
</p>
|
</p>
|
||||||
)}
|
</div>
|
||||||
|
<textarea
|
||||||
|
name="openAiPrompt"
|
||||||
|
maxLength={500}
|
||||||
|
rows={5}
|
||||||
|
defaultValue={chatPrompt(workspace)}
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
|
placeholder="Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed."
|
||||||
|
required={true}
|
||||||
|
wrap="soft"
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={() => setHasChanges(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||||
{success && (
|
{success && (
|
||||||
<p className="text-green-600 dark:text-green-400 text-sm">
|
<p className="text-green-400 text-sm">Success: {success}</p>
|
||||||
Success: {success}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-2 md:p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-2 md:p-6 space-x-2 border-t rounded-b border-gray-600">
|
||||||
|
{canDelete && (
|
||||||
<button
|
<button
|
||||||
onClick={deleteWorkspace}
|
onClick={deleteWorkspace}
|
||||||
type="button"
|
type="button"
|
||||||
className="border border-transparent text-gray-500 bg-white hover:bg-red-100 rounded-lg whitespace-nowrap text-sm font-medium px-5 py-2.5 hover:text-red-900 focus:z-10 dark:bg-transparent dark:text-gray-300 dark:hover:text-white dark:hover:bg-red-600"
|
className="transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-transparent text-white hover:text-white hover:bg-red-600"
|
||||||
>
|
>
|
||||||
Delete Workspace
|
Delete Workspace
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 whitespace-nowrap text-sm font-medium px-2 md:px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
className="transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
{saving ? "Updating..." : "Update workspace"}
|
{saving ? "Updating..." : "Update workspace"}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,202 +0,0 @@
|
|||||||
import React, { useState, useCallback, useEffect } from "react";
|
|
||||||
import Workspace from "../../../../models/workspace";
|
|
||||||
import paths from "../../../../utils/paths";
|
|
||||||
import FileUploadProgress from "./FileUploadProgress";
|
|
||||||
import { useDropzone } from "react-dropzone";
|
|
||||||
import { v4 } from "uuid";
|
|
||||||
import System from "../../../../models/system";
|
|
||||||
import { Frown } from "react-feather";
|
|
||||||
import showToast from "../../../../utils/toast";
|
|
||||||
|
|
||||||
export default function UploadToWorkspace({ workspace, fileTypes }) {
|
|
||||||
const [ready, setReady] = useState(null);
|
|
||||||
const [files, setFiles] = useState([]);
|
|
||||||
|
|
||||||
const handleUploadSuccess = () => {
|
|
||||||
showToast("File uploaded successfully", "success");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUploadError = (message) => {
|
|
||||||
showToast(`Error uploading file: ${message}`, "error");
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDrop = useCallback(async (acceptedFiles, rejections) => {
|
|
||||||
const newAccepted = acceptedFiles.map((file) => {
|
|
||||||
return {
|
|
||||||
uid: v4(),
|
|
||||||
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.checkDocumentProcessorOnline();
|
|
||||||
setReady(online);
|
|
||||||
}
|
|
||||||
checkProcessorOnline();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
|
||||||
onDrop,
|
|
||||||
accept: {
|
|
||||||
...fileTypes,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteWorkspace = async () => {
|
|
||||||
if (
|
|
||||||
!window.confirm(
|
|
||||||
`You are about to delete your entire ${workspace.name} workspace. This will remove all vector embeddings on your vector database.\n\nThe original source files will remain untouched. This action is irreversible.`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
await Workspace.delete(workspace.slug);
|
|
||||||
workspace.slug === slug
|
|
||||||
? (window.location = paths.home())
|
|
||||||
: window.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ready === null) {
|
|
||||||
return (
|
|
||||||
<ModalWrapper deleteWorkspace={deleteWorkspace}>
|
|
||||||
<div className="outline-none transition-all cursor-wait duration-300 bg-stone-400 bg-opacity-20 flex h-[20rem] overflow-y-scroll overflow-x-hidden rounded-lg">
|
|
||||||
<div className="flex flex-col gap-y-1 w-full h-full items-center justify-center">
|
|
||||||
<p className="text-slate-400 text-xs">
|
|
||||||
Checking document processor is online - please wait.
|
|
||||||
</p>
|
|
||||||
<p className="text-slate-400 text-xs">
|
|
||||||
this should only take a few moments.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ready === false) {
|
|
||||||
return (
|
|
||||||
<ModalWrapper deleteWorkspace={deleteWorkspace}>
|
|
||||||
<div className="outline-none transition-all duration-300 bg-red-200 flex h-[20rem] overflow-y-scroll overflow-x-hidden rounded-lg">
|
|
||||||
<div className="flex flex-col gap-y-1 w-full h-full items-center justify-center md:px-0 px-2">
|
|
||||||
<Frown className="w-8 h-8 text-red-800" />
|
|
||||||
<p className="text-red-800 text-xs text-center">
|
|
||||||
Document processor is offline.
|
|
||||||
</p>
|
|
||||||
<p className="text-red-800 text-[10px] md:text-xs text-center">
|
|
||||||
you cannot upload documents from the UI right now
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalWrapper deleteWorkspace={deleteWorkspace}>
|
|
||||||
<div
|
|
||||||
{...getRootProps()}
|
|
||||||
className="outline-none transition-all cursor-pointer duration-300 hover:bg-opacity-40 bg-stone-400 bg-opacity-20 flex h-[20rem] overflow-y-scroll overflow-x-hidden rounded-lg"
|
|
||||||
>
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
{files.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full">
|
|
||||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="w-10 h-10 mb-3 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 flex-col w-full p-4 gap-y-2">
|
|
||||||
{files.map((file) => (
|
|
||||||
<FileUploadProgress
|
|
||||||
key={file.uid}
|
|
||||||
file={file.file}
|
|
||||||
slug={workspace.slug}
|
|
||||||
rejected={file?.rejected}
|
|
||||||
reason={file?.reason}
|
|
||||||
onUploadSuccess={handleUploadSuccess}
|
|
||||||
onUploadError={handleUploadError}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 dark:text-stone-400 text-xs ">
|
|
||||||
supported file extensions are{" "}
|
|
||||||
<code className="text-xs bg-gray-200 text-gray-800 dark:bg-stone-800 dark:text-slate-400 font-mono rounded-sm px-1">
|
|
||||||
{Object.values(fileTypes).flat().join(" ")}
|
|
||||||
</code>
|
|
||||||
</p>
|
|
||||||
</ModalWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModalWrapper({ deleteWorkspace, children }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll">
|
|
||||||
<div className="flex flex-col gap-y-1 w-full">
|
|
||||||
<div className="flex flex-col mb-2">
|
|
||||||
<p className="text-gray-800 dark:text-stone-200 text-base ">
|
|
||||||
Add documents to your workspace.
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-600 dark:text-stone-400 text-xs ">
|
|
||||||
These files will be uploaded to the document processor running on
|
|
||||||
this AnythingLLM instance. These files are not sent or shared with
|
|
||||||
a third party.
|
|
||||||
</p>
|
|
||||||
{process.env.NODE_ENV !== "production" && (
|
|
||||||
<div className="mt-2 text-gray-600 dark:text-stone-400 text-xs">
|
|
||||||
<div className="w-[1px] bg-stone-400 w-full" />
|
|
||||||
Local Environment Notice: You must have the{" "}
|
|
||||||
<code className="text-xs bg-gray-200 text-gray-800 dark:bg-stone-800 dark:text-slate-400 font-mono rounded-sm px-1">
|
|
||||||
python document processor app
|
|
||||||
</code>{" "}
|
|
||||||
running for these documents to process.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
|
||||||
<button
|
|
||||||
onClick={deleteWorkspace}
|
|
||||||
type="button"
|
|
||||||
className="border border-transparent text-gray-500 bg-white hover:bg-red-100 rounded-lg text-sm font-medium px-5 py-2.5 hover:text-red-900 focus:z-10 dark:bg-transparent dark:text-gray-300 dark:hover:text-white dark:hover:bg-red-600"
|
|
||||||
>
|
|
||||||
Delete Workspace
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,23 +1,15 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, lazy, Suspense, memo } from "react";
|
||||||
import { Archive, Sliders, UploadCloud, X } from "react-feather";
|
import { X } from "react-feather";
|
||||||
import DocumentSettings from "./Documents";
|
|
||||||
import WorkspaceSettings from "./Settings";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Workspace from "../../../models/workspace";
|
import Workspace from "../../../models/workspace";
|
||||||
import System from "../../../models/system";
|
import System from "../../../models/system";
|
||||||
import UploadToWorkspace from "./Upload";
|
import { isMobile } from "react-device-detect";
|
||||||
|
|
||||||
const TABS = {
|
const DocumentSettings = lazy(() => import("./Documents"));
|
||||||
documents: DocumentSettings,
|
const WorkspaceSettings = lazy(() => import("./Settings"));
|
||||||
settings: WorkspaceSettings,
|
|
||||||
upload: UploadToWorkspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
const noop = () => false;
|
const noop = () => {};
|
||||||
export default function ManageWorkspace({
|
const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
||||||
hideModal = noop,
|
|
||||||
providedSlug = null,
|
|
||||||
}) {
|
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
const [selectedTab, setSelectedTab] = useState("documents");
|
const [selectedTab, setSelectedTab] = useState("documents");
|
||||||
const [workspace, setWorkspace] = useState(null);
|
const [workspace, setWorkspace] = useState(null);
|
||||||
@ -37,110 +29,99 @@ export default function ManageWorkspace({
|
|||||||
setWorkspace(workspace);
|
setWorkspace(workspace);
|
||||||
}
|
}
|
||||||
fetchWorkspace();
|
fetchWorkspace();
|
||||||
}, [selectedTab, slug]);
|
}, [providedSlug, slug]);
|
||||||
|
|
||||||
if (!workspace) return null;
|
if (!workspace) return null;
|
||||||
|
|
||||||
const Component = TABS[selectedTab || "documents"];
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center">
|
<div className="w-screen h-screen fixed top-0 left-0 flex justify-center items-center z-99">
|
||||||
<div
|
<div className="backdrop h-full w-full absolute top-0 z-10" />
|
||||||
className="flex fixed top-0 left-0 right-0 w-full h-full"
|
<div className={`absolute max-h-full transition duration-300 z-20`}>
|
||||||
onClick={hideModal}
|
<div className="relative max-w-lg mx-auto bg-main-gradient rounded-[12px] shadow border-2 border-slate-300/10">
|
||||||
/>
|
<div className="p-6">
|
||||||
<div className="relative w-full max-w-2xl max-h-full">
|
<h1 className="text-white text-lg font-semibold">
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
Editing "{workspace.name}"
|
||||||
<div className="flex flex-col gap-y-1 border-b dark:border-gray-600 px-4 pt-4 ">
|
</h1>
|
||||||
<div className="flex items-start justify-between rounded-t ">
|
<p className="text-white mt-4">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
Editing these settings are only available on a desktop device.
|
||||||
Update "{workspace.name}"
|
Please access this page on your desktop to continue.
|
||||||
</h3>
|
</p>
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
data-modal-hide="staticModal"
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen fixed top-0 left-0 flex justify-center items-center z-99">
|
||||||
|
<div className="backdrop h-full w-full absolute top-0 z-10" />
|
||||||
|
<div className={`absolute max-h-full w-3/4 transition duration-300 z-20`}>
|
||||||
|
<div className="relative bg-main-gradient rounded-[12px] shadow border-2 border-slate-300/10">
|
||||||
|
<div className="absolute top-[-18px] left-1/2 transform -translate-x-1/2 bg-sidebar-button p-1 rounded-xl shadow border-2 border-slate-300/10">
|
||||||
|
<div className="flex gap-x-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedTab("documents")}
|
||||||
|
className={`px-4 py-2 rounded-[8px] font-semibold text-white hover:bg-switch-selected hover:bg-opacity-60 ${
|
||||||
|
selectedTab === "documents"
|
||||||
|
? "bg-switch-selected shadow-md"
|
||||||
|
: "bg-sidebar-button"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Documents
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedTab("settings")}
|
||||||
|
className={`px-4 py-2 rounded-[8px] font-semibold text-white hover:bg-switch-selected hover:bg-opacity-60 ${
|
||||||
|
selectedTab === "settings"
|
||||||
|
? "bg-switch-selected shadow-md"
|
||||||
|
: "bg-sidebar-button"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start justify-between p-2 rounded-t border-gray-500/50">
|
||||||
|
<button
|
||||||
|
onClick={hideModal}
|
||||||
|
type="button"
|
||||||
|
className="transition-all duration-300 text-gray-400 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:border-white/60 bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<WorkspaceSettingTabs
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
selectedTab={selectedTab}
|
<div className={selectedTab === "documents" ? "" : "hidden"}>
|
||||||
changeTab={setSelectedTab}
|
<DocumentSettings workspace={workspace} fileTypes={fileTypes} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Component
|
<div className={selectedTab === "settings" ? "" : "hidden"}>
|
||||||
hideModal={hideModal}
|
<WorkspaceSettings workspace={workspace} fileTypes={fileTypes} />
|
||||||
workspace={workspace}
|
</div>
|
||||||
fileTypes={fileTypes}
|
</Suspense>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function WorkspaceSettingTabs({ selectedTab, changeTab }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ul className="flex md:flex-wrap overflow-x-scroll no-scroll -mb-px text-sm gap-x-2 font-medium text-center text-gray-500 dark:text-gray-400">
|
|
||||||
<WorkspaceTab
|
|
||||||
active={selectedTab === "documents"}
|
|
||||||
displayName="Documents"
|
|
||||||
tabName="documents"
|
|
||||||
icon={<Archive className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
onClick={changeTab}
|
|
||||||
/>
|
|
||||||
<WorkspaceTab
|
|
||||||
active={selectedTab === "upload"}
|
|
||||||
displayName="Upload Docs"
|
|
||||||
tabName="upload"
|
|
||||||
icon={<UploadCloud className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
onClick={changeTab}
|
|
||||||
/>
|
|
||||||
<WorkspaceTab
|
|
||||||
active={selectedTab === "settings"}
|
|
||||||
displayName="Settings"
|
|
||||||
tabName="settings"
|
|
||||||
icon={<Sliders className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
onClick={changeTab}
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkspaceTab({
|
|
||||||
active = false,
|
|
||||||
displayName,
|
|
||||||
tabName,
|
|
||||||
icon = "",
|
|
||||||
onClick,
|
|
||||||
}) {
|
|
||||||
const classes = active
|
|
||||||
? "text-blue-600 border-blue-600 active dark:text-blue-400 dark:border-blue-400 bg-blue-500 bg-opacity-5"
|
|
||||||
: "border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300";
|
|
||||||
return (
|
|
||||||
<li className="mr-2">
|
|
||||||
<button
|
|
||||||
disabled={active}
|
|
||||||
onClick={() => onClick(tabName)}
|
|
||||||
className={
|
|
||||||
"flex items-center gap-x-1 p-4 border-b-2 rounded-t-lg group whitespace-nowrap " +
|
|
||||||
classes
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon} {displayName}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export default memo(ManageWorkspace);
|
||||||
export function useManageWorkspaceModal() {
|
export function useManageWorkspaceModal() {
|
||||||
const [showing, setShowing] = useState(false);
|
const [showing, setShowing] = useState(false);
|
||||||
const showModal = () => {
|
const showModal = () => {
|
||||||
setShowing(true);
|
setShowing(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
setShowing(false);
|
setShowing(false);
|
||||||
};
|
};
|
||||||
|
@ -23,17 +23,16 @@ export default function NewWorkspaceModal({ hideModal = noop }) {
|
|||||||
className="flex fixed top-0 left-0 right-0 w-full h-full"
|
className="flex fixed top-0 left-0 right-0 w-full h-full"
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
/>
|
/>
|
||||||
<div className="relative w-full max-w-2xl max-h-full">
|
<div className="relative w-[500px] max-h-full">
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="relative bg-modal-gradient rounded-lg shadow-md border-2 border-accent">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-white/10">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
Create a New Workspace
|
New Workspace
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
data-modal-hide="staticModal"
|
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
</button>
|
</button>
|
||||||
@ -52,7 +51,7 @@ export default function NewWorkspaceModal({ hideModal = noop }) {
|
|||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 w-full text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
placeholder="My Workspace"
|
placeholder="My Workspace"
|
||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -63,25 +62,14 @@ export default function NewWorkspaceModal({ hideModal = noop }) {
|
|||||||
Error: {error}
|
Error: {error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-800 dark:text-slate-200 text-xs md:text-sm">
|
|
||||||
After creating a workspace you will be able to add and remove
|
|
||||||
documents from it.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-white/10 rounded-b">
|
||||||
<button
|
|
||||||
onClick={hideModal}
|
|
||||||
type="button"
|
|
||||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
Create Workspace
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import System from "../../../models/system";
|
import System from "../../../models/system";
|
||||||
import { AUTH_TOKEN, AUTH_USER } from "../../../utils/constants";
|
import { AUTH_TOKEN, AUTH_USER } from "../../../utils/constants";
|
||||||
import useLogo from "../../../hooks/useLogo";
|
import useLogo from "../../../hooks/useLogo";
|
||||||
|
import paths from "../../../utils/paths";
|
||||||
|
|
||||||
export default function MultiUserAuth() {
|
export default function MultiUserAuth() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -19,7 +20,7 @@ export default function MultiUserAuth() {
|
|||||||
if (valid && !!token && !!user) {
|
if (valid && !!token && !!user) {
|
||||||
window.localStorage.setItem(AUTH_USER, JSON.stringify(user));
|
window.localStorage.setItem(AUTH_USER, JSON.stringify(user));
|
||||||
window.localStorage.setItem(AUTH_TOKEN, token);
|
window.localStorage.setItem(AUTH_TOKEN, token);
|
||||||
window.location.reload();
|
window.location = paths.home();
|
||||||
} else {
|
} else {
|
||||||
setError(message);
|
setError(message);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -29,66 +30,52 @@ export default function MultiUserAuth() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleLogin}>
|
<form onSubmit={handleLogin}>
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="flex flex-col justify-center items-center relative rounded-2xl shadow border-2 border-slate-300 border-opacity-20 w-[400px] login-input-gradient">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between pt-11 pb-9 rounded-t">
|
||||||
<div className="flex items-center flex-col">
|
<div className="flex items-center flex-col">
|
||||||
<img src={_initLogo} alt="Logo" className="w-1/2" />
|
<h3 className="text-md md:text-2xl font-bold text-gray-900 dark:text-white text-center">
|
||||||
<h3 className="text-md md:text-xl font-semibold text-gray-900 dark:text-white">
|
Sign In
|
||||||
This instance is password protected.
|
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-6 flex h-full w-full">
|
<div className="px-12 space-y-6 flex h-full w-full">
|
||||||
<div className="w-full flex flex-col gap-y-4">
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label
|
|
||||||
htmlFor="username"
|
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>
|
|
||||||
Instance Username
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
placeholder="Username"
|
||||||
|
className="bg-opacity-40 border-gray-300 text-sm rounded-lg block w-full p-2.5 bg-[#222628] placeholder-[#FFFFFF99] text-white focus:ring-blue-500 focus:border-blue-500"
|
||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>
|
|
||||||
Instance Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
placeholder="Password"
|
||||||
|
className="bg-opacity-40 border-gray-300 text-sm rounded-lg block w-full p-2.5 bg-[#222628] placeholder-[#FFFFFF99] text-white focus:ring-blue-500 focus:border-blue-500"
|
||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-800 dark:text-slate-200 md:text-sm text-xs">
|
|
||||||
You will only have to enter this password once. After successful
|
|
||||||
login it will be stored in your browser.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex items-center p-12 space-x-2 border-gray-200 rounded-b dark:border-gray-600 w-full">
|
||||||
<button
|
<button
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-white text-sm font-bold px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-white dark:text-neutral-700 dark:border-white dark:hover:text-white dark:hover:bg-slate-600 dark:focus:ring-gray-600 w-full"
|
||||||
>
|
>
|
||||||
{loading ? "Validating..." : "Submit"}
|
{loading ? "Validating..." : "Login"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import System from "../../../models/system";
|
import System from "../../../models/system";
|
||||||
import { AUTH_TOKEN } from "../../../utils/constants";
|
import { AUTH_TOKEN } from "../../../utils/constants";
|
||||||
import useLogo from "../../../hooks/useLogo";
|
import useLogo from "../../../hooks/useLogo";
|
||||||
|
import paths from "../../../utils/paths";
|
||||||
|
|
||||||
export default function SingleUserAuth() {
|
export default function SingleUserAuth() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -18,7 +19,7 @@ export default function SingleUserAuth() {
|
|||||||
const { valid, token, message } = await System.requestToken(data);
|
const { valid, token, message } = await System.requestToken(data);
|
||||||
if (valid && !!token) {
|
if (valid && !!token) {
|
||||||
window.localStorage.setItem(AUTH_TOKEN, token);
|
window.localStorage.setItem(AUTH_TOKEN, token);
|
||||||
window.location.reload();
|
window.location = paths.home();
|
||||||
} else {
|
} else {
|
||||||
setError(message);
|
setError(message);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -28,29 +29,22 @@ export default function SingleUserAuth() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleLogin}>
|
<form onSubmit={handleLogin}>
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="flex flex-col justify-center items-center relative bg-white rounded-2xl shadow dark:bg-stone-700 border-2 border-slate-300 border-opacity-20 w-[400px] login-input-gradient">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between pt-11 pb-9 rounded-t dark:border-gray-600">
|
||||||
<div className="flex items-center flex-col">
|
<div className="flex items-center flex-col">
|
||||||
<img src={_initLogo} alt="Logo" className="w-1/2" />
|
<h3 className="text-md md:text-2xl font-bold text-gray-900 dark:text-white text-center">
|
||||||
<h3 className="text-md md:text-xl font-semibold text-gray-900 dark:text-white">
|
Sign In
|
||||||
This instance is password protected.
|
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-6 flex h-full w-full">
|
<div className="px-12 space-y-6 flex h-full w-full">
|
||||||
<div className="w-full flex flex-col gap-y-4">
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>
|
|
||||||
Workspace Password
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
placeholder="Password"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-neutral-800 bg-opacity-40 border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-[#222628] dark:bg-opacity-40 dark:placeholder-[#FFFFFF99] dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
required={true}
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
@ -60,19 +54,15 @@ export default function SingleUserAuth() {
|
|||||||
Error: {error}
|
Error: {error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-800 dark:text-slate-200 md:text-sm text-xs">
|
|
||||||
You will only have to enter this password once. After successful
|
|
||||||
login it will be stored in your browser.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex items-center p-12 space-x-2 border-gray-200 rounded-b dark:border-gray-600 w-full">
|
||||||
<button
|
<button
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-white text-sm font-bold px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-white dark:text-neutral-700 dark:border-white dark:hover:text-white dark:hover:bg-slate-600 dark:focus:ring-gray-600 w-full"
|
||||||
>
|
>
|
||||||
{loading ? "Validating..." : "Submit"}
|
{loading ? "Validating..." : "Login"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,16 +3,31 @@ import System from "../../../models/system";
|
|||||||
import SingleUserAuth from "./SingleUserAuth";
|
import SingleUserAuth from "./SingleUserAuth";
|
||||||
import MultiUserAuth from "./MultiUserAuth";
|
import MultiUserAuth from "./MultiUserAuth";
|
||||||
import {
|
import {
|
||||||
AUTH_TIMESTAMP,
|
|
||||||
AUTH_TOKEN,
|
AUTH_TOKEN,
|
||||||
AUTH_USER,
|
AUTH_USER,
|
||||||
|
AUTH_TIMESTAMP,
|
||||||
} from "../../../utils/constants";
|
} from "../../../utils/constants";
|
||||||
|
import useLogo from "../../../hooks/useLogo";
|
||||||
|
|
||||||
export default function PasswordModal({ mode = "single" }) {
|
export default function PasswordModal({ mode = "single" }) {
|
||||||
|
const { logo: _initLogo } = useLogo();
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-gray-600 dark:bg-stone-800 flex items-center justify-center">
|
<div className="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-zinc-800 flex items-center justify-center">
|
||||||
<div className="flex fixed top-0 left-0 right-0 w-full h-full" />
|
<div
|
||||||
<div className="relative w-full max-w-2xl max-h-full">
|
className="fixed top-0 left-0 right-0 bottom-0 z-40 animate-slow-pulse"
|
||||||
|
style={{
|
||||||
|
background: `
|
||||||
|
radial-gradient(circle at center, transparent 40%, black 100%),
|
||||||
|
linear-gradient(180deg, #FF8585 0%, #D4A447 100%)
|
||||||
|
`,
|
||||||
|
width: "575px",
|
||||||
|
filter: "blur(200px)",
|
||||||
|
margin: "auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center h-full w-full z-50">
|
||||||
|
<img src={_initLogo} className="mb-20 w-80 opacity-80" alt="logo" />
|
||||||
{mode === "single" ? <SingleUserAuth /> : <MultiUserAuth />}
|
{mode === "single" ? <SingleUserAuth /> : <MultiUserAuth />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
export default function PreLoader() {
|
export default function PreLoader({ size = "16" }) {
|
||||||
return (
|
return (
|
||||||
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent"></div>
|
<div
|
||||||
|
className={`h-${size} w-${size} animate-spin rounded-full border-4 border-solid border-primary border-t-transparent`}
|
||||||
|
></div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,20 +6,54 @@ import paths from "../../utils/paths";
|
|||||||
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "../../utils/constants";
|
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "../../utils/constants";
|
||||||
import { userFromStorage } from "../../utils/request";
|
import { userFromStorage } from "../../utils/request";
|
||||||
import System from "../../models/system";
|
import System from "../../models/system";
|
||||||
|
import UserMenu from "../UserMenu";
|
||||||
|
|
||||||
// Used only for Multi-user mode only as we permission specific pages based on auth role.
|
// Used only for Multi-user mode only as we permission specific pages based on auth role.
|
||||||
// When in single user mode we just bypass any authchecks.
|
// When in single user mode we just bypass any authchecks.
|
||||||
function useIsAuthenticated() {
|
function useIsAuthenticated() {
|
||||||
const [isAuthd, setIsAuthed] = useState(null);
|
const [isAuthd, setIsAuthed] = useState(null);
|
||||||
|
const [shouldRedirectToOnboarding, setShouldRedirectToOnboarding] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validateSession = async () => {
|
const validateSession = async () => {
|
||||||
const multiUserMode = (await System.keys()).MultiUserMode;
|
const {
|
||||||
if (!multiUserMode) {
|
MultiUserMode,
|
||||||
|
RequiresAuth,
|
||||||
|
OpenAiKey = false,
|
||||||
|
AzureOpenAiKey = false,
|
||||||
|
} = await System.keys();
|
||||||
|
|
||||||
|
// Check for the onboarding redirect condition
|
||||||
|
if (
|
||||||
|
!MultiUserMode &&
|
||||||
|
!RequiresAuth && // Not in Multi-user AND no password set.
|
||||||
|
!OpenAiKey &&
|
||||||
|
!AzureOpenAiKey // AND no LLM API Key set at all.
|
||||||
|
) {
|
||||||
|
setShouldRedirectToOnboarding(true);
|
||||||
setIsAuthed(true);
|
setIsAuthed(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!MultiUserMode && !RequiresAuth) {
|
||||||
|
setIsAuthed(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single User password mode check
|
||||||
|
if (!MultiUserMode && RequiresAuth) {
|
||||||
|
const localAuthToken = localStorage.getItem(AUTH_TOKEN);
|
||||||
|
if (!localAuthToken) {
|
||||||
|
setIsAuthed(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await validateSessionTokenForUser();
|
||||||
|
setIsAuthed(isValid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const localUser = localStorage.getItem(AUTH_USER);
|
const localUser = localStorage.getItem(AUTH_USER);
|
||||||
const localAuthToken = localStorage.getItem(AUTH_TOKEN);
|
const localAuthToken = localStorage.getItem(AUTH_TOKEN);
|
||||||
if (!localUser || !localAuthToken) {
|
if (!localUser || !localAuthToken) {
|
||||||
@ -41,24 +75,40 @@ function useIsAuthenticated() {
|
|||||||
validateSession();
|
validateSession();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return isAuthd;
|
return { isAuthd, shouldRedirectToOnboarding };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminRoute({ Component }) {
|
export function AdminRoute({ Component }) {
|
||||||
const authed = useIsAuthenticated();
|
const { isAuthd, shouldRedirectToOnboarding } = useIsAuthenticated();
|
||||||
if (authed === null) return <FullScreenLoader />;
|
if (isAuthd === null) return <FullScreenLoader />;
|
||||||
|
|
||||||
|
if (shouldRedirectToOnboarding) {
|
||||||
|
return <Navigate to={paths.onboarding()} />;
|
||||||
|
}
|
||||||
|
|
||||||
const user = userFromStorage();
|
const user = userFromStorage();
|
||||||
return authed && user?.role === "admin" ? (
|
return isAuthd && user?.role === "admin" ? (
|
||||||
|
<UserMenu>
|
||||||
<Component />
|
<Component />
|
||||||
|
</UserMenu>
|
||||||
) : (
|
) : (
|
||||||
<Navigate to={paths.home()} />
|
<Navigate to={paths.home()} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PrivateRoute({ Component }) {
|
export default function PrivateRoute({ Component }) {
|
||||||
const authed = useIsAuthenticated();
|
const { isAuthd, shouldRedirectToOnboarding } = useIsAuthenticated();
|
||||||
if (authed === null) return <FullScreenLoader />;
|
if (isAuthd === null) return <FullScreenLoader />;
|
||||||
|
|
||||||
return authed ? <Component /> : <Navigate to={paths.home()} />;
|
if (shouldRedirectToOnboarding) {
|
||||||
|
return <Navigate to="/onboarding" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAuthd ? (
|
||||||
|
<UserMenu>
|
||||||
|
<Component />
|
||||||
|
</UserMenu>
|
||||||
|
) : (
|
||||||
|
<Navigate to={paths.login()} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
394
frontend/src/components/SettingsSidebar/index.jsx
Normal file
394
frontend/src/components/SettingsSidebar/index.jsx
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
// import IndexCount from "../Sidebar/IndexCount";
|
||||||
|
// import LLMStatus from "../Sidebar/LLMStatus";
|
||||||
|
import paths from "../../utils/paths";
|
||||||
|
import useLogo from "../../hooks/useLogo";
|
||||||
|
import {
|
||||||
|
DiscordLogo,
|
||||||
|
EnvelopeSimple,
|
||||||
|
SquaresFour,
|
||||||
|
Users,
|
||||||
|
BookOpen,
|
||||||
|
ChatCenteredText,
|
||||||
|
Eye,
|
||||||
|
Key,
|
||||||
|
ChatText,
|
||||||
|
Database,
|
||||||
|
DownloadSimple,
|
||||||
|
Lock,
|
||||||
|
GithubLogo,
|
||||||
|
DotsThree,
|
||||||
|
House,
|
||||||
|
X,
|
||||||
|
List,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
import useUser from "../../hooks/useUser";
|
||||||
|
import { USER_BACKGROUND_COLOR } from "../../utils/constants";
|
||||||
|
|
||||||
|
export default function SettingsSidebar() {
|
||||||
|
const { logo } = useLogo();
|
||||||
|
const sidebarRef = useRef(null);
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={sidebarRef}
|
||||||
|
style={{ height: "calc(100% - 32px)" }}
|
||||||
|
className="transition-all duration-500 relative m-[16px] rounded-[26px] bg-sidebar border-4 border-accent min-w-[250px] p-[18px]"
|
||||||
|
>
|
||||||
|
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||||
|
{/* Header Information */}
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex shrink-0 max-w-[65%] items-center justify-start ml-2">
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt="Logo"
|
||||||
|
className="rounded max-h-[40px]"
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-x-2 items-center text-slate-500">
|
||||||
|
<a
|
||||||
|
href={paths.home()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-60 text-sm font-medium uppercase mt-4 mb-0 ml-2">
|
||||||
|
Settings
|
||||||
|
</div>
|
||||||
|
{/* Primary Body */}
|
||||||
|
<div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
|
||||||
|
<div className="h-auto sidebar-items">
|
||||||
|
<div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll">
|
||||||
|
{/* Admin Settings */}
|
||||||
|
{user?.role === "admin" && (
|
||||||
|
<>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.system()}
|
||||||
|
btnText="System Preferences"
|
||||||
|
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.invites()}
|
||||||
|
btnText="Invitation"
|
||||||
|
icon={
|
||||||
|
<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.users()}
|
||||||
|
btnText="Users"
|
||||||
|
icon={<Users className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.workspaces()}
|
||||||
|
btnText="Workspaces"
|
||||||
|
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.chats()}
|
||||||
|
btnText="Workspace Chat"
|
||||||
|
icon={
|
||||||
|
<ChatCenteredText className="h-5 w-5 flex-shrink-0" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* General Settings */}
|
||||||
|
<Option
|
||||||
|
href={paths.general.appearance()}
|
||||||
|
btnText="Appearance"
|
||||||
|
icon={<Eye className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.apiKeys()}
|
||||||
|
btnText="API Keys"
|
||||||
|
icon={<Key className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.llmPreference()}
|
||||||
|
btnText="LLM Preference"
|
||||||
|
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.vectorDatabase()}
|
||||||
|
btnText="Vector Database"
|
||||||
|
icon={<Database className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.exportImport()}
|
||||||
|
btnText="Export or Import"
|
||||||
|
icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.security()}
|
||||||
|
btnText="Security"
|
||||||
|
icon={<Lock className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{/* <div className="flex flex-col gap-y-2">
|
||||||
|
<div className="w-full flex items-center justify-between">
|
||||||
|
<LLMStatus />
|
||||||
|
<IndexCount />
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex justify-center mt-2">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<a
|
||||||
|
href={paths.github()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<GithubLogo weight="fill" className="h-5 w-5 " />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={paths.docs()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<BookOpen weight="fill" className="h-5 w-5 " />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={paths.discord()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<DiscordLogo
|
||||||
|
weight="fill"
|
||||||
|
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<button className="invisible transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border">
|
||||||
|
<DotsThree className="h-5 w-5 group-hover:stroke-slate-200" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidebarMobileHeader() {
|
||||||
|
const { logo } = useLogo();
|
||||||
|
const { user } = useUser();
|
||||||
|
const sidebarRef = useRef(null);
|
||||||
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
|
const [showBgOverlay, setShowBgOverlay] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleBg() {
|
||||||
|
if (showSidebar) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowBgOverlay(true);
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
setShowBgOverlay(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleBg();
|
||||||
|
}, [showSidebar]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="fixed top-0 left-0 right-0 z-10 flex justify-between items-center px-4 py-2 bg-sidebar text-slate-200 shadow-lg h-16">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSidebar(true)}
|
||||||
|
className="rounded-md p-2 flex items-center justify-center text-slate-200"
|
||||||
|
>
|
||||||
|
<List className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center justify-center flex-grow">
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt="Logo"
|
||||||
|
className="block mx-auto h-6 w-auto"
|
||||||
|
style={{ maxHeight: "40px", objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-12"></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
transform: showSidebar ? `translateX(0vw)` : `translateX(-100vw)`,
|
||||||
|
}}
|
||||||
|
className={`z-99 fixed top-0 left-0 transition-all duration-500 w-[100vw] h-[100vh]`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
showBgOverlay
|
||||||
|
? "transition-all opacity-1"
|
||||||
|
: "transition-none opacity-0"
|
||||||
|
} duration-500 fixed top-0 left-0 ${USER_BACKGROUND_COLOR} bg-opacity-75 w-screen h-screen`}
|
||||||
|
onClick={() => setShowSidebar(false)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={sidebarRef}
|
||||||
|
className="h-[100vh] fixed top-0 left-0 rounded-r-[26px] bg-sidebar w-[80%] p-[18px] "
|
||||||
|
>
|
||||||
|
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||||
|
{/* Header Information */}
|
||||||
|
<div className="flex w-full items-center justify-between gap-x-4">
|
||||||
|
<div className="flex shrink-1 w-fit items-center justify-start">
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt="Logo"
|
||||||
|
className="rounded w-full max-h-[40px]"
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-x-2 items-center text-slate-500 shrink-0">
|
||||||
|
<a
|
||||||
|
href={paths.home()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<House className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Primary Body */}
|
||||||
|
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-hidden ">
|
||||||
|
<div className="h-auto md:sidebar-items md:dark:sidebar-items">
|
||||||
|
<div
|
||||||
|
style={{ height: "calc(100vw - -3rem)" }}
|
||||||
|
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
|
||||||
|
>
|
||||||
|
{user?.role === "admin" && (
|
||||||
|
<>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.system()}
|
||||||
|
btnText="System Preferences"
|
||||||
|
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.invites()}
|
||||||
|
btnText="Invitation"
|
||||||
|
icon={
|
||||||
|
<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.users()}
|
||||||
|
btnText="Users"
|
||||||
|
icon={<Users className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.workspaces()}
|
||||||
|
btnText="Workspaces"
|
||||||
|
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.admin.chats()}
|
||||||
|
btnText="Workspace Chat"
|
||||||
|
icon={
|
||||||
|
<ChatCenteredText className="h-5 w-5 flex-shrink-0" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* General Settings */}
|
||||||
|
<Option
|
||||||
|
href={paths.general.appearance()}
|
||||||
|
btnText="Appearance"
|
||||||
|
icon={<Eye className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.apiKeys()}
|
||||||
|
btnText="API Keys"
|
||||||
|
icon={<Key className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.llmPreference()}
|
||||||
|
btnText="LLM Preference"
|
||||||
|
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.vectorDatabase()}
|
||||||
|
btnText="Vector Database"
|
||||||
|
icon={<Database className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.exportImport()}
|
||||||
|
btnText="Export or Import"
|
||||||
|
icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.general.security()}
|
||||||
|
btnText="Security"
|
||||||
|
icon={<Lock className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex justify-center mt-2">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<a
|
||||||
|
href={paths.github()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<GithubLogo weight="fill" className="h-5 w-5 " />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={paths.docs()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<BookOpen weight="fill" className="h-5 w-5 " />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={paths.discord()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<DiscordLogo
|
||||||
|
weight="fill"
|
||||||
|
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{/* <button className="invisible transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border">
|
||||||
|
<DotsThree className="h-5 w-5 group-hover:stroke-slate-200" />
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Option = ({ btnText, icon, href }) => {
|
||||||
|
const isActive = window.location.pathname === href;
|
||||||
|
return (
|
||||||
|
<div className="flex gap-x-2 items-center justify-between text-white">
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className={`
|
||||||
|
transition-all duration-[200ms]
|
||||||
|
flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 rounded justify-start items-center border
|
||||||
|
${
|
||||||
|
isActive
|
||||||
|
? "bg-menu-item-selected-gradient border-slate-100 border-opacity-50 font-medium"
|
||||||
|
: "hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{React.cloneElement(icon, { weight: isActive ? "fill" : "regular" })}
|
||||||
|
<p className="text-sm leading-loose text-opacity-60 whitespace-nowrap overflow-hidden ">
|
||||||
|
{btnText}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Book, Settings } from "react-feather";
|
|
||||||
import * as Skeleton from "react-loading-skeleton";
|
import * as Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
import Workspace from "../../../models/workspace";
|
import Workspace from "../../../models/workspace";
|
||||||
@ -8,10 +7,12 @@ import ManageWorkspace, {
|
|||||||
} from "../../Modals/MangeWorkspace";
|
} from "../../Modals/MangeWorkspace";
|
||||||
import paths from "../../../utils/paths";
|
import paths from "../../../utils/paths";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import { GearSix, SquaresFour } from "@phosphor-icons/react";
|
||||||
|
|
||||||
export default function ActiveWorkspaces() {
|
export default function ActiveWorkspaces() {
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [settingHover, setSettingHover] = useState(false);
|
||||||
const [workspaces, setWorkspaces] = useState([]);
|
const [workspaces, setWorkspaces] = useState([]);
|
||||||
const [selectedWs, setSelectedWs] = useState(null);
|
const [selectedWs, setSelectedWs] = useState(null);
|
||||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||||
@ -51,31 +52,55 @@ export default function ActiveWorkspaces() {
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={isActive ? null : paths.workspace.chat(workspace.slug)}
|
href={isActive ? null : paths.workspace.chat(workspace.slug)}
|
||||||
className={`flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center ${
|
className={`
|
||||||
|
transition-all duration-[200ms]
|
||||||
|
flex flex-grow w-[75%] gap-x-2 py-[9px] px-[12px] rounded-lg text-slate-200 justify-start items-center border
|
||||||
|
hover:bg-workspace-item-selected-gradient hover:border-slate-100 hover:border-opacity-50
|
||||||
|
${
|
||||||
isActive
|
isActive
|
||||||
? "bg-gray-100 dark:bg-stone-600"
|
? "bg-workspace-item-selected-gradient border-slate-100 border-opacity-50"
|
||||||
: "hover:bg-slate-100 dark:hover:bg-stone-900 "
|
: "bg-workspace-item-gradient bg-opacity-60 border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row justify-between w-full">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<SquaresFour
|
||||||
|
weight={isActive ? "fill" : "regular"}
|
||||||
|
className="h-5 w-5 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className={`text-white text-sm leading-loose font-medium whitespace-nowrap overflow-hidden ${
|
||||||
|
isActive ? "" : "text-opacity-80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Book className="h-4 w-4 flex-shrink-0" />
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold whitespace-nowrap overflow-hidden ">
|
|
||||||
{workspace.name}
|
{workspace.name}
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
onMouseEnter={() => setSettingHover(true)}
|
||||||
|
onMouseLeave={() => setSettingHover(false)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedWs(workspace);
|
setSelectedWs(workspace);
|
||||||
showModal();
|
showModal();
|
||||||
}}
|
}}
|
||||||
className="rounded-md bg-stone-200 p-2 h-[36px] w-[15%] flex items-center justify-center text-slate-800 hover:bg-stone-300 group dark:bg-stone-800 dark:text-slate-200 dark:hover:bg-stone-900 dark:border dark:border-stone-800"
|
className="rounded-md flex items-center justify-center text-white ml-auto"
|
||||||
>
|
>
|
||||||
<Settings className="h-3.5 w-3.5 transition-all duration-300 group-hover:rotate-90" />
|
<GearSix
|
||||||
|
weight={settingHover ? "fill" : "regular"}
|
||||||
|
hidden={!isActive}
|
||||||
|
className="h-[20px] w-[20px] transition-all duration-300"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{showing && !!selectedWs && (
|
{showing && (
|
||||||
<ManageWorkspace hideModal={hideModal} providedSlug={selectedWs.slug} />
|
<ManageWorkspace
|
||||||
|
hideModal={hideModal}
|
||||||
|
providedSlug={selectedWs ? selectedWs.slug : null}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,188 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
X,
|
|
||||||
Archive,
|
|
||||||
Lock,
|
|
||||||
Users,
|
|
||||||
Database,
|
|
||||||
MessageSquare,
|
|
||||||
Eye,
|
|
||||||
Key,
|
|
||||||
} from "react-feather";
|
|
||||||
import SystemSettingsModal, {
|
|
||||||
useSystemSettingsModal,
|
|
||||||
} from "../../Modals/Settings";
|
|
||||||
import useLogo from "../../../hooks/useLogo";
|
|
||||||
import System from "../../../models/system";
|
|
||||||
|
|
||||||
const OVERLAY_ID = "anything-llm-system-overlay";
|
|
||||||
const OVERLAY_CLASSES = {
|
|
||||||
enabled: ["z-10", "opacity-1"],
|
|
||||||
disabled: ["-z-10", "opacity-0"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SettingsOverlay() {
|
|
||||||
const { logo } = useLogo();
|
|
||||||
const [tab, setTab] = useState(null);
|
|
||||||
const [settings, setSettings] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const { showing, hideModal, showModal } = useSystemSettingsModal();
|
|
||||||
const selectTab = (tab = null) => {
|
|
||||||
setTab(tab);
|
|
||||||
showModal(true);
|
|
||||||
};
|
|
||||||
const handleModalClose = () => {
|
|
||||||
hideModal();
|
|
||||||
setTab(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchKeys() {
|
|
||||||
const _settings = await System.keys();
|
|
||||||
setSettings(_settings);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
fetchKeys();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id={OVERLAY_ID}
|
|
||||||
className="absolute left-0 rounded-[26px] top-0 w-full h-full opacity-0 -z-10 p-[18px] transition-all duration-300 bg-white dark:bg-black-900 flex flex-col overflow-x-hidden items-between"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex shrink-0 max-w-[50%] items-center justify-start">
|
|
||||||
<img
|
|
||||||
src={logo}
|
|
||||||
alt="Logo"
|
|
||||||
className="rounded max-h-[40px]"
|
|
||||||
style={{ objectFit: "contain" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-x-2 items-center text-slate-500">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setTab(null);
|
|
||||||
hideOverlay();
|
|
||||||
}}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-stone-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4 " />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
|
|
||||||
<div className="h-auto sidebar-items dark:sidebar-items">
|
|
||||||
<p className="text-sm leading-loose my-2 text-slate-800 dark:text-slate-200 ">
|
|
||||||
Select a setting to configure
|
|
||||||
</p>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex flex-col gap-y-4 h-[65vh] pb-8 overflow-y-scroll no-scroll">
|
|
||||||
<div className="rounded-lg w-[90%] h-[36px] bg-stone-600 animate-pulse" />
|
|
||||||
<div className="rounded-lg w-[90%] h-[36px] bg-stone-600 animate-pulse" />
|
|
||||||
<div className="rounded-lg w-[90%] h-[36px] bg-stone-600 animate-pulse" />
|
|
||||||
<div className="rounded-lg w-[90%] h-[36px] bg-stone-600 animate-pulse" />
|
|
||||||
<div className="rounded-lg w-[90%] h-[36px] bg-stone-600 animate-pulse" />
|
|
||||||
<div className="rounded-lg w-[90%] h-[36px] bg-stone-600 animate-pulse" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-y-4 h-[65vh] pb-8 overflow-y-scroll no-scroll">
|
|
||||||
{!settings?.MultiUserMode && (
|
|
||||||
<Option
|
|
||||||
btnText="Appearance"
|
|
||||||
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
isActive={tab === "appearance"}
|
|
||||||
onClick={() => selectTab("appearance")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Option
|
|
||||||
btnText="LLM Preference"
|
|
||||||
icon={<MessageSquare className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
isActive={tab === "llm"}
|
|
||||||
onClick={() => selectTab("llm")}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
btnText="Vector Database"
|
|
||||||
icon={<Database className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
isActive={tab === "vectordb"}
|
|
||||||
onClick={() => selectTab("vectordb")}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
btnText="Export or Import"
|
|
||||||
icon={<Archive className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
isActive={tab === "exportimport"}
|
|
||||||
onClick={() => selectTab("exportimport")}
|
|
||||||
/>
|
|
||||||
{!settings?.MultiUserMode && (
|
|
||||||
<>
|
|
||||||
<Option
|
|
||||||
btnText="Password Protection"
|
|
||||||
icon={<Lock className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
isActive={tab === "password"}
|
|
||||||
onClick={() => selectTab("password")}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
btnText="Multi User Mode"
|
|
||||||
icon={<Users className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
isActive={tab === "multiuser"}
|
|
||||||
onClick={() => selectTab("multiuser")}
|
|
||||||
/>
|
|
||||||
<Option
|
|
||||||
btnText="API Key"
|
|
||||||
icon={<Key className="h-4 w-4 flex-shrink-0" />}
|
|
||||||
isActive={tab === "apikey"}
|
|
||||||
onClick={() => selectTab("apikey")}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{showing && !!tab && (
|
|
||||||
<SystemSettingsModal tab={tab} hideModal={handleModalClose} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Option = ({ btnText, icon, isActive, onClick }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-x-2 items-center justify-between">
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={`flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center ${
|
|
||||||
isActive
|
|
||||||
? "bg-gray-100 dark:bg-stone-600"
|
|
||||||
: "hover:bg-slate-100 dark:hover:bg-stone-900 "
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold whitespace-nowrap overflow-hidden ">
|
|
||||||
{btnText}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function showOverlay() {
|
|
||||||
document
|
|
||||||
.getElementById(OVERLAY_ID)
|
|
||||||
.classList.remove(...OVERLAY_CLASSES.disabled);
|
|
||||||
document.getElementById(OVERLAY_ID).classList.add(...OVERLAY_CLASSES.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideOverlay() {
|
|
||||||
document
|
|
||||||
.getElementById(OVERLAY_ID)
|
|
||||||
.classList.remove(...OVERLAY_CLASSES.enabled);
|
|
||||||
document
|
|
||||||
.getElementById(OVERLAY_ID)
|
|
||||||
.classList.add(...OVERLAY_CLASSES.disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSystemSettingsOverlay() {
|
|
||||||
return { showOverlay, hideOverlay };
|
|
||||||
}
|
|
@ -1,34 +1,32 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { LogOut, Menu, Package, Plus, Shield } from "react-feather";
|
||||||
import {
|
import {
|
||||||
AtSign,
|
Wrench,
|
||||||
|
GithubLogo,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
GitHub,
|
DiscordLogo,
|
||||||
LogOut,
|
DotsThree,
|
||||||
Menu,
|
} from "@phosphor-icons/react";
|
||||||
Package,
|
// import IndexCount from "./IndexCount";
|
||||||
Plus,
|
// import LLMStatus from "./LLMStatus";
|
||||||
Shield,
|
|
||||||
Tool,
|
|
||||||
X,
|
|
||||||
} from "react-feather";
|
|
||||||
import IndexCount from "./IndexCount";
|
|
||||||
import LLMStatus from "./LLMStatus";
|
|
||||||
import NewWorkspaceModal, {
|
import NewWorkspaceModal, {
|
||||||
useNewWorkspaceModal,
|
useNewWorkspaceModal,
|
||||||
} from "../Modals/NewWorkspace";
|
} from "../Modals/NewWorkspace";
|
||||||
import ActiveWorkspaces from "./ActiveWorkspaces";
|
import ActiveWorkspaces from "./ActiveWorkspaces";
|
||||||
import paths from "../../utils/paths";
|
import paths from "../../utils/paths";
|
||||||
import Discord from "../Icons/Discord";
|
|
||||||
import useUser from "../../hooks/useUser";
|
import useUser from "../../hooks/useUser";
|
||||||
import { userFromStorage } from "../../utils/request";
|
import { userFromStorage } from "../../utils/request";
|
||||||
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "../../utils/constants";
|
import {
|
||||||
|
AUTH_TIMESTAMP,
|
||||||
|
AUTH_TOKEN,
|
||||||
|
AUTH_USER,
|
||||||
|
USER_BACKGROUND_COLOR,
|
||||||
|
} from "../../utils/constants";
|
||||||
import useLogo from "../../hooks/useLogo";
|
import useLogo from "../../hooks/useLogo";
|
||||||
import SettingsOverlay, { useSystemSettingsOverlay } from "./SettingsOverlay";
|
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const { logo } = useLogo();
|
const { logo } = useLogo();
|
||||||
const sidebarRef = useRef(null);
|
const sidebarRef = useRef(null);
|
||||||
const { showOverlay } = useSystemSettingsOverlay();
|
|
||||||
const {
|
const {
|
||||||
showing: showingNewWsModal,
|
showing: showingNewWsModal,
|
||||||
showModal: showNewWsModal,
|
showModal: showNewWsModal,
|
||||||
@ -40,13 +38,12 @@ export default function Sidebar() {
|
|||||||
<div
|
<div
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
style={{ height: "calc(100% - 32px)" }}
|
style={{ height: "calc(100% - 32px)" }}
|
||||||
className="relative transition-all duration-500 relative m-[16px] rounded-[26px] bg-white dark:bg-black-900 min-w-[15.5%] p-[18px] "
|
className="transition-all duration-500 relative m-[16px] rounded-[26px] bg-sidebar border-4 border-accent min-w-[250px] p-[18px]"
|
||||||
>
|
>
|
||||||
<SettingsOverlay />
|
<div className="flex flex-col h-full overflow-x-hidden">
|
||||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
|
||||||
{/* Header Information */}
|
{/* Header Information */}
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex shrink-0 max-w-[50%] items-center justify-start">
|
<div className="flex shrink-0 max-w-[65%] items-center justify-start">
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
@ -54,32 +51,30 @@ export default function Sidebar() {
|
|||||||
style={{ objectFit: "contain" }}
|
style={{ objectFit: "contain" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-x-2 items-center text-slate-500">
|
<div className="flex gap-x-2 items-center text-slate-200">
|
||||||
<AdminHome />
|
{/* <AdminHome /> */}
|
||||||
<SettingsButton onClick={showOverlay} />
|
<SettingsButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Primary Body */}
|
{/* Primary Body */}
|
||||||
<div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
|
<div className="flex-grow flex flex-col">
|
||||||
<div className="h-auto sidebar-items dark:sidebar-items">
|
<div className="flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll">
|
||||||
<div className="flex flex-col gap-y-4 h-[65vh] pb-8 overflow-y-scroll no-scroll">
|
|
||||||
<div className="flex gap-x-2 items-center justify-between">
|
<div className="flex gap-x-2 items-center justify-between">
|
||||||
<button
|
<button
|
||||||
onClick={showNewWsModal}
|
onClick={showNewWsModal}
|
||||||
className="flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900"
|
className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-4 bg-white rounded-lg text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-5 w-5" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
<p className="text-sidebar text-sm font-semibold">
|
||||||
New workspace
|
New Workspace
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ActiveWorkspaces />
|
<ActiveWorkspaces />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex flex-col flex-grow justify-end mb-2">
|
||||||
<div>
|
{/* <div className="flex flex-col gap-y-2">
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<div className="w-full flex items-center justify-between">
|
<div className="w-full flex items-center justify-between">
|
||||||
<LLMStatus />
|
<LLMStatus />
|
||||||
<IndexCount />
|
<IndexCount />
|
||||||
@ -87,45 +82,45 @@ export default function Sidebar() {
|
|||||||
<a
|
<a
|
||||||
href={paths.feedback()}
|
href={paths.feedback()}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 dark:border-transparent rounded-lg text-slate-800 dark:text-slate-200 justify-center items-center hover:bg-slate-100 dark:bg-stone-800 dark:hover:bg-stone-900"
|
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-transparent rounded-lg text-slate-200 justify-center items-center bg-stone-800 hover:bg-stone-900"
|
||||||
>
|
>
|
||||||
<AtSign className="h-4 w-4" />
|
<AtSign className="h-4 w-4" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
<p className="text-slate-200 text-xs leading-loose font-semibold">
|
||||||
Feedback form
|
Feedback form
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
<ManagedHosting />
|
<ManagedHosting />
|
||||||
<LogoutButton />
|
<LogoutButton />
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex items-end justify-between mt-2">
|
<div className="flex justify-center mt-2">
|
||||||
<div className="flex gap-x-1 items-center">
|
<div className="flex space-x-4">
|
||||||
<a
|
<a
|
||||||
href={paths.github()}
|
href={paths.github()}
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
>
|
>
|
||||||
<GitHub className="h-4 w-4 " />
|
<GithubLogo weight="fill" className="h-5 w-5 " />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={paths.docs()}
|
href={paths.docs()}
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
>
|
>
|
||||||
<BookOpen className="h-4 w-4 " />
|
<BookOpen weight="fill" className="h-5 w-5 " />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={paths.discord()}
|
href={paths.discord()}
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 dark:bg-slate-800 hover:bg-slate-800 group"
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
>
|
>
|
||||||
<Discord className="h-4 w-4 stroke-slate-400 group-hover:stroke-slate-200 dark:group-hover:stroke-slate-200" />
|
<DiscordLogo
|
||||||
|
weight="fill"
|
||||||
|
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<button className="invisible transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border">
|
||||||
|
<DotsThree className="h-5 w-5 group-hover:stroke-slate-200" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<a
|
|
||||||
href={paths.mailToMintplex()}
|
|
||||||
className="transition-all duration-300 text-xs text-slate-500 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400"
|
|
||||||
>
|
|
||||||
@MintplexLabs
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,7 +136,6 @@ export function SidebarMobileHeader() {
|
|||||||
const sidebarRef = useRef(null);
|
const sidebarRef = useRef(null);
|
||||||
const [showSidebar, setShowSidebar] = useState(false);
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
const [showBgOverlay, setShowBgOverlay] = useState(false);
|
const [showBgOverlay, setShowBgOverlay] = useState(false);
|
||||||
const { showOverlay } = useSystemSettingsOverlay();
|
|
||||||
const {
|
const {
|
||||||
showing: showingNewWsModal,
|
showing: showingNewWsModal,
|
||||||
showModal: showNewWsModal,
|
showModal: showNewWsModal,
|
||||||
@ -165,21 +159,22 @@ export function SidebarMobileHeader() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between relative top-0 left-0 w-full rounded-b-lg px-2 pb-4 bg-white dark:bg-black-900 text-slate-800 dark:text-slate-200">
|
<div className="fixed top-0 left-0 right-0 z-10 flex justify-between items-center px-4 py-2 bg-sidebar text-slate-200 shadow-lg h-16">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowSidebar(true)}
|
onClick={() => setShowSidebar(true)}
|
||||||
className="rounded-md bg-stone-200 p-2 flex items-center justify-center text-slate-800 hover:bg-stone-300 group dark:bg-stone-800 dark:text-slate-200 dark:hover:bg-stone-900 dark:border dark:border-stone-800"
|
className="rounded-md p-2 flex items-center justify-center text-slate-200"
|
||||||
>
|
>
|
||||||
<Menu className="h-6 w-6" />
|
<Menu className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex shrink-0 w-fit items-center justify-start">
|
<div className="flex items-center justify-center flex-grow">
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
className="rounded w-full max-h-[40px]"
|
className="block mx-auto h-6 w-auto"
|
||||||
style={{ objectFit: "contain" }}
|
style={{ maxHeight: "40px", objectFit: "contain" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-12"></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -192,14 +187,13 @@ export function SidebarMobileHeader() {
|
|||||||
showBgOverlay
|
showBgOverlay
|
||||||
? "transition-all opacity-1"
|
? "transition-all opacity-1"
|
||||||
: "transition-none opacity-0"
|
: "transition-none opacity-0"
|
||||||
} duration-500 fixed top-0 left-0 bg-black-900 bg-opacity-75 w-screen h-screen`}
|
} duration-500 fixed top-0 left-0 ${USER_BACKGROUND_COLOR} bg-opacity-75 w-screen h-screen`}
|
||||||
onClick={() => setShowSidebar(false)}
|
onClick={() => setShowSidebar(false)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
className="relative h-[100vh] fixed top-0 left-0 rounded-r-[26px] bg-white dark:bg-black-900 w-[80%] p-[18px] "
|
className="relative h-[100vh] fixed top-0 left-0 rounded-r-[26px] bg-sidebar w-[80%] p-[18px] "
|
||||||
>
|
>
|
||||||
<SettingsOverlay />
|
|
||||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||||
{/* Header Information */}
|
{/* Header Information */}
|
||||||
<div className="flex w-full items-center justify-between gap-x-4">
|
<div className="flex w-full items-center justify-between gap-x-4">
|
||||||
@ -212,14 +206,13 @@ export function SidebarMobileHeader() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-x-2 items-center text-slate-500 shink-0">
|
<div className="flex gap-x-2 items-center text-slate-500 shink-0">
|
||||||
<AdminHome />
|
<SettingsButton />
|
||||||
<SettingsButton onClick={showOverlay} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Primary Body */}
|
{/* Primary Body */}
|
||||||
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-hidden ">
|
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-hidden ">
|
||||||
<div className="h-auto md:sidebar-items md:dark:sidebar-items">
|
<div className="h-auto md:sidebar-items">
|
||||||
<div
|
<div
|
||||||
style={{ height: "calc(100vw - -3rem)" }}
|
style={{ height: "calc(100vw - -3rem)" }}
|
||||||
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
|
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
|
||||||
@ -227,11 +220,11 @@ export function SidebarMobileHeader() {
|
|||||||
<div className="flex gap-x-2 items-center justify-between">
|
<div className="flex gap-x-2 items-center justify-between">
|
||||||
<button
|
<button
|
||||||
onClick={showNewWsModal}
|
onClick={showNewWsModal}
|
||||||
className="flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900"
|
className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-4 bg-white rounded-lg text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-5 w-5" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
<p className="text-sidebar text-sm font-semibold">
|
||||||
New workspace
|
New Workspace
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -239,53 +232,34 @@ export function SidebarMobileHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<div className="w-full flex items-center justify-between">
|
|
||||||
<LLMStatus />
|
|
||||||
<IndexCount />
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={paths.feedback()}
|
|
||||||
target="_blank"
|
|
||||||
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 dark:border-transparent rounded-lg text-slate-800 dark:text-slate-200 justify-center items-center hover:bg-slate-100 dark:bg-stone-800 dark:hover:bg-stone-900"
|
|
||||||
>
|
|
||||||
<AtSign className="h-4 w-4" />
|
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
|
||||||
Feedback form
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
<ManagedHosting />
|
|
||||||
<LogoutButton />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex items-end justify-between mt-2">
|
<div className="flex justify-center mt-2">
|
||||||
<div className="flex gap-x-1 items-center">
|
<div className="flex space-x-4">
|
||||||
<a
|
<a
|
||||||
href={paths.github()}
|
href={paths.github()}
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
>
|
>
|
||||||
<GitHub className="h-4 w-4 " />
|
<GithubLogo weight="fill" className="h-5 w-5 " />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={paths.docs()}
|
href={paths.docs()}
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
>
|
>
|
||||||
<BookOpen className="h-4 w-4 " />
|
<BookOpen weight="fill" className="h-5 w-5 " />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={paths.discord()}
|
href={paths.discord()}
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 dark:bg-slate-800 hover:bg-slate-800 group"
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
>
|
>
|
||||||
<Discord className="h-4 w-4 stroke-slate-400 group-hover:stroke-slate-200 dark:group-hover:stroke-slate-200" />
|
<DiscordLogo
|
||||||
|
weight="fill"
|
||||||
|
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
{/* <button className="invisible transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border">
|
||||||
|
<DotsThree className="h-5 w-5 group-hover:stroke-slate-200" />
|
||||||
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
<a
|
|
||||||
href={paths.mailToMintplex()}
|
|
||||||
className="transition-all duration-300 text-xs text-slate-500 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400"
|
|
||||||
>
|
|
||||||
@MintplexLabs
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -303,7 +277,7 @@ function AdminHome() {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={paths.admin.system()}
|
href={paths.admin.system()}
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-stone-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
className="transition-all duration-300 p-2 rounded-full text-slate-400 bg-stone-800 hover:bg-slate-800 hover:text-slate-200"
|
||||||
>
|
>
|
||||||
<Shield className="h-4 w-4" />
|
<Shield className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
@ -323,27 +297,24 @@ function LogoutButton() {
|
|||||||
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||||
window.location.replace(paths.home());
|
window.location.replace(paths.home());
|
||||||
}}
|
}}
|
||||||
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 dark:border-transparent rounded-lg text-slate-800 dark:text-slate-200 justify-center items-center hover:bg-slate-100 dark:bg-stone-800 dark:hover:bg-stone-900"
|
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-transparent rounded-lg text-slate-200 justify-center items-center bg-stone-800 hover:bg-stone-900"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
<p className="text-slate-200 text-xs leading-loose font-semibold">
|
||||||
Log out of {user.username}
|
Log out of {user.username}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsButton({ onClick }) {
|
function SettingsButton() {
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
if (!!user && user?.role !== "admin") return null;
|
|
||||||
return (
|
return (
|
||||||
<button
|
<a
|
||||||
onClick={onClick}
|
href={paths.general.llmPreference()}
|
||||||
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-stone-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
>
|
>
|
||||||
<Tool className="h-4 w-4 " />
|
<Wrench className="h-4 w-4" weight="fill" />
|
||||||
</button>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,10 +324,10 @@ function ManagedHosting() {
|
|||||||
<a
|
<a
|
||||||
href={paths.hosting()}
|
href={paths.hosting()}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 dark:border-transparent rounded-lg text-slate-800 dark:text-slate-200 justify-center items-center hover:bg-slate-100 dark:bg-stone-800 dark:hover:bg-stone-900"
|
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-transparent rounded-lg text-slate-200 justify-center items-center bg-stone-800 hover:bg-stone-900"
|
||||||
>
|
>
|
||||||
<Package className="h-4 w-4" />
|
<Package className="h-4 w-4" />
|
||||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
<p className="text-slate-200 text-xs leading-loose font-semibold">
|
||||||
Managed cloud hosting
|
Managed cloud hosting
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useRef, useEffect } from "react";
|
import React, { useRef, useEffect } from "react";
|
||||||
import JAZZ from "@metamask/jazzicon";
|
import JAZZ from "@metamask/jazzicon";
|
||||||
|
|
||||||
export default function Jazzicon({ size = 10, user }) {
|
export default function Jazzicon({ size = 10, user, role }) {
|
||||||
const divRef = useRef(null);
|
const divRef = useRef(null);
|
||||||
const seed = user?.uid
|
const seed = user?.uid
|
||||||
? toPseudoRandomInteger(user.uid)
|
? toPseudoRandomInteger(user.uid)
|
||||||
@ -14,7 +14,12 @@ export default function Jazzicon({ size = 10, user }) {
|
|||||||
divRef.current.appendChild(result);
|
divRef.current.appendChild(result);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return <div className="flex" ref={divRef} />;
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex ${role === "user" ? "border-2 rounded-full" : ""}`}
|
||||||
|
ref={divRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPseudoRandomInteger(uidString = "") {
|
function toPseudoRandomInteger(uidString = "") {
|
||||||
|
74
frontend/src/components/UserMenu/index.jsx
Normal file
74
frontend/src/components/UserMenu/index.jsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import paths from "../../utils/paths";
|
||||||
|
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "../../utils/constants";
|
||||||
|
import { Person, SignOut } from "@phosphor-icons/react";
|
||||||
|
import { userFromStorage } from "../../utils/request";
|
||||||
|
|
||||||
|
export default function UserMenu({ children }) {
|
||||||
|
if (isMobile) return <>{children}</>;
|
||||||
|
return (
|
||||||
|
<div className="w-auto h-auto">
|
||||||
|
<UserButton />
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useLoginMode() {
|
||||||
|
const user = !!window.localStorage.getItem(AUTH_USER);
|
||||||
|
const token = !!window.localStorage.getItem(AUTH_TOKEN);
|
||||||
|
|
||||||
|
if (user && token) return "multi";
|
||||||
|
if (!user && token) return "single";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userDisplay() {
|
||||||
|
const user = userFromStorage();
|
||||||
|
return user?.username?.slice(0, 2) || "AA";
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserButton() {
|
||||||
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
const mode = useLoginMode();
|
||||||
|
|
||||||
|
if (mode === null) return null;
|
||||||
|
return (
|
||||||
|
<div className="absolute top-9 right-10 w-fit h-fit z-99">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowMenu(!showMenu)}
|
||||||
|
type="button"
|
||||||
|
className="uppercase transition-all duration-300 w-[35px] h-[35px] text-base font-semibold rounded-full flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient justify-center text-white p-2 hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
{mode === "multi" ? userDisplay() : <Person size={14} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showMenu && (
|
||||||
|
<div className="w-fit rounded-lg absolute top-12 right-0 bg-sidebar p-4 flex items-center-justify-center">
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<a
|
||||||
|
href={paths.mailToMintplex()}
|
||||||
|
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
Support
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
window.localStorage.removeItem(AUTH_USER);
|
||||||
|
window.localStorage.removeItem(AUTH_TOKEN);
|
||||||
|
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||||
|
window.location.replace(paths.home());
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
39
frontend/src/components/VectorDBOption/index.jsx
Normal file
39
frontend/src/components/VectorDBOption/index.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function VectorDBOption({
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
image,
|
||||||
|
checked = false,
|
||||||
|
onClick,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div onClick={() => onClick(value)}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={value}
|
||||||
|
className="peer hidden"
|
||||||
|
checked={checked}
|
||||||
|
readOnly={true}
|
||||||
|
formNoValidate={true}
|
||||||
|
/>
|
||||||
|
<label className="transition-all duration-300 inline-flex flex-col h-full w-60 cursor-pointer items-start justify-between rounded-2xl bg-preference-gradient border-2 border-transparent shadow-md px-5 py-4 text-white hover:bg-selected-preference-gradient hover:text-underline hover:border-white/60 peer-checked:border-white peer-checked:border-opacity-90 peer-checked:bg-selected-preference-gradient">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img src={image} alt={name} className="h-10 w-10 rounded" />
|
||||||
|
<div className="ml-4 text-sm font-semibold">{name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs font-base text-white tracking-wide">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={`https://${link}`}
|
||||||
|
className="mt-2 text-xs text-white font-medium underline"
|
||||||
|
>
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { memo, useState } from "react";
|
import { memo, useState, useEffect, useRef } from "react";
|
||||||
import { Maximize2, Minimize2 } from "react-feather";
|
import { X } from "react-feather";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { decode as HTMLDecode } from "he";
|
import { decode as HTMLDecode } from "he";
|
||||||
|
import { CaretRight, FileText } from "@phosphor-icons/react";
|
||||||
|
import truncate from "truncate";
|
||||||
|
|
||||||
function combineLikeSources(sources) {
|
function combineLikeSources(sources) {
|
||||||
const combined = {};
|
const combined = {};
|
||||||
@ -19,81 +21,149 @@ function combineLikeSources(sources) {
|
|||||||
|
|
||||||
export default function Citations({ sources = [] }) {
|
export default function Citations({ sources = [] }) {
|
||||||
if (sources.length === 0) return null;
|
if (sources.length === 0) return null;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedSource, setSelectedSource] = useState(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col mt-4 justify-left">
|
<div className="flex flex-col mt-4 justify-left">
|
||||||
<div className="flex flex-col justify-left overflow-x-scroll ">
|
<button
|
||||||
<div className="w-full flex overflow-x-scroll items-center gap-4 mt-1 doc__source">
|
onClick={() => setOpen(!open)}
|
||||||
|
className={`text-white/50 font-medium italic text-sm text-left ml-14 pt-2 ${
|
||||||
|
open ? "pb-2" : ""
|
||||||
|
} hover:text-white/75 transition-all duration-300`}
|
||||||
|
>
|
||||||
|
{open ? "Hide Citations" : "Show Citations"}
|
||||||
|
<CaretRight
|
||||||
|
className={`w-3.5 h-3.5 inline-block ml-1 transform transition-transform duration-300 ${
|
||||||
|
open ? "rotate-90" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className="flex flex-wrap md:justify-between md:flex-row flex-col items-center justify-start overflow-x-scroll mt-1 doc__source">
|
||||||
{combineLikeSources(sources).map((source) => (
|
{combineLikeSources(sources).map((source) => (
|
||||||
<Citation id={source?.id || v4()} source={source} />
|
<Citation
|
||||||
|
key={source?.id || v4()}
|
||||||
|
source={source}
|
||||||
|
onClick={() => setSelectedSource(source)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<p className="w-fit text-gray-700 dark:text-stone-400 text-xs mt-1">
|
{selectedSource && (
|
||||||
*citations may not be relevant to end result.
|
<CitationDetailModal
|
||||||
</p>
|
source={selectedSource}
|
||||||
|
onClose={() => setSelectedSource(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Citation = memo(({ source, id }) => {
|
const Citation = memo(({ source, onClick }) => {
|
||||||
const [maximized, setMaximized] = useState(false);
|
const { title } = source;
|
||||||
const { references = 0, title, text } = source;
|
if (!title) return null;
|
||||||
if (title?.length === 0 || text?.length === 0) return null;
|
|
||||||
const handleMinMax = () => {
|
const truncatedTitle = truncateMiddle(title);
|
||||||
setMaximized(!maximized);
|
|
||||||
Array.from(
|
|
||||||
document?.querySelectorAll(
|
|
||||||
`div[data-citation]:not([data-citation="${id}"])`
|
|
||||||
)
|
|
||||||
).forEach((el) => {
|
|
||||||
const func = maximized ? "remove" : "add";
|
|
||||||
el.classList[func]("hidden");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={id || v4()}
|
className="flex flex-row justify-center items-center cursor-pointer text-sky-400"
|
||||||
data-citation={id || v4()}
|
style={{ width: "24%" }}
|
||||||
className={`transition-all duration-300 relative flex flex-col w-full md:w-80 h-40 bg-gray-100 dark:bg-stone-800 border border-gray-700 dark:border-stone-800 rounded-lg shrink-0 ${
|
onClick={onClick}
|
||||||
maximized ? "md:w-full h-fit pb-4" : ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="rounded-t-lg bg-gray-300 dark:bg-stone-900 px-4 py-2 w-full h-fit flex items-center justify-between">
|
<FileText className="w-6 h-6" weight="bold" />
|
||||||
<p className="text-base text-gray-800 dark:text-slate-400 italic truncate w-3/4">
|
<p className="text-sm font-medium whitespace-nowrap">{truncatedTitle}</p>
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={handleMinMax}
|
|
||||||
className="hover:dark:bg-stone-800 hover:bg-gray-200 dark:text-slate-400 text-gray-800 rounded-full p-1"
|
|
||||||
>
|
|
||||||
{maximized ? (
|
|
||||||
<Minimize2 className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`overflow-hidden relative w-full ${
|
|
||||||
maximized ? "overflow-y-scroll" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="px-2 py-1 text-xs whitespace-pre-line text-gray-800 dark:text-slate-300 italic">
|
|
||||||
{references > 1 && (
|
|
||||||
<p className="text-xs text-gray-500 dark:text-slate-500 mb-2">
|
|
||||||
referenced {references} times.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{HTMLDecode(text)}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
className={`absolute bottom-0 flex w-full h-[20px] fade-up-border rounded-b-lg ${
|
|
||||||
maximized ? "hidden" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function SkeletonLine() {
|
||||||
|
const numOfBoxes = Math.floor(Math.random() * 5) + 2;
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-2 mb-2">
|
||||||
|
{Array.from({ length: numOfBoxes }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-white/20 rounded"
|
||||||
|
style={{
|
||||||
|
width: `${Math.random() * 150 + 50}px`,
|
||||||
|
height: "20px",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CitationDetailModal({ source, onClose }) {
|
||||||
|
const { references, title, text } = source;
|
||||||
|
const dialogRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (source && dialogRef.current) {
|
||||||
|
dialogRef.current.showModal();
|
||||||
|
}
|
||||||
|
}, [source]);
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
if (dialogRef.current) {
|
||||||
|
dialogRef.current.close();
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
className="bg-transparent outline-none fixed top-0 left-0 w-full h-full flex items-center justify-center z-10"
|
||||||
|
>
|
||||||
|
<div className="relative w-full max-w-2xl bg-main-gradient rounded-lg shadow border border-white/10 overflow-hidden">
|
||||||
|
<div className="flex items-start justify-between p-6 border-b rounded-t border-gray-500/50">
|
||||||
|
<div className="flex flex-col flex-grow mr-4">
|
||||||
|
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||||
|
{truncate(title, 52)}
|
||||||
|
</h3>
|
||||||
|
{references > 1 && (
|
||||||
|
<p className="text-xs text-gray-400 mt-2">
|
||||||
|
Referenced {references} times.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleModalClose}
|
||||||
|
type="button"
|
||||||
|
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<X className="text-gray-300 text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="h-full w-full overflow-y-auto"
|
||||||
|
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||||
|
>
|
||||||
|
<div className="p-6 space-y-2 flex-col">
|
||||||
|
{[...Array(3)].map((_, idx) => (
|
||||||
|
<SkeletonLine key={idx} />
|
||||||
|
))}
|
||||||
|
<p className="text-white whitespace-pre-line">{HTMLDecode(text)}</p>
|
||||||
|
<div className="mb-6">
|
||||||
|
{[...Array(3)].map((_, idx) => (
|
||||||
|
<SkeletonLine key={idx} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateMiddle(title) {
|
||||||
|
if (title.length <= 18) return title;
|
||||||
|
|
||||||
|
const startStr = title.substr(0, 9);
|
||||||
|
const endStr = title.substr(-9);
|
||||||
|
|
||||||
|
return `${startStr}...${endStr}`;
|
||||||
|
}
|
||||||
|
@ -4,49 +4,50 @@ import Jazzicon from "../../../../UserIcon";
|
|||||||
import renderMarkdown from "../../../../../utils/chat/markdown";
|
import renderMarkdown from "../../../../../utils/chat/markdown";
|
||||||
import { userFromStorage } from "../../../../../utils/request";
|
import { userFromStorage } from "../../../../../utils/request";
|
||||||
import Citations from "../Citation";
|
import Citations from "../Citation";
|
||||||
|
import {
|
||||||
|
AI_BACKGROUND_COLOR,
|
||||||
|
USER_BACKGROUND_COLOR,
|
||||||
|
} from "../../../../../utils/constants";
|
||||||
|
|
||||||
const HistoricalMessage = forwardRef(
|
const HistoricalMessage = forwardRef(
|
||||||
({ message, role, workspace, sources = [], error = false }, ref) => {
|
({ message, role, workspace, sources = [], error = false }, ref) => {
|
||||||
if (role === "user") {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end mb-4 items-start">
|
<div
|
||||||
<div className="mr-2 py-1 px-4 w-fit md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm">
|
ref={ref}
|
||||||
<span
|
className={`flex justify-center items-end w-full ${
|
||||||
className={`inline-block p-2 rounded-lg whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base`}
|
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{message}
|
<div
|
||||||
</span>
|
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||||
</div>
|
>
|
||||||
<Jazzicon size={30} user={{ uid: userFromStorage()?.username }} />
|
<div className="flex gap-x-5">
|
||||||
</div>
|
<Jazzicon
|
||||||
);
|
size={36}
|
||||||
}
|
user={{
|
||||||
|
uid:
|
||||||
|
role === "user"
|
||||||
|
? userFromStorage()?.username
|
||||||
|
: workspace.slug,
|
||||||
|
}}
|
||||||
|
role={role}
|
||||||
|
/>
|
||||||
|
|
||||||
if (error) {
|
{error ? (
|
||||||
return (
|
|
||||||
<div className="flex justify-start mb-4 items-end">
|
|
||||||
<Jazzicon size={30} user={{ uid: workspace.slug }} />
|
|
||||||
<div className="ml-2 max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm">
|
|
||||||
<span
|
<span
|
||||||
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
|
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
|
||||||
>
|
>
|
||||||
<AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could not
|
<AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could
|
||||||
respond to message.
|
not respond to message.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="flex justify-start items-end mb-4">
|
|
||||||
<Jazzicon size={30} user={{ uid: workspace.slug }} />
|
|
||||||
<div className="ml-2 py-3 px-4 overflow-x-scroll w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm">
|
|
||||||
<span
|
<span
|
||||||
className="no-scroll whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base flex flex-col gap-y-1"
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(message) }}
|
dangerouslySetInnerHTML={{ __html: renderMarkdown(message) }}
|
||||||
/>
|
/>
|
||||||
<Citations sources={sources} />
|
)}
|
||||||
|
</div>
|
||||||
|
{role === "assistant" && <Citations sources={sources} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -9,18 +9,25 @@ const PromptReply = forwardRef(
|
|||||||
{ uuid, reply, pending, error, workspace, sources = [], closed = true },
|
{ uuid, reply, pending, error, workspace, sources = [], closed = true },
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
if (!reply && !sources.length === 0 && !pending && !error) return null;
|
const assistantBackgroundColor = "bg-historical-msg-system";
|
||||||
|
|
||||||
|
if (!reply && sources.length === 0 && !pending && !error) return null;
|
||||||
|
|
||||||
if (pending) {
|
if (pending) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="chat__message flex justify-start mb-4 items-end"
|
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
|
||||||
>
|
>
|
||||||
<Jazzicon size={30} user={{ uid: workspace.slug }} />
|
<div className="py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
|
||||||
<div className="ml-2 pt-2 px-6 w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm">
|
<div className="flex gap-x-5">
|
||||||
<span className={`inline-block p-2`}>
|
<Jazzicon
|
||||||
<div className="dot-falling"></div>
|
size={36}
|
||||||
</span>
|
user={{ uid: workspace.slug }}
|
||||||
|
role="assistant"
|
||||||
|
/>
|
||||||
|
<div className="mt-3 ml-5 dot-falling"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -28,15 +35,23 @@ const PromptReply = forwardRef(
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="chat__message flex justify-start mb-4 items-center">
|
<div
|
||||||
<Jazzicon size={30} user={{ uid: workspace.slug }} />
|
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
|
||||||
<div className="ml-2 py-3 px-4 rounded-br-3xl rounded-tr-3xl rounded-tl-xl text-slate-100 ">
|
>
|
||||||
<div className="bg-red-50 text-red-500 rounded-lg w-fit flex flex-col p-2">
|
<div className="py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
|
||||||
<span className={`inline-block`}>
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon
|
||||||
|
size={36}
|
||||||
|
user={{ uid: workspace.slug }}
|
||||||
|
role="assistant"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
|
||||||
|
>
|
||||||
<AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could
|
<AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could
|
||||||
not respond to message.
|
not respond to message.
|
||||||
</span>
|
|
||||||
<span className="text-xs">Reason: {error || "unknown"}</span>
|
<span className="text-xs">Reason: {error || "unknown"}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -44,13 +59,23 @@ const PromptReply = forwardRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={uuid} ref={ref} className="mb-4 flex justify-start items-end">
|
<div
|
||||||
<Jazzicon size={30} user={{ uid: workspace.slug }} />
|
key={uuid}
|
||||||
<div className="ml-2 py-3 px-4 overflow-x-scroll w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm">
|
ref={ref}
|
||||||
|
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
|
||||||
|
>
|
||||||
|
<div className="py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
|
||||||
|
<div className="flex gap-x-5">
|
||||||
|
<Jazzicon
|
||||||
|
size={36}
|
||||||
|
user={{ uid: workspace.slug }}
|
||||||
|
role="assistant"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
className="whitespace-pre-line text-slate-800 dark:text-slate-200 flex flex-col gap-y-1 font-[500] md:font-semibold text-sm md:text-base"
|
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }}
|
dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<Citations sources={sources} />
|
<Citations sources={sources} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Frown } from "react-feather";
|
|
||||||
import HistoricalMessage from "./HistoricalMessage";
|
import HistoricalMessage from "./HistoricalMessage";
|
||||||
import PromptReply from "./PromptReply";
|
import PromptReply from "./PromptReply";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useManageWorkspaceModal } from "../../../Modals/MangeWorkspace";
|
||||||
|
import ManageWorkspace from "../../../Modals/MangeWorkspace";
|
||||||
|
|
||||||
export default function ChatHistory({ history = [], workspace }) {
|
export default function ChatHistory({ history = [], workspace }) {
|
||||||
const replyRef = useRef(null);
|
const replyRef = useRef(null);
|
||||||
|
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (replyRef.current) {
|
if (replyRef.current) {
|
||||||
@ -16,21 +18,37 @@ export default function ChatHistory({ history = [], workspace }) {
|
|||||||
|
|
||||||
if (history.length === 0) {
|
if (history.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[89%] md:mt-0 pb-5 w-full justify-center items-center">
|
<div className="flex flex-col h-full md:mt-0 pb-48 w-full justify-end items-center">
|
||||||
<div className="w-fit flex items-center gap-x-2">
|
<div className="flex flex-col items-start">
|
||||||
<Frown className="h-4 w-4 text-slate-400" />
|
<p className="text-white/60 text-lg font-base -ml-6 py-4">
|
||||||
<p className="text-slate-400">No chat history found.</p>
|
Welcome to your new workspace.
|
||||||
</div>
|
|
||||||
<p className="text-slate-400 text-xs">
|
|
||||||
Send your first message to get started.
|
|
||||||
</p>
|
</p>
|
||||||
|
<div className="w-full text-center">
|
||||||
|
<p className="text-white/60 text-lg font-base inline-flex items-center gap-x-2">
|
||||||
|
To get started either{" "}
|
||||||
|
<span
|
||||||
|
className="underline font-medium cursor-pointer"
|
||||||
|
onClick={showModal}
|
||||||
|
>
|
||||||
|
upload a document
|
||||||
|
</span>
|
||||||
|
or <b className="font-medium italic">send a chat.</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showing && (
|
||||||
|
<ManageWorkspace
|
||||||
|
hideModal={hideModal}
|
||||||
|
providedSlug={workspace.slug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-[89%] pb-[100px] md:pt-[50px] md:pt-0 md:pb-5 mx-2 md:mx-0 overflow-y-scroll flex flex-col justify-start no-scroll"
|
className="h-[89%] pb-[100px] pt-6 md:pt-0 md:pb-20 md:mx-0 overflow-y-scroll flex flex-col justify-start no-scroll"
|
||||||
id="chat-history"
|
id="chat-history"
|
||||||
>
|
>
|
||||||
{history.map((props, index) => {
|
{history.map((props, index) => {
|
||||||
@ -64,6 +82,10 @@ export default function ChatHistory({ history = [], workspace }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{showing && (
|
||||||
|
<ManageWorkspace hideModal={hideModal} providedSlug={workspace.slug} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import React, { useState, useRef, memo, useEffect } from "react";
|
import { Gear, PaperPlaneRight } from "@phosphor-icons/react";
|
||||||
|
import React, { useState, useRef } from "react";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { Loader, Menu, X } from "react-feather";
|
import { Loader } from "react-feather";
|
||||||
|
import ManageWorkspace, {
|
||||||
|
useManageWorkspaceModal,
|
||||||
|
} from "../../../Modals/MangeWorkspace";
|
||||||
|
|
||||||
export default function PromptInput({
|
export default function PromptInput({
|
||||||
workspace,
|
workspace,
|
||||||
@ -10,13 +14,15 @@ export default function PromptInput({
|
|||||||
inputDisabled,
|
inputDisabled,
|
||||||
buttonDisabled,
|
buttonDisabled,
|
||||||
}) {
|
}) {
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
const [_, setFocused] = useState(false);
|
const [_, setFocused] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
submit(e);
|
submit(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const captureEnter = (event) => {
|
const captureEnter = (event) => {
|
||||||
if (event.keyCode == 13) {
|
if (event.keyCode == 13) {
|
||||||
if (!event.shiftKey) {
|
if (!event.shiftKey) {
|
||||||
@ -24,6 +30,7 @@ export default function PromptInput({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const adjustTextArea = (event) => {
|
const adjustTextArea = (event) => {
|
||||||
if (isMobile) return false;
|
if (isMobile) return false;
|
||||||
const element = event.target;
|
const element = event.target;
|
||||||
@ -34,41 +41,15 @@ export default function PromptInput({
|
|||||||
: "1px";
|
: "1px";
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTextCommand = (command = "") => {
|
|
||||||
const storageKey = `workspace_chat_mode_${workspace.slug}`;
|
|
||||||
if (command === "/query") {
|
|
||||||
window.localStorage.setItem(storageKey, "query");
|
|
||||||
window.dispatchEvent(new Event("workspace_chat_mode_update"));
|
|
||||||
return;
|
|
||||||
} else if (command === "/conversation") {
|
|
||||||
window.localStorage.setItem(storageKey, "chat");
|
|
||||||
window.dispatchEvent(new Event("workspace_chat_mode_update"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange({ target: { value: `${command} ${message}` } });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0">
|
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center overflow-hidden">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="flex flex-col gap-y-1 bg-white dark:bg-black-900 md:bg-transparent rounded-t-lg md:w-3/4 w-full mx-auto"
|
className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl"
|
||||||
>
|
>
|
||||||
<div className="flex items-center py-2 px-4 rounded-lg">
|
<div className="flex items-center rounded-lg md:mb-4">
|
||||||
<CommandMenu
|
<div className="w-[600px] bg-main-gradient shadow-2xl border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden">
|
||||||
workspace={workspace}
|
<div className="flex items-center w-full border-b-2 border-gray-500/50">
|
||||||
show={showMenu}
|
|
||||||
handleClick={setTextCommand}
|
|
||||||
hide={() => setShowMenu(false)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowMenu(!showMenu)}
|
|
||||||
type="button"
|
|
||||||
className="p-2 text-slate-500 bg-transparent rounded-md hover:bg-gray-200 dark:hover:bg-stone-500 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<Menu className="w-4 h-4 md:h-6 md:w-6" />
|
|
||||||
</button>
|
|
||||||
<textarea
|
<textarea
|
||||||
onKeyUp={adjustTextArea}
|
onKeyUp={adjustTextArea}
|
||||||
onKeyDown={captureEnter}
|
onKeyDown={captureEnter}
|
||||||
@ -82,125 +63,46 @@ export default function PromptInput({
|
|||||||
adjustTextArea(e);
|
adjustTextArea(e);
|
||||||
}}
|
}}
|
||||||
value={message}
|
value={message}
|
||||||
className="cursor-text max-h-[100px] md:min-h-[40px] block mx-2 md:mx-4 p-2.5 w-full text-[16px] md:text-sm rounded-lg border bg-gray-50 border-gray-300 placeholder-gray-400 text-gray-900 dark:text-white dark:bg-stone-600 dark:border-stone-700 dark:placeholder-stone-400"
|
className="cursor-text max-h-[100px] md:min-h-[40px] mx-2 md:mx-0 py-2 w-full text-[16px] md:text-md text-white bg-transparent placeholder:text-white/60 resize-none active:outline-none focus:outline-none flex-grow"
|
||||||
placeholder={
|
placeholder={"Send a message"}
|
||||||
isMobile
|
|
||||||
? "Enter your message here."
|
|
||||||
: "Shift + Enter for newline. Enter to submit."
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={buttonDisabled}
|
disabled={buttonDisabled}
|
||||||
className="inline-flex justify-center p-0 md:p-2 rounded-full cursor-pointer text-black-900 dark:text-slate-200 hover:bg-gray-200 dark:hover:bg-stone-500 group"
|
className="inline-flex justify-center rounded-2xl cursor-pointer text-white/60 hover:text-white group ml-4"
|
||||||
>
|
>
|
||||||
{buttonDisabled ? (
|
{buttonDisabled ? (
|
||||||
<Loader className="w-6 h-6 animate-spin" />
|
<Loader className="w-6 h-6 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<svg
|
<PaperPlaneRight className="w-7 h-7 my-3" weight="fill" />
|
||||||
aria-hidden="true"
|
|
||||||
className="w-6 h-6 rotate-45 fill-gray-500 dark:fill-slate-500 group-hover:dark:fill-slate-200"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
|
|
||||||
</svg>
|
|
||||||
)}
|
)}
|
||||||
<span className="sr-only">Send message</span>
|
<span className="sr-only">Send message</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Tracking workspaceSlug={workspace.slug} />
|
<div className="flex justify-between py-3.5">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Gear
|
||||||
|
onClick={showModal}
|
||||||
|
className="w-7 h-7 text-white/60 hover:text-white cursor-pointer"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
{/* <TextT
|
||||||
|
className="w-7 h-7 text-white/30 cursor-not-allowed"
|
||||||
|
weight="fill"
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
{/* <Microphone
|
||||||
|
className="w-7 h-7 text-white/30 cursor-not-allowed"
|
||||||
|
weight="fill"
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
{showing && (
|
||||||
);
|
<ManageWorkspace hideModal={hideModal} providedSlug={workspace.slug} />
|
||||||
}
|
)}
|
||||||
|
|
||||||
const Tracking = memo(({ workspaceSlug }) => {
|
|
||||||
const storageKey = `workspace_chat_mode_${workspaceSlug}`;
|
|
||||||
const [chatMode, setChatMode] = useState(
|
|
||||||
window.localStorage.getItem(storageKey) ?? "chat"
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function watchForChatModeChange() {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
window.addEventListener(`workspace_chat_mode_update`, () => {
|
|
||||||
try {
|
|
||||||
const chatMode = window.localStorage.getItem(storageKey);
|
|
||||||
setChatMode(chatMode);
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
watchForChatModeChange();
|
|
||||||
}, [workspaceSlug]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col md:flex-row w-full justify-center items-center gap-2 mb-2 px-4 mx:px-0">
|
|
||||||
<p className="bg-gray-200 dark:bg-stone-600 text-gray-800 dark:text-slate-400 text-xs px-2 rounded-lg font-mono text-center">
|
|
||||||
Chat mode: {chatMode}
|
|
||||||
</p>
|
|
||||||
<p className="text-slate-400 text-xs text-center">
|
|
||||||
Responses from system may produce inaccurate or invalid responses - use
|
|
||||||
with caution.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function CommandMenu({ workspace, show, handleClick, hide }) {
|
|
||||||
if (!show) return null;
|
|
||||||
const COMMANDS = [
|
|
||||||
{
|
|
||||||
cmd: "/conversation",
|
|
||||||
description: "- switch to chat mode (remembers recent chat history) .",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: "/query",
|
|
||||||
description: "- switch to query mode (does not remember previous chats).",
|
|
||||||
},
|
|
||||||
{ cmd: "/reset", description: "- clear current chat history." },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute top-[-25vh] md:top-[-23vh] min-h-[200px] flex flex-col rounded-lg border border-slate-400 p-2 pt-4 bg-gray-50 dark:bg-stone-600">
|
|
||||||
<div className="flex justify-between items-center border-b border-slate-400 px-2 py-1 ">
|
|
||||||
<p className="text-gray-800 dark:text-slate-200">Available Commands</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={hide}
|
|
||||||
className="p-2 rounded-lg hover:bg-gray-200 hover:dark:bg-slate-500 rounded-full text-gray-800 dark:text-slate-400"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{COMMANDS.map((item, i) => {
|
|
||||||
const { cmd, description } = item;
|
|
||||||
return (
|
|
||||||
<div className="border-b border-slate-400 p-1">
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
handleClick(cmd);
|
|
||||||
hide();
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 flex items-center rounded-lg hover:bg-gray-300 hover:dark:bg-slate-500 gap-x-1 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<p className="text-gray-800 dark:text-slate-200 font-semibold">
|
|
||||||
{cmd}
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-800 dark:text-slate-300 text-sm">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -68,10 +68,10 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 w-full md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full md:min-w-[82%] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="flex flex-col h-full w-full flex">
|
<div className="flex flex-col h-full w-full md:mt-0 mt-[40px]">
|
||||||
<ChatHistory history={chatHistory} workspace={workspace} />
|
<ChatHistory history={chatHistory} workspace={workspace} />
|
||||||
<PromptInput
|
<PromptInput
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
|
@ -3,16 +3,18 @@ import * as Skeleton from "react-loading-skeleton";
|
|||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
|
||||||
export default function LoadingChat() {
|
export default function LoadingChat() {
|
||||||
|
const highlightColor = "#3D4147";
|
||||||
|
const baseColor = "#2C2F35";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 w-full md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="100px"
|
height="100px"
|
||||||
width="100%"
|
width="100%"
|
||||||
baseColor={"#2a3a53"}
|
highlightColor={highlightColor}
|
||||||
highlightColor={"#395073"}
|
baseColor={baseColor}
|
||||||
count={1}
|
count={1}
|
||||||
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex justify-start"
|
containerClassName="flex justify-start"
|
||||||
@ -20,8 +22,8 @@ export default function LoadingChat() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="100px"
|
height="100px"
|
||||||
width={isMobile ? "70%" : "45%"}
|
width={isMobile ? "70%" : "45%"}
|
||||||
baseColor={"#2a3a53"}
|
baseColor={baseColor}
|
||||||
highlightColor={"#395073"}
|
highlightColor={highlightColor}
|
||||||
count={1}
|
count={1}
|
||||||
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex justify-end"
|
containerClassName="flex justify-end"
|
||||||
@ -29,8 +31,8 @@ export default function LoadingChat() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="100px"
|
height="100px"
|
||||||
width={isMobile ? "55%" : "30%"}
|
width={isMobile ? "55%" : "30%"}
|
||||||
baseColor={"#2a3a53"}
|
baseColor={baseColor}
|
||||||
highlightColor={"#395073"}
|
highlightColor={highlightColor}
|
||||||
count={1}
|
count={1}
|
||||||
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex justify-start"
|
containerClassName="flex justify-start"
|
||||||
@ -38,8 +40,8 @@ export default function LoadingChat() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="100px"
|
height="100px"
|
||||||
width={isMobile ? "88%" : "25%"}
|
width={isMobile ? "88%" : "25%"}
|
||||||
baseColor={"#2a3a53"}
|
baseColor={baseColor}
|
||||||
highlightColor={"#395073"}
|
highlightColor={highlightColor}
|
||||||
count={1}
|
count={1}
|
||||||
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex justify-end"
|
containerClassName="flex justify-end"
|
||||||
@ -47,8 +49,8 @@ export default function LoadingChat() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="160px"
|
height="160px"
|
||||||
width="100%"
|
width="100%"
|
||||||
baseColor={"#2a3a53"}
|
baseColor={baseColor}
|
||||||
highlightColor={"#395073"}
|
highlightColor={highlightColor}
|
||||||
count={1}
|
count={1}
|
||||||
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="max-w-full md:max-w-[75%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex justify-start"
|
containerClassName="flex justify-start"
|
||||||
|
@ -1,27 +1,22 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import usePrefersDarkMode from "./usePrefersDarkMode";
|
|
||||||
import System from "../models/system";
|
import System from "../models/system";
|
||||||
import AnythingLLMDark from "../media/logo/anything-llm-dark.png";
|
import AnythingLLM from "../media/logo/anything-llm.png";
|
||||||
import AnythingLLMLight from "../media/logo/anything-llm-light.png";
|
|
||||||
|
|
||||||
export default function useLogo() {
|
export default function useLogo() {
|
||||||
const [logo, setLogo] = useState("");
|
const [logo, setLogo] = useState("");
|
||||||
const prefersDarkMode = usePrefersDarkMode();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchInstanceLogo() {
|
async function fetchInstanceLogo() {
|
||||||
try {
|
try {
|
||||||
const logoURL = await System.fetchLogo(!prefersDarkMode);
|
const logoURL = await System.fetchLogo();
|
||||||
logoURL
|
logoURL ? setLogo(logoURL) : setLogo(AnythingLLM);
|
||||||
? setLogo(logoURL)
|
|
||||||
: setLogo(prefersDarkMode ? AnythingLLMLight : AnythingLLMDark);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setLogo(prefersDarkMode ? AnythingLLMLight : AnythingLLMDark);
|
setLogo(AnythingLLM);
|
||||||
console.error("Failed to fetch logo:", err);
|
console.error("Failed to fetch logo:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchInstanceLogo();
|
fetchInstanceLogo();
|
||||||
}, [prefersDarkMode]);
|
}, []);
|
||||||
|
|
||||||
return { logo };
|
return { logo };
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,9 @@ html,
|
|||||||
body {
|
body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
font-family: "plus-jakarta-sans", -apple-system, BlinkMacSystemFont, Segoe UI,
|
||||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||||
|
sans-serif;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,12 +26,8 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "AvenirNextW10-Bold";
|
font-family: "plus-jakarta-sans";
|
||||||
src: url("../public/fonts/AvenirNext.ttf");
|
src: url("../public/fonts/PlusJakartaSans.ttf");
|
||||||
}
|
|
||||||
|
|
||||||
.Avenir {
|
|
||||||
font-family: AvenirNextW10-Bold;
|
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,11 +102,6 @@ a {
|
|||||||
right: 0px;
|
right: 0px;
|
||||||
height: 4em;
|
height: 4em;
|
||||||
top: 69vh;
|
top: 69vh;
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(173, 3, 3, 0),
|
|
||||||
rgb(255 255 255) 50%
|
|
||||||
);
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@ -123,11 +115,6 @@ a {
|
|||||||
right: 0px;
|
right: 0px;
|
||||||
height: 4em;
|
height: 4em;
|
||||||
top: 69vh;
|
top: 69vh;
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(173, 3, 3, 0),
|
|
||||||
rgb(20 20 20) 50%
|
|
||||||
);
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@ -164,9 +151,9 @@ a {
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: #5fa4fa;
|
background-color: #eeeeee;
|
||||||
color: #5fa4fa;
|
color: #5fa4fa;
|
||||||
box-shadow: 9999px 0 0 0 #5fa4fa;
|
box-shadow: 9999px 0 0 0 #eeeeee;
|
||||||
animation: dot-falling 1.5s infinite linear;
|
animation: dot-falling 1.5s infinite linear;
|
||||||
animation-delay: 0.1s;
|
animation-delay: 0.1s;
|
||||||
}
|
}
|
||||||
@ -183,8 +170,8 @@ a {
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: #5fa4fa;
|
background-color: #eeeeee;
|
||||||
color: #5fa4fa;
|
color: #eeeeee;
|
||||||
animation: dot-falling-before 1.5s infinite linear;
|
animation: dot-falling-before 1.5s infinite linear;
|
||||||
animation-delay: 0s;
|
animation-delay: 0s;
|
||||||
}
|
}
|
||||||
@ -193,8 +180,8 @@ a {
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: #5fa4fa;
|
background-color: #eeeeee;
|
||||||
color: #5fa4fa;
|
color: #eeeeee;
|
||||||
animation: dot-falling-after 1.5s infinite linear;
|
animation: dot-falling-after 1.5s infinite linear;
|
||||||
animation-delay: 0.2s;
|
animation-delay: 0.2s;
|
||||||
}
|
}
|
||||||
@ -207,7 +194,7 @@ a {
|
|||||||
25%,
|
25%,
|
||||||
50%,
|
50%,
|
||||||
75% {
|
75% {
|
||||||
box-shadow: 9999px 0 0 0 #5fa4fa;
|
box-shadow: 9999px 0 0 0 #eeeeee;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
@ -223,7 +210,7 @@ a {
|
|||||||
25%,
|
25%,
|
||||||
50%,
|
50%,
|
||||||
75% {
|
75% {
|
||||||
box-shadow: 9984px 0 0 0 #5fa4fa;
|
box-shadow: 9984px 0 0 0 #eeeeee;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
@ -239,7 +226,7 @@ a {
|
|||||||
25%,
|
25%,
|
||||||
50%,
|
50%,
|
||||||
75% {
|
75% {
|
||||||
box-shadow: 10014px 0 0 0 #5fa4fa;
|
box-shadow: 10014px 0 0 0 #eeeeee;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
@ -298,3 +285,55 @@ dialog::backdrop {
|
|||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slow-pulse {
|
||||||
|
transform: scale(1);
|
||||||
|
animation: subtlePulse 20s infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes subtlePulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes subtleShift {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input-gradient {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(61, 65, 71, 0.3) 0%,
|
||||||
|
rgba(44, 47, 53, 0.3) 100%
|
||||||
|
) !important;
|
||||||
|
box-shadow: 0px 4px 30px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-fill {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
@ -188,50 +188,6 @@ const Admin = {
|
|||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadLogo: async function (formData) {
|
|
||||||
return await fetch(`${API_BASE}/system/upload-logo`, {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
headers: baseHeaders(),
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) throw new Error("Error uploading logo.");
|
|
||||||
return { success: true, error: null };
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeCustomLogo: async function () {
|
|
||||||
return await fetch(`${API_BASE}/system/remove-logo`, {
|
|
||||||
headers: baseHeaders(),
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.ok) return { success: true, error: null };
|
|
||||||
throw new Error("Error removing logo!");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setWelcomeMessages: async function (messages) {
|
|
||||||
return fetch(`${API_BASE}/system/set-welcome-messages`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: baseHeaders(),
|
|
||||||
body: JSON.stringify({ messages }),
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok)
|
|
||||||
throw new Error(res.statusText || "Error setting welcome messages.");
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// API Keys
|
// API Keys
|
||||||
getApiKeys: async function () {
|
getApiKeys: async function () {
|
||||||
|
@ -121,6 +121,18 @@ const System = {
|
|||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
isMultiUserMode: async () => {
|
||||||
|
return await fetch(`${API_BASE}/system/multi-user-mode`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: baseHeaders(),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => res?.multiUserMode)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
deleteDocument: async (name, meta) => {
|
deleteDocument: async (name, meta) => {
|
||||||
return await fetch(`${API_BASE}/system/remove-document`, {
|
return await fetch(`${API_BASE}/system/remove-document`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@ -162,6 +174,7 @@ const System = {
|
|||||||
return await fetch(`${API_BASE}/system/upload-logo`, {
|
return await fetch(`${API_BASE}/system/upload-logo`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
|
headers: baseHeaders(),
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error("Error uploading logo.");
|
if (!res.ok) throw new Error("Error uploading logo.");
|
||||||
@ -172,8 +185,8 @@ const System = {
|
|||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchLogo: async function (light = false) {
|
fetchLogo: async function () {
|
||||||
return await fetch(`${API_BASE}/system/logo${light ? "/light" : ""}`, {
|
return await fetch(`${API_BASE}/system/logo`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
cache: "no-cache",
|
cache: "no-cache",
|
||||||
})
|
})
|
||||||
@ -187,8 +200,25 @@ const System = {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
isDefaultLogo: async function () {
|
||||||
|
return await fetch(`${API_BASE}/system/is-default-logo`, {
|
||||||
|
method: "GET",
|
||||||
|
cache: "no-cache",
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error("Failed to get is default logo!");
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((res) => res?.isDefaultLogo)
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
},
|
||||||
removeCustomLogo: async function () {
|
removeCustomLogo: async function () {
|
||||||
return await fetch(`${API_BASE}/system/remove-logo`)
|
return await fetch(`${API_BASE}/system/remove-logo`, {
|
||||||
|
headers: baseHeaders(),
|
||||||
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.ok) return { success: true, error: null };
|
if (res.ok) return { success: true, error: null };
|
||||||
throw new Error("Error removing logo!");
|
throw new Error("Error removing logo!");
|
||||||
@ -246,8 +276,8 @@ const System = {
|
|||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getApiKey: async function () {
|
getApiKeys: async function () {
|
||||||
return fetch(`${API_BASE}/system/api-key`, {
|
return fetch(`${API_BASE}/system/api-keys`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: baseHeaders(),
|
headers: baseHeaders(),
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import Admin from "../../../../models/admin";
|
import Admin from "../../../../models/admin";
|
||||||
import truncate from "truncate";
|
import truncate from "truncate";
|
||||||
import { X } from "react-feather";
|
import { X, Trash } from "@phosphor-icons/react";
|
||||||
|
|
||||||
export default function ChatRow({ chat }) {
|
export default function ChatRow({ chat }) {
|
||||||
const rowRef = useRef(null);
|
const rowRef = useRef(null);
|
||||||
@ -18,19 +18,22 @@ export default function ChatRow({ chat }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr ref={rowRef} className="bg-transparent">
|
<tr
|
||||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
ref={rowRef}
|
||||||
|
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4 font-medium whitespace-nowrap text-white">
|
||||||
{chat.id}
|
{chat.id}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
<td className="px-6 py-4 font-medium whitespace-nowrap text-white">
|
||||||
{chat.user?.username}
|
{chat.user?.username}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 font-mono">{chat.workspace?.name}</td>
|
<td className="px-6 py-4">{chat.workspace?.name}</td>
|
||||||
<td
|
<td
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document.getElementById(`chat-${chat.id}-prompt`)?.showModal();
|
document.getElementById(`chat-${chat.id}-prompt`)?.showModal();
|
||||||
}}
|
}}
|
||||||
className="px-6 py-4 hover:dark:bg-stone-700 hover:bg-gray-100 cursor-pointer"
|
className="px-6 py-4 border-transparent cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
{truncate(chat.prompt, 40)}
|
{truncate(chat.prompt, 40)}
|
||||||
</td>
|
</td>
|
||||||
@ -38,7 +41,7 @@ export default function ChatRow({ chat }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
document.getElementById(`chat-${chat.id}-response`)?.showModal();
|
document.getElementById(`chat-${chat.id}-response`)?.showModal();
|
||||||
}}
|
}}
|
||||||
className="px-6 py-4 hover:dark:bg-stone-600 hover:bg-gray-100 cursor-pointer"
|
className="px-6 py-4 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
{truncate(JSON.parse(chat.response)?.text, 40)}
|
{truncate(JSON.parse(chat.response)?.text, 40)}
|
||||||
</td>
|
</td>
|
||||||
@ -46,9 +49,9 @@ export default function ChatRow({ chat }) {
|
|||||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="font-medium text-red-600 dark:text-red-300 px-2 py-1 rounded-lg hover:bg-red-50 hover:dark:bg-red-800 hover:dark:bg-opacity-20"
|
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
||||||
>
|
>
|
||||||
Delete
|
<Trash className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -69,22 +72,20 @@ const TextPreview = ({ text, modalName }) => {
|
|||||||
return (
|
return (
|
||||||
<dialog id={modalName} className="bg-transparent outline-none w-full">
|
<dialog id={modalName} className="bg-transparent outline-none w-full">
|
||||||
<div className="relative w-full max-w-2xl max-h-full min-w-1/2">
|
<div className="relative w-full max-w-2xl max-h-full min-w-1/2">
|
||||||
<div className="min-w-1/2 relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="min-w-1/2 relative rounded-lg shadow bg-stone-700">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-xl font-semibold text-white">Viewing Text</h3>
|
||||||
Viewing Text
|
|
||||||
</h3>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => hideModal(modalName)}
|
onClick={() => hideModal(modalName)}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="text-gray-400 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:bg-gray-600 hover:text-white"
|
||||||
data-modal-hide="staticModal"
|
data-modal-hide="staticModal"
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full p-4 w-full flex">
|
<div className="w-full p-4 flex">
|
||||||
<pre className="w-full flex h-[200px] py-2 px-4 overflow-scroll rounded-lg bg-stone-400 bg-gray-200 text-gray-800 dark:text-slate-800 font-mono">
|
<pre className="w-full flex h-[200px] py-2 px-4 overflow-scroll rounded-lg bg-gray-200 text-slate-800">
|
||||||
{text}
|
{text}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,30 +1,31 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import * as Skeleton from "react-loading-skeleton";
|
import * as Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
|
||||||
import Admin from "../../../models/admin";
|
import Admin from "../../../models/admin";
|
||||||
import useQuery from "../../../hooks/useQuery";
|
import useQuery from "../../../hooks/useQuery";
|
||||||
import ChatRow from "./ChatRow";
|
import ChatRow from "./ChatRow";
|
||||||
|
|
||||||
export default function AdminChats() {
|
export default function AdminChats() {
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="flex flex-col w-full px-1 md:px-8">
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
<div className="w-full flex flex-col gap-y-1">
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
<div className="items-center flex gap-x-4">
|
<div className="items-center flex gap-x-4">
|
||||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
<p className="text-2xl font-semibold text-white">
|
||||||
Workspace Chats
|
Workspace Chats
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-base text-slate-600 dark:text-slate-200">
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
These are all the recorded chats and messages that have been sent
|
These are all the recorded chats and messages that have been sent
|
||||||
by users ordered by their creation date.
|
by users ordered by their creation date.
|
||||||
</p>
|
</p>
|
||||||
@ -38,7 +39,6 @@ export default function AdminChats() {
|
|||||||
|
|
||||||
function ChatsContainer() {
|
function ChatsContainer() {
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const darkMode = usePrefersDarkMode();
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [chats, setChats] = useState([]);
|
const [chats, setChats] = useState([]);
|
||||||
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
|
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
|
||||||
@ -77,8 +77,8 @@ function ChatsContainer() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="80vh"
|
height="80vh"
|
||||||
width="100%"
|
width="100%"
|
||||||
baseColor={darkMode ? "#2a3a53" : null}
|
highlightColor="#3D4147"
|
||||||
highlightColor={darkMode ? "#395073" : null}
|
baseColor="#2C2F35"
|
||||||
count={1}
|
count={1}
|
||||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex w-full"
|
containerClassName="flex w-full"
|
||||||
@ -88,8 +88,8 @@ function ChatsContainer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<table className="md:w-full w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||||
Id
|
Id
|
||||||
@ -110,7 +110,7 @@ function ChatsContainer() {
|
|||||||
Sent At
|
Sent At
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||||
Actions
|
{" "}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -123,7 +123,7 @@ function ChatsContainer() {
|
|||||||
<div className="flex w-full justify-between items-center">
|
<div className="flex w-full justify-between items-center">
|
||||||
<button
|
<button
|
||||||
onClick={handlePrevious}
|
onClick={handlePrevious}
|
||||||
className="px-4 py-2 rounded-lg border border-gray-800 dark:border-slate-200 text-gray-800 text-slate-200 disabled:invisible"
|
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
|
||||||
disabled={offset === 0}
|
disabled={offset === 0}
|
||||||
>
|
>
|
||||||
{" "}
|
{" "}
|
||||||
@ -131,7 +131,7 @@ function ChatsContainer() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
className="px-4 py-2 rounded-lg border border-gray-800 dark:border-slate-200 text-gray-800 text-slate-200 disabled:invisible"
|
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
|
||||||
disabled={!canNext}
|
disabled={!canNext}
|
||||||
>
|
>
|
||||||
Next Page
|
Next Page
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { titleCase } from "text-case";
|
import { titleCase } from "text-case";
|
||||||
import Admin from "../../../../models/admin";
|
import Admin from "../../../../models/admin";
|
||||||
|
import { Trash } from "@phosphor-icons/react";
|
||||||
|
|
||||||
export default function InviteRow({ invite }) {
|
export default function InviteRow({ invite }) {
|
||||||
const rowRef = useRef(null);
|
const rowRef = useRef(null);
|
||||||
@ -39,11 +40,11 @@ export default function InviteRow({ invite }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr ref={rowRef} className="bg-transparent">
|
<tr
|
||||||
<td
|
ref={rowRef}
|
||||||
scope="row"
|
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white font-mono"
|
|
||||||
>
|
>
|
||||||
|
<td scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||||
{titleCase(status)}
|
{titleCase(status)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
@ -61,16 +62,18 @@ export default function InviteRow({ invite }) {
|
|||||||
<button
|
<button
|
||||||
onClick={copyInviteLink}
|
onClick={copyInviteLink}
|
||||||
disabled={copied}
|
disabled={copied}
|
||||||
className="font-medium text-blue-600 dark:text-blue-300 px-2 py-1 rounded-lg hover:bg-blue-50 hover:dark:bg-blue-800 hover:dark:bg-opacity-20"
|
className="font-medium text-blue-300 rounded-lg hover:text-white hover:text-opacity-60 hover:underline"
|
||||||
>
|
>
|
||||||
{copied ? "Copied" : "Copy Invite Link"}
|
{copied ? "Copied" : "Copy Invite Link"}
|
||||||
</button>
|
</button>
|
||||||
|
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="font-medium text-red-600 dark:text-red-300 px-2 py-1 rounded-lg hover:bg-red-50 hover:dark:bg-red-800 hover:dark:bg-opacity-20"
|
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
||||||
>
|
>
|
||||||
Deactivate
|
<Trash className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
</td>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { X } from "react-feather";
|
import { X } from "react-feather";
|
||||||
import Admin from "../../../../models/admin";
|
import Admin from "../../../../models/admin";
|
||||||
|
|
||||||
const DIALOG_ID = `new-invite-modal`;
|
const DIALOG_ID = `new-invite-modal`;
|
||||||
|
|
||||||
function hideModal() {
|
function hideModal() {
|
||||||
@ -39,16 +40,16 @@ export default function NewInviteModal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||||
<div className="relative w-full max-w-2xl max-h-full">
|
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-xl font-semibold text-white">
|
||||||
Create new invite
|
Create new invite
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
data-modal-hide="staticModal"
|
data-modal-hide="staticModal"
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
@ -58,38 +59,36 @@ export default function NewInviteModal() {
|
|||||||
<div className="p-6 space-y-6 flex h-full w-full">
|
<div className="p-6 space-y-6 flex h-full w-full">
|
||||||
<div className="w-full flex flex-col gap-y-4">
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||||
Error: {error}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{invite && (
|
{invite && (
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
|
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="rounded-lg px-4 py-2 text-gray-800 bg-gray-100 dark:text-slate-200 dark:bg-stone-800"
|
className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-800 dark:text-slate-200 text-xs md:text-sm">
|
<p className="text-white text-xs md:text-sm">
|
||||||
After creation you will be able to copy the invite and send it
|
After creation you will be able to copy the invite and send it
|
||||||
to a new user where they can create an account as a default
|
to a new user where they can create an account as a default
|
||||||
user.
|
user.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
{!invite ? (
|
{!invite ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
Create Invite
|
Create Invite
|
||||||
</button>
|
</button>
|
||||||
@ -99,7 +98,7 @@ export default function NewInviteModal() {
|
|||||||
onClick={copyInviteLink}
|
onClick={copyInviteLink}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={copied}
|
disabled={copied}
|
||||||
className="w-full disabled:bg-green-200 disabled:text-green-600 text-gray-800 bg-gray-100 px-4 py-2 rounded-lg dark:text-slate-200 dark:bg-stone-900"
|
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center"
|
||||||
>
|
>
|
||||||
{copied ? "Copied Link" : "Copy Invite Link"}
|
{copied ? "Copied Link" : "Copy Invite Link"}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import * as Skeleton from "react-loading-skeleton";
|
import * as Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
@ -11,29 +13,27 @@ import NewInviteModal, { NewInviteModalId } from "./NewInviteModal";
|
|||||||
|
|
||||||
export default function AdminInvites() {
|
export default function AdminInvites() {
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="flex flex-col w-full px-1 md:px-8">
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
<div className="w-full flex flex-col gap-y-1">
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
<div className="items-center flex gap-x-4">
|
<div className="items-center flex gap-x-4">
|
||||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
<p className="text-2xl font-semibold text-white">Invitations</p>
|
||||||
Invitations
|
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
document?.getElementById(NewInviteModalId)?.showModal()
|
document?.getElementById(NewInviteModalId)?.showModal()
|
||||||
}
|
}
|
||||||
className="border border-slate-800 dark:border-slate-200 px-4 py-1 rounded-lg text-slate-800 dark:text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-800 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-800"
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
>
|
>
|
||||||
<Mail className="h-4 w-4" /> Create Invite Link
|
<Mail className="h-4 w-4" /> Create Invite Link
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-base text-slate-600 dark:text-slate-200">
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
Create invitation links for people in your organization to accept
|
Create invitation links for people in your organization to accept
|
||||||
and sign up with. Invitations can only be used by a single user.
|
and sign up with. Invitations can only be used by a single user.
|
||||||
</p>
|
</p>
|
||||||
@ -64,8 +64,8 @@ function InvitationsContainer() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="80vh"
|
height="80vh"
|
||||||
width="100%"
|
width="100%"
|
||||||
baseColor={darkMode ? "#2a3a53" : null}
|
highlightColor="#3D4147"
|
||||||
highlightColor={darkMode ? "#395073" : null}
|
baseColor="#2C2F35"
|
||||||
count={1}
|
count={1}
|
||||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex w-full"
|
containerClassName="flex w-full"
|
||||||
@ -74,8 +74,8 @@ function InvitationsContainer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-6 py-3">
|
<th scope="col" className="px-6 py-3">
|
||||||
Status
|
Status
|
||||||
@ -90,7 +90,7 @@ function InvitationsContainer() {
|
|||||||
Created
|
Created
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||||
Actions
|
{" "}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import Admin from "../../../models/admin";
|
import Admin from "../../../models/admin";
|
||||||
import showToast from "../../../utils/toast";
|
import showToast from "../../../utils/toast";
|
||||||
@ -39,11 +41,11 @@ export default function AdminSystem() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<form
|
<form
|
||||||
@ -51,35 +53,35 @@ export default function AdminSystem() {
|
|||||||
onChange={() => setHasChanges(true)}
|
onChange={() => setHasChanges(true)}
|
||||||
className="flex w-full"
|
className="flex w-full"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col w-full px-1 md:px-8">
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
<div className="w-full flex flex-col gap-y-1">
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
<div className="items-center flex gap-x-4">
|
<div className="items-center flex gap-x-4">
|
||||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
<p className="text-2xl font-semibold text-white">
|
||||||
System Preferences
|
System Preferences
|
||||||
</p>
|
</p>
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="border border-slate-800 dark:border-slate-200 px-4 py-1 rounded-lg text-slate-800 dark:text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-800 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-800"
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
>
|
>
|
||||||
{saving ? "Saving..." : "Save changes"}
|
{saving ? "Saving..." : "Save changes"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-base text-slate-600 dark:text-slate-200">
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
These are the overall settings and configurations of your
|
These are the overall settings and configurations of your
|
||||||
instance.
|
instance.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-4">
|
<div className="my-5">
|
||||||
<div className="flex flex-col gap-y-2 mb-2.5">
|
<div className="flex flex-col gap-y-2 mb-2.5">
|
||||||
<label className="leading-tight font-medium text-black dark:text-white">
|
<label className="leading-tight font-semibold text-white">
|
||||||
Users can delete workspaces
|
Users can delete workspaces
|
||||||
</label>
|
</label>
|
||||||
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
<p className="leading-tight text-sm text-white text-opacity-60 w-96">
|
||||||
allow non-admin users to delete workspaces that they are a
|
Allow non-admin users to delete workspaces that they are a
|
||||||
part of. This would delete the workspace for everyone.
|
part of. This would delete the workspace for everyone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +93,7 @@ export default function AdminSystem() {
|
|||||||
onChange={(e) => setCanDelete(e.target.checked)}
|
onChange={(e) => setCanDelete(e.target.checked)}
|
||||||
className="peer sr-only"
|
className="peer sr-only"
|
||||||
/>
|
/>
|
||||||
<div className="peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"></div>
|
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -101,7 +103,7 @@ export default function AdminSystem() {
|
|||||||
<label className="leading-tight font-medium text-black dark:text-white">
|
<label className="leading-tight font-medium text-black dark:text-white">
|
||||||
Limit messages per user per day
|
Limit messages per user per day
|
||||||
</label>
|
</label>
|
||||||
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
<p className="leading-tight text-sm text-white text-opacity-60 w-96">
|
||||||
Restrict non-admin users to a number of successful queries or
|
Restrict non-admin users to a number of successful queries or
|
||||||
chats within a 24 hour window. Enable this to prevent users
|
chats within a 24 hour window. Enable this to prevent users
|
||||||
from running up OpenAI costs.
|
from running up OpenAI costs.
|
||||||
@ -121,7 +123,7 @@ export default function AdminSystem() {
|
|||||||
}}
|
}}
|
||||||
className="peer sr-only"
|
className="peer sr-only"
|
||||||
/>
|
/>
|
||||||
<div className="peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"></div>
|
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { X } from "react-feather";
|
import { X } from "react-feather";
|
||||||
import Admin from "../../../../models/admin";
|
import Admin from "../../../../models/admin";
|
||||||
|
|
||||||
const DIALOG_ID = `new-user-modal`;
|
const DIALOG_ID = `new-user-modal`;
|
||||||
|
|
||||||
function hideModal() {
|
function hideModal() {
|
||||||
@ -24,15 +25,15 @@ export default function NewUserModal() {
|
|||||||
return (
|
return (
|
||||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||||
<div className="relative w-full max-w-2xl max-h-full">
|
<div className="relative w-full max-w-2xl max-h-full">
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-xl font-semibold text-white">
|
||||||
Add user to instance
|
Add user to instance
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
data-modal-hide="staticModal"
|
data-modal-hide="staticModal"
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
@ -44,14 +45,14 @@ export default function NewUserModal() {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="username"
|
htmlFor="username"
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
Username
|
Username
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder="User's username"
|
placeholder="User's username"
|
||||||
minLength={2}
|
minLength={2}
|
||||||
required={true}
|
required={true}
|
||||||
@ -61,14 +62,14 @@ export default function NewUserModal() {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="password"
|
name="password"
|
||||||
type="text"
|
type="text"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder="User's initial password"
|
placeholder="User's initial password"
|
||||||
required={true}
|
required={true}
|
||||||
minLength={8}
|
minLength={8}
|
||||||
@ -78,7 +79,7 @@ export default function NewUserModal() {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="role"
|
htmlFor="role"
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
Role
|
Role
|
||||||
</label>
|
</label>
|
||||||
@ -86,34 +87,32 @@ export default function NewUserModal() {
|
|||||||
name="role"
|
name="role"
|
||||||
required={true}
|
required={true}
|
||||||
defaultValue={"default"}
|
defaultValue={"default"}
|
||||||
className="rounded-lg bg-gray-50 px-4 py-2 text-sm text-gray-800 outline-none dark:text-slate-200 dark:bg-stone-600"
|
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value="default">Default</option>
|
<option value="default">Default</option>
|
||||||
<option value="admin">Administrator</option>
|
<option value="admin">Administrator</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||||
Error: {error}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-800 dark:text-slate-200 text-xs md:text-sm">
|
<p className="text-white text-xs md:text-sm">
|
||||||
After creating a user they will need to login with their
|
After creating a user they will need to login with their
|
||||||
initial login to get access.
|
initial login to get access.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
Add user
|
Add user
|
||||||
</button>
|
</button>
|
||||||
|
@ -3,11 +3,14 @@ import { X } from "react-feather";
|
|||||||
import Admin from "../../../../../models/admin";
|
import Admin from "../../../../../models/admin";
|
||||||
|
|
||||||
export const EditUserModalId = (user) => `edit-user-${user.id}-modal`;
|
export const EditUserModalId = (user) => `edit-user-${user.id}-modal`;
|
||||||
|
|
||||||
export default function EditUserModal({ user }) {
|
export default function EditUserModal({ user }) {
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
document.getElementById(EditUserModalId(user)).close();
|
document.getElementById(EditUserModalId(user)).close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (e) => {
|
const handleUpdate = async (e) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -24,16 +27,16 @@ export default function EditUserModal({ user }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog id={EditUserModalId(user)} className="bg-transparent outline-none">
|
<dialog id={EditUserModalId(user)} className="bg-transparent outline-none">
|
||||||
<div className="relative w-[75vw] max-w-2xl max-h-full">
|
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-xl font-semibold text-white">
|
||||||
Edit {user.username}
|
Edit {user.username}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
data-modal-hide="staticModal"
|
data-modal-hide="staticModal"
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
@ -45,14 +48,14 @@ export default function EditUserModal({ user }) {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="username"
|
htmlFor="username"
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
Username
|
Username
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder="User's username"
|
placeholder="User's username"
|
||||||
minLength={2}
|
minLength={2}
|
||||||
defaultValue={user.username}
|
defaultValue={user.username}
|
||||||
@ -63,14 +66,14 @@ export default function EditUserModal({ user }) {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
New Password
|
New Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="password"
|
name="password"
|
||||||
type="text"
|
type="text"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder={`${user.username}'s new password`}
|
placeholder={`${user.username}'s new password`}
|
||||||
minLength={8}
|
minLength={8}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -79,7 +82,7 @@ export default function EditUserModal({ user }) {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="role"
|
htmlFor="role"
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
Role
|
Role
|
||||||
</label>
|
</label>
|
||||||
@ -87,30 +90,28 @@ export default function EditUserModal({ user }) {
|
|||||||
name="role"
|
name="role"
|
||||||
required={true}
|
required={true}
|
||||||
defaultValue={user.role}
|
defaultValue={user.role}
|
||||||
className="rounded-lg bg-gray-50 px-4 py-2 text-sm text-gray-800 outline-none dark:text-slate-200 dark:bg-stone-600"
|
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value="default">Default</option>
|
<option value="default">Default</option>
|
||||||
<option value="admin">Administrator</option>
|
<option value="admin">Administrator</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||||
Error: {error}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
Update user
|
Update user
|
||||||
</button>
|
</button>
|
||||||
|
@ -2,6 +2,7 @@ import { useRef, useState } from "react";
|
|||||||
import { titleCase } from "text-case";
|
import { titleCase } from "text-case";
|
||||||
import Admin from "../../../../models/admin";
|
import Admin from "../../../../models/admin";
|
||||||
import EditUserModal, { EditUserModalId } from "./EditUserModal";
|
import EditUserModal, { EditUserModalId } from "./EditUserModal";
|
||||||
|
import { DotsThreeOutline } from "@phosphor-icons/react";
|
||||||
|
|
||||||
export default function UserRow({ currUser, user }) {
|
export default function UserRow({ currUser, user }) {
|
||||||
const rowRef = useRef(null);
|
const rowRef = useRef(null);
|
||||||
@ -29,11 +30,11 @@ export default function UserRow({ currUser, user }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr ref={rowRef} className="bg-transparent">
|
<tr
|
||||||
<th
|
ref={rowRef}
|
||||||
scope="row"
|
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
|
||||||
>
|
>
|
||||||
|
<th scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||||
{user.username}
|
{user.username}
|
||||||
</th>
|
</th>
|
||||||
<td className="px-6 py-4">{titleCase(user.role)}</td>
|
<td className="px-6 py-4">{titleCase(user.role)}</td>
|
||||||
@ -43,9 +44,9 @@ export default function UserRow({ currUser, user }) {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
document?.getElementById(EditUserModalId(user))?.showModal()
|
document?.getElementById(EditUserModalId(user))?.showModal()
|
||||||
}
|
}
|
||||||
className="font-medium text-blue-600 dark:text-blue-300 px-2 py-1 rounded-lg hover:bg-blue-50 hover:dark:bg-blue-800 hover:dark:bg-opacity-20"
|
className="font-medium text-white text-opacity-80 rounded-lg hover:text-white px-2 py-1 hover:text-opacity-60 hover:bg-white hover:bg-opacity-10"
|
||||||
>
|
>
|
||||||
Edit
|
<DotsThreeOutline weight="fill" className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
{currUser.id !== user.id && (
|
{currUser.id !== user.id && (
|
||||||
<>
|
<>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import * as Skeleton from "react-loading-skeleton";
|
import * as Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
import { UserPlus } from "react-feather";
|
import { UserPlus } from "react-feather";
|
||||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
|
||||||
import Admin from "../../../models/admin";
|
import Admin from "../../../models/admin";
|
||||||
import UserRow from "./UserRow";
|
import UserRow from "./UserRow";
|
||||||
import useUser from "../../../hooks/useUser";
|
import useUser from "../../../hooks/useUser";
|
||||||
@ -12,29 +13,27 @@ import NewUserModal, { NewUserModalId } from "./NewUserModal";
|
|||||||
|
|
||||||
export default function AdminUsers() {
|
export default function AdminUsers() {
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="flex flex-col w-full px-1 md:px-8">
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
<div className="w-full flex flex-col gap-y-1">
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
<div className="items-center flex gap-x-4">
|
<div className="items-center flex gap-x-4">
|
||||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
<p className="text-2xl font-semibold text-white">Users</p>
|
||||||
Instance users
|
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
document?.getElementById(NewUserModalId)?.showModal()
|
document?.getElementById(NewUserModalId)?.showModal()
|
||||||
}
|
}
|
||||||
className="border border-slate-800 dark:border-slate-200 px-4 py-1 rounded-lg text-slate-800 dark:text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-800 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-800"
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
>
|
>
|
||||||
<UserPlus className="h-4 w-4" /> Add user
|
<UserPlus className="h-4 w-4" /> Add user
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-base text-slate-600 dark:text-slate-200">
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
These are all the accounts which have an account on this instance.
|
These are all the accounts which have an account on this instance.
|
||||||
Removing an account will instantly remove their access to this
|
Removing an account will instantly remove their access to this
|
||||||
instance.
|
instance.
|
||||||
@ -50,7 +49,6 @@ export default function AdminUsers() {
|
|||||||
|
|
||||||
function UsersContainer() {
|
function UsersContainer() {
|
||||||
const { user: currUser } = useUser();
|
const { user: currUser } = useUser();
|
||||||
const darkMode = usePrefersDarkMode();
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -67,8 +65,8 @@ function UsersContainer() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="80vh"
|
height="80vh"
|
||||||
width="100%"
|
width="100%"
|
||||||
baseColor={darkMode ? "#2a3a53" : null}
|
highlightColor="#3D4147"
|
||||||
highlightColor={darkMode ? "#395073" : null}
|
baseColor="#2C2F35"
|
||||||
count={1}
|
count={1}
|
||||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex w-full"
|
containerClassName="flex w-full"
|
||||||
@ -77,8 +75,8 @@ function UsersContainer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||||
Username
|
Username
|
||||||
@ -87,10 +85,10 @@ function UsersContainer() {
|
|||||||
Role
|
Role
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="px-6 py-3">
|
<th scope="col" className="px-6 py-3">
|
||||||
Created On
|
Date Added
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||||
Actions
|
{" "}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -21,16 +21,16 @@ export default function NewWorkspaceModal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||||
<div className="relative w-full max-w-2xl max-h-full">
|
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-xl font-semibold text-white">
|
||||||
Add workspace to Instance
|
Create new workspace
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
data-modal-hide="staticModal"
|
data-modal-hide="staticModal"
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
@ -42,14 +42,14 @@ export default function NewWorkspaceModal() {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
Workspace name
|
Workspace name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
placeholder="My workspace"
|
placeholder="My workspace"
|
||||||
minLength={4}
|
minLength={4}
|
||||||
required={true}
|
required={true}
|
||||||
@ -57,27 +57,25 @@ export default function NewWorkspaceModal() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||||
Error: {error}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-800 dark:text-slate-200 text-xs md:text-sm">
|
<p className="text-white text-opacity-60 text-xs md:text-sm">
|
||||||
After creating this workspace only admins will be able to see
|
After creating this workspace only admins will be able to see
|
||||||
it. You can add users after it has been created.
|
it. You can add users after it has been created.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-600">
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
Create workspace
|
Create workspace
|
||||||
</button>
|
</button>
|
||||||
|
@ -5,11 +5,14 @@ import { titleCase } from "text-case";
|
|||||||
|
|
||||||
export const EditWorkspaceUsersModalId = (workspace) =>
|
export const EditWorkspaceUsersModalId = (workspace) =>
|
||||||
`edit-workspace-${workspace.id}-modal`;
|
`edit-workspace-${workspace.id}-modal`;
|
||||||
|
|
||||||
export default function EditWorkspaceUsersModal({ workspace, users }) {
|
export default function EditWorkspaceUsersModal({ workspace, users }) {
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
document.getElementById(EditWorkspaceUsersModalId(workspace)).close();
|
document.getElementById(EditWorkspaceUsersModalId(workspace)).close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (e) => {
|
const handleUpdate = async (e) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -36,16 +39,16 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
|||||||
id={EditWorkspaceUsersModalId(workspace)}
|
id={EditWorkspaceUsersModalId(workspace)}
|
||||||
className="bg-transparent outline-none"
|
className="bg-transparent outline-none"
|
||||||
>
|
>
|
||||||
<div className="relative w-[75vw] max-w-2xl max-h-full">
|
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-xl font-semibold text-white">
|
||||||
Edit {workspace.name}
|
Edit {workspace.name}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
data-modal-hide="staticModal"
|
data-modal-hide="staticModal"
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
@ -61,7 +64,7 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
|||||||
<div
|
<div
|
||||||
key={`workspace-${workspace.id}-user-${user.id}`}
|
key={`workspace-${workspace.id}-user-${user.id}`}
|
||||||
data-workspace={workspace.id}
|
data-workspace={workspace.id}
|
||||||
className="flex items-center pl-4 border border-gray-200 rounded dark:border-gray-400 group hover:bg-stone-600 cursor-pointer"
|
className="flex items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document
|
document
|
||||||
.getElementById(
|
.getElementById(
|
||||||
@ -76,11 +79,11 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
value="yes"
|
value="yes"
|
||||||
name={`user-${user.id}`}
|
name={`user-${user.id}`}
|
||||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 pointer-events-none"
|
className="w-4 h-4 text-blue-600 bg-zinc-900 border border-gray-500/50 rounded focus:ring-blue-500 focus:border-blue-500 pointer-events-none"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={`user-${user.id}`}
|
htmlFor={`user-${user.id}`}
|
||||||
className="pointer-events-none w-full py-4 ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
className="pointer-events-none w-full py-4 ml-2 text-sm font-medium text-white"
|
||||||
>
|
>
|
||||||
{titleCase(user.username)}
|
{titleCase(user.username)}
|
||||||
</label>
|
</label>
|
||||||
@ -90,7 +93,7 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
|||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full p-4 flex dark:text-slate-200 text-gray-800 items-center pl-4 border border-gray-200 rounded dark:border-gray-400 group hover:bg-stone-600 cursor-pointer"
|
className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document
|
document
|
||||||
.getElementById(`workspace-${workspace.id}-select-all`)
|
.getElementById(`workspace-${workspace.id}-select-all`)
|
||||||
@ -108,7 +111,7 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full p-4 flex dark:text-slate-200 text-gray-800 items-center pl-4 border border-gray-200 rounded dark:border-gray-400 group hover:bg-stone-600 cursor-pointer"
|
className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document
|
document
|
||||||
.getElementById(`workspace-${workspace.id}-select-all`)
|
.getElementById(`workspace-${workspace.id}-select-all`)
|
||||||
@ -126,23 +129,21 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||||
Error: {error}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
Update workspace
|
Update workspace
|
||||||
</button>
|
</button>
|
||||||
|
@ -4,6 +4,7 @@ import paths from "../../../../utils/paths";
|
|||||||
import EditWorkspaceUsersModal, {
|
import EditWorkspaceUsersModal, {
|
||||||
EditWorkspaceUsersModalId,
|
EditWorkspaceUsersModalId,
|
||||||
} from "./EditWorkspaceUsersModal";
|
} from "./EditWorkspaceUsersModal";
|
||||||
|
import { DotsThreeOutline, LinkSimple, Trash } from "@phosphor-icons/react";
|
||||||
|
|
||||||
export default function WorkspaceRow({ workspace, users }) {
|
export default function WorkspaceRow({ workspace, users }) {
|
||||||
const rowRef = useRef(null);
|
const rowRef = useRef(null);
|
||||||
@ -20,20 +21,20 @@ export default function WorkspaceRow({ workspace, users }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr ref={rowRef} className="bg-transparent">
|
<tr
|
||||||
<th
|
ref={rowRef}
|
||||||
scope="row"
|
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
|
||||||
>
|
>
|
||||||
|
<th scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||||
{workspace.name}
|
{workspace.name}
|
||||||
</th>
|
</th>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4 flex items-center">
|
||||||
<a
|
<a
|
||||||
href={paths.workspace.chat(workspace.slug)}
|
href={paths.workspace.chat(workspace.slug)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-500"
|
className="text-white flex items-center hover:underline"
|
||||||
>
|
>
|
||||||
{workspace.slug}
|
<LinkSimple className="mr-2 w-5 h-5" /> {workspace.slug}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">{workspace.userIds?.length}</td>
|
<td className="px-6 py-4">{workspace.userIds?.length}</td>
|
||||||
@ -45,15 +46,15 @@ export default function WorkspaceRow({ workspace, users }) {
|
|||||||
?.getElementById(EditWorkspaceUsersModalId(workspace))
|
?.getElementById(EditWorkspaceUsersModalId(workspace))
|
||||||
?.showModal()
|
?.showModal()
|
||||||
}
|
}
|
||||||
className="font-medium text-blue-600 dark:text-blue-300 px-2 py-1 rounded-lg hover:bg-blue-50 hover:dark:bg-blue-800 hover:dark:bg-opacity-20"
|
className="font-medium rounded-lg hover:text-white hover:text-opacity-60 px-2 py-1 hover:bg-white hover:bg-opacity-10"
|
||||||
>
|
>
|
||||||
Edit Users
|
<DotsThreeOutline weight="fill" className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="font-medium text-red-600 dark:text-red-300 px-2 py-1 rounded-lg hover:bg-red-50 hover:dark:bg-red-800 hover:dark:bg-opacity-20"
|
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
||||||
>
|
>
|
||||||
Delete
|
<Trash className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import * as Skeleton from "react-loading-skeleton";
|
import * as Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
@ -11,29 +13,29 @@ import NewWorkspaceModal, { NewWorkspaceModalId } from "./NewWorkspaceModal";
|
|||||||
|
|
||||||
export default function AdminWorkspaces() {
|
export default function AdminWorkspaces() {
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="flex flex-col w-full px-1 md:px-8">
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
<div className="w-full flex flex-col gap-y-1">
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
<div className="items-center flex gap-x-4">
|
<div className="items-center flex gap-x-4">
|
||||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
<p className="text-2xl font-semibold text-white">
|
||||||
Instance workspaces
|
Instance workspaces
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
document?.getElementById(NewWorkspaceModalId)?.showModal()
|
document?.getElementById(NewWorkspaceModalId)?.showModal()
|
||||||
}
|
}
|
||||||
className="border border-slate-800 dark:border-slate-200 px-4 py-1 rounded-lg text-slate-800 dark:text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-800 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-800"
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
>
|
>
|
||||||
<BookOpen className="h-4 w-4" /> New Workspace
|
<BookOpen className="h-4 w-4" /> New Workspace
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-base text-slate-600 dark:text-slate-200">
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
These are all the workspaces that exist on this instance. Removing
|
These are all the workspaces that exist on this instance. Removing
|
||||||
a workspace will delete all of it's associated chats and settings.
|
a workspace will delete all of it's associated chats and settings.
|
||||||
</p>
|
</p>
|
||||||
@ -68,8 +70,8 @@ function WorkspacesContainer() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="80vh"
|
height="80vh"
|
||||||
width="100%"
|
width="100%"
|
||||||
baseColor={darkMode ? "#2a3a53" : null}
|
highlightColor="#3D4147"
|
||||||
highlightColor={darkMode ? "#395073" : null}
|
baseColor="#2C2F35"
|
||||||
count={1}
|
count={1}
|
||||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex w-full"
|
containerClassName="flex w-full"
|
||||||
@ -78,8 +80,8 @@ function WorkspacesContainer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||||
Name
|
Name
|
||||||
@ -94,7 +96,7 @@ function WorkspacesContainer() {
|
|||||||
Created On
|
Created On
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||||
Actions
|
{" "}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Admin from "../../../../models/admin";
|
import Admin from "../../../../models/admin";
|
||||||
import showToast from "../../../../utils/toast";
|
import showToast from "../../../../utils/toast";
|
||||||
|
import { Trash } from "@phosphor-icons/react";
|
||||||
|
import { userFromStorage } from "../../../../utils/request";
|
||||||
|
import System from "../../../../models/system";
|
||||||
|
|
||||||
export default function ApiKeyRow({ apiKey }) {
|
export default function ApiKeyRow({ apiKey }) {
|
||||||
const rowRef = useRef(null);
|
const rowRef = useRef(null);
|
||||||
@ -15,9 +18,13 @@ export default function ApiKeyRow({ apiKey }) {
|
|||||||
if (rowRef?.current) {
|
if (rowRef?.current) {
|
||||||
rowRef.current.remove();
|
rowRef.current.remove();
|
||||||
}
|
}
|
||||||
await Admin.deleteApiKey(apiKey.id);
|
|
||||||
|
const user = userFromStorage();
|
||||||
|
const Model = !!user ? Admin : System;
|
||||||
|
await Model.deleteApiKey(apiKey.id);
|
||||||
showToast("API Key permanently deleted", "info");
|
showToast("API Key permanently deleted", "info");
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyApiKey = () => {
|
const copyApiKey = () => {
|
||||||
if (!apiKey) return false;
|
if (!apiKey) return false;
|
||||||
window.navigator.clipboard.writeText(apiKey.secret);
|
window.navigator.clipboard.writeText(apiKey.secret);
|
||||||
@ -37,30 +44,30 @@ export default function ApiKeyRow({ apiKey }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr ref={rowRef} className="bg-transparent">
|
<tr
|
||||||
<td
|
ref={rowRef}
|
||||||
scope="row"
|
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white font-mono"
|
|
||||||
>
|
>
|
||||||
|
<td scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||||
{apiKey.secret}
|
{apiKey.secret}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4 text-center">
|
||||||
{apiKey.createdBy?.username || "unknown user"}
|
{apiKey.createdBy?.username || "--"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">{apiKey.createdAt}</td>
|
<td className="px-6 py-4">{apiKey.createdAt}</td>
|
||||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||||
<button
|
<button
|
||||||
onClick={copyApiKey}
|
onClick={copyApiKey}
|
||||||
disabled={copied}
|
disabled={copied}
|
||||||
className="font-medium text-blue-600 dark:text-blue-300 px-2 py-1 rounded-lg hover:bg-blue-50 hover:dark:bg-blue-800 hover:dark:bg-opacity-20"
|
className="font-medium text-blue-300 rounded-lg hover:text-white hover:text-opacity-60 hover:underline"
|
||||||
>
|
>
|
||||||
{copied ? "Copied" : "Copy API Key"}
|
{copied ? "Copied" : "Copy API Key"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="font-medium text-red-600 dark:text-red-300 px-2 py-1 rounded-lg hover:bg-red-50 hover:dark:bg-red-800 hover:dark:bg-opacity-20"
|
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
||||||
>
|
>
|
||||||
Deactivate API Key
|
<Trash className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
@ -2,6 +2,9 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { X } from "react-feather";
|
import { X } from "react-feather";
|
||||||
import Admin from "../../../../models/admin";
|
import Admin from "../../../../models/admin";
|
||||||
import paths from "../../../../utils/paths";
|
import paths from "../../../../utils/paths";
|
||||||
|
import { userFromStorage } from "../../../../utils/request";
|
||||||
|
import System from "../../../../models/system";
|
||||||
|
|
||||||
const DIALOG_ID = `new-api-key-modal`;
|
const DIALOG_ID = `new-api-key-modal`;
|
||||||
|
|
||||||
function hideModal() {
|
function hideModal() {
|
||||||
@ -17,7 +20,10 @@ export default function NewApiKeyModal() {
|
|||||||
const handleCreate = async (e) => {
|
const handleCreate = async (e) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { apiKey: newApiKey, error } = await Admin.generateApiKey();
|
const user = userFromStorage();
|
||||||
|
const Model = !!user ? Admin : System;
|
||||||
|
|
||||||
|
const { apiKey: newApiKey, error } = await Model.generateApiKey();
|
||||||
if (!!newApiKey) setApiKey(newApiKey);
|
if (!!newApiKey) setApiKey(newApiKey);
|
||||||
setError(error);
|
setError(error);
|
||||||
};
|
};
|
||||||
@ -38,16 +44,16 @@ export default function NewApiKeyModal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||||
<div className="relative w-full max-w-2xl max-h-full">
|
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-xl font-semibold text-white">
|
||||||
Create new API key
|
Create new API key
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
data-modal-hide="staticModal"
|
data-modal-hide="staticModal"
|
||||||
>
|
>
|
||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
@ -57,44 +63,42 @@ export default function NewApiKeyModal() {
|
|||||||
<div className="p-6 space-y-6 flex h-full w-full">
|
<div className="p-6 space-y-6 flex h-full w-full">
|
||||||
<div className="w-full flex flex-col gap-y-4">
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||||
Error: {error}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{apiKey && (
|
{apiKey && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={`${apiKey.secret}`}
|
defaultValue={`${apiKey.secret}`}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="rounded-lg px-4 py-2 text-gray-800 bg-gray-100 dark:text-slate-200 dark:bg-stone-800"
|
className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-800 dark:text-slate-200 text-xs md:text-sm">
|
<p className="text-white text-xs md:text-sm">
|
||||||
Once created the API key can be used to programmatically
|
Once created the API key can be used to programmatically
|
||||||
access and configure this AnythingLLM instance.
|
access and configure this AnythingLLM instance.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href={paths.apiDocs()}
|
href={paths.apiDocs()}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 dark:text-blue-300 hover:underline"
|
className="text-blue-400 hover:underline"
|
||||||
>
|
>
|
||||||
Read the API documentation →
|
Read the API documentation →
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
{!apiKey ? (
|
{!apiKey ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
Create API key
|
Create API key
|
||||||
</button>
|
</button>
|
||||||
@ -104,7 +108,7 @@ export default function NewApiKeyModal() {
|
|||||||
onClick={copyApiKey}
|
onClick={copyApiKey}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={copied}
|
disabled={copied}
|
||||||
className="w-full disabled:bg-green-200 disabled:text-green-600 text-gray-800 bg-gray-100 px-4 py-2 rounded-lg dark:text-slate-200 dark:bg-stone-900"
|
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center"
|
||||||
>
|
>
|
||||||
{copied ? "Copied API key" : "Copy API key"}
|
{copied ? "Copied API key" : "Copy API key"}
|
||||||
</button>
|
</button>
|
@ -1,47 +1,48 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import * as Skeleton from "react-loading-skeleton";
|
import * as Skeleton from "react-loading-skeleton";
|
||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
import { PlusCircle } from "react-feather";
|
import { PlusCircle } from "react-feather";
|
||||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
|
||||||
import Admin from "../../../models/admin";
|
import Admin from "../../../models/admin";
|
||||||
import ApiKeyRow from "./ApiKeyRow";
|
import ApiKeyRow from "./ApiKeyRow";
|
||||||
import NewApiKeyModal, { NewApiKeyModalId } from "./NewApiKeyModal";
|
import NewApiKeyModal, { NewApiKeyModalId } from "./NewApiKeyModal";
|
||||||
import paths from "../../../utils/paths";
|
import paths from "../../../utils/paths";
|
||||||
|
import { userFromStorage } from "../../../utils/request";
|
||||||
|
import System from "../../../models/system";
|
||||||
|
|
||||||
export default function AdminApiKeys() {
|
export default function AdminApiKeys() {
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="flex flex-col w-full px-1 md:px-8">
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
<div className="w-full flex flex-col gap-y-1">
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
<div className="items-center flex gap-x-4">
|
<div className="items-center flex gap-x-4">
|
||||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
<p className="text-2xl font-semibold text-white">API Keys</p>
|
||||||
API Keys
|
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
document?.getElementById(NewApiKeyModalId)?.showModal()
|
document?.getElementById(NewApiKeyModalId)?.showModal()
|
||||||
}
|
}
|
||||||
className="border border-slate-800 dark:border-slate-200 px-4 py-1 rounded-lg text-slate-800 dark:text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-800 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-800"
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
>
|
>
|
||||||
<PlusCircle className="h-4 w-4" /> Generate New API Key
|
<PlusCircle className="h-4 w-4" /> Generate New API Key
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-base text-slate-600 dark:text-slate-200">
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
API keys allow the holder to programmatically access and manage
|
API keys allow the holder to programmatically access and manage
|
||||||
this AnythingLLM instance.
|
this AnythingLLM instance.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href={paths.apiDocs()}
|
href={paths.apiDocs()}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 dark:text-blue-300 hover:underline"
|
className="text-sm font-base text-blue-300 hover:underline"
|
||||||
>
|
>
|
||||||
Read the API documentation →
|
Read the API documentation →
|
||||||
</a>
|
</a>
|
||||||
@ -55,12 +56,14 @@ export default function AdminApiKeys() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ApiKeysContainer() {
|
function ApiKeysContainer() {
|
||||||
const darkMode = usePrefersDarkMode();
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [apiKeys, setApiKeys] = useState([]);
|
const [apiKeys, setApiKeys] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchExistingKeys() {
|
async function fetchExistingKeys() {
|
||||||
const { apiKeys: foundKeys } = await Admin.getApiKeys();
|
const user = userFromStorage();
|
||||||
|
const Model = !!user ? Admin : System;
|
||||||
|
|
||||||
|
const { apiKeys: foundKeys } = await Model.getApiKeys();
|
||||||
setApiKeys(foundKeys);
|
setApiKeys(foundKeys);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -72,8 +75,8 @@ function ApiKeysContainer() {
|
|||||||
<Skeleton.default
|
<Skeleton.default
|
||||||
height="80vh"
|
height="80vh"
|
||||||
width="100%"
|
width="100%"
|
||||||
baseColor={darkMode ? "#2a3a53" : null}
|
highlightColor="#3D4147"
|
||||||
highlightColor={darkMode ? "#395073" : null}
|
baseColor="#2C2F35"
|
||||||
count={1}
|
count={1}
|
||||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||||
containerClassName="flex w-full"
|
containerClassName="flex w-full"
|
||||||
@ -82,10 +85,10 @@ function ApiKeysContainer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-6 py-3">
|
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||||
API Key
|
API Key
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="px-6 py-3">
|
<th scope="col" className="px-6 py-3">
|
||||||
@ -95,7 +98,7 @@ function ApiKeysContainer() {
|
|||||||
Created
|
Created
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||||
Actions
|
{" "}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
@ -1,27 +1,30 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import Admin from "../../../models/admin";
|
import Admin from "../../../models/admin";
|
||||||
import AnythingLLMLight from "../../../media/logo/anything-llm-light.png";
|
import AnythingLLM from "../../../media/logo/anything-llm.png";
|
||||||
import AnythingLLMDark from "../../../media/logo/anything-llm-dark.png";
|
|
||||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
|
||||||
import useLogo from "../../../hooks/useLogo";
|
import useLogo from "../../../hooks/useLogo";
|
||||||
import System from "../../../models/system";
|
import System from "../../../models/system";
|
||||||
import EditingChatBubble from "../../../components/EditingChatBubble";
|
import EditingChatBubble from "../../../components/EditingChatBubble";
|
||||||
import showToast from "../../../utils/toast";
|
import showToast from "../../../utils/toast";
|
||||||
|
import { Plus } from "@phosphor-icons/react";
|
||||||
|
|
||||||
export default function Appearance() {
|
export default function Appearance() {
|
||||||
const { logo: _initLogo } = useLogo();
|
const { logo: _initLogo } = useLogo();
|
||||||
const [logo, setLogo] = useState("");
|
const [logo, setLogo] = useState("");
|
||||||
const prefersDarkMode = usePrefersDarkMode();
|
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function setInitLogo() {
|
async function logoInit() {
|
||||||
setLogo(_initLogo || "");
|
setLogo(_initLogo || "");
|
||||||
|
const _isDefaultLogo = await System.isDefaultLogo();
|
||||||
|
setIsDefaultLogo(_isDefaultLogo);
|
||||||
}
|
}
|
||||||
setInitLogo();
|
logoInit();
|
||||||
}, [_initLogo]);
|
}, [_initLogo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -36,29 +39,36 @@ export default function Appearance() {
|
|||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (!file) return false;
|
if (!file) return false;
|
||||||
|
|
||||||
|
const objectURL = URL.createObjectURL(file);
|
||||||
|
setLogo(objectURL);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("logo", file);
|
formData.append("logo", file);
|
||||||
const { success, error } = await Admin.uploadLogo(formData);
|
const { success, error } = await System.uploadLogo(formData);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
showToast(`Failed to upload logo: ${error}`, "error");
|
showToast(`Failed to upload logo: ${error}`, "error");
|
||||||
|
setLogo(_initLogo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoURL = await System.fetchLogo();
|
|
||||||
setLogo(logoURL);
|
|
||||||
showToast("Image uploaded successfully.", "success");
|
showToast("Image uploaded successfully.", "success");
|
||||||
|
setIsDefaultLogo(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveLogo = async () => {
|
const handleRemoveLogo = async () => {
|
||||||
const { success, error } = await Admin.removeCustomLogo();
|
setLogo("");
|
||||||
|
setIsDefaultLogo(true);
|
||||||
|
|
||||||
|
const { success, error } = await System.removeCustomLogo();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
console.error("Failed to remove logo:", error);
|
console.error("Failed to remove logo:", error);
|
||||||
showToast(`Failed to remove logo: ${error}`, "error");
|
showToast(`Failed to remove logo: ${error}`, "error");
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
setLogo(logoURL);
|
||||||
|
setIsDefaultLogo(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoURL = await System.fetchLogo();
|
|
||||||
setLogo(logoURL);
|
|
||||||
showToast("Image successfully removed.", "success");
|
showToast("Image successfully removed.", "success");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,7 +99,7 @@ export default function Appearance() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMessageSave = async () => {
|
const handleMessageSave = async () => {
|
||||||
const { success, error } = await Admin.setWelcomeMessages(messages);
|
const { success, error } = await System.setWelcomeMessages(messages);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
showToast(`Failed to update welcome messages: ${error}`, "error");
|
showToast(`Failed to update welcome messages: ${error}`, "error");
|
||||||
return;
|
return;
|
||||||
@ -99,29 +109,31 @@ export default function Appearance() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="px-1 md:px-8">
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
<div className="mb-6">
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
<div className="items-center flex gap-x-4">
|
||||||
|
<p className="text-2xl font-semibold text-white">
|
||||||
Appearance Settings
|
Appearance Settings
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-sm font-base text-slate-600 dark:text-slate-200">
|
</div>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
Customize the appearance settings of your platform.
|
Customize the appearance settings of your platform.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6">
|
<div className="my-6">
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<h2 className="leading-tight font-medium text-black dark:text-white">
|
<h2 className="leading-tight font-medium text-white">
|
||||||
Custom Logo
|
Custom Logo
|
||||||
</h2>
|
</h2>
|
||||||
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
<p className="text-sm font-base text-white/60">
|
||||||
Change the logo that appears in the sidebar.
|
Upload your custom logo to make your chatbot yours.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex md:flex-row flex-col items-center">
|
<div className="flex md:flex-row flex-col items-center">
|
||||||
@ -129,34 +141,45 @@ export default function Appearance() {
|
|||||||
src={logo}
|
src={logo}
|
||||||
alt="Uploaded Logo"
|
alt="Uploaded Logo"
|
||||||
className="w-48 h-48 object-contain mr-6"
|
className="w-48 h-48 object-contain mr-6"
|
||||||
onError={(e) =>
|
hidden={isDefaultLogo}
|
||||||
(e.target.src = prefersDarkMode
|
onError={(e) => (e.target.src = AnythingLLM)}
|
||||||
? AnythingLLMLight
|
|
||||||
: AnythingLLMDark)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-row gap-x-8">
|
||||||
<div className="mb-4">
|
<label
|
||||||
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
className="mt-5 transition-all duration-300 hover:opacity-60"
|
||||||
Upload Image
|
hidden={!isDefaultLogo}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
|
id="logo-upload"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileUpload}
|
onChange={handleFileUpload}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
className="w-80 py-4 bg-zinc-900/50 rounded-2xl border-2 border-dashed border-white border-opacity-60 justify-center items-center inline-flex cursor-pointer"
|
||||||
|
htmlFor="logo-upload"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="rounded-full bg-white/40">
|
||||||
|
<Plus className="w-6 h-6 text-black/80 m-2" />
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
|
||||||
|
Add a custom logo
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-60 text-xs font-medium py-1">
|
||||||
|
Recommended size: 800 x 200
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={handleRemoveLogo}
|
onClick={handleRemoveLogo}
|
||||||
className="ml-4 cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
className="text-white text-base font-medium hover:text-opacity-60"
|
||||||
>
|
>
|
||||||
Remove Custom Logo
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
Upload your logo. Recommended size: 800x200.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@ -164,11 +187,11 @@ export default function Appearance() {
|
|||||||
<h2 className="leading-tight font-medium text-black dark:text-white">
|
<h2 className="leading-tight font-medium text-black dark:text-white">
|
||||||
Custom Messages
|
Custom Messages
|
||||||
</h2>
|
</h2>
|
||||||
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
<p className="text-sm font-base text-white/60">
|
||||||
Change the default messages that are displayed to the users.
|
Customize the automatic messages displayed to your users.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex flex-col gap-y-6">
|
<div className="mt-6 flex flex-col gap-y-6 bg-zinc-900 rounded-lg px-6 pt-4 max-w-[700px]">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => (
|
||||||
<div key={index} className="flex flex-col gap-y-2">
|
<div key={index} className="flex flex-col gap-y-2">
|
||||||
{message.user && (
|
{message.user && (
|
||||||
@ -191,18 +214,24 @@ export default function Appearance() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="flex gap-4 mt-4 justify-between">
|
<div className="flex gap-4 mt-12 justify-between pb-7">
|
||||||
<button
|
<button
|
||||||
className="self-end text-orange-500 hover:text-orange-700 transition"
|
className="self-end text-white hover:text-white/60 transition"
|
||||||
onClick={() => addMessage("response")}
|
onClick={() => addMessage("response")}
|
||||||
>
|
>
|
||||||
+ System Message
|
<div className="flex items-center justify-start">
|
||||||
|
<Plus className="w-5 h-5 m-2" weight="fill" /> New System
|
||||||
|
Message
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="self-end text-orange-500 hover:text-orange-700 transition"
|
className="self-end text-sky-400 hover:text-sky-400/60 transition"
|
||||||
onClick={() => addMessage("user")}
|
onClick={() => addMessage("user")}
|
||||||
>
|
>
|
||||||
+ User Message
|
<div className="flex items-center">
|
||||||
|
<Plus className="w-5 h-5 m-2" weight="fill" /> New User
|
||||||
|
Message
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
190
frontend/src/pages/GeneralSettings/ExportImport/index.jsx
Normal file
190
frontend/src/pages/GeneralSettings/ExportImport/index.jsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import Admin from "../../../models/admin";
|
||||||
|
import showToast from "../../../utils/toast";
|
||||||
|
import { CloudArrowUp, DownloadSimple } from "@phosphor-icons/react";
|
||||||
|
import System from "../../../models/system";
|
||||||
|
import { API_BASE } from "../../../utils/constants";
|
||||||
|
|
||||||
|
export default function GeneralExportImport() {
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
|
{!isMobile && <Sidebar />}
|
||||||
|
<div
|
||||||
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
|
>
|
||||||
|
{isMobile && <SidebarMobileHeader />}
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
|
<div className="items-center flex gap-x-4">
|
||||||
|
<p className="text-2xl font-semibold text-white">
|
||||||
|
Export or Import
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
Have multiple AnythingLLM instances or simply want to backup or
|
||||||
|
re-import data from another instance? You can do so here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-sm font-medium py-4">
|
||||||
|
This will not automatically sync your vector database embeddings.
|
||||||
|
</div>
|
||||||
|
<ImportData />
|
||||||
|
<ExportData />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportData() {
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
|
||||||
|
const startInput = () => inputRef?.current?.click();
|
||||||
|
|
||||||
|
const handleUpload = async (e) => {
|
||||||
|
setLoading(true);
|
||||||
|
e.preventDefault();
|
||||||
|
setFile(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
showToast("Invalid file upload", "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFile(file);
|
||||||
|
setLoading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file, file.name);
|
||||||
|
const { success, error } = await System.importData(formData);
|
||||||
|
if (!success) {
|
||||||
|
showToast(`Failed to import data: ${error}`, "error");
|
||||||
|
} else {
|
||||||
|
setResult(true);
|
||||||
|
showToast(`Successfully imported ${file.name}`, "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
setFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={startInput}
|
||||||
|
className="max-w-[600px] py-4 bg-zinc-900/50 rounded-2xl border-2 border-dashed border-white border-opacity-60 justify-center items-center inline-flex transition-all duration-300 hover:opacity-60 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center gap-2 animate-pulse">
|
||||||
|
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
|
||||||
|
Importing
|
||||||
|
</div>
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-white border-t-transparent " />
|
||||||
|
</div>
|
||||||
|
) : !!result ? (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<CloudArrowUp className="w-8 h-8 text-green-400" />
|
||||||
|
<div className="text-green-400 text-opacity-80 text-sm font-semibold py-1">
|
||||||
|
Import Successful
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={handleUpload}
|
||||||
|
name="import"
|
||||||
|
type="file"
|
||||||
|
multiple="false"
|
||||||
|
accept=".zip"
|
||||||
|
hidden={true}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<CloudArrowUp className="w-8 h-8 text-white/80" />
|
||||||
|
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
|
||||||
|
Import AnythingLLM Data
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-60 text-xs font-medium py-1">
|
||||||
|
This must be an export from an AnythingLLM instance.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExportData() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
|
||||||
|
const exportData = async function () {
|
||||||
|
setLoading(true);
|
||||||
|
const { filename, error } = await System.dataExport();
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (!filename) {
|
||||||
|
showToast(`Failed to export data: ${error}`, "error");
|
||||||
|
} else {
|
||||||
|
setResult(filename);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = `${API_BASE}/system/data-exports/${filename}`;
|
||||||
|
link.target = "_blank";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={exportData}
|
||||||
|
className="transition-all max-w-[600px] bg-white rounded-lg justify-center items-center my-8 text-zinc-900 border-transparent border-2 cursor-not-allowed animate-pulse"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div className="duration-300 text-center text-sm font-bold py-3">
|
||||||
|
Exporting
|
||||||
|
</div>
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-zinc-900 border-t-transparent " />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!result) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={`${API_BASE}/system/data-exports/${result}`}
|
||||||
|
className="transition-all max-w-[600px] bg-green-100 hover:bg-zinc-900/50 hover:text-white hover:border-white rounded-lg justify-center items-center my-8 text-zinc-900 border-transparent border-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div className="duration-300 text-center text-sm font-bold py-3">
|
||||||
|
Download Data Export
|
||||||
|
</div>
|
||||||
|
<DownloadSimple className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={exportData}
|
||||||
|
className="transition-all max-w-[600px] bg-white rounded-lg justify-center items-center my-8 cursor-pointer text-zinc-900 border-transparent border-2 hover:bg-zinc-900/50 hover:text-white hover:border-white"
|
||||||
|
>
|
||||||
|
<div className="duration-300 text-center text-sm font-bold py-3">
|
||||||
|
Export AnythingLLM Data
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
256
frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
Normal file
256
frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import System from "../../../models/system";
|
||||||
|
import showToast from "../../../utils/toast";
|
||||||
|
import OpenAiLogo from "../../../media/llmprovider/openai.png";
|
||||||
|
import AzureOpenAiLogo from "../../../media/llmprovider/azure.png";
|
||||||
|
import AnthropicLogo from "../../../media/llmprovider/anthropic.png";
|
||||||
|
import PreLoader from "../../../components/Preloader";
|
||||||
|
import LLMProviderOption from "../../../components/LLMProviderOption";
|
||||||
|
|
||||||
|
export default function GeneralLLMPreference() {
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [llmChoice, setLLMChoice] = useState("openai");
|
||||||
|
const [settings, setSettings] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
const data = {};
|
||||||
|
const form = new FormData(e.target);
|
||||||
|
for (var [key, value] of form.entries()) data[key] = value;
|
||||||
|
const { error } = await System.updateSystem(data);
|
||||||
|
if (error) {
|
||||||
|
showToast(`Failed to save LLM settings: ${error}`, "error");
|
||||||
|
} else {
|
||||||
|
showToast("LLM preferences saved successfully.", "success");
|
||||||
|
}
|
||||||
|
setSaving(false);
|
||||||
|
setHasChanges(!!error ? true : false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLLMChoice = (selection) => {
|
||||||
|
setLLMChoice(selection);
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchKeys() {
|
||||||
|
const _settings = await System.keys();
|
||||||
|
setSettings(_settings);
|
||||||
|
setLLMChoice(_settings?.LLMProvider);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
fetchKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
|
{!isMobile && <Sidebar />}
|
||||||
|
{loading ? (
|
||||||
|
<div
|
||||||
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll animate-pulse"
|
||||||
|
>
|
||||||
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
|
<PreLoader />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
|
>
|
||||||
|
{isMobile && <SidebarMobileHeader />}
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onChange={() => setHasChanges(true)}
|
||||||
|
className="flex w-full"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
|
<div className="items-center flex gap-x-4">
|
||||||
|
<p className="text-2xl font-semibold text-white">
|
||||||
|
LLM Preference
|
||||||
|
</p>
|
||||||
|
{hasChanges && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
|
>
|
||||||
|
{saving ? "Saving..." : "Save changes"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
These are the credentials and settings for your preferred LLM
|
||||||
|
chat & embedding provider. Its important these keys are
|
||||||
|
current and correct or else AnythingLLM will not function
|
||||||
|
properly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-sm font-medium py-4">
|
||||||
|
LLM Providers
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]">
|
||||||
|
<input hidden={true} name="LLMProvider" value={llmChoice} />
|
||||||
|
<LLMProviderOption
|
||||||
|
name="OpenAI"
|
||||||
|
value="openai"
|
||||||
|
link="openai.com"
|
||||||
|
description="The standard option for most non-commercial use. Provides both chat and embedding."
|
||||||
|
checked={llmChoice === "openai"}
|
||||||
|
image={OpenAiLogo}
|
||||||
|
onClick={updateLLMChoice}
|
||||||
|
/>
|
||||||
|
<LLMProviderOption
|
||||||
|
name="Azure OpenAI"
|
||||||
|
value="azure"
|
||||||
|
link="azure.microsoft.com"
|
||||||
|
description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding."
|
||||||
|
checked={llmChoice === "azure"}
|
||||||
|
image={AzureOpenAiLogo}
|
||||||
|
onClick={updateLLMChoice}
|
||||||
|
/>
|
||||||
|
<LLMProviderOption
|
||||||
|
name="Anthropic Claude 2"
|
||||||
|
value="anthropic-claude-2"
|
||||||
|
link="anthropic.com"
|
||||||
|
description="[COMING SOON] A friendly AI Assistant hosted by Anthropic. Provides chat services only!"
|
||||||
|
checked={llmChoice === "anthropic-claude-2"}
|
||||||
|
image={AnthropicLogo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
|
||||||
|
{llmChoice === "openai" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="OpenAiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="OpenAI API Key"
|
||||||
|
defaultValue={settings?.OpenAiKey ? "*".repeat(20) : ""}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Chat Model Selection
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="OpenAiModelPref"
|
||||||
|
defaultValue={settings?.OpenAiModelPref}
|
||||||
|
required={true}
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
>
|
||||||
|
{["gpt-3.5-turbo", "gpt-4"].map((model) => {
|
||||||
|
return (
|
||||||
|
<option key={model} value={model}>
|
||||||
|
{model}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{llmChoice === "azure" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Azure Service Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="AzureOpenAiEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="https://my-azure.openai.azure.com"
|
||||||
|
defaultValue={settings?.AzureOpenAiEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="AzureOpenAiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Azure OpenAI API Key"
|
||||||
|
defaultValue={
|
||||||
|
settings?.AzureOpenAiKey ? "*".repeat(20) : ""
|
||||||
|
}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Chat Model Deployment Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="AzureOpenAiModelPref"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Azure OpenAI chat model deployment name"
|
||||||
|
defaultValue={settings?.AzureOpenAiModelPref}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Embedding Model Deployment Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="AzureOpenAiEmbeddingModelPref"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Azure OpenAI embedding model deployment name"
|
||||||
|
defaultValue={settings?.AzureOpenAiEmbeddingModelPref}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{llmChoice === "anthropic-claude-2" && (
|
||||||
|
<div className="w-full h-40 items-center justify-center flex">
|
||||||
|
<p className="text-gray-800 dark:text-slate-400">
|
||||||
|
This provider is unavailable and cannot be used in
|
||||||
|
AnythingLLM currently.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
337
frontend/src/pages/GeneralSettings/Security/index.jsx
Normal file
337
frontend/src/pages/GeneralSettings/Security/index.jsx
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import showToast from "../../../utils/toast";
|
||||||
|
import System from "../../../models/system";
|
||||||
|
import paths from "../../../utils/paths";
|
||||||
|
import {
|
||||||
|
AUTH_TIMESTAMP,
|
||||||
|
AUTH_TOKEN,
|
||||||
|
AUTH_USER,
|
||||||
|
} from "../../../utils/constants";
|
||||||
|
import PreLoader from "../../../components/Preloader";
|
||||||
|
|
||||||
|
export default function GeneralSecurity() {
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
|
{!isMobile && <Sidebar />}
|
||||||
|
<div
|
||||||
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
|
>
|
||||||
|
{isMobile && <SidebarMobileHeader />}
|
||||||
|
<MultiUserMode />
|
||||||
|
<PasswordProtection />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MultiUserMode() {
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [useMultiUserMode, setUseMultiUserMode] = useState(false);
|
||||||
|
const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
if (useMultiUserMode) {
|
||||||
|
const form = new FormData(e.target);
|
||||||
|
const data = {
|
||||||
|
username: form.get("username"),
|
||||||
|
password: form.get("password"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { success, error } = await System.setupMultiUser(data);
|
||||||
|
if (success) {
|
||||||
|
showToast("Multi-User mode enabled successfully.", "success");
|
||||||
|
setSaving(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.localStorage.removeItem(AUTH_USER);
|
||||||
|
window.localStorage.removeItem(AUTH_TOKEN);
|
||||||
|
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||||
|
window.location = paths.admin.users();
|
||||||
|
}, 2_000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`Failed to enable Multi-User mode: ${error}`, "error");
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchIsMultiUserMode() {
|
||||||
|
setLoading(true);
|
||||||
|
const multiUserModeEnabled = await System.isMultiUserMode();
|
||||||
|
setMultiUserModeEnabled(multiUserModeEnabled);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
fetchIsMultiUserMode();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="h-1/2 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] md:min-w-[82%] p-[18px] h-full overflow-y-scroll">
|
||||||
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
|
<PreLoader />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onChange={() => setHasChanges(true)}
|
||||||
|
className="flex w-full"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
|
<div className="items-center flex gap-x-4">
|
||||||
|
<p className="text-2xl font-semibold text-white">Multi-User Mode</p>
|
||||||
|
{hasChanges && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
|
>
|
||||||
|
{saving ? "Saving..." : "Save changes"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
Set up your instance to support your team by activating Multi-User
|
||||||
|
Mode.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full max-h-full">
|
||||||
|
<div className="relative rounded-lg">
|
||||||
|
<div className="flex items-start justify-between px-6 py-4"></div>
|
||||||
|
<div className="space-y-6 flex h-full w-full">
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="">
|
||||||
|
<label className="mb-2.5 block font-medium text-white">
|
||||||
|
{multiUserModeEnabled
|
||||||
|
? "Multi-User Mode is Enabled"
|
||||||
|
: "Enable Multi-User Mode"}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="relative inline-flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
onClick={() => setUseMultiUserMode(!useMultiUserMode)}
|
||||||
|
checked={useMultiUserMode}
|
||||||
|
className="peer sr-only pointer-events-none"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
hidden={multiUserModeEnabled}
|
||||||
|
className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"
|
||||||
|
></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{useMultiUserMode && (
|
||||||
|
<div className="w-full flex flex-col gap-y-2 my-5">
|
||||||
|
<div className="w-80">
|
||||||
|
<label
|
||||||
|
htmlFor="username"
|
||||||
|
className="block mb-3 font-medium text-white"
|
||||||
|
>
|
||||||
|
Admin account username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
|
||||||
|
placeholder="Your admin username"
|
||||||
|
minLength={2}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={multiUserModeEnabled}
|
||||||
|
defaultValue={multiUserModeEnabled ? "********" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 w-80">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block mb-3 font-medium text-white"
|
||||||
|
>
|
||||||
|
Admin account password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
|
||||||
|
placeholder="Your admin password"
|
||||||
|
minLength={8}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
defaultValue={multiUserModeEnabled ? "********" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between space-x-14">
|
||||||
|
<p className="text-white/80 text-xs rounded-lg w-96">
|
||||||
|
By default, you will be the only admin. As an admin you will
|
||||||
|
need to create accounts for all new users or admins. Do not lose
|
||||||
|
your password as only an Admin user can reset passwords.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PasswordProtection() {
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false);
|
||||||
|
const [usePassword, setUsePassword] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (multiUserModeEnabled) return false;
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
const form = new FormData(e.target);
|
||||||
|
const data = {
|
||||||
|
usePassword,
|
||||||
|
newPassword: form.get("password"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { success, error } = await System.updateSystemPassword(data);
|
||||||
|
if (success) {
|
||||||
|
showToast("Your page will refresh in a few seconds.", "success");
|
||||||
|
setSaving(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.localStorage.removeItem(AUTH_USER);
|
||||||
|
window.localStorage.removeItem(AUTH_TOKEN);
|
||||||
|
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||||
|
window.location.reload();
|
||||||
|
}, 3_000);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
showToast(`Failed to update password: ${error}`, "error");
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchIsMultiUserMode() {
|
||||||
|
setLoading(true);
|
||||||
|
const multiUserModeEnabled = await System.isMultiUserMode();
|
||||||
|
const settings = await System.keys();
|
||||||
|
setMultiUserModeEnabled(multiUserModeEnabled);
|
||||||
|
setUsePassword(settings?.RequiresAuth);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
fetchIsMultiUserMode();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="h-1/2 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] md:min-w-[82%] p-[18px] h-full overflow-y-scroll">
|
||||||
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
|
<PreLoader />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (multiUserModeEnabled) return null;
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onChange={() => setHasChanges(true)}
|
||||||
|
className="flex w-full"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
|
<div className="items-center flex gap-x-4">
|
||||||
|
<p className="text-2xl font-semibold text-white">
|
||||||
|
Password Protection
|
||||||
|
</p>
|
||||||
|
{hasChanges && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
|
>
|
||||||
|
{saving ? "Saving..." : "Save changes"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
Protect your AnythingLLM instance with a password. If you forget
|
||||||
|
this there is no recovery method so ensure you save this password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full max-h-full">
|
||||||
|
<div className="relative rounded-lg">
|
||||||
|
<div className="flex items-start justify-between px-6 py-4"></div>
|
||||||
|
<div className="space-y-6 flex h-full w-full">
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="">
|
||||||
|
<label className="mb-2.5 block font-medium text-white">
|
||||||
|
Password Protect Instance
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="relative inline-flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
onClick={() => setUsePassword(!usePassword)}
|
||||||
|
checked={usePassword}
|
||||||
|
className="peer sr-only pointer-events-none"
|
||||||
|
/>
|
||||||
|
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{usePassword && (
|
||||||
|
<div className="w-full flex flex-col gap-y-2 my-5">
|
||||||
|
<div className="mt-4 w-80">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block mb-3 font-medium text-white"
|
||||||
|
>
|
||||||
|
Instance password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
|
||||||
|
placeholder="Your Instance Password"
|
||||||
|
minLength={8}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
defaultValue={usePassword ? "********" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between space-x-14">
|
||||||
|
<p className="text-white/80 text-xs rounded-lg w-96">
|
||||||
|
By default, you will be the only admin. As an admin you will
|
||||||
|
need to create accounts for all new users or admins. Do not lose
|
||||||
|
your password as only an Admin user can reset passwords.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
339
frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx
Normal file
339
frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import Sidebar, {
|
||||||
|
SidebarMobileHeader,
|
||||||
|
} from "../../../components/SettingsSidebar";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import System from "../../../models/system";
|
||||||
|
import showToast from "../../../utils/toast";
|
||||||
|
import ChromaLogo from "../../../media/vectordbs/chroma.png";
|
||||||
|
import PineconeLogo from "../../../media/vectordbs/pinecone.png";
|
||||||
|
import LanceDbLogo from "../../../media/vectordbs/lancedb.png";
|
||||||
|
import WeaviateLogo from "../../../media/vectordbs/weaviate.png";
|
||||||
|
import QDrantLogo from "../../../media/vectordbs/qdrant.png";
|
||||||
|
import PreLoader from "../../../components/Preloader";
|
||||||
|
import VectorDBOption from "../../../components/VectorDBOption";
|
||||||
|
|
||||||
|
export default function GeneralVectorDatabase() {
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [vectorDB, setVectorDB] = useState("lancedb");
|
||||||
|
const [settings, setSettings] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchKeys() {
|
||||||
|
const _settings = await System.keys();
|
||||||
|
setSettings(_settings);
|
||||||
|
setVectorDB(_settings?.VectorDB || "lancedb");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
fetchKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateVectorChoice = (selection) => {
|
||||||
|
setHasChanges(true);
|
||||||
|
setVectorDB(selection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
const data = {};
|
||||||
|
const form = new FormData(e.target);
|
||||||
|
for (var [key, value] of form.entries()) data[key] = value;
|
||||||
|
const { error } = await System.updateSystem(data);
|
||||||
|
if (error) {
|
||||||
|
showToast(`Failed to save settings: ${error}`, "error");
|
||||||
|
} else {
|
||||||
|
showToast("Settings saved successfully.", "success");
|
||||||
|
}
|
||||||
|
setSaving(false);
|
||||||
|
setHasChanges(!!error ? true : false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
|
{!isMobile && <Sidebar />}
|
||||||
|
{loading ? (
|
||||||
|
<div
|
||||||
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll animate-pulse"
|
||||||
|
>
|
||||||
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
|
<PreLoader />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
|
>
|
||||||
|
{isMobile && <SidebarMobileHeader />}
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onChange={() => setHasChanges(true)}
|
||||||
|
className="flex w-full"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
|
<div className="items-center flex gap-x-4">
|
||||||
|
<p className="text-2xl font-semibold text-white">
|
||||||
|
Vector Database
|
||||||
|
</p>
|
||||||
|
{hasChanges && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
|
||||||
|
>
|
||||||
|
{saving ? "Saving..." : "Save changes"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
These are the credentials and settings for how your
|
||||||
|
AnythingLLM instance will function. It's important these keys
|
||||||
|
are current and correct.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-sm font-medium py-4">
|
||||||
|
Select your preferred vector database provider
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]">
|
||||||
|
<input hidden={true} name="VectorDB" value={vectorDB} />
|
||||||
|
<VectorDBOption
|
||||||
|
name="Chroma"
|
||||||
|
value="chroma"
|
||||||
|
link="trychroma.com"
|
||||||
|
description="Open source vector database you can host yourself or on the cloud."
|
||||||
|
checked={vectorDB === "chroma"}
|
||||||
|
image={ChromaLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
<VectorDBOption
|
||||||
|
name="Pinecone"
|
||||||
|
value="pinecone"
|
||||||
|
link="pinecone.io"
|
||||||
|
description="100% cloud-based vector database for enterprise use cases."
|
||||||
|
checked={vectorDB === "pinecone"}
|
||||||
|
image={PineconeLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
<VectorDBOption
|
||||||
|
name="QDrant"
|
||||||
|
value="qdrant"
|
||||||
|
link="qdrant.tech"
|
||||||
|
description="Open source local and distributed cloud vector database."
|
||||||
|
checked={vectorDB === "qdrant"}
|
||||||
|
image={QDrantLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
<VectorDBOption
|
||||||
|
name="Weaviate"
|
||||||
|
value="weaviate"
|
||||||
|
link="weaviate.io"
|
||||||
|
description="Open source local and cloud hosted multi-modal vector database."
|
||||||
|
checked={vectorDB === "weaviate"}
|
||||||
|
image={WeaviateLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
<VectorDBOption
|
||||||
|
name="LanceDB"
|
||||||
|
value="lancedb"
|
||||||
|
link="lancedb.com"
|
||||||
|
description="100% local vector DB that runs on the same instance as AnythingLLM."
|
||||||
|
checked={vectorDB === "lancedb"}
|
||||||
|
image={LanceDbLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
|
||||||
|
{vectorDB === "pinecone" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone DB API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="PineConeKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Pinecone API Key"
|
||||||
|
defaultValue={
|
||||||
|
settings?.PineConeKey ? "*".repeat(20) : ""
|
||||||
|
}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone Index Environment
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="PineConeEnvironment"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="us-gcp-west-1"
|
||||||
|
defaultValue={settings?.PineConeEnvironment}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone Index Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="PineConeIndex"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="my-index"
|
||||||
|
defaultValue={settings?.PineConeIndex}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vectorDB === "chroma" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Chroma Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="ChromaEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:8000"
|
||||||
|
defaultValue={settings?.ChromaEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Header
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="ChromaApiHeader"
|
||||||
|
autoComplete="off"
|
||||||
|
type="text"
|
||||||
|
defaultValue={settings?.ChromaApiHeader}
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="X-Api-Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="ChromaApiKey"
|
||||||
|
autoComplete="off"
|
||||||
|
type="password"
|
||||||
|
defaultValue={
|
||||||
|
settings?.ChromaApiKey ? "*".repeat(20) : ""
|
||||||
|
}
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="sk-myApiKeyToAccessMyChromaInstance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vectorDB === "lancedb" && (
|
||||||
|
<div className="w-full h-40 items-center justify-center flex">
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
There is no configuration needed for LanceDB.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vectorDB === "qdrant" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
QDrant API Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="QdrantEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:6633"
|
||||||
|
defaultValue={settings?.QdrantEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="QdrantApiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="wOeqxsYP4....1244sba"
|
||||||
|
defaultValue={settings?.QdrantApiKey}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vectorDB === "weaviate" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Weaviate Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="WeaviateEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:8080"
|
||||||
|
defaultValue={settings?.WeaviateEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="WeaviateApiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="sk-123Abcweaviate"
|
||||||
|
defaultValue={settings?.WeaviateApiKey}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
11
frontend/src/pages/Login/index.jsx
Normal file
11
frontend/src/pages/Login/index.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PasswordModal, {
|
||||||
|
usePasswordModal,
|
||||||
|
} from "../../components/Modals/Password";
|
||||||
|
import { FullScreenLoader } from "../../components/Preloader";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const { loading, mode } = usePasswordModal();
|
||||||
|
if (loading) return <FullScreenLoader />;
|
||||||
|
return <PasswordModal mode={mode} />;
|
||||||
|
}
|
@ -6,6 +6,7 @@ import PasswordModal, {
|
|||||||
} from "../../components/Modals/Password";
|
} from "../../components/Modals/Password";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { FullScreenLoader } from "../../components/Preloader";
|
import { FullScreenLoader } from "../../components/Preloader";
|
||||||
|
import UserMenu from "../../components/UserMenu";
|
||||||
|
|
||||||
export default function Main() {
|
export default function Main() {
|
||||||
const { loading, requiresAuth, mode } = usePasswordModal();
|
const { loading, requiresAuth, mode } = usePasswordModal();
|
||||||
@ -16,9 +17,11 @@ export default function Main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<UserMenu>
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<DefaultChatContainer />
|
<DefaultChatContainer />
|
||||||
</div>
|
</div>
|
||||||
|
</UserMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,136 @@
|
|||||||
|
import React, { memo, useEffect, useState } from "react";
|
||||||
|
import System from "../../../../../models/system";
|
||||||
|
import AnythingLLM from "../../../../../media/logo/anything-llm.png";
|
||||||
|
import useLogo from "../../../../../hooks/useLogo";
|
||||||
|
import { Plus } from "@phosphor-icons/react";
|
||||||
|
import showToast from "../../../../../utils/toast";
|
||||||
|
|
||||||
|
function AppearanceSetup({ nextStep }) {
|
||||||
|
const { logo: _initLogo } = useLogo();
|
||||||
|
const [logo, setLogo] = useState("");
|
||||||
|
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function logoInit() {
|
||||||
|
setLogo(_initLogo || "");
|
||||||
|
const _isDefaultLogo = await System.isDefaultLogo();
|
||||||
|
setIsDefaultLogo(_isDefaultLogo);
|
||||||
|
}
|
||||||
|
logoInit();
|
||||||
|
}, [_initLogo]);
|
||||||
|
|
||||||
|
const handleFileUpload = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return false;
|
||||||
|
|
||||||
|
const objectURL = URL.createObjectURL(file);
|
||||||
|
setLogo(objectURL);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("logo", file);
|
||||||
|
const { success, error } = await System.uploadLogo(formData);
|
||||||
|
if (!success) {
|
||||||
|
showToast(`Failed to upload logo: ${error}`, "error");
|
||||||
|
setLogo(_initLogo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("Image uploaded successfully.", "success");
|
||||||
|
setIsDefaultLogo(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveLogo = async () => {
|
||||||
|
setLogo("");
|
||||||
|
setIsDefaultLogo(true);
|
||||||
|
|
||||||
|
const { success, error } = await System.removeCustomLogo();
|
||||||
|
if (!success) {
|
||||||
|
console.error("Failed to remove logo:", error);
|
||||||
|
showToast(`Failed to remove logo: ${error}`, "error");
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
setLogo(logoURL);
|
||||||
|
setIsDefaultLogo(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("Image successfully removed.", "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col w-full px-10 py-12">
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<h2 className="text-white text-sm font-medium">Custom Logo</h2>
|
||||||
|
<p className="text-sm font-base text-white/60">
|
||||||
|
Upload your custom logo to make your chatbot yours.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex md:flex-row flex-col items-center">
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt="Uploaded Logo"
|
||||||
|
className="w-48 h-48 object-contain mr-6"
|
||||||
|
hidden={isDefaultLogo}
|
||||||
|
onError={(e) => (e.target.src = AnythingLLM)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row gap-x-8">
|
||||||
|
<label className="mt-5 hover:opacity-60" hidden={!isDefaultLogo}>
|
||||||
|
<input
|
||||||
|
id="logo-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-80 py-4 bg-zinc-900/50 rounded-2xl border-2 border-dashed border-white border-opacity-60 justify-center items-center inline-flex cursor-pointer"
|
||||||
|
htmlFor="logo-upload"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="rounded-full bg-white/40">
|
||||||
|
<Plus className="w-6 h-6 text-black/80 m-2" />
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
|
||||||
|
Add a custom logo
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-60 text-xs font-medium py-1">
|
||||||
|
Recommended size: 800 x 200
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveLogo}
|
||||||
|
className="text-white text-base font-medium hover:text-opacity-60"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between items-center p-6 space-x-6 border-t rounded-b border-gray-500/50">
|
||||||
|
<div className="w-96 text-white text-opacity-80 text-xs font-base">
|
||||||
|
Want to customize the automatic messages in your chat? Find more
|
||||||
|
customization options on the appearance settings page.
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={nextStep}
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={nextStep}
|
||||||
|
type="button"
|
||||||
|
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default memo(AppearanceSetup);
|
@ -0,0 +1,60 @@
|
|||||||
|
import React, { memo } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import paths from "../../../../../utils/paths";
|
||||||
|
import Workspace from "../../../../../models/workspace";
|
||||||
|
|
||||||
|
function CreateFirstWorkspace() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleCreate = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = new FormData(e.target);
|
||||||
|
const { workspace, error } = await Workspace.new({
|
||||||
|
name: form.get("name"),
|
||||||
|
});
|
||||||
|
if (!!workspace) {
|
||||||
|
navigate(paths.home());
|
||||||
|
} else {
|
||||||
|
alert(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleCreate} className="flex flex-col w-full">
|
||||||
|
<div className="flex flex-col w-full md:px-8 py-12">
|
||||||
|
<div className="space-y-6 flex h-full w-96">
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Workspace name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
|
placeholder="My workspace"
|
||||||
|
minLength={4}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
||||||
|
>
|
||||||
|
Finish
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default memo(CreateFirstWorkspace);
|
@ -0,0 +1,231 @@
|
|||||||
|
import React, { memo, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import OpenAiLogo from "../../../../../media/llmprovider/openai.png";
|
||||||
|
import AzureOpenAiLogo from "../../../../../media/llmprovider/azure.png";
|
||||||
|
import AnthropicLogo from "../../../../../media/llmprovider/anthropic.png";
|
||||||
|
import System from "../../../../../models/system";
|
||||||
|
import PreLoader from "../../../../../components/Preloader";
|
||||||
|
import LLMProviderOption from "../../../../../components/LLMProviderOption";
|
||||||
|
|
||||||
|
function LLMSelection({ nextStep, prevStep, currentStep }) {
|
||||||
|
const [llmChoice, setLLMChoice] = useState("openai");
|
||||||
|
const [settings, setSettings] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const updateLLMChoice = (selection) => {
|
||||||
|
setLLMChoice(selection);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchKeys() {
|
||||||
|
const _settings = await System.keys();
|
||||||
|
setSettings(_settings);
|
||||||
|
setLLMChoice(_settings?.LLMProvider);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStep === 1) {
|
||||||
|
fetchKeys();
|
||||||
|
}
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const data = {};
|
||||||
|
const formData = new FormData(form);
|
||||||
|
for (var [key, value] of formData.entries()) data[key] = value;
|
||||||
|
const { error } = await System.updateSystem(data);
|
||||||
|
if (error) {
|
||||||
|
alert(`Failed to save LLM settings: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextStep();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex justify-center items-center p-20">
|
||||||
|
<PreLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col w-full">
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-8 py-12">
|
||||||
|
<div className="text-white text-sm font-medium pb-4">
|
||||||
|
LLM Providers
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]">
|
||||||
|
<input hidden={true} name="LLMProvider" defaultValue={llmChoice} />
|
||||||
|
<LLMProviderOption
|
||||||
|
name="OpenAI"
|
||||||
|
value="openai"
|
||||||
|
link="openai.com"
|
||||||
|
description="The standard option for most non-commercial use. Provides both chat and embedding."
|
||||||
|
checked={llmChoice === "openai"}
|
||||||
|
image={OpenAiLogo}
|
||||||
|
onClick={updateLLMChoice}
|
||||||
|
/>
|
||||||
|
<LLMProviderOption
|
||||||
|
name="Azure OpenAI"
|
||||||
|
value="azure"
|
||||||
|
link="azure.microsoft.com"
|
||||||
|
description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding."
|
||||||
|
checked={llmChoice === "azure"}
|
||||||
|
image={AzureOpenAiLogo}
|
||||||
|
onClick={updateLLMChoice}
|
||||||
|
/>
|
||||||
|
<LLMProviderOption
|
||||||
|
name="Anthropic Claude 2"
|
||||||
|
value="anthropic-claude-2"
|
||||||
|
link="anthropic.com"
|
||||||
|
description="[COMING SOON] A friendly AI Assistant hosted by Anthropic. Provides chat services only!"
|
||||||
|
checked={llmChoice === "anthropic-claude-2"}
|
||||||
|
image={AnthropicLogo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
|
||||||
|
{llmChoice === "openai" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="OpenAiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="OpenAI API Key"
|
||||||
|
defaultValue={settings?.OpenAiKey ? "*".repeat(20) : ""}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Chat Model Selection
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="OpenAiModelPref"
|
||||||
|
defaultValue={settings?.OpenAiModelPref}
|
||||||
|
required={true}
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
>
|
||||||
|
{["gpt-3.5-turbo", "gpt-4"].map((model) => {
|
||||||
|
return (
|
||||||
|
<option key={model} value={model}>
|
||||||
|
{model}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{llmChoice === "azure" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Azure Service Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="AzureOpenAiEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="https://my-azure.openai.azure.com"
|
||||||
|
defaultValue={settings?.AzureOpenAiEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="AzureOpenAiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Azure OpenAI API Key"
|
||||||
|
defaultValue={
|
||||||
|
settings?.AzureOpenAiKey ? "*".repeat(20) : ""
|
||||||
|
}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Chat Model Deployment Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="AzureOpenAiModelPref"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Azure OpenAI chat model deployment name"
|
||||||
|
defaultValue={settings?.AzureOpenAiModelPref}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Embedding Model Deployment Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="AzureOpenAiEmbeddingModelPref"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Azure OpenAI embedding model deployment name"
|
||||||
|
defaultValue={settings?.AzureOpenAiEmbeddingModelPref}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{llmChoice === "anthropic-claude-2" && (
|
||||||
|
<div className="w-full h-40 items-center justify-center flex">
|
||||||
|
<p className="text-gray-800 dark:text-slate-400">
|
||||||
|
This provider is unavailable and cannot be used in AnythingLLM
|
||||||
|
currently.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
|
<button
|
||||||
|
onClick={prevStep}
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(LLMSelection);
|
@ -0,0 +1,121 @@
|
|||||||
|
import React, { useState, memo } from "react";
|
||||||
|
import System from "../../../../../models/system";
|
||||||
|
import {
|
||||||
|
AUTH_TIMESTAMP,
|
||||||
|
AUTH_TOKEN,
|
||||||
|
AUTH_USER,
|
||||||
|
} from "../../../../../utils/constants";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
|
// Multi-user mode step
|
||||||
|
function MultiUserSetup({ nextStep, prevStep }) {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = {
|
||||||
|
username: formData.get("username"),
|
||||||
|
password: formData.get("password"),
|
||||||
|
};
|
||||||
|
const { success, error } = await System.setupMultiUser(data);
|
||||||
|
if (!success) {
|
||||||
|
alert(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-request token with credentials that was just set so they
|
||||||
|
// are not redirected to login after completion.
|
||||||
|
const { user, token } = await System.requestToken(data);
|
||||||
|
window.localStorage.setItem(AUTH_USER, JSON.stringify(user));
|
||||||
|
window.localStorage.setItem(AUTH_TOKEN, token);
|
||||||
|
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||||
|
|
||||||
|
nextStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNewUsername = (e) => setPassword(e.target.value);
|
||||||
|
const setNewPassword = (e) => setPassword(e.target.value);
|
||||||
|
const handleUsernameChange = debounce(setNewUsername, 500);
|
||||||
|
const handlePasswordChange = debounce(setNewPassword, 500);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col w-full md:px-8 py-12">
|
||||||
|
<div className="space-y-6 flex h-full w-96">
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Admin account username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
|
placeholder="Your admin username"
|
||||||
|
minLength={6}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={handleUsernameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Admin account password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
|
placeholder="Your admin password"
|
||||||
|
minLength={8}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="w-96 text-white text-opacity-80 text-xs font-base">
|
||||||
|
Username must be at least 6 characters long. Password must be at
|
||||||
|
least 8 characters long.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between items-center p-6 space-x-6 border-t rounded-b border-gray-500/50">
|
||||||
|
<div className="w-96 text-white text-opacity-80 text-xs font-base">
|
||||||
|
By default, you will be the only admin. As an admin you will need to
|
||||||
|
create accounts for all new users or admins. Do not lose your
|
||||||
|
password as only admins can reset passwords.
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={prevStep}
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="border px-4 py-2 rounded-lg text-sm items-center flex gap-x-2
|
||||||
|
border-slate-200 text-slate-800 bg-slate-200 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow
|
||||||
|
disabled:border-gray-400 disabled:text-slate-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||||
|
disabled={!(!!username && !!password)}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default memo(MultiUserSetup);
|
@ -0,0 +1,107 @@
|
|||||||
|
import React, { memo, useState } from "react";
|
||||||
|
import System from "../../../../../models/system";
|
||||||
|
import {
|
||||||
|
AUTH_TIMESTAMP,
|
||||||
|
AUTH_TOKEN,
|
||||||
|
AUTH_USER,
|
||||||
|
} from "../../../../../utils/constants";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
|
function PasswordProtection({ goToStep, prevStep }) {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const { error } = await System.updateSystemPassword({
|
||||||
|
usePassword: true,
|
||||||
|
newPassword: formData.get("password"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
alert(`Failed to set password: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-request token with password that was just set so they
|
||||||
|
// are not redirected to login after completion.
|
||||||
|
const { token } = await System.requestToken({
|
||||||
|
password: formData.get("password"),
|
||||||
|
});
|
||||||
|
window.localStorage.removeItem(AUTH_USER);
|
||||||
|
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||||
|
window.localStorage.setItem(AUTH_TOKEN, token);
|
||||||
|
|
||||||
|
goToStep(7);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkip = () => {
|
||||||
|
goToStep(7);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNewPassword = (e) => setPassword(e.target.value);
|
||||||
|
const handlePasswordChange = debounce(setNewPassword, 500);
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<form className="flex flex-col w-full" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-8 py-12">
|
||||||
|
<div className="w-full flex flex-col gap-y-2 my-5">
|
||||||
|
<div className="w-80">
|
||||||
|
<div className="flex flex-col mb-3 ">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block font-medium text-white"
|
||||||
|
>
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<p className="text-slate-300 text-xs">
|
||||||
|
must be at least 8 characters.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
name="password"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
|
||||||
|
placeholder="Your Instance Password"
|
||||||
|
minLength={8}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
|
<button
|
||||||
|
onClick={prevStep}
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSkip}
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!password}
|
||||||
|
className="border px-4 py-2 rounded-lg text-sm items-center flex gap-x-2
|
||||||
|
border-slate-200 text-slate-800 bg-slate-200 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow
|
||||||
|
disabled:border-gray-400 disabled:text-slate-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default memo(PasswordProtection);
|
@ -0,0 +1,47 @@
|
|||||||
|
import React, { memo } from "react";
|
||||||
|
|
||||||
|
// How many people will be using your instance step
|
||||||
|
function UserModeSelection({ goToStep, prevStep }) {
|
||||||
|
const justMeClicked = () => {
|
||||||
|
goToStep(5);
|
||||||
|
};
|
||||||
|
|
||||||
|
const myTeamClicked = () => {
|
||||||
|
goToStep(6);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col justify-center items-center px-20 py-20">
|
||||||
|
<div className="w-80 text-white text-center text-2xl font-base">
|
||||||
|
How many people will be using your instance?
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 justify-center my-8">
|
||||||
|
<button
|
||||||
|
onClick={justMeClicked}
|
||||||
|
className="transition-all duration-200 border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
||||||
|
>
|
||||||
|
Just Me
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={myTeamClicked}
|
||||||
|
className="transition-all duration-200 border border-slate-200 px-5 py-2.5 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
||||||
|
>
|
||||||
|
My Team
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
|
<button
|
||||||
|
onClick={prevStep}
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar transition-all duration-300"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(UserModeSelection);
|
@ -0,0 +1,310 @@
|
|||||||
|
import React, { memo, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import VectorDBOption from "../../../../../components/VectorDBOption";
|
||||||
|
import ChromaLogo from "../../../../../media/vectordbs/chroma.png";
|
||||||
|
import PineconeLogo from "../../../../../media/vectordbs/pinecone.png";
|
||||||
|
import LanceDbLogo from "../../../../../media/vectordbs/lancedb.png";
|
||||||
|
import WeaviateLogo from "../../../../../media/vectordbs/weaviate.png";
|
||||||
|
import QDrantLogo from "../../../../../media/vectordbs/qdrant.png";
|
||||||
|
import System from "../../../../../models/system";
|
||||||
|
import PreLoader from "../../../../../components/Preloader";
|
||||||
|
|
||||||
|
function VectorDatabaseConnection({ nextStep, prevStep, currentStep }) {
|
||||||
|
const [vectorDB, setVectorDB] = useState("lancedb");
|
||||||
|
const [settings, setSettings] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchKeys() {
|
||||||
|
const _settings = await System.keys();
|
||||||
|
setSettings(_settings);
|
||||||
|
setVectorDB(_settings?.VectorDB || "lancedb");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
if (currentStep === 2) {
|
||||||
|
fetchKeys();
|
||||||
|
}
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
const updateVectorChoice = (selection) => {
|
||||||
|
setVectorDB(selection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e, formElement) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = formElement || e.target;
|
||||||
|
const data = {};
|
||||||
|
const formData = new FormData(form);
|
||||||
|
for (var [key, value] of formData.entries()) data[key] = value;
|
||||||
|
const { error } = await System.updateSystem(data);
|
||||||
|
if (error) {
|
||||||
|
alert(`Failed to save settings: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextStep();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex justify-center items-center p-20">
|
||||||
|
<PreLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col w-full">
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-8 py-12">
|
||||||
|
<div className="text-white text-sm font-medium pb-4">
|
||||||
|
Select your preferred vector database provider
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]">
|
||||||
|
<input hidden={true} name="VectorDB" value={vectorDB} />
|
||||||
|
<VectorDBOption
|
||||||
|
name="Chroma"
|
||||||
|
value="chroma"
|
||||||
|
link="trychroma.com"
|
||||||
|
description="Open source vector database you can host yourself or on the cloud."
|
||||||
|
checked={vectorDB === "chroma"}
|
||||||
|
image={ChromaLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
<VectorDBOption
|
||||||
|
name="Pinecone"
|
||||||
|
value="pinecone"
|
||||||
|
link="pinecone.io"
|
||||||
|
description="100% cloud-based vector database for enterprise use cases."
|
||||||
|
checked={vectorDB === "pinecone"}
|
||||||
|
image={PineconeLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
<VectorDBOption
|
||||||
|
name="QDrant"
|
||||||
|
value="qdrant"
|
||||||
|
link="qdrant.tech"
|
||||||
|
description="Open source local and distributed cloud vector database."
|
||||||
|
checked={vectorDB === "qdrant"}
|
||||||
|
image={QDrantLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
<VectorDBOption
|
||||||
|
name="Weaviate"
|
||||||
|
value="weaviate"
|
||||||
|
link="weaviate.io"
|
||||||
|
description="Open source local and cloud hosted multi-modal vector database."
|
||||||
|
checked={vectorDB === "weaviate"}
|
||||||
|
image={WeaviateLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
<VectorDBOption
|
||||||
|
name="LanceDB"
|
||||||
|
value="lancedb"
|
||||||
|
link="lancedb.com"
|
||||||
|
description="100% local vector DB that runs on the same instance as AnythingLLM."
|
||||||
|
checked={vectorDB === "lancedb"}
|
||||||
|
image={LanceDbLogo}
|
||||||
|
onClick={updateVectorChoice}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
|
||||||
|
{vectorDB === "pinecone" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone DB API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="PineConeKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Pinecone API Key"
|
||||||
|
defaultValue={settings?.PineConeKey ? "*".repeat(20) : ""}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone Index Environment
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="PineConeEnvironment"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="us-gcp-west-1"
|
||||||
|
defaultValue={settings?.PineConeEnvironment}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone Index Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="PineConeIndex"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="my-index"
|
||||||
|
defaultValue={settings?.PineConeIndex}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vectorDB === "chroma" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Chroma Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="ChromaEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:8000"
|
||||||
|
defaultValue={settings?.ChromaEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Header
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="ChromaApiHeader"
|
||||||
|
autoComplete="off"
|
||||||
|
type="text"
|
||||||
|
defaultValue={settings?.ChromaApiHeader}
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="X-Api-Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="ChromaApiKey"
|
||||||
|
autoComplete="off"
|
||||||
|
type="password"
|
||||||
|
defaultValue={settings?.ChromaApiKey ? "*".repeat(20) : ""}
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="sk-myApiKeyToAccessMyChromaInstance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vectorDB === "lancedb" && (
|
||||||
|
<div className="w-full h-10 items-center justify-center flex">
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
There is no configuration needed for LanceDB.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vectorDB === "qdrant" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
QDrant API Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="QdrantEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:6633"
|
||||||
|
defaultValue={settings?.QdrantEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="QdrantApiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="wOeqxsYP4....1244sba"
|
||||||
|
defaultValue={settings?.QdrantApiKey}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vectorDB === "weaviate" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Weaviate Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="WeaviateEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:8080"
|
||||||
|
defaultValue={settings?.WeaviateEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="WeaviateApiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="sk-123Abcweaviate"
|
||||||
|
defaultValue={settings?.WeaviateApiKey}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
|
<button
|
||||||
|
onClick={prevStep}
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(VectorDatabaseConnection);
|
109
frontend/src/pages/OnboardingFlow/OnboardingModal/index.jsx
Normal file
109
frontend/src/pages/OnboardingFlow/OnboardingModal/index.jsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { X } from "react-feather";
|
||||||
|
import LLMSelection from "./Steps/LLMSelection";
|
||||||
|
import VectorDatabaseConnection from "./Steps/VectorDatabaseConnection";
|
||||||
|
import AppearanceSetup from "./Steps/AppearanceSetup";
|
||||||
|
import UserModeSelection from "./Steps/UserModeSelection";
|
||||||
|
import PasswordProtection from "./Steps/PasswordProtection";
|
||||||
|
import MultiUserSetup from "./Steps/MultiUserSetup";
|
||||||
|
import CreateFirstWorkspace from "./Steps/CreateFirstWorkspace";
|
||||||
|
|
||||||
|
const DIALOG_ID = "onboarding-modal";
|
||||||
|
|
||||||
|
function hideModal() {
|
||||||
|
document.getElementById(DIALOG_ID)?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const STEPS = {
|
||||||
|
1: {
|
||||||
|
title: "LLM Preference",
|
||||||
|
description:
|
||||||
|
"These are the credentials and settings for your preferred LLM chat & embedding provider.",
|
||||||
|
component: LLMSelection,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
title: "Vector Database",
|
||||||
|
description:
|
||||||
|
"These are the credentials and settings for how your AnythingLLM instance will function.",
|
||||||
|
component: VectorDatabaseConnection,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
title: "Appearance",
|
||||||
|
description: "Customize the appearance of your AnythingLLM instance.",
|
||||||
|
component: AppearanceSetup,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
title: "User Mode Setup",
|
||||||
|
description: "Choose how many people will be using your instance.",
|
||||||
|
component: UserModeSelection,
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
title: "Password Protect",
|
||||||
|
description:
|
||||||
|
"Protect your instance with a password. It is important to save this password as it cannot be recovered.",
|
||||||
|
component: PasswordProtection,
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
title: "Multi-User Mode",
|
||||||
|
description:
|
||||||
|
"Setup your instance to support your team by activating multi-user mode.",
|
||||||
|
component: MultiUserSetup,
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
title: "Create Workspace",
|
||||||
|
description: "To get started, create a new workspace.",
|
||||||
|
component: CreateFirstWorkspace,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OnboardingModalId = DIALOG_ID;
|
||||||
|
export default function OnboardingModal() {
|
||||||
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
setCurrentStep((prevStep) => prevStep + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevStep = () => {
|
||||||
|
if (currentStep === 1) return hideModal();
|
||||||
|
setCurrentStep((prevStep) => prevStep - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToStep = (step) => {
|
||||||
|
setCurrentStep(step);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { component: StepComponent, ...step } = STEPS[currentStep];
|
||||||
|
return (
|
||||||
|
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||||
|
<div className="relative max-h-full">
|
||||||
|
<div className="relative bg-main-gradient rounded-2xl shadow border-2 border-slate-300/10">
|
||||||
|
<div className="flex items-start justify-between p-8 border-b rounded-t border-gray-500/50">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-xl font-semibold text-white">{step.title}</h3>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
{step.description || ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={hideModal}
|
||||||
|
type="button"
|
||||||
|
className="text-gray-400 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:border-white/60 bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
|
>
|
||||||
|
<X className="text-gray-300 text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-6 flex h-full w-full justify-center">
|
||||||
|
<StepComponent
|
||||||
|
currentStep={currentStep}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
goToStep={goToStep}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
}
|
31
frontend/src/pages/OnboardingFlow/index.jsx
Normal file
31
frontend/src/pages/OnboardingFlow/index.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import OnboardingModal, { OnboardingModalId } from "./OnboardingModal";
|
||||||
|
import useLogo from "../../hooks/useLogo";
|
||||||
|
|
||||||
|
export default function OnboardingFlow() {
|
||||||
|
const { logo } = useLogo();
|
||||||
|
|
||||||
|
function showModal() {
|
||||||
|
document?.getElementById(OnboardingModalId)?.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex items-center justify-center">
|
||||||
|
<div className="w-fit p-20 py-24 border-2 border-slate-300/10 rounded-2xl bg-main-gradient shadow-lg">
|
||||||
|
<div className="text-white text-2xl font-base text-center">
|
||||||
|
Welcome to
|
||||||
|
</div>
|
||||||
|
<img src={logo} alt="logo" className="w-80 mx-auto m-3 mb-11" />
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<button
|
||||||
|
className="border border-slate-200 px-5 py-2.5 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow animate-pulse"
|
||||||
|
onClick={showModal}
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<OnboardingModal />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -36,7 +36,7 @@ function ShowWorkspaceChat() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
<WorkspaceChatContainer loading={loading} workspace={workspace} />
|
<WorkspaceChatContainer loading={loading} workspace={workspace} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,3 +3,6 @@ export const API_BASE = import.meta.env.VITE_API_BASE || "/api";
|
|||||||
export const AUTH_USER = "anythingllm_user";
|
export const AUTH_USER = "anythingllm_user";
|
||||||
export const AUTH_TOKEN = "anythingllm_authToken";
|
export const AUTH_TOKEN = "anythingllm_authToken";
|
||||||
export const AUTH_TIMESTAMP = "anythingllm_authTimestamp";
|
export const AUTH_TIMESTAMP = "anythingllm_authTimestamp";
|
||||||
|
|
||||||
|
export const USER_BACKGROUND_COLOR = "bg-historical-msg-user";
|
||||||
|
export const AI_BACKGROUND_COLOR = "bg-historical-msg-system";
|
||||||
|
34
frontend/src/utils/directories.js
Normal file
34
frontend/src/utils/directories.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export function formatDate(dateString) {
|
||||||
|
const date = isNaN(new Date(dateString).getTime())
|
||||||
|
? new Date()
|
||||||
|
: new Date(dateString);
|
||||||
|
const options = { year: "numeric", month: "short", day: "numeric" };
|
||||||
|
const formattedDate = date.toLocaleDateString("en-US", options);
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileExtension(path) {
|
||||||
|
const match = path.match(/[^\/\\&\?]+\.\w{1,4}(?=([\?&].*$|$))/);
|
||||||
|
return match ? match[0].split(".").pop() : "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncate(str, n) {
|
||||||
|
const fileExtensionPattern = /(\..+)$/;
|
||||||
|
const extensionMatch = str.match(fileExtensionPattern);
|
||||||
|
|
||||||
|
if (str.length <= n) return str;
|
||||||
|
|
||||||
|
if (extensionMatch && extensionMatch[1]) {
|
||||||
|
const extension = extensionMatch[1];
|
||||||
|
const nameWithoutExtension = str.replace(fileExtensionPattern, "");
|
||||||
|
const truncationPoint = Math.max(0, n - extension.length - 4);
|
||||||
|
const truncatedName =
|
||||||
|
nameWithoutExtension.substr(0, truncationPoint) +
|
||||||
|
"..." +
|
||||||
|
nameWithoutExtension.slice(-4);
|
||||||
|
|
||||||
|
return truncatedName + extension;
|
||||||
|
} else {
|
||||||
|
return str.length > n ? str.substr(0, n - 8) + "..." + str.slice(-4) : str;
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,12 @@ export default {
|
|||||||
home: () => {
|
home: () => {
|
||||||
return "/";
|
return "/";
|
||||||
},
|
},
|
||||||
|
login: () => {
|
||||||
|
return "/login";
|
||||||
|
},
|
||||||
|
onboarding: () => {
|
||||||
|
return "/onboarding";
|
||||||
|
},
|
||||||
github: () => {
|
github: () => {
|
||||||
return "https://github.com/Mintplex-Labs/anything-llm";
|
return "https://github.com/Mintplex-Labs/anything-llm";
|
||||||
},
|
},
|
||||||
@ -33,6 +39,26 @@ export default {
|
|||||||
apiDocs: () => {
|
apiDocs: () => {
|
||||||
return `${API_BASE}/docs`;
|
return `${API_BASE}/docs`;
|
||||||
},
|
},
|
||||||
|
general: {
|
||||||
|
llmPreference: () => {
|
||||||
|
return "/general/llm-preference";
|
||||||
|
},
|
||||||
|
vectorDatabase: () => {
|
||||||
|
return "/general/vector-database";
|
||||||
|
},
|
||||||
|
exportImport: () => {
|
||||||
|
return "/general/export-import";
|
||||||
|
},
|
||||||
|
security: () => {
|
||||||
|
return "/general/security";
|
||||||
|
},
|
||||||
|
appearance: () => {
|
||||||
|
return "/general/appearance";
|
||||||
|
},
|
||||||
|
apiKeys: () => {
|
||||||
|
return "/general/api-keys";
|
||||||
|
},
|
||||||
|
},
|
||||||
admin: {
|
admin: {
|
||||||
system: () => {
|
system: () => {
|
||||||
return `/admin/system-preferences`;
|
return `/admin/system-preferences`;
|
||||||
@ -49,11 +75,5 @@ export default {
|
|||||||
chats: () => {
|
chats: () => {
|
||||||
return "/admin/workspace-chats";
|
return "/admin/workspace-chats";
|
||||||
},
|
},
|
||||||
appearance: () => {
|
|
||||||
return "/admin/appearance";
|
|
||||||
},
|
|
||||||
apiKeys: () => {
|
|
||||||
return "/admin/api-keys";
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -3,11 +3,52 @@ export default {
|
|||||||
content: ["./src/**/*.{js,jsx}"],
|
content: ["./src/**/*.{js,jsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
rotate: {
|
||||||
|
'270': '270deg',
|
||||||
|
'360': '360deg',
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
'black-900': '#141414',
|
'black-900': '#141414',
|
||||||
|
'accent': '#3D4147',
|
||||||
|
'sidebar-button': '#31353A',
|
||||||
|
'sidebar': '#25272C',
|
||||||
|
'historical-msg-system': 'rgba(255, 255, 255, 0.05);',
|
||||||
|
'historical-msg-user': '#2C2F35',
|
||||||
},
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'preference-gradient': 'linear-gradient(180deg, #5A5C63 0%, rgba(90, 92, 99, 0.28) 100%);',
|
||||||
|
'chat-msg-user-gradient': 'linear-gradient(180deg, #3D4147 0%, #2C2F35 100%);',
|
||||||
|
'selected-preference-gradient': 'linear-gradient(180deg, #313236 0%, rgba(63.40, 64.90, 70.13, 0) 100%);',
|
||||||
|
'main-gradient': 'linear-gradient(180deg, #3D4147 0%, #2C2F35 100%)',
|
||||||
|
'modal-gradient': 'linear-gradient(180deg, #3D4147 0%, #2C2F35 100%)',
|
||||||
|
'sidebar-gradient': 'linear-gradient(90deg, #5B616A 0%, #3F434B 100%)',
|
||||||
|
'menu-item-gradient': 'linear-gradient(90deg, #3D4147 0%, #2C2F35 100%)',
|
||||||
|
'menu-item-selected-gradient': 'linear-gradient(90deg, #5B616A 0%, #3F434B 100%)',
|
||||||
|
'workspace-item-gradient': 'linear-gradient(90deg, #3D4147 0%, #2C2F35 100%)',
|
||||||
|
'workspace-item-selected-gradient': 'linear-gradient(90deg, #5B616A 0%, #3F434B 100%)',
|
||||||
|
'switch-selected': 'linear-gradient(146deg, #5B616A 0%, #3F434B 100%)',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
'sans': ['plus-jakarta-sans', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
sweep: 'sweep 0.5s ease-in-out',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
sweep: {
|
||||||
|
'0%': { transform: 'scaleX(0)', transformOrigin: 'bottom left' },
|
||||||
|
'100%': { transform: 'scaleX(1)', transformOrigin: 'bottom left' },
|
||||||
|
},
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: 0 },
|
||||||
|
'100%': { opacity: 1 },
|
||||||
|
},
|
||||||
|
fadeOut: {
|
||||||
|
'0%': { opacity: 1 },
|
||||||
|
'100%': { opacity: 0 },
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,6 +447,11 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@phosphor-icons/react@^2.0.13":
|
||||||
|
version "2.0.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.0.13.tgz#4944b08859d16a6efdbd1e073b5e0ef7e8f55cb9"
|
||||||
|
integrity sha512-lRjFfCv4pU8vDnPgZ8/QFzYmAJS08Vx+J2/+Ldh217pXaxvaayBZMC/3EinuMwmMylc97+XYCMPdH+y10I+f0g==
|
||||||
|
|
||||||
"@remix-run/router@1.6.3":
|
"@remix-run/router@1.6.3":
|
||||||
version "1.6.3"
|
version "1.6.3"
|
||||||
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.6.3.tgz"
|
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.6.3.tgz"
|
||||||
@ -1642,6 +1647,11 @@ locate-path@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^5.0.0"
|
p-locate "^5.0.0"
|
||||||
|
|
||||||
|
lodash.debounce@^4.0.8:
|
||||||
|
version "4.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||||
|
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
||||||
|
|
||||||
lodash.merge@^4.6.2:
|
lodash.merge@^4.6.2:
|
||||||
version "4.6.2"
|
version "4.6.2"
|
||||||
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
||||||
|
3
server/.gitignore
vendored
3
server/.gitignore
vendored
@ -1,8 +1,7 @@
|
|||||||
.env.production
|
.env.production
|
||||||
.env.development
|
.env.development
|
||||||
storage/assets/*
|
storage/assets/*
|
||||||
!storage/assets/anything-llm-dark.png
|
!storage/assets/anything-llm.png
|
||||||
!storage/assets/anything-llm-light.png
|
|
||||||
storage/documents/*
|
storage/documents/*
|
||||||
storage/vector-cache/*.json
|
storage/vector-cache/*.json
|
||||||
storage/exports
|
storage/exports
|
||||||
|
@ -32,7 +32,7 @@ const {
|
|||||||
validFilename,
|
validFilename,
|
||||||
renameLogoFile,
|
renameLogoFile,
|
||||||
removeCustomLogo,
|
removeCustomLogo,
|
||||||
DARK_LOGO_FILENAME,
|
LOGO_FILENAME,
|
||||||
} = require("../utils/files/logo");
|
} = require("../utils/files/logo");
|
||||||
const { Telemetry } = require("../models/telemetry");
|
const { Telemetry } = require("../models/telemetry");
|
||||||
const { WelcomeMessages } = require("../models/welcomeMessages");
|
const { WelcomeMessages } = require("../models/welcomeMessages");
|
||||||
@ -317,7 +317,7 @@ function systemEndpoints(app) {
|
|||||||
updateENV(
|
updateENV(
|
||||||
{
|
{
|
||||||
AuthToken: "",
|
AuthToken: "",
|
||||||
JWTSecret: process.env.JWT_SECRET ?? v4(),
|
JWTSecret: process.env.JWT_SECRET || v4(),
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@ -325,12 +325,27 @@ function systemEndpoints(app) {
|
|||||||
await Telemetry.sendTelemetry("enabled_multi_user_mode");
|
await Telemetry.sendTelemetry("enabled_multi_user_mode");
|
||||||
response.status(200).json({ success: !!user, error });
|
response.status(200).json({ success: !!user, error });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
await User.delete({});
|
||||||
|
await SystemSettings.updateSettings({
|
||||||
|
multi_user_mode: false,
|
||||||
|
});
|
||||||
|
|
||||||
console.log(e.message, e);
|
console.log(e.message, e);
|
||||||
response.sendStatus(500).end();
|
response.sendStatus(500).end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get("/system/multi-user-mode", async (request, response) => {
|
||||||
|
try {
|
||||||
|
const multiUserMode = await SystemSettings.isMultiUserMode();
|
||||||
|
response.status(200).json({ multiUserMode });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.message, e);
|
||||||
|
response.sendStatus(500).end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/system/data-export", [validatedRequest], async (_, response) => {
|
app.get("/system/data-export", [validatedRequest], async (_, response) => {
|
||||||
try {
|
try {
|
||||||
const { filename, error } = await exportData();
|
const { filename, error } = await exportData();
|
||||||
@ -341,10 +356,7 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(
|
app.get("/system/data-exports/:filename", (request, response) => {
|
||||||
"/system/data-exports/:filename",
|
|
||||||
[validatedRequest],
|
|
||||||
(request, response) => {
|
|
||||||
const exportLocation = __dirname + "/../storage/exports/";
|
const exportLocation = __dirname + "/../storage/exports/";
|
||||||
const sanitized = path
|
const sanitized = path
|
||||||
.normalize(request.params.filename)
|
.normalize(request.params.filename)
|
||||||
@ -366,9 +378,10 @@ function systemEndpoints(app) {
|
|||||||
msg: "Problem downloading the file",
|
msg: "Problem downloading the file",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// delete on download because endpoint is not authenticated.
|
||||||
|
fs.rmSync(finalDestination);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
"/system/data-import",
|
"/system/data-import",
|
||||||
@ -380,9 +393,9 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get("/system/logo/:mode?", async function (request, response) {
|
app.get("/system/logo", async function (request, response) {
|
||||||
try {
|
try {
|
||||||
const defaultFilename = getDefaultFilename(request.params.mode);
|
const defaultFilename = getDefaultFilename();
|
||||||
const logoPath = await determineLogoFilepath(defaultFilename);
|
const logoPath = await determineLogoFilepath(defaultFilename);
|
||||||
const { buffer, size, mime } = fetchLogo(logoPath);
|
const { buffer, size, mime } = fetchLogo(logoPath);
|
||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
@ -443,6 +456,17 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get("/system/is-default-logo", async (request, response) => {
|
||||||
|
try {
|
||||||
|
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
||||||
|
const isDefaultLogo = currentLogoFilename === LOGO_FILENAME;
|
||||||
|
response.status(200).json({ isDefaultLogo });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing the logo request:", error);
|
||||||
|
response.status(500).json({ message: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
"/system/remove-logo",
|
"/system/remove-logo",
|
||||||
[validatedRequest],
|
[validatedRequest],
|
||||||
@ -458,7 +482,7 @@ function systemEndpoints(app) {
|
|||||||
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
||||||
await removeCustomLogo(currentLogoFilename);
|
await removeCustomLogo(currentLogoFilename);
|
||||||
const { success, error } = await SystemSettings.updateSettings({
|
const { success, error } = await SystemSettings.updateSettings({
|
||||||
logo_filename: DARK_LOGO_FILENAME,
|
logo_filename: LOGO_FILENAME,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.status(success ? 200 : 500).json({
|
return response.status(success ? 200 : 500).json({
|
||||||
@ -546,15 +570,15 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get("/system/api-key", [validatedRequest], async (_, response) => {
|
app.get("/system/api-keys", [validatedRequest], async (_, response) => {
|
||||||
try {
|
try {
|
||||||
if (response.locals.multiUserMode) {
|
if (response.locals.multiUserMode) {
|
||||||
return response.sendStatus(401).end();
|
return response.sendStatus(401).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = await ApiKey.get({});
|
const apiKeys = await ApiKey.where({});
|
||||||
return response.status(200).json({
|
return response.status(200).json({
|
||||||
apiKey,
|
apiKeys,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -575,7 +599,6 @@ function systemEndpoints(app) {
|
|||||||
return response.sendStatus(401).end();
|
return response.sendStatus(401).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApiKey.delete();
|
|
||||||
const { apiKey, error } = await ApiKey.create();
|
const { apiKey, error } = await ApiKey.create();
|
||||||
return response.status(200).json({
|
return response.status(200).json({
|
||||||
apiKey,
|
apiKey,
|
||||||
|
@ -54,7 +54,7 @@ const User = {
|
|||||||
|
|
||||||
delete: async function (clause = {}) {
|
delete: async function (clause = {}) {
|
||||||
try {
|
try {
|
||||||
await prisma.users.delete({ where: clause });
|
await prisma.users.deleteMany({ where: clause });
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
|
@ -29,6 +29,7 @@ const WorkspaceChats = {
|
|||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
|
include: true,
|
||||||
},
|
},
|
||||||
...(limit !== null ? { take: limit } : {}),
|
...(limit !== null ? { take: limit } : {}),
|
||||||
orderBy: {
|
orderBy: {
|
||||||
@ -48,6 +49,7 @@ const WorkspaceChats = {
|
|||||||
const chats = await prisma.workspace_chats.findMany({
|
const chats = await prisma.workspace_chats.findMany({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
include: true,
|
||||||
},
|
},
|
||||||
...(limit !== null ? { take: limit } : {}),
|
...(limit !== null ? { take: limit } : {}),
|
||||||
orderBy: {
|
orderBy: {
|
||||||
|
BIN
server/storage/anythingllm.db.bak
Normal file
BIN
server/storage/anythingllm.db.bak
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user