mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-04 22:10:12 +01: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": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@phosphor-icons/react": "^2.0.13",
|
||||
"buffer": "^6.0.3",
|
||||
"he": "^1.2.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"markdown-it": "^13.0.1",
|
||||
"pluralize": "^8.0.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 { ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import Login from "./pages/Login";
|
||||
|
||||
const Main = lazy(() => import("./pages/Main"));
|
||||
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 AdminChats = lazy(() => import("./pages/Admin/Chats"));
|
||||
const AdminSystem = lazy(() => import("./pages/Admin/System"));
|
||||
const AdminAppearance = lazy(() => import("./pages/Admin/Appearance"));
|
||||
const AdminApiKeys = lazy(() => import("./pages/Admin/ApiKeys"));
|
||||
const GeneralAppearance = lazy(() =>
|
||||
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() {
|
||||
return (
|
||||
<Suspense fallback={<div />}>
|
||||
<ContextWrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<Main />} />
|
||||
<Route path="/" element={<PrivateRoute Component={Main} />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/workspace/:slug"
|
||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||
/>
|
||||
<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 */}
|
||||
<Route
|
||||
path="/admin/system-preferences"
|
||||
@ -49,14 +92,9 @@ export default function App() {
|
||||
path="/admin/workspace-chats"
|
||||
element={<AdminRoute Component={AdminChats} />}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/appearance"
|
||||
element={<AdminRoute Component={AdminAppearance} />}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/api-keys"
|
||||
element={<AdminRoute Component={AdminApiKeys} />}
|
||||
/>
|
||||
|
||||
{/* Onboarding Flow */}
|
||||
<Route path="/onboarding" element={<OnboardingFlow />} />
|
||||
</Routes>
|
||||
<ToastContainer />
|
||||
</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 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 }) {
|
||||
const isUser = type === "user";
|
||||
const backgroundColor = isUser ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex w-full mt-2 items-center ${
|
||||
popMsg ? "chat__message" : ""
|
||||
} ${isUser ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div className={`flex justify-center items-end w-full ${backgroundColor}`}>
|
||||
<div
|
||||
className={`p-4 max-w-full md:max-w-[75%] ${
|
||||
isUser
|
||||
? "bg-slate-200 dark:bg-amber-800"
|
||||
: "bg-orange-100 dark:bg-stone-700"
|
||||
} rounded-b-2xl ${isUser ? "rounded-tl-2xl" : "rounded-tr-2xl"} ${
|
||||
isUser ? "rounded-tr-sm" : "rounded-tl-sm"
|
||||
}`}
|
||||
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
|
||||
>
|
||||
{message && (
|
||||
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
||||
<div className="flex gap-x-5">
|
||||
<Jazzicon
|
||||
size={36}
|
||||
user={{ uid: isUser ? userFromStorage()?.username : "system" }}
|
||||
role={type}
|
||||
/>
|
||||
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -8,6 +8,12 @@ import { isMobile } from "react-device-detect";
|
||||
import { SidebarMobileHeader } from "../Sidebar";
|
||||
import ChatBubble from "../ChatBubble";
|
||||
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() {
|
||||
const [mockMsgs, setMockMessages] = useState([]);
|
||||
@ -30,201 +36,265 @@ export default function DefaultChatContainer() {
|
||||
const MESSAGES = [
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex w-full mt-2 justify-start ${
|
||||
popMsg ? "chat__message" : ""
|
||||
}`}
|
||||
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR} md:mt-0 mt-[40px]`}
|
||||
>
|
||||
<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
|
||||
Mintplex Labs that turns <i>anything</i> into a trained chatbot you
|
||||
can query and chat with. AnythingLLM is a BYOK (bring-your-own-keys)
|
||||
software so there is no subscription, fee, or charges for this
|
||||
software outside of the services you want to use with it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>,
|
||||
<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"} />
|
||||
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex w-full mt-2 justify-start ${
|
||||
popMsg ? "chat__message" : ""
|
||||
}`}
|
||||
>
|
||||
<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
|
||||
OpenAi, GPT-4, LangChain, PineconeDB, ChromaDB, and other services
|
||||
together in a neat package with no fuss to increase your
|
||||
productivity by 100x.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>,
|
||||
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex w-full mt-2 justify-start ${
|
||||
popMsg ? "chat__message" : ""
|
||||
}`}
|
||||
>
|
||||
<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
|
||||
overhead you wont even notice it's there! No GPU needed. Cloud and
|
||||
on-premises installation is available as well.
|
||||
<br />
|
||||
The AI tooling ecosystem gets more powerful everyday. AnythingLLM
|
||||
makes it easy to use.
|
||||
</p>
|
||||
<a
|
||||
href={paths.github()}
|
||||
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"
|
||||
>
|
||||
<GitMerge className="h-4 w-4" />
|
||||
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
|
||||
Create an issue on Github
|
||||
</p>
|
||||
</a>
|
||||
</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">
|
||||
How do I get started?!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>,
|
||||
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex w-full mt-2 justify-start ${
|
||||
popMsg ? "chat__message" : ""
|
||||
}`}
|
||||
>
|
||||
<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{" "}
|
||||
<b>"Workspaces"</b>. Workspaces are buckets of files, documents,
|
||||
images, PDFs, and other files which will be transformed into
|
||||
something LLM's can understand and use in conversation.
|
||||
<br />
|
||||
<br />
|
||||
You can add and remove files at anytime.
|
||||
</p>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
|
||||
Create your first workspace
|
||||
</p>
|
||||
</button>
|
||||
</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>
|
||||
</React.Fragment>,
|
||||
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex w-full mt-2 justify-start ${
|
||||
popMsg ? "chat__message" : ""
|
||||
}`}
|
||||
>
|
||||
<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.
|
||||
<br />
|
||||
<br />
|
||||
AnythingLLM offers two ways of talking with your data:
|
||||
<br />
|
||||
<br />
|
||||
<i>Query:</i> Your chats will return data or inferences found with
|
||||
the documents in your workspace it has access to. Adding more
|
||||
documents to the Workspace make it smarter!
|
||||
<br />
|
||||
<br />
|
||||
<i>Conversational:</i> Your documents + your on-going chat history
|
||||
both contribute to the LLM knowledge at the same time. Great for
|
||||
appending real-time text-based info or corrections and
|
||||
misunderstandings the LLM might have.
|
||||
<br />
|
||||
<br />
|
||||
You can toggle between either mode <i>in the middle of chatting!</i>
|
||||
</p>
|
||||
</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">
|
||||
Wow, this sounds amazing, let me try it out already!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>,
|
||||
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex w-full mt-2 justify-start ${
|
||||
popMsg ? "chat__message" : ""
|
||||
}`}
|
||||
>
|
||||
<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!
|
||||
</p>
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-1 md:gap-4">
|
||||
<a
|
||||
href={paths.github()}
|
||||
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"
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
>
|
||||
<GitHub className="h-4 w-4" />
|
||||
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
|
||||
Star on GitHub
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
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"
|
||||
Welcome to AnythingLLM, AnythingLLM is an open-source AI tool by
|
||||
Mintplex Labs that turns anything into a trained chatbot you can
|
||||
query and chat with. AnythingLLM is a BYOK (bring-your-own-keys)
|
||||
software so there is no subscription, fee, or charges for this
|
||||
software outside of the services you want to use with 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`}
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
|
||||
Contact Mintplex Labs
|
||||
</p>
|
||||
</a>
|
||||
AnythingLLM is the easiest way to put powerful AI products like
|
||||
OpenAi, GPT-4, LangChain, PineconeDB, ChromaDB, and other services
|
||||
together in a neat package with no fuss to increase your
|
||||
productivity by 100x.
|
||||
</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"} />
|
||||
<div>
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
>
|
||||
AnythingLLM can run totally locally on your machine with little
|
||||
overhead you wont even notice it's there! No GPU needed. Cloud
|
||||
and on-premises installation is available as well.
|
||||
<br />
|
||||
The AI tooling ecosystem gets more powerful everyday.
|
||||
AnythingLLM makes it easy to use.
|
||||
</span>
|
||||
<a
|
||||
href={paths.github()}
|
||||
target="_blank"
|
||||
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" />
|
||||
<p>Create an issue on Github</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>,
|
||||
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex justify-center items-end w-full ${USER_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: 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`}
|
||||
>
|
||||
How do I get started?!
|
||||
</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"} />
|
||||
<div>
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
>
|
||||
It's simple. All collections are organized into buckets we call{" "}
|
||||
"Workspaces". Workspaces are buckets of files, documents,
|
||||
images, PDFs, and other files which will be transformed into
|
||||
something LLM's can understand and use in conversation.
|
||||
<br />
|
||||
<br />
|
||||
You can add and remove files at anytime.
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={showNewWsModal}
|
||||
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" />
|
||||
<p>Create your first workspace</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>,
|
||||
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex justify-center items-end w-full ${USER_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: 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`}
|
||||
>
|
||||
AnythingLLM is more than a smarter Dropbox.
|
||||
<br />
|
||||
<br />
|
||||
AnythingLLM offers two ways of talking with your data:
|
||||
<br />
|
||||
<br />
|
||||
<i>Query:</i> Your chats will return data or inferences found with
|
||||
the documents in your workspace it has access to. Adding more
|
||||
documents to the Workspace make it smarter!
|
||||
<br />
|
||||
<br />
|
||||
<i>Conversational:</i> Your documents + your on-going chat history
|
||||
both contribute to the LLM knowledge at the same time. Great for
|
||||
appending real-time text-based info or corrections and
|
||||
misunderstandings the LLM might have.
|
||||
<br />
|
||||
<br />
|
||||
You can toggle between either mode{" "}
|
||||
<i>in the middle of chatting!</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>,
|
||||
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`flex justify-center items-end w-full ${USER_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: 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`}
|
||||
>
|
||||
Wow, this sounds amazing, let me try it out already!
|
||||
</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"} />
|
||||
<div>
|
||||
<span
|
||||
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
|
||||
>
|
||||
Have Fun!
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-1 md:gap-4">
|
||||
<a
|
||||
href={paths.github()}
|
||||
target="_blank"
|
||||
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" />
|
||||
<p>Star on GitHub</p>
|
||||
</a>
|
||||
<a
|
||||
href={paths.mailToMintplex()}
|
||||
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" />
|
||||
<p>Contact Mintplex Labs</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -259,7 +329,7 @@ export default function DefaultChatContainer() {
|
||||
return (
|
||||
<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-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 />}
|
||||
{fetchedMessages.length === 0
|
||||
|
@ -14,25 +14,27 @@ export default function EditingChatBubble({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex w-full mt-2 items-center ${
|
||||
className={`relative flex w-full mt-2 items-start ${
|
||||
isUser ? "justify-end" : "justify-start"
|
||||
}`}
|
||||
>
|
||||
{isUser && (
|
||||
<button
|
||||
className="flex items-center text-red-500 hover:text-red-700 transition mr-2"
|
||||
onClick={() => removeMessage(index)}
|
||||
>
|
||||
<X className="mr-2" size={20} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
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)}
|
||||
>
|
||||
<X className="m-0.5" size={20} />
|
||||
</button>
|
||||
<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
|
||||
? "bg-slate-200 dark:bg-amber-800"
|
||||
: "bg-orange-100 dark:bg-stone-700"
|
||||
} rounded-b-2xl ${isUser ? "rounded-tl-2xl" : "rounded-tr-2xl"} ${
|
||||
isUser ? "rounded-tr-sm" : "rounded-tl-sm"
|
||||
? "rounded-tr-[40px] rounded-tl-[40px] rounded-bl-[40px]"
|
||||
: "rounded-br-[40px] rounded-tl-[40px] rounded-tr-[40px]"
|
||||
}
|
||||
}`}
|
||||
onDoubleClick={() => setIsEditing(true)}
|
||||
>
|
||||
@ -45,23 +47,16 @@ export default function EditingChatBubble({
|
||||
setIsEditing(false);
|
||||
}}
|
||||
autoFocus
|
||||
className="w-full"
|
||||
/>
|
||||
) : (
|
||||
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}
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -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 System from "../../../../models/system";
|
||||
import EditingChatBubble from "../../../EditingChatBubble";
|
||||
import AnythingLLMLight from "../../../../media/logo/anything-llm-light.png";
|
||||
import AnythingLLMDark from "../../../../media/logo/anything-llm-dark.png";
|
||||
import AnythingLLM from "../../../../media/logo/anything-llm.png";
|
||||
import showToast from "../../../../utils/toast";
|
||||
|
||||
export default function Appearance() {
|
||||
@ -120,11 +119,7 @@ export default function Appearance() {
|
||||
src={logo}
|
||||
alt="Uploaded Logo"
|
||||
className="w-48 h-48 object-contain mr-6"
|
||||
onError={(e) =>
|
||||
(e.target.src = prefersDarkMode
|
||||
? AnythingLLMLight
|
||||
: AnythingLLMDark)
|
||||
}
|
||||
onError={(e) => (e.target.src = AnythingLLM)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4">
|
@ -115,10 +115,7 @@ export default function LLMSelection({
|
||||
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"
|
||||
>
|
||||
{[
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4",
|
||||
].map((model) => {
|
||||
{["gpt-3.5-turbo", "gpt-4"].map((model) => {
|
||||
return (
|
||||
<option key={model} value={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 {
|
||||
FileMinus,
|
||||
FilePlus,
|
||||
Folder,
|
||||
FolderMinus,
|
||||
FolderPlus,
|
||||
Zap,
|
||||
} from "react-feather";
|
||||
import { nFormatter } from "../../../../../utils/numbers";
|
||||
import System from "../../../../../models/system";
|
||||
import UploadFile from "../UploadFile";
|
||||
import PreLoader from "../../../../Preloader";
|
||||
import { useEffect, useState } from "react";
|
||||
import FolderRow from "./FolderRow";
|
||||
import pluralize from "pluralize";
|
||||
|
||||
export default function Directory({
|
||||
files,
|
||||
parent = null,
|
||||
nested = 0,
|
||||
toggleSelection,
|
||||
isSelected,
|
||||
loading,
|
||||
setLoading,
|
||||
fileTypes,
|
||||
workspace,
|
||||
fetchKeys,
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
setHighlightWorkspace,
|
||||
moveToWorkspace,
|
||||
setLoadingMessage,
|
||||
loadingMessage,
|
||||
}) {
|
||||
const [isExpanded, toggleExpanded] = useState(false);
|
||||
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);
|
||||
const [amountSelected, setAmountSelected] = useState(0);
|
||||
|
||||
const toggleSelection = (item) => {
|
||||
setSelectedItems((prevSelectedItems) => {
|
||||
const newSelectedItems = { ...prevSelectedItems };
|
||||
|
||||
if (item.type === "folder") {
|
||||
const isCurrentlySelected = isFolderCompletelySelected(item);
|
||||
if (isCurrentlySelected) {
|
||||
item.items.forEach((file) => delete newSelectedItems[file.id]);
|
||||
} else {
|
||||
item.items.forEach((file) => (newSelectedItems[file.id] = true));
|
||||
}
|
||||
} else {
|
||||
if (newSelectedItems[item.id]) {
|
||||
delete newSelectedItems[item.id];
|
||||
} else {
|
||||
newSelectedItems[item.id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return newSelectedItems;
|
||||
});
|
||||
};
|
||||
|
||||
if (files.type === "folder") {
|
||||
return (
|
||||
<div style={{ marginLeft: nested }} className="mb-2">
|
||||
<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>
|
||||
)}
|
||||
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 (
|
||||
<div className="px-8 pb-8">
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="flex items-center justify-between w-[560px] px-5">
|
||||
<h3 className="text-white text-base font-bold">My Documents</h3>
|
||||
</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
|
||||
className="flex gap-x-2 items-center cursor-pointer w-full"
|
||||
onClick={() => toggleExpanded(!isExpanded)}
|
||||
className="overflow-y-auto pb-9"
|
||||
style={{ height: "calc(100% - 40px)" }}
|
||||
>
|
||||
<h2 className="text-base md:text-2xl">{files.name}</h2>
|
||||
{files.items.some((files) => files.type === "folder") ? (
|
||||
<p className="text-xs italic">{files.items.length} folders</p>
|
||||
{loading ? (
|
||||
<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>
|
||||
) : !!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}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<p className="text-xs italic">
|
||||
{files.items.length} documents |{" "}
|
||||
{nFormatter(
|
||||
files.items.reduce((a, b) => a + b.token_count_estimate, 0)
|
||||
)}{" "}
|
||||
tokens
|
||||
</p>
|
||||
<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>
|
||||
{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 (
|
||||
<div className="ml-[20px] my-2" id={meta.id}>
|
||||
<div className="flex items-center">
|
||||
{meta?.cached && (
|
||||
<button
|
||||
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>
|
||||
{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
|
||||
onMouseEnter={() => setHighlightWorkspace(true)}
|
||||
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"
|
||||
>
|
||||
Move {amountSelected} {pluralize("file", amountSelected)} to
|
||||
workspace
|
||||
</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`}
|
||||
>
|
||||
<button onClick={() => toggleSelection(`${parent}/${name}`)}>
|
||||
{isSelected(`${parent}/${name}`) ? (
|
||||
<FileMinus className="w-6 h-6 stroke-red-800 hover:fill-red-500" />
|
||||
) : (
|
||||
<FilePlus className="w-6 h-6 hover:stroke-green-800 hover:fill-green-500" />
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className="w-full items-center flex cursor-pointer"
|
||||
onClick={() => toggleDetails(!showDetails)}
|
||||
>
|
||||
<h3 className="text-sm">{name}</h3>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<UploadFile
|
||||
fileTypes={fileTypes}
|
||||
workspace={workspace}
|
||||
fetchKeys={fetchKeys}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleDelete(`${parent}/${name}`, meta)}
|
||||
className="flex items-center justify-end w-full"
|
||||
>
|
||||
<button className="text-sm text-slate-400 dark:text-stone-500 hover:text-red-500">
|
||||
Purge Document
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect, memo } from "react";
|
||||
import Workspace from "../../../../../models/workspace";
|
||||
import truncate from "truncate";
|
||||
import { humanFileSize, milliToHms } from "../../../../../utils/numbers";
|
||||
import { CheckCircle, XCircle } from "react-feather";
|
||||
import { Grid } from "react-loading-icons";
|
||||
import Workspace from "../../../../../../models/workspace";
|
||||
import { humanFileSize, milliToHms } from "../../../../../../utils/numbers";
|
||||
import PreLoader from "../../../../../Preloader";
|
||||
|
||||
function FileUploadProgressComponent({
|
||||
slug,
|
||||
@ -44,17 +44,15 @@ function FileUploadProgressComponent({
|
||||
|
||||
if (rejected) {
|
||||
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">
|
||||
<XCircle className="w-6 h-6 stroke-white bg-red-500 rounded-full p-1 w-full h-full" />
|
||||
</div>
|
||||
<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)}
|
||||
</p>
|
||||
<p className="text-red-700 dark:text-red-400 text-xs font-mono">
|
||||
{reason}
|
||||
</p>
|
||||
<p className="text-red-400 text-xs font-medium">{reason}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -62,43 +60,41 @@ function FileUploadProgressComponent({
|
||||
|
||||
if (status === "failed") {
|
||||
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">
|
||||
<XCircle className="w-6 h-6 stroke-white bg-red-500 rounded-full p-1 w-full h-full" />
|
||||
</div>
|
||||
<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)}
|
||||
</p>
|
||||
<p className="text-red-700 dark:text-red-400 text-xs font-mono">
|
||||
{error}
|
||||
</p>
|
||||
<p className="text-red-400 text-xs font-medium">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
{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" />
|
||||
)}
|
||||
</div>
|
||||
<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)}
|
||||
</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)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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 System from "../../../../models/system";
|
||||
import { ArrowsDownUp } from "@phosphor-icons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Workspace from "../../../../models/workspace";
|
||||
import paths from "../../../../utils/paths";
|
||||
import { useParams } from "react-router-dom";
|
||||
import System from "../../../../models/system";
|
||||
import Directory from "./Directory";
|
||||
import ConfirmationModal from "./ConfirmationModal";
|
||||
import { AlertTriangle } from "react-feather";
|
||||
import showToast from "../../../../utils/toast";
|
||||
import WorkspaceDirectory from "./WorkspaceDirectory";
|
||||
|
||||
export default function DocumentSettings({ workspace }) {
|
||||
const { slug } = useParams();
|
||||
const COST_PER_TOKEN = 0.0004;
|
||||
|
||||
export default function DocumentSettings({ workspace, fileTypes }) {
|
||||
const [highlightWorkspace, setHighlightWorkspace] = useState(false);
|
||||
const [availableDocs, setAvailableDocs] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const [directories, setDirectories] = useState(null);
|
||||
const [originalDocuments, setOriginalDocuments] = useState([]);
|
||||
const [selectedFiles, setSelectFiles] = useState([]);
|
||||
const [hasFiles, setHasFiles] = useState(true);
|
||||
const [canDelete, setCanDelete] = useState(false);
|
||||
const [workspaceDocs, setWorkspaceDocs] = useState([]);
|
||||
const [selectedItems, setSelectedItems] = useState({});
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [movedItems, setMovedItems] = useState([]);
|
||||
const [embeddingsCost, setEmbeddingsCost] = useState(0);
|
||||
const [loadingMessage, setLoadingMessage] = useState("");
|
||||
|
||||
async function fetchKeys(refetchWorkspace = false) {
|
||||
setLoading(true);
|
||||
const localFiles = await System.localFiles();
|
||||
const currentWorkspace = refetchWorkspace
|
||||
? await Workspace.bySlug(slug ?? workspace.slug)
|
||||
? await Workspace.bySlug(workspace.slug)
|
||||
: workspace;
|
||||
const originalDocs =
|
||||
currentWorkspace.documents.map((doc) => doc.docpath) || [];
|
||||
const hasAnyFiles = localFiles.items.some(
|
||||
(folder) => folder?.items?.length > 0
|
||||
);
|
||||
|
||||
const canDelete = await System.getCanDeleteWorkspaces();
|
||||
setCanDelete(canDelete);
|
||||
setDirectories(localFiles);
|
||||
setOriginalDocuments([...originalDocs]);
|
||||
setSelectFiles([...originalDocs]);
|
||||
setHasFiles(hasAnyFiles);
|
||||
const documentsInWorkspace =
|
||||
currentWorkspace.documents.map((doc) => doc.docpath) || [];
|
||||
|
||||
// Documents that are not in the workspace
|
||||
const availableDocs = {
|
||||
...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);
|
||||
}
|
||||
|
||||
@ -43,56 +76,20 @@ export default function DocumentSettings({ workspace }) {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
setLoading(true);
|
||||
showToast("Updating workspace...", "info", { autoClose: false });
|
||||
setShowConfirmation(false);
|
||||
setLoadingMessage("This may take a while for large documents");
|
||||
|
||||
const changes = docChanges();
|
||||
await Workspace.modifyEmbeddings(workspace.slug, changes)
|
||||
const changesToSend = {
|
||||
adds: movedItems.map((item) => `${item.folderName}/${item.name}`),
|
||||
};
|
||||
|
||||
setSelectedItems({});
|
||||
setHasChanges(false);
|
||||
setHighlightWorkspace(false);
|
||||
await Workspace.modifyEmbeddings(workspace.slug, changesToSend)
|
||||
.then((res) => {
|
||||
if (res && res.workspace) {
|
||||
showToast("Workspace updated successfully.", "success", {
|
||||
@ -108,122 +105,110 @@ export default function DocumentSettings({ workspace }) {
|
||||
});
|
||||
});
|
||||
|
||||
setSaving(false);
|
||||
setMovedItems([]);
|
||||
await fetchKeys(true);
|
||||
setLoading(false);
|
||||
setLoadingMessage("");
|
||||
};
|
||||
|
||||
const isSelected = (filepath) => {
|
||||
const isFolder = !filepath.includes("/");
|
||||
return isFolder
|
||||
? selectedFiles.some((doc) => doc.includes(filepath.split("/")[0]))
|
||||
: selectedFiles.some((doc) => doc.includes(filepath));
|
||||
};
|
||||
const moveSelectedItemsToWorkspace = () => {
|
||||
setHighlightWorkspace(false);
|
||||
setHasChanges(true);
|
||||
|
||||
const toggleSelection = (filepath) => {
|
||||
const isFolder = !filepath.includes("/");
|
||||
const parent = isFolder ? filepath : filepath.split("/")[0];
|
||||
const newMovedItems = [];
|
||||
|
||||
if (isSelected(filepath)) {
|
||||
const updatedDocs = isFolder
|
||||
? selectedFiles.filter((doc) => !doc.includes(parent))
|
||||
: selectedFiles.filter((doc) => !doc.includes(filepath));
|
||||
setSelectFiles([...new Set(updatedDocs)]);
|
||||
} else {
|
||||
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];
|
||||
for (const itemId of Object.keys(selectedItems)) {
|
||||
for (const folder of availableDocs.items) {
|
||||
const foundItem = folder.items.find((file) => file.id === itemId);
|
||||
if (foundItem) {
|
||||
newMovedItems.push({ ...foundItem, folderName: folder.name });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const combined = [...selectedFiles, ...newDocs];
|
||||
setSelectFiles([...new Set(combined)]);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
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">
|
||||
<p className="text-slate-200 dark:text-stone-300 text-center">
|
||||
loading workspace files
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
let totalTokenCount = 0;
|
||||
newMovedItems.forEach((item) => {
|
||||
const { cached, token_count_estimate } = item;
|
||||
if (!cached) {
|
||||
totalTokenCount += token_count_estimate;
|
||||
}
|
||||
});
|
||||
|
||||
const dollarAmount = (totalTokenCount / 1000) * COST_PER_TOKEN;
|
||||
setEmbeddingsCost(dollarAmount);
|
||||
setMovedItems([...movedItems, ...newMovedItems]);
|
||||
|
||||
let newAvailableDocs = JSON.parse(JSON.stringify(availableDocs));
|
||||
let newWorkspaceDocs = JSON.parse(JSON.stringify(workspaceDocs));
|
||||
|
||||
for (const itemId of Object.keys(selectedItems)) {
|
||||
let foundItem = null;
|
||||
let foundFolderIndex = null;
|
||||
|
||||
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 (
|
||||
<>
|
||||
{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
|
||||
files={directories}
|
||||
toggleSelection={toggleSelection}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-x-6 justify-center">
|
||||
<Directory
|
||||
files={availableDocs}
|
||||
loading={loading}
|
||||
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 className="flex items-center">
|
||||
<ArrowsDownUp className="text-white text-base font-bold rotate-90 w-11 h-11" />
|
||||
</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">
|
||||
<button
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ import React, { useState, useRef, useEffect } from "react";
|
||||
import Workspace from "../../../../models/workspace";
|
||||
import paths from "../../../../utils/paths";
|
||||
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
|
||||
// to the backend.
|
||||
@ -20,11 +23,14 @@ function castToType(key, value) {
|
||||
}
|
||||
|
||||
export default function WorkspaceSettings({ workspace }) {
|
||||
const { slug } = useParams();
|
||||
const formEl = useRef(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [success, setSuccess] = useState(null);
|
||||
const [totalVectors, setTotalVectors] = useState(null);
|
||||
const [canDelete, setCanDelete] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function setTimer() {
|
||||
@ -43,6 +49,17 @@ export default function WorkspaceSettings({ workspace }) {
|
||||
setTimer();
|
||||
}, [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) => {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
@ -61,6 +78,7 @@ export default function WorkspaceSettings({ workspace }) {
|
||||
setError(message);
|
||||
}
|
||||
setSaving(false);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const deleteWorkspace = async () => {
|
||||
@ -78,172 +96,189 @@ export default function WorkspaceSettings({ workspace }) {
|
||||
|
||||
return (
|
||||
<form ref={formEl} onSubmit={handleUpdate}>
|
||||
<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 ">
|
||||
Edit your workspace's settings
|
||||
</p>
|
||||
<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 min-w-[900px]">
|
||||
<div className="text-white text-opacity-60 text-sm font-bold uppercase py-6 border-b-2 border-white/10">
|
||||
Workspace Settings
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
disabled={true}
|
||||
defaultValue={workspace?.slug}
|
||||
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"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<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>
|
||||
</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"
|
||||
>
|
||||
Workspace Name
|
||||
</label>
|
||||
<p className="text-xs text-gray-600 dark:text-stone-400">
|
||||
This will only change the display name of your workspace.
|
||||
<div className="w-1/2">
|
||||
<h3 className="text-white text-sm font-semibold">
|
||||
Number of vectors
|
||||
</h3>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium my-[2px]">
|
||||
Total number of vectors in your vector database.
|
||||
</p>
|
||||
{totalVectors !== null ? (
|
||||
<p className="text-white text-opacity-60 text-sm font-medium">
|
||||
{totalVectors}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
minLength={2}
|
||||
maxLength={80}
|
||||
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"
|
||||
placeholder="My Workspace"
|
||||
required={true}
|
||||
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"
|
||||
>
|
||||
LLM Temperature
|
||||
</label>
|
||||
<p className="text-xs text-gray-600 dark:text-stone-400">
|
||||
This setting controls how "random" or dynamic your chat
|
||||
responses will be.
|
||||
<br />
|
||||
The higher the number (2.0 maximum) the more random and
|
||||
incoherent.
|
||||
<br />
|
||||
Recommended: 0.7
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
name="openAiTemp"
|
||||
type="number"
|
||||
min={0.0}
|
||||
max={2.0}
|
||||
step={0.1}
|
||||
onWheel={(e) => e.target.blur()}
|
||||
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"
|
||||
placeholder="0.7"
|
||||
required={true}
|
||||
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"
|
||||
>
|
||||
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
|
||||
</label>
|
||||
<p className="text-xs text-gray-600 dark:text-stone-400">
|
||||
The number of previous chats that will be included in the
|
||||
response's short-term memory.
|
||||
<br />
|
||||
Recommend 20. Anything more than 45 is likely to lead to
|
||||
continuous chat failures depending on message size.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
name="openAiHistory"
|
||||
type="number"
|
||||
min={1}
|
||||
max={45}
|
||||
step={1}
|
||||
onWheel={(e) => e.target.blur()}
|
||||
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"
|
||||
placeholder="20"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
onChange={() => setHasChanges(true)}
|
||||
/>
|
||||
) : (
|
||||
<PreLoader size="4" />
|
||||
)}
|
||||
</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 className="flex flex-col">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-white"
|
||||
>
|
||||
Workspace Name
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
This will only change the display name of your workspace.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
minLength={2}
|
||||
maxLength={80}
|
||||
defaultValue={workspace?.name}
|
||||
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"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
onChange={() => setHasChanges(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</p>
|
||||
)}
|
||||
{success && (
|
||||
<p className="text-green-600 dark:text-green-400 text-sm">
|
||||
Success: {success}
|
||||
</p>
|
||||
)}
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-white"
|
||||
>
|
||||
LLM Temperature
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
This setting controls how "random" or dynamic your chat
|
||||
responses will be.
|
||||
<br />
|
||||
The higher the number (2.0 maximum) the more random and
|
||||
incoherent.
|
||||
<br />
|
||||
<i>Recommended: 0.7</i>
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
name="openAiTemp"
|
||||
type="number"
|
||||
min={0.0}
|
||||
max={2.0}
|
||||
step={0.1}
|
||||
onWheel={(e) => e.target.blur()}
|
||||
defaultValue={workspace?.openAiTemp ?? 0.7}
|
||||
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"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
onChange={() => setHasChanges(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex flex-col gap-y-1 mb-4">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Chat History
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium">
|
||||
The number of previous chats that will be included in the
|
||||
response's short-term memory.
|
||||
<i>Recommend 20. </i>
|
||||
Anything more than 45 is likely to lead to continuous chat
|
||||
failures depending on message size.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
name="openAiHistory"
|
||||
type="number"
|
||||
min={1}
|
||||
max={45}
|
||||
step={1}
|
||||
onWheel={(e) => e.target.blur()}
|
||||
defaultValue={workspace?.openAiHistory ?? 20}
|
||||
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"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
onChange={() => setHasChanges(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-1/2">
|
||||
<div className="w-3/4">
|
||||
<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>
|
||||
</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 && (
|
||||
<p className="text-green-400 text-sm">Success: {success}</p>
|
||||
)}
|
||||
</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">
|
||||
<button
|
||||
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 justify-between p-2 md:p-6 space-x-2 border-t rounded-b border-gray-600">
|
||||
{canDelete && (
|
||||
<button
|
||||
onClick={deleteWorkspace}
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
)}
|
||||
{hasChanges && (
|
||||
<button
|
||||
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"}
|
||||
</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 { Archive, Sliders, UploadCloud, X } from "react-feather";
|
||||
import DocumentSettings from "./Documents";
|
||||
import WorkspaceSettings from "./Settings";
|
||||
import React, { useState, useEffect, lazy, Suspense, memo } from "react";
|
||||
import { X } from "react-feather";
|
||||
import { useParams } from "react-router-dom";
|
||||
import Workspace from "../../../models/workspace";
|
||||
import System from "../../../models/system";
|
||||
import UploadToWorkspace from "./Upload";
|
||||
import { isMobile } from "react-device-detect";
|
||||
|
||||
const TABS = {
|
||||
documents: DocumentSettings,
|
||||
settings: WorkspaceSettings,
|
||||
upload: UploadToWorkspace,
|
||||
};
|
||||
const DocumentSettings = lazy(() => import("./Documents"));
|
||||
const WorkspaceSettings = lazy(() => import("./Settings"));
|
||||
|
||||
const noop = () => false;
|
||||
export default function ManageWorkspace({
|
||||
hideModal = noop,
|
||||
providedSlug = null,
|
||||
}) {
|
||||
const noop = () => {};
|
||||
const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
||||
const { slug } = useParams();
|
||||
const [selectedTab, setSelectedTab] = useState("documents");
|
||||
const [workspace, setWorkspace] = useState(null);
|
||||
@ -37,110 +29,99 @@ export default function ManageWorkspace({
|
||||
setWorkspace(workspace);
|
||||
}
|
||||
fetchWorkspace();
|
||||
}, [selectedTab, slug]);
|
||||
}, [providedSlug, slug]);
|
||||
|
||||
if (!workspace) return null;
|
||||
|
||||
const Component = TABS[selectedTab || "documents"];
|
||||
if (isMobile) {
|
||||
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 transition duration-300 z-20`}>
|
||||
<div className="relative max-w-lg mx-auto bg-main-gradient rounded-[12px] shadow border-2 border-slate-300/10">
|
||||
<div className="p-6">
|
||||
<h1 className="text-white text-lg font-semibold">
|
||||
Editing "{workspace.name}"
|
||||
</h1>
|
||||
<p className="text-white mt-4">
|
||||
Editing these settings are only available on a desktop device.
|
||||
Please access this page on your desktop to continue.
|
||||
</p>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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="flex fixed top-0 left-0 right-0 w-full h-full"
|
||||
onClick={hideModal}
|
||||
/>
|
||||
<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="flex flex-col gap-y-1 border-b dark:border-gray-600 px-4 pt-4 ">
|
||||
<div className="flex items-start justify-between rounded-t ">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Update "{workspace.name}"
|
||||
</h3>
|
||||
<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={hideModal}
|
||||
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"
|
||||
data-modal-hide="staticModal"
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
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>
|
||||
<WorkspaceSettingTabs
|
||||
selectedTab={selectedTab}
|
||||
changeTab={setSelectedTab}
|
||||
/>
|
||||
</div>
|
||||
<Component
|
||||
hideModal={hideModal}
|
||||
workspace={workspace}
|
||||
fileTypes={fileTypes}
|
||||
/>
|
||||
<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" />
|
||||
</button>
|
||||
</div>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<div className={selectedTab === "documents" ? "" : "hidden"}>
|
||||
<DocumentSettings workspace={workspace} fileTypes={fileTypes} />
|
||||
</div>
|
||||
<div className={selectedTab === "settings" ? "" : "hidden"}>
|
||||
<WorkspaceSettings workspace={workspace} fileTypes={fileTypes} />
|
||||
</div>
|
||||
</Suspense>
|
||||
</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() {
|
||||
const [showing, setShowing] = useState(false);
|
||||
const showModal = () => {
|
||||
setShowing(true);
|
||||
};
|
||||
|
||||
const hideModal = () => {
|
||||
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"
|
||||
onClick={hideModal}
|
||||
/>
|
||||
<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="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<div className="relative w-[500px] max-h-full">
|
||||
<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 border-white/10">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Create a New Workspace
|
||||
New Workspace
|
||||
</h3>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
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"
|
||||
data-modal-hide="staticModal"
|
||||
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>
|
||||
@ -52,7 +51,7 @@ export default function NewWorkspaceModal({ hideModal = noop }) {
|
||||
name="name"
|
||||
type="text"
|
||||
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"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
@ -63,25 +62,14 @@ export default function NewWorkspaceModal({ hideModal = noop }) {
|
||||
Error: {error}
|
||||
</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 className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
<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>
|
||||
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-white/10 rounded-b">
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import System from "../../../models/system";
|
||||
import { AUTH_TOKEN, AUTH_USER } from "../../../utils/constants";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
import paths from "../../../utils/paths";
|
||||
|
||||
export default function MultiUserAuth() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -19,7 +20,7 @@ export default function MultiUserAuth() {
|
||||
if (valid && !!token && !!user) {
|
||||
window.localStorage.setItem(AUTH_USER, JSON.stringify(user));
|
||||
window.localStorage.setItem(AUTH_TOKEN, token);
|
||||
window.location.reload();
|
||||
window.location = paths.home();
|
||||
} else {
|
||||
setError(message);
|
||||
setLoading(false);
|
||||
@ -29,66 +30,52 @@ export default function MultiUserAuth() {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleLogin}>
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<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 pt-11 pb-9 rounded-t">
|
||||
<div className="flex items-center flex-col">
|
||||
<img src={_initLogo} alt="Logo" className="w-1/2" />
|
||||
<h3 className="text-md md:text-xl font-semibold text-gray-900 dark:text-white">
|
||||
This instance is password protected.
|
||||
<h3 className="text-md md:text-2xl font-bold text-gray-900 dark:text-white text-center">
|
||||
Sign In
|
||||
</h3>
|
||||
</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>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Instance Username
|
||||
</label>
|
||||
<input
|
||||
name="username"
|
||||
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}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Instance Password
|
||||
</label>
|
||||
<input
|
||||
name="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}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</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 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
|
||||
disabled={loading}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import System from "../../../models/system";
|
||||
import { AUTH_TOKEN } from "../../../utils/constants";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
import paths from "../../../utils/paths";
|
||||
|
||||
export default function SingleUserAuth() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -18,7 +19,7 @@ export default function SingleUserAuth() {
|
||||
const { valid, token, message } = await System.requestToken(data);
|
||||
if (valid && !!token) {
|
||||
window.localStorage.setItem(AUTH_TOKEN, token);
|
||||
window.location.reload();
|
||||
window.location = paths.home();
|
||||
} else {
|
||||
setError(message);
|
||||
setLoading(false);
|
||||
@ -28,29 +29,22 @@ export default function SingleUserAuth() {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleLogin}>
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<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 pt-11 pb-9 rounded-t dark:border-gray-600">
|
||||
<div className="flex items-center flex-col">
|
||||
<img src={_initLogo} alt="Logo" className="w-1/2" />
|
||||
<h3 className="text-md md:text-xl font-semibold text-gray-900 dark:text-white">
|
||||
This instance is password protected.
|
||||
<h3 className="text-md md:text-2xl font-bold text-gray-900 dark:text-white text-center">
|
||||
Sign In
|
||||
</h3>
|
||||
</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>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Workspace Password
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
id="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-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}
|
||||
autoComplete="off"
|
||||
/>
|
||||
@ -60,19 +54,15 @@ export default function SingleUserAuth() {
|
||||
Error: {error}
|
||||
</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 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
|
||||
disabled={loading}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,16 +3,31 @@ import System from "../../../models/system";
|
||||
import SingleUserAuth from "./SingleUserAuth";
|
||||
import MultiUserAuth from "./MultiUserAuth";
|
||||
import {
|
||||
AUTH_TIMESTAMP,
|
||||
AUTH_TOKEN,
|
||||
AUTH_USER,
|
||||
AUTH_TIMESTAMP,
|
||||
} from "../../../utils/constants";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
|
||||
export default function PasswordModal({ mode = "single" }) {
|
||||
const { logo: _initLogo } = useLogo();
|
||||
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="flex fixed top-0 left-0 right-0 w-full h-full" />
|
||||
<div className="relative w-full max-w-2xl max-h-full">
|
||||
<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="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 />}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,8 @@
|
||||
export default function PreLoader() {
|
||||
export default function PreLoader({ size = "16" }) {
|
||||
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 { userFromStorage } from "../../utils/request";
|
||||
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.
|
||||
// When in single user mode we just bypass any authchecks.
|
||||
function useIsAuthenticated() {
|
||||
const [isAuthd, setIsAuthed] = useState(null);
|
||||
const [shouldRedirectToOnboarding, setShouldRedirectToOnboarding] =
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const validateSession = async () => {
|
||||
const multiUserMode = (await System.keys()).MultiUserMode;
|
||||
if (!multiUserMode) {
|
||||
const {
|
||||
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);
|
||||
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 localAuthToken = localStorage.getItem(AUTH_TOKEN);
|
||||
if (!localUser || !localAuthToken) {
|
||||
@ -41,24 +75,40 @@ function useIsAuthenticated() {
|
||||
validateSession();
|
||||
}, []);
|
||||
|
||||
return isAuthd;
|
||||
return { isAuthd, shouldRedirectToOnboarding };
|
||||
}
|
||||
|
||||
export function AdminRoute({ Component }) {
|
||||
const authed = useIsAuthenticated();
|
||||
if (authed === null) return <FullScreenLoader />;
|
||||
const { isAuthd, shouldRedirectToOnboarding } = useIsAuthenticated();
|
||||
if (isAuthd === null) return <FullScreenLoader />;
|
||||
|
||||
if (shouldRedirectToOnboarding) {
|
||||
return <Navigate to={paths.onboarding()} />;
|
||||
}
|
||||
|
||||
const user = userFromStorage();
|
||||
return authed && user?.role === "admin" ? (
|
||||
<Component />
|
||||
return isAuthd && user?.role === "admin" ? (
|
||||
<UserMenu>
|
||||
<Component />
|
||||
</UserMenu>
|
||||
) : (
|
||||
<Navigate to={paths.home()} />
|
||||
);
|
||||
}
|
||||
|
||||
export default function PrivateRoute({ Component }) {
|
||||
const authed = useIsAuthenticated();
|
||||
if (authed === null) return <FullScreenLoader />;
|
||||
const { isAuthd, shouldRedirectToOnboarding } = useIsAuthenticated();
|
||||
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 { Book, Settings } from "react-feather";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import Workspace from "../../../models/workspace";
|
||||
@ -8,10 +7,12 @@ import ManageWorkspace, {
|
||||
} from "../../Modals/MangeWorkspace";
|
||||
import paths from "../../../utils/paths";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { GearSix, SquaresFour } from "@phosphor-icons/react";
|
||||
|
||||
export default function ActiveWorkspaces() {
|
||||
const { slug } = useParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [settingHover, setSettingHover] = useState(false);
|
||||
const [workspaces, setWorkspaces] = useState([]);
|
||||
const [selectedWs, setSelectedWs] = useState(null);
|
||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||
@ -51,31 +52,55 @@ export default function ActiveWorkspaces() {
|
||||
>
|
||||
<a
|
||||
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 ${
|
||||
isActive
|
||||
? "bg-gray-100 dark:bg-stone-600"
|
||||
: "hover:bg-slate-100 dark:hover:bg-stone-900 "
|
||||
}`}
|
||||
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
|
||||
? "bg-workspace-item-selected-gradient border-slate-100 border-opacity-50"
|
||||
: "bg-workspace-item-gradient bg-opacity-60 border-transparent"
|
||||
}`}
|
||||
>
|
||||
<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}
|
||||
</p>
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
{workspace.name}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onMouseEnter={() => setSettingHover(true)}
|
||||
onMouseLeave={() => setSettingHover(false)}
|
||||
onClick={() => {
|
||||
setSelectedWs(workspace);
|
||||
showModal();
|
||||
}}
|
||||
className="rounded-md flex items-center justify-center text-white ml-auto"
|
||||
>
|
||||
<GearSix
|
||||
weight={settingHover ? "fill" : "regular"}
|
||||
hidden={!isActive}
|
||||
className="h-[20px] w-[20px] transition-all duration-300"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedWs(workspace);
|
||||
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"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5 transition-all duration-300 group-hover:rotate-90" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{showing && !!selectedWs && (
|
||||
<ManageWorkspace hideModal={hideModal} providedSlug={selectedWs.slug} />
|
||||
{showing && (
|
||||
<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 { LogOut, Menu, Package, Plus, Shield } from "react-feather";
|
||||
import {
|
||||
AtSign,
|
||||
Wrench,
|
||||
GithubLogo,
|
||||
BookOpen,
|
||||
GitHub,
|
||||
LogOut,
|
||||
Menu,
|
||||
Package,
|
||||
Plus,
|
||||
Shield,
|
||||
Tool,
|
||||
X,
|
||||
} from "react-feather";
|
||||
import IndexCount from "./IndexCount";
|
||||
import LLMStatus from "./LLMStatus";
|
||||
DiscordLogo,
|
||||
DotsThree,
|
||||
} from "@phosphor-icons/react";
|
||||
// import IndexCount from "./IndexCount";
|
||||
// import LLMStatus from "./LLMStatus";
|
||||
import NewWorkspaceModal, {
|
||||
useNewWorkspaceModal,
|
||||
} from "../Modals/NewWorkspace";
|
||||
import ActiveWorkspaces from "./ActiveWorkspaces";
|
||||
import paths from "../../utils/paths";
|
||||
import Discord from "../Icons/Discord";
|
||||
import useUser from "../../hooks/useUser";
|
||||
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 SettingsOverlay, { useSystemSettingsOverlay } from "./SettingsOverlay";
|
||||
|
||||
export default function Sidebar() {
|
||||
const { logo } = useLogo();
|
||||
const sidebarRef = useRef(null);
|
||||
const { showOverlay } = useSystemSettingsOverlay();
|
||||
const {
|
||||
showing: showingNewWsModal,
|
||||
showModal: showNewWsModal,
|
||||
@ -40,13 +38,12 @@ export default function Sidebar() {
|
||||
<div
|
||||
ref={sidebarRef}
|
||||
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="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||
<div className="flex flex-col h-full overflow-x-hidden">
|
||||
{/* Header Information */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex shrink-0 max-w-[50%] items-center justify-start">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex shrink-0 max-w-[65%] items-center justify-start">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
@ -54,32 +51,30 @@ export default function Sidebar() {
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2 items-center text-slate-500">
|
||||
<AdminHome />
|
||||
<SettingsButton onClick={showOverlay} />
|
||||
<div className="flex gap-x-2 items-center text-slate-200">
|
||||
{/* <AdminHome /> */}
|
||||
<SettingsButton />
|
||||
</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">
|
||||
<div className="flex gap-x-2 items-center justify-between">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
||||
New workspace
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<ActiveWorkspaces />
|
||||
<div className="flex-grow flex flex-col">
|
||||
<div className="flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll">
|
||||
<div className="flex gap-x-2 items-center justify-between">
|
||||
<button
|
||||
onClick={showNewWsModal}
|
||||
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-5 w-5" />
|
||||
<p className="text-sidebar text-sm font-semibold">
|
||||
New Workspace
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<ActiveWorkspaces />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex flex-col flex-grow justify-end mb-2">
|
||||
{/* <div className="flex flex-col gap-y-2">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<LLMStatus />
|
||||
<IndexCount />
|
||||
@ -87,45 +82,45 @@ export default function Sidebar() {
|
||||
<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"
|
||||
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" />
|
||||
<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
|
||||
</p>
|
||||
</a>
|
||||
<ManagedHosting />
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-end justify-between mt-2">
|
||||
<div className="flex gap-x-1 items-center">
|
||||
<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 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
|
||||
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
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
@ -141,7 +136,6 @@ export function SidebarMobileHeader() {
|
||||
const sidebarRef = useRef(null);
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const [showBgOverlay, setShowBgOverlay] = useState(false);
|
||||
const { showOverlay } = useSystemSettingsOverlay();
|
||||
const {
|
||||
showing: showingNewWsModal,
|
||||
showModal: showNewWsModal,
|
||||
@ -165,21 +159,22 @@ export function SidebarMobileHeader() {
|
||||
|
||||
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
|
||||
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" />
|
||||
</button>
|
||||
<div className="flex shrink-0 w-fit items-center justify-start">
|
||||
<div className="flex items-center justify-center flex-grow">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="rounded w-full max-h-[40px]"
|
||||
style={{ objectFit: "contain" }}
|
||||
className="block mx-auto h-6 w-auto"
|
||||
style={{ maxHeight: "40px", objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-12"></div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@ -192,14 +187,13 @@ export function SidebarMobileHeader() {
|
||||
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`}
|
||||
} duration-500 fixed top-0 left-0 ${USER_BACKGROUND_COLOR} bg-opacity-75 w-screen h-screen`}
|
||||
onClick={() => setShowSidebar(false)}
|
||||
/>
|
||||
<div
|
||||
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">
|
||||
{/* Header Information */}
|
||||
<div className="flex w-full items-center justify-between gap-x-4">
|
||||
@ -212,14 +206,13 @@ export function SidebarMobileHeader() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2 items-center text-slate-500 shink-0">
|
||||
<AdminHome />
|
||||
<SettingsButton onClick={showOverlay} />
|
||||
<SettingsButton />
|
||||
</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 className="h-auto md:sidebar-items">
|
||||
<div
|
||||
style={{ height: "calc(100vw - -3rem)" }}
|
||||
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">
|
||||
<button
|
||||
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" />
|
||||
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
||||
New workspace
|
||||
<Plus className="h-5 w-5" />
|
||||
<p className="text-sidebar text-sm font-semibold">
|
||||
New Workspace
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
@ -239,53 +232,34 @@ export function SidebarMobileHeader() {
|
||||
</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 */}
|
||||
<div className="flex items-end justify-between mt-2">
|
||||
<div className="flex gap-x-1 items-center">
|
||||
<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 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
|
||||
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
|
||||
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>
|
||||
{/* <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>
|
||||
<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>
|
||||
@ -303,7 +277,7 @@ function AdminHome() {
|
||||
return (
|
||||
<a
|
||||
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" />
|
||||
</a>
|
||||
@ -323,27 +297,24 @@ function LogoutButton() {
|
||||
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||
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" />
|
||||
<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}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsButton({ onClick }) {
|
||||
const { user } = useUser();
|
||||
|
||||
if (!!user && user?.role !== "admin") return null;
|
||||
function SettingsButton() {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
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"
|
||||
<a
|
||||
href={paths.general.llmPreference()}
|
||||
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 " />
|
||||
</button>
|
||||
<Wrench className="h-4 w-4" weight="fill" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@ -353,10 +324,10 @@ function ManagedHosting() {
|
||||
<a
|
||||
href={paths.hosting()}
|
||||
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" />
|
||||
<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
|
||||
</p>
|
||||
</a>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useRef, useEffect } from "react";
|
||||
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 seed = user?.uid
|
||||
? toPseudoRandomInteger(user.uid)
|
||||
@ -14,7 +14,12 @@ export default function Jazzicon({ size = 10, user }) {
|
||||
divRef.current.appendChild(result);
|
||||
}, []); // 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 = "") {
|
||||
|
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 { Maximize2, Minimize2 } from "react-feather";
|
||||
import { memo, useState, useEffect, useRef } from "react";
|
||||
import { X } from "react-feather";
|
||||
import { v4 } from "uuid";
|
||||
import { decode as HTMLDecode } from "he";
|
||||
import { CaretRight, FileText } from "@phosphor-icons/react";
|
||||
import truncate from "truncate";
|
||||
|
||||
function combineLikeSources(sources) {
|
||||
const combined = {};
|
||||
@ -19,81 +21,149 @@ function combineLikeSources(sources) {
|
||||
|
||||
export default function Citations({ sources = [] }) {
|
||||
if (sources.length === 0) return null;
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedSource, setSelectedSource] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mt-4 justify-left">
|
||||
<div className="flex flex-col justify-left overflow-x-scroll ">
|
||||
<div className="w-full flex overflow-x-scroll items-center gap-4 mt-1 doc__source">
|
||||
<button
|
||||
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) => (
|
||||
<Citation id={source?.id || v4()} source={source} />
|
||||
<Citation
|
||||
key={source?.id || v4()}
|
||||
source={source}
|
||||
onClick={() => setSelectedSource(source)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="w-fit text-gray-700 dark:text-stone-400 text-xs mt-1">
|
||||
*citations may not be relevant to end result.
|
||||
</p>
|
||||
)}
|
||||
{selectedSource && (
|
||||
<CitationDetailModal
|
||||
source={selectedSource}
|
||||
onClose={() => setSelectedSource(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Citation = memo(({ source, id }) => {
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
const { references = 0, title, text } = source;
|
||||
if (title?.length === 0 || text?.length === 0) return null;
|
||||
const handleMinMax = () => {
|
||||
setMaximized(!maximized);
|
||||
Array.from(
|
||||
document?.querySelectorAll(
|
||||
`div[data-citation]:not([data-citation="${id}"])`
|
||||
)
|
||||
).forEach((el) => {
|
||||
const func = maximized ? "remove" : "add";
|
||||
el.classList[func]("hidden");
|
||||
});
|
||||
};
|
||||
const Citation = memo(({ source, onClick }) => {
|
||||
const { title } = source;
|
||||
if (!title) return null;
|
||||
|
||||
const truncatedTitle = truncateMiddle(title);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id || v4()}
|
||||
data-citation={id || v4()}
|
||||
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 ${
|
||||
maximized ? "md:w-full h-fit pb-4" : ""
|
||||
}`}
|
||||
className="flex flex-row justify-center items-center cursor-pointer text-sky-400"
|
||||
style={{ width: "24%" }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<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">
|
||||
<p className="text-base text-gray-800 dark:text-slate-400 italic truncate w-3/4">
|
||||
{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>
|
||||
<FileText className="w-6 h-6" weight="bold" />
|
||||
<p className="text-sm font-medium whitespace-nowrap">{truncatedTitle}</p>
|
||||
</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 { userFromStorage } from "../../../../../utils/request";
|
||||
import Citations from "../Citation";
|
||||
import {
|
||||
AI_BACKGROUND_COLOR,
|
||||
USER_BACKGROUND_COLOR,
|
||||
} from "../../../../../utils/constants";
|
||||
|
||||
const HistoricalMessage = forwardRef(
|
||||
({ message, role, workspace, sources = [], error = false }, ref) => {
|
||||
if (role === "user") {
|
||||
return (
|
||||
<div className="flex justify-end mb-4 items-start">
|
||||
<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">
|
||||
<span
|
||||
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`}
|
||||
>
|
||||
{message}
|
||||
</span>
|
||||
</div>
|
||||
<Jazzicon size={30} user={{ uid: userFromStorage()?.username }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (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
|
||||
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
|
||||
respond to message.
|
||||
</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
|
||||
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"
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(message) }}
|
||||
/>
|
||||
<Citations sources={sources} />
|
||||
<div
|
||||
ref={ref}
|
||||
className={`flex justify-center items-end w-full ${
|
||||
role === "user" ? USER_BACKGROUND_COLOR : 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:
|
||||
role === "user"
|
||||
? userFromStorage()?.username
|
||||
: workspace.slug,
|
||||
}}
|
||||
role={role}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<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
|
||||
not respond to message.
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
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) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{role === "assistant" && <Citations sources={sources} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,18 +9,25 @@ const PromptReply = forwardRef(
|
||||
{ uuid, reply, pending, error, workspace, sources = [], closed = true },
|
||||
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) {
|
||||
return (
|
||||
<div
|
||||
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="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">
|
||||
<span className={`inline-block p-2`}>
|
||||
<div className="dot-falling"></div>
|
||||
</span>
|
||||
<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"
|
||||
/>
|
||||
<div className="mt-3 ml-5 dot-falling"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -28,15 +35,23 @@ const PromptReply = forwardRef(
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="chat__message flex justify-start mb-4 items-center">
|
||||
<Jazzicon size={30} user={{ uid: workspace.slug }} />
|
||||
<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">
|
||||
<span className={`inline-block`}>
|
||||
<div
|
||||
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
|
||||
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 respond to message.
|
||||
<span className="text-xs">Reason: {error || "unknown"}</span>
|
||||
</span>
|
||||
<span className="text-xs">Reason: {error || "unknown"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,13 +59,23 @@ const PromptReply = forwardRef(
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={uuid} ref={ref} className="mb-4 flex justify-start items-end">
|
||||
<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
|
||||
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"
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }}
|
||||
/>
|
||||
<div
|
||||
key={uuid}
|
||||
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
|
||||
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) }}
|
||||
/>
|
||||
</div>
|
||||
<Citations sources={sources} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Frown } from "react-feather";
|
||||
import HistoricalMessage from "./HistoricalMessage";
|
||||
import PromptReply from "./PromptReply";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useManageWorkspaceModal } from "../../../Modals/MangeWorkspace";
|
||||
import ManageWorkspace from "../../../Modals/MangeWorkspace";
|
||||
|
||||
export default function ChatHistory({ history = [], workspace }) {
|
||||
const replyRef = useRef(null);
|
||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||
|
||||
useEffect(() => {
|
||||
if (replyRef.current) {
|
||||
@ -16,21 +18,37 @@ export default function ChatHistory({ history = [], workspace }) {
|
||||
|
||||
if (history.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col h-[89%] md:mt-0 pb-5 w-full justify-center items-center">
|
||||
<div className="w-fit flex items-center gap-x-2">
|
||||
<Frown className="h-4 w-4 text-slate-400" />
|
||||
<p className="text-slate-400">No chat history found.</p>
|
||||
<div className="flex flex-col h-full md:mt-0 pb-48 w-full justify-end items-center">
|
||||
<div className="flex flex-col items-start">
|
||||
<p className="text-white/60 text-lg font-base -ml-6 py-4">
|
||||
Welcome to your new workspace.
|
||||
</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>
|
||||
<p className="text-slate-400 text-xs">
|
||||
Send your first message to get started.
|
||||
</p>
|
||||
{showing && (
|
||||
<ManageWorkspace
|
||||
hideModal={hideModal}
|
||||
providedSlug={workspace.slug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
{history.map((props, index) => {
|
||||
@ -64,6 +82,10 @@ export default function ChatHistory({ history = [], workspace }) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{showing && (
|
||||
<ManageWorkspace hideModal={hideModal} providedSlug={workspace.slug} />
|
||||
)}
|
||||
</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 { Loader, Menu, X } from "react-feather";
|
||||
import { Loader } from "react-feather";
|
||||
import ManageWorkspace, {
|
||||
useManageWorkspaceModal,
|
||||
} from "../../../Modals/MangeWorkspace";
|
||||
|
||||
export default function PromptInput({
|
||||
workspace,
|
||||
@ -10,13 +14,15 @@ export default function PromptInput({
|
||||
inputDisabled,
|
||||
buttonDisabled,
|
||||
}) {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||
const formRef = useRef(null);
|
||||
const [_, setFocused] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
setFocused(false);
|
||||
submit(e);
|
||||
};
|
||||
|
||||
const captureEnter = (event) => {
|
||||
if (event.keyCode == 13) {
|
||||
if (!event.shiftKey) {
|
||||
@ -24,6 +30,7 @@ export default function PromptInput({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const adjustTextArea = (event) => {
|
||||
if (isMobile) return false;
|
||||
const element = event.target;
|
||||
@ -34,173 +41,68 @@ export default function PromptInput({
|
||||
: "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 (
|
||||
<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
|
||||
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">
|
||||
<CommandMenu
|
||||
workspace={workspace}
|
||||
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
|
||||
onKeyUp={adjustTextArea}
|
||||
onKeyDown={captureEnter}
|
||||
onChange={onChange}
|
||||
required={true}
|
||||
maxLength={240}
|
||||
disabled={inputDisabled}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={(e) => {
|
||||
setFocused(false);
|
||||
adjustTextArea(e);
|
||||
}}
|
||||
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"
|
||||
placeholder={
|
||||
isMobile
|
||||
? "Enter your message here."
|
||||
: "Shift + Enter for newline. Enter to submit."
|
||||
}
|
||||
/>
|
||||
<button
|
||||
ref={formRef}
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{buttonDisabled ? (
|
||||
<Loader className="w-6 h-6 animate-spin" />
|
||||
) : (
|
||||
<svg
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
<Tracking workspaceSlug={workspace.slug} />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
<div className="flex items-center rounded-lg md:mb-4">
|
||||
<div className="w-[600px] bg-main-gradient shadow-2xl border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden">
|
||||
<div className="flex items-center w-full border-b-2 border-gray-500/50">
|
||||
<textarea
|
||||
onKeyUp={adjustTextArea}
|
||||
onKeyDown={captureEnter}
|
||||
onChange={onChange}
|
||||
required={true}
|
||||
maxLength={240}
|
||||
disabled={inputDisabled}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={(e) => {
|
||||
setFocused(false);
|
||||
adjustTextArea(e);
|
||||
}}
|
||||
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"
|
||||
value={message}
|
||||
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={"Send a message"}
|
||||
/>
|
||||
<button
|
||||
ref={formRef}
|
||||
type="submit"
|
||||
disabled={buttonDisabled}
|
||||
className="inline-flex justify-center rounded-2xl cursor-pointer text-white/60 hover:text-white group ml-4"
|
||||
>
|
||||
<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>
|
||||
{buttonDisabled ? (
|
||||
<Loader className="w-6 h-6 animate-spin" />
|
||||
) : (
|
||||
<PaperPlaneRight className="w-7 h-7 my-3" weight="fill" />
|
||||
)}
|
||||
<span className="sr-only">Send message</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<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>
|
||||
{showing && (
|
||||
<ManageWorkspace hideModal={hideModal} providedSlug={workspace.slug} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -68,10 +68,10 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
return (
|
||||
<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-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 />}
|
||||
<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} />
|
||||
<PromptInput
|
||||
workspace={workspace}
|
||||
|
@ -3,16 +3,18 @@ import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
|
||||
export default function LoadingChat() {
|
||||
const highlightColor = "#3D4147";
|
||||
const baseColor = "#2C2F35";
|
||||
return (
|
||||
<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-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
|
||||
height="100px"
|
||||
width="100%"
|
||||
baseColor={"#2a3a53"}
|
||||
highlightColor={"#395073"}
|
||||
highlightColor={highlightColor}
|
||||
baseColor={baseColor}
|
||||
count={1}
|
||||
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"
|
||||
@ -20,8 +22,8 @@ export default function LoadingChat() {
|
||||
<Skeleton.default
|
||||
height="100px"
|
||||
width={isMobile ? "70%" : "45%"}
|
||||
baseColor={"#2a3a53"}
|
||||
highlightColor={"#395073"}
|
||||
baseColor={baseColor}
|
||||
highlightColor={highlightColor}
|
||||
count={1}
|
||||
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"
|
||||
@ -29,8 +31,8 @@ export default function LoadingChat() {
|
||||
<Skeleton.default
|
||||
height="100px"
|
||||
width={isMobile ? "55%" : "30%"}
|
||||
baseColor={"#2a3a53"}
|
||||
highlightColor={"#395073"}
|
||||
baseColor={baseColor}
|
||||
highlightColor={highlightColor}
|
||||
count={1}
|
||||
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"
|
||||
@ -38,8 +40,8 @@ export default function LoadingChat() {
|
||||
<Skeleton.default
|
||||
height="100px"
|
||||
width={isMobile ? "88%" : "25%"}
|
||||
baseColor={"#2a3a53"}
|
||||
highlightColor={"#395073"}
|
||||
baseColor={baseColor}
|
||||
highlightColor={highlightColor}
|
||||
count={1}
|
||||
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"
|
||||
@ -47,8 +49,8 @@ export default function LoadingChat() {
|
||||
<Skeleton.default
|
||||
height="160px"
|
||||
width="100%"
|
||||
baseColor={"#2a3a53"}
|
||||
highlightColor={"#395073"}
|
||||
baseColor={baseColor}
|
||||
highlightColor={highlightColor}
|
||||
count={1}
|
||||
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"
|
||||
|
@ -1,27 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import usePrefersDarkMode from "./usePrefersDarkMode";
|
||||
import System from "../models/system";
|
||||
import AnythingLLMDark from "../media/logo/anything-llm-dark.png";
|
||||
import AnythingLLMLight from "../media/logo/anything-llm-light.png";
|
||||
import AnythingLLM from "../media/logo/anything-llm.png";
|
||||
|
||||
export default function useLogo() {
|
||||
const [logo, setLogo] = useState("");
|
||||
const prefersDarkMode = usePrefersDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchInstanceLogo() {
|
||||
try {
|
||||
const logoURL = await System.fetchLogo(!prefersDarkMode);
|
||||
logoURL
|
||||
? setLogo(logoURL)
|
||||
: setLogo(prefersDarkMode ? AnythingLLMLight : AnythingLLMDark);
|
||||
const logoURL = await System.fetchLogo();
|
||||
logoURL ? setLogo(logoURL) : setLogo(AnythingLLM);
|
||||
} catch (err) {
|
||||
setLogo(prefersDarkMode ? AnythingLLMLight : AnythingLLMDark);
|
||||
setLogo(AnythingLLM);
|
||||
console.error("Failed to fetch logo:", err);
|
||||
}
|
||||
}
|
||||
fetchInstanceLogo();
|
||||
}, [prefersDarkMode]);
|
||||
}, []);
|
||||
|
||||
return { logo };
|
||||
}
|
||||
|
@ -6,8 +6,9 @@ html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-family: "plus-jakarta-sans", -apple-system, BlinkMacSystemFont, Segoe UI,
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||
sans-serif;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@ -25,12 +26,8 @@ a {
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AvenirNextW10-Bold";
|
||||
src: url("../public/fonts/AvenirNext.ttf");
|
||||
}
|
||||
|
||||
.Avenir {
|
||||
font-family: AvenirNextW10-Bold;
|
||||
font-family: "plus-jakarta-sans";
|
||||
src: url("../public/fonts/PlusJakartaSans.ttf");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@ -105,11 +102,6 @@ a {
|
||||
right: 0px;
|
||||
height: 4em;
|
||||
top: 69vh;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(173, 3, 3, 0),
|
||||
rgb(255 255 255) 50%
|
||||
);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
@ -123,11 +115,6 @@ a {
|
||||
right: 0px;
|
||||
height: 4em;
|
||||
top: 69vh;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(173, 3, 3, 0),
|
||||
rgb(20 20 20) 50%
|
||||
);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
@ -164,9 +151,9 @@ a {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #5fa4fa;
|
||||
background-color: #eeeeee;
|
||||
color: #5fa4fa;
|
||||
box-shadow: 9999px 0 0 0 #5fa4fa;
|
||||
box-shadow: 9999px 0 0 0 #eeeeee;
|
||||
animation: dot-falling 1.5s infinite linear;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
@ -183,8 +170,8 @@ a {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #5fa4fa;
|
||||
color: #5fa4fa;
|
||||
background-color: #eeeeee;
|
||||
color: #eeeeee;
|
||||
animation: dot-falling-before 1.5s infinite linear;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
@ -193,8 +180,8 @@ a {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #5fa4fa;
|
||||
color: #5fa4fa;
|
||||
background-color: #eeeeee;
|
||||
color: #eeeeee;
|
||||
animation: dot-falling-after 1.5s infinite linear;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@ -207,7 +194,7 @@ a {
|
||||
25%,
|
||||
50%,
|
||||
75% {
|
||||
box-shadow: 9999px 0 0 0 #5fa4fa;
|
||||
box-shadow: 9999px 0 0 0 #eeeeee;
|
||||
}
|
||||
|
||||
100% {
|
||||
@ -223,7 +210,7 @@ a {
|
||||
25%,
|
||||
50%,
|
||||
75% {
|
||||
box-shadow: 9984px 0 0 0 #5fa4fa;
|
||||
box-shadow: 9984px 0 0 0 #eeeeee;
|
||||
}
|
||||
|
||||
100% {
|
||||
@ -239,7 +226,7 @@ a {
|
||||
25%,
|
||||
50%,
|
||||
75% {
|
||||
box-shadow: 10014px 0 0 0 #5fa4fa;
|
||||
box-shadow: 10014px 0 0 0 #eeeeee;
|
||||
}
|
||||
|
||||
100% {
|
||||
@ -298,3 +285,55 @@ dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
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 };
|
||||
});
|
||||
},
|
||||
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
|
||||
getApiKeys: async function () {
|
||||
|
@ -121,6 +121,18 @@ const System = {
|
||||
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) => {
|
||||
return await fetch(`${API_BASE}/system/remove-document`, {
|
||||
method: "DELETE",
|
||||
@ -162,6 +174,7 @@ const System = {
|
||||
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.");
|
||||
@ -172,8 +185,8 @@ const System = {
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
fetchLogo: async function (light = false) {
|
||||
return await fetch(`${API_BASE}/system/logo${light ? "/light" : ""}`, {
|
||||
fetchLogo: async function () {
|
||||
return await fetch(`${API_BASE}/system/logo`, {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
})
|
||||
@ -187,8 +200,25 @@ const System = {
|
||||
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 () {
|
||||
return await fetch(`${API_BASE}/system/remove-logo`)
|
||||
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!");
|
||||
@ -246,8 +276,8 @@ const System = {
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
getApiKey: async function () {
|
||||
return fetch(`${API_BASE}/system/api-key`, {
|
||||
getApiKeys: async function () {
|
||||
return fetch(`${API_BASE}/system/api-keys`, {
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useRef } from "react";
|
||||
import Admin from "../../../../models/admin";
|
||||
import truncate from "truncate";
|
||||
import { X } from "react-feather";
|
||||
import { X, Trash } from "@phosphor-icons/react";
|
||||
|
||||
export default function ChatRow({ chat }) {
|
||||
const rowRef = useRef(null);
|
||||
@ -18,19 +18,22 @@ export default function ChatRow({ chat }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr ref={rowRef} className="bg-transparent">
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<tr
|
||||
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}
|
||||
</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}
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono">{chat.workspace?.name}</td>
|
||||
<td className="px-6 py-4">{chat.workspace?.name}</td>
|
||||
<td
|
||||
onClick={() => {
|
||||
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)}
|
||||
</td>
|
||||
@ -38,7 +41,7 @@ export default function ChatRow({ chat }) {
|
||||
onClick={() => {
|
||||
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)}
|
||||
</td>
|
||||
@ -46,9 +49,9 @@ export default function ChatRow({ chat }) {
|
||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||
<button
|
||||
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>
|
||||
</td>
|
||||
</tr>
|
||||
@ -69,22 +72,20 @@ const TextPreview = ({ text, modalName }) => {
|
||||
return (
|
||||
<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="min-w-1/2 relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Viewing Text
|
||||
</h3>
|
||||
<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 border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-white">Viewing Text</h3>
|
||||
<button
|
||||
onClick={() => hideModal(modalName)}
|
||||
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"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full p-4 w-full 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">
|
||||
<div className="w-full p-4 flex">
|
||||
<pre className="w-full flex h-[200px] py-2 px-4 overflow-scroll rounded-lg bg-gray-200 text-slate-800">
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
|
@ -1,30 +1,31 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
||||
import Sidebar, {
|
||||
SidebarMobileHeader,
|
||||
} from "../../../components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
||||
import Admin from "../../../models/admin";
|
||||
import useQuery from "../../../hooks/useQuery";
|
||||
import ChatRow from "./ChatRow";
|
||||
|
||||
export default function AdminChats() {
|
||||
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 />}
|
||||
<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-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 />}
|
||||
<div className="flex flex-col w-full px-1 md:px-8">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<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-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
<p className="text-2xl font-semibold text-white">
|
||||
Workspace Chats
|
||||
</p>
|
||||
</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
|
||||
by users ordered by their creation date.
|
||||
</p>
|
||||
@ -38,7 +39,6 @@ export default function AdminChats() {
|
||||
|
||||
function ChatsContainer() {
|
||||
const query = useQuery();
|
||||
const darkMode = usePrefersDarkMode();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [chats, setChats] = useState([]);
|
||||
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
|
||||
@ -77,8 +77,8 @@ function ChatsContainer() {
|
||||
<Skeleton.default
|
||||
height="80vh"
|
||||
width="100%"
|
||||
baseColor={darkMode ? "#2a3a53" : null}
|
||||
highlightColor={darkMode ? "#395073" : null}
|
||||
highlightColor="#3D4147"
|
||||
baseColor="#2C2F35"
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||
containerClassName="flex w-full"
|
||||
@ -88,8 +88,8 @@ function ChatsContainer() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className="md:w-full w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
||||
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||
Id
|
||||
@ -110,7 +110,7 @@ function ChatsContainer() {
|
||||
Sent At
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||
Actions
|
||||
{" "}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -123,7 +123,7 @@ function ChatsContainer() {
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<button
|
||||
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}
|
||||
>
|
||||
{" "}
|
||||
@ -131,7 +131,7 @@ function ChatsContainer() {
|
||||
</button>
|
||||
<button
|
||||
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}
|
||||
>
|
||||
Next Page
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { titleCase } from "text-case";
|
||||
import Admin from "../../../../models/admin";
|
||||
import { Trash } from "@phosphor-icons/react";
|
||||
|
||||
export default function InviteRow({ invite }) {
|
||||
const rowRef = useRef(null);
|
||||
@ -39,11 +40,11 @@ export default function InviteRow({ invite }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr ref={rowRef} className="bg-transparent">
|
||||
<td
|
||||
scope="row"
|
||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white font-mono"
|
||||
>
|
||||
<tr
|
||||
ref={rowRef}
|
||||
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||
>
|
||||
<td scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||
{titleCase(status)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
@ -61,16 +62,18 @@ export default function InviteRow({ invite }) {
|
||||
<button
|
||||
onClick={copyInviteLink}
|
||||
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"}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Deactivate
|
||||
</button>
|
||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
||||
>
|
||||
<Trash className="h-5 w-5" />
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { X } from "react-feather";
|
||||
import Admin from "../../../../models/admin";
|
||||
|
||||
const DIALOG_ID = `new-invite-modal`;
|
||||
|
||||
function hideModal() {
|
||||
@ -39,16 +40,16 @@ export default function NewInviteModal() {
|
||||
|
||||
return (
|
||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||
<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="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Create new invite
|
||||
</h3>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
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" />
|
||||
@ -58,38 +59,36 @@ export default function NewInviteModal() {
|
||||
<div className="p-6 space-y-6 flex h-full w-full">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</p>
|
||||
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||
)}
|
||||
{invite && (
|
||||
<input
|
||||
type="url"
|
||||
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
|
||||
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
|
||||
to a new user where they can create an account as a default
|
||||
user.
|
||||
</p>
|
||||
</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 ? (
|
||||
<>
|
||||
<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"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -99,7 +98,7 @@ export default function NewInviteModal() {
|
||||
onClick={copyInviteLink}
|
||||
type="button"
|
||||
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"}
|
||||
</button>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
||||
import Sidebar, {
|
||||
SidebarMobileHeader,
|
||||
} from "../../../components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
@ -11,29 +13,27 @@ import NewInviteModal, { NewInviteModalId } from "./NewInviteModal";
|
||||
|
||||
export default function AdminInvites() {
|
||||
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 />}
|
||||
<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-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 />}
|
||||
<div className="flex flex-col w-full px-1 md:px-8">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<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-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
Invitations
|
||||
</p>
|
||||
<p className="text-2xl font-semibold text-white">Invitations</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
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
|
||||
</button>
|
||||
</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
|
||||
and sign up with. Invitations can only be used by a single user.
|
||||
</p>
|
||||
@ -64,8 +64,8 @@ function InvitationsContainer() {
|
||||
<Skeleton.default
|
||||
height="80vh"
|
||||
width="100%"
|
||||
baseColor={darkMode ? "#2a3a53" : null}
|
||||
highlightColor={darkMode ? "#395073" : null}
|
||||
highlightColor="#3D4147"
|
||||
baseColor="#2C2F35"
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||
containerClassName="flex w-full"
|
||||
@ -74,8 +74,8 @@ function InvitationsContainer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
||||
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Status
|
||||
@ -90,7 +90,7 @@ function InvitationsContainer() {
|
||||
Created
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||
Actions
|
||||
{" "}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
||||
import Sidebar, {
|
||||
SidebarMobileHeader,
|
||||
} from "../../../components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import Admin from "../../../models/admin";
|
||||
import showToast from "../../../utils/toast";
|
||||
@ -39,11 +41,11 @@ export default function AdminSystem() {
|
||||
}, []);
|
||||
|
||||
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 />}
|
||||
<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-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 />}
|
||||
<form
|
||||
@ -51,35 +53,35 @@ export default function AdminSystem() {
|
||||
onChange={() => setHasChanges(true)}
|
||||
className="flex w-full"
|
||||
>
|
||||
<div className="flex flex-col w-full px-1 md:px-8">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<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-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
<p className="text-2xl font-semibold text-white">
|
||||
System Preferences
|
||||
</p>
|
||||
{hasChanges && (
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
)}
|
||||
</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
|
||||
instance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="my-4">
|
||||
<div className="my-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
|
||||
</label>
|
||||
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
||||
allow non-admin users to delete workspaces that they are a
|
||||
<p className="leading-tight text-sm text-white text-opacity-60 w-96">
|
||||
Allow non-admin users to delete workspaces that they are a
|
||||
part of. This would delete the workspace for everyone.
|
||||
</p>
|
||||
</div>
|
||||
@ -91,7 +93,7 @@ export default function AdminSystem() {
|
||||
onChange={(e) => setCanDelete(e.target.checked)}
|
||||
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>
|
||||
</label>
|
||||
</div>
|
||||
@ -101,7 +103,7 @@ export default function AdminSystem() {
|
||||
<label className="leading-tight font-medium text-black dark:text-white">
|
||||
Limit messages per user per day
|
||||
</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
|
||||
chats within a 24 hour window. Enable this to prevent users
|
||||
from running up OpenAI costs.
|
||||
@ -121,7 +123,7 @@ export default function AdminSystem() {
|
||||
}}
|
||||
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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "react-feather";
|
||||
import Admin from "../../../../models/admin";
|
||||
|
||||
const DIALOG_ID = `new-user-modal`;
|
||||
|
||||
function hideModal() {
|
||||
@ -24,15 +25,15 @@ export default function NewUserModal() {
|
||||
return (
|
||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||
<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="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Add user to instance
|
||||
</h3>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
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" />
|
||||
@ -44,14 +45,14 @@ export default function NewUserModal() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
<input
|
||||
name="username"
|
||||
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"
|
||||
minLength={2}
|
||||
required={true}
|
||||
@ -61,14 +62,14 @@ export default function NewUserModal() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
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"
|
||||
required={true}
|
||||
minLength={8}
|
||||
@ -78,7 +79,7 @@ export default function NewUserModal() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
@ -86,34 +87,32 @@ export default function NewUserModal() {
|
||||
name="role"
|
||||
required={true}
|
||||
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="admin">Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</p>
|
||||
<p className="text-red-400 text-sm">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
|
||||
initial login to get access.
|
||||
</p>
|
||||
</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
|
||||
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"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
@ -3,11 +3,14 @@ import { X } from "react-feather";
|
||||
import Admin from "../../../../../models/admin";
|
||||
|
||||
export const EditUserModalId = (user) => `edit-user-${user.id}-modal`;
|
||||
|
||||
export default function EditUserModal({ user }) {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const hideModal = () => {
|
||||
document.getElementById(EditUserModalId(user)).close();
|
||||
};
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
setError(null);
|
||||
e.preventDefault();
|
||||
@ -24,16 +27,16 @@ export default function EditUserModal({ user }) {
|
||||
|
||||
return (
|
||||
<dialog id={EditUserModalId(user)} className="bg-transparent outline-none">
|
||||
<div className="relative w-[75vw] max-w-2xl max-h-full">
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Edit {user.username}
|
||||
</h3>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
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" />
|
||||
@ -45,14 +48,14 @@ export default function EditUserModal({ user }) {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
<input
|
||||
name="username"
|
||||
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"
|
||||
minLength={2}
|
||||
defaultValue={user.username}
|
||||
@ -63,14 +66,14 @@ export default function EditUserModal({ user }) {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
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`}
|
||||
minLength={8}
|
||||
autoComplete="off"
|
||||
@ -79,7 +82,7 @@ export default function EditUserModal({ user }) {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
@ -87,30 +90,28 @@ export default function EditUserModal({ user }) {
|
||||
name="role"
|
||||
required={true}
|
||||
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="admin">Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</p>
|
||||
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||
)}
|
||||
</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
|
||||
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"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
@ -2,6 +2,7 @@ import { useRef, useState } from "react";
|
||||
import { titleCase } from "text-case";
|
||||
import Admin from "../../../../models/admin";
|
||||
import EditUserModal, { EditUserModalId } from "./EditUserModal";
|
||||
import { DotsThreeOutline } from "@phosphor-icons/react";
|
||||
|
||||
export default function UserRow({ currUser, user }) {
|
||||
const rowRef = useRef(null);
|
||||
@ -29,11 +30,11 @@ export default function UserRow({ currUser, user }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr ref={rowRef} className="bg-transparent">
|
||||
<th
|
||||
scope="row"
|
||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<tr
|
||||
ref={rowRef}
|
||||
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||
>
|
||||
<th scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||
{user.username}
|
||||
</th>
|
||||
<td className="px-6 py-4">{titleCase(user.role)}</td>
|
||||
@ -43,9 +44,9 @@ export default function UserRow({ currUser, user }) {
|
||||
onClick={() =>
|
||||
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>
|
||||
{currUser.id !== user.id && (
|
||||
<>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
||||
import Sidebar, {
|
||||
SidebarMobileHeader,
|
||||
} from "../../../components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import { UserPlus } from "react-feather";
|
||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
||||
import Admin from "../../../models/admin";
|
||||
import UserRow from "./UserRow";
|
||||
import useUser from "../../../hooks/useUser";
|
||||
@ -12,29 +13,27 @@ import NewUserModal, { NewUserModalId } from "./NewUserModal";
|
||||
|
||||
export default function AdminUsers() {
|
||||
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 />}
|
||||
<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-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 />}
|
||||
<div className="flex flex-col w-full px-1 md:px-8">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<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-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
Instance users
|
||||
</p>
|
||||
<p className="text-2xl font-semibold text-white">Users</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
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
|
||||
</button>
|
||||
</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.
|
||||
Removing an account will instantly remove their access to this
|
||||
instance.
|
||||
@ -50,7 +49,6 @@ export default function AdminUsers() {
|
||||
|
||||
function UsersContainer() {
|
||||
const { user: currUser } = useUser();
|
||||
const darkMode = usePrefersDarkMode();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [users, setUsers] = useState([]);
|
||||
useEffect(() => {
|
||||
@ -67,8 +65,8 @@ function UsersContainer() {
|
||||
<Skeleton.default
|
||||
height="80vh"
|
||||
width="100%"
|
||||
baseColor={darkMode ? "#2a3a53" : null}
|
||||
highlightColor={darkMode ? "#395073" : null}
|
||||
highlightColor="#3D4147"
|
||||
baseColor="#2C2F35"
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||
containerClassName="flex w-full"
|
||||
@ -77,8 +75,8 @@ function UsersContainer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
||||
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||
Username
|
||||
@ -87,10 +85,10 @@ function UsersContainer() {
|
||||
Role
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Created On
|
||||
Date Added
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||
Actions
|
||||
{" "}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -21,16 +21,16 @@ export default function NewWorkspaceModal() {
|
||||
|
||||
return (
|
||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||
<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="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Add workspace to Instance
|
||||
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Create new workspace
|
||||
</h3>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
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" />
|
||||
@ -42,14 +42,14 @@ export default function NewWorkspaceModal() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
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"
|
||||
minLength={4}
|
||||
required={true}
|
||||
@ -57,27 +57,25 @@ export default function NewWorkspaceModal() {
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</p>
|
||||
<p className="text-red-400 text-sm">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
|
||||
it. You can add users after it has been created.
|
||||
</p>
|
||||
</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
|
||||
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"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
@ -5,11 +5,14 @@ import { titleCase } from "text-case";
|
||||
|
||||
export const EditWorkspaceUsersModalId = (workspace) =>
|
||||
`edit-workspace-${workspace.id}-modal`;
|
||||
|
||||
export default function EditWorkspaceUsersModal({ workspace, users }) {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const hideModal = () => {
|
||||
document.getElementById(EditWorkspaceUsersModalId(workspace)).close();
|
||||
};
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
setError(null);
|
||||
e.preventDefault();
|
||||
@ -36,16 +39,16 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
||||
id={EditWorkspaceUsersModalId(workspace)}
|
||||
className="bg-transparent outline-none"
|
||||
>
|
||||
<div className="relative w-[75vw] max-w-2xl max-h-full">
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Edit {workspace.name}
|
||||
</h3>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
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" />
|
||||
@ -61,7 +64,7 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
||||
<div
|
||||
key={`workspace-${workspace.id}-user-${user.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={() => {
|
||||
document
|
||||
.getElementById(
|
||||
@ -76,11 +79,11 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
||||
type="checkbox"
|
||||
value="yes"
|
||||
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
|
||||
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)}
|
||||
</label>
|
||||
@ -90,7 +93,7 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
||||
<div className="flex items-center gap-x-4">
|
||||
<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={() => {
|
||||
document
|
||||
.getElementById(`workspace-${workspace.id}-select-all`)
|
||||
@ -108,7 +111,7 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
||||
</button>
|
||||
<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={() => {
|
||||
document
|
||||
.getElementById(`workspace-${workspace.id}-select-all`)
|
||||
@ -126,23 +129,21 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</p>
|
||||
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||
)}
|
||||
</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
|
||||
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"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
@ -4,6 +4,7 @@ import paths from "../../../../utils/paths";
|
||||
import EditWorkspaceUsersModal, {
|
||||
EditWorkspaceUsersModalId,
|
||||
} from "./EditWorkspaceUsersModal";
|
||||
import { DotsThreeOutline, LinkSimple, Trash } from "@phosphor-icons/react";
|
||||
|
||||
export default function WorkspaceRow({ workspace, users }) {
|
||||
const rowRef = useRef(null);
|
||||
@ -20,20 +21,20 @@ export default function WorkspaceRow({ workspace, users }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr ref={rowRef} className="bg-transparent">
|
||||
<th
|
||||
scope="row"
|
||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<tr
|
||||
ref={rowRef}
|
||||
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||
>
|
||||
<th scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||
{workspace.name}
|
||||
</th>
|
||||
<td className="px-6 py-4">
|
||||
<td className="px-6 py-4 flex items-center">
|
||||
<a
|
||||
href={paths.workspace.chat(workspace.slug)}
|
||||
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>
|
||||
</td>
|
||||
<td className="px-6 py-4">{workspace.userIds?.length}</td>
|
||||
@ -45,15 +46,15 @@ export default function WorkspaceRow({ workspace, users }) {
|
||||
?.getElementById(EditWorkspaceUsersModalId(workspace))
|
||||
?.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
|
||||
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>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
||||
import Sidebar, {
|
||||
SidebarMobileHeader,
|
||||
} from "../../../components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
@ -11,29 +13,29 @@ import NewWorkspaceModal, { NewWorkspaceModalId } from "./NewWorkspaceModal";
|
||||
|
||||
export default function AdminWorkspaces() {
|
||||
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 />}
|
||||
<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-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 />}
|
||||
<div className="flex flex-col w-full px-1 md:px-8">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<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-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
<p className="text-2xl font-semibold text-white">
|
||||
Instance workspaces
|
||||
</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
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
|
||||
</button>
|
||||
</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
|
||||
a workspace will delete all of it's associated chats and settings.
|
||||
</p>
|
||||
@ -68,8 +70,8 @@ function WorkspacesContainer() {
|
||||
<Skeleton.default
|
||||
height="80vh"
|
||||
width="100%"
|
||||
baseColor={darkMode ? "#2a3a53" : null}
|
||||
highlightColor={darkMode ? "#395073" : null}
|
||||
highlightColor="#3D4147"
|
||||
baseColor="#2C2F35"
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||
containerClassName="flex w-full"
|
||||
@ -78,8 +80,8 @@ function WorkspacesContainer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
||||
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||
Name
|
||||
@ -94,7 +96,7 @@ function WorkspacesContainer() {
|
||||
Created On
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||
Actions
|
||||
{" "}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Admin from "../../../../models/admin";
|
||||
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 }) {
|
||||
const rowRef = useRef(null);
|
||||
@ -15,9 +18,13 @@ export default function ApiKeyRow({ apiKey }) {
|
||||
if (rowRef?.current) {
|
||||
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");
|
||||
};
|
||||
|
||||
const copyApiKey = () => {
|
||||
if (!apiKey) return false;
|
||||
window.navigator.clipboard.writeText(apiKey.secret);
|
||||
@ -37,30 +44,30 @@ export default function ApiKeyRow({ apiKey }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr ref={rowRef} className="bg-transparent">
|
||||
<td
|
||||
scope="row"
|
||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white font-mono"
|
||||
>
|
||||
<tr
|
||||
ref={rowRef}
|
||||
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||
>
|
||||
<td scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||
{apiKey.secret}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{apiKey.createdBy?.username || "unknown user"}
|
||||
<td className="px-6 py-4 text-center">
|
||||
{apiKey.createdBy?.username || "--"}
|
||||
</td>
|
||||
<td className="px-6 py-4">{apiKey.createdAt}</td>
|
||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||
<button
|
||||
onClick={copyApiKey}
|
||||
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"}
|
||||
</button>
|
||||
<button
|
||||
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>
|
||||
</td>
|
||||
</tr>
|
@ -2,6 +2,9 @@ import React, { useEffect, useState } from "react";
|
||||
import { X } from "react-feather";
|
||||
import Admin from "../../../../models/admin";
|
||||
import paths from "../../../../utils/paths";
|
||||
import { userFromStorage } from "../../../../utils/request";
|
||||
import System from "../../../../models/system";
|
||||
|
||||
const DIALOG_ID = `new-api-key-modal`;
|
||||
|
||||
function hideModal() {
|
||||
@ -17,7 +20,10 @@ export default function NewApiKeyModal() {
|
||||
const handleCreate = async (e) => {
|
||||
setError(null);
|
||||
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);
|
||||
setError(error);
|
||||
};
|
||||
@ -38,16 +44,16 @@ export default function NewApiKeyModal() {
|
||||
|
||||
return (
|
||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||
<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="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Create new API key
|
||||
</h3>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
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" />
|
||||
@ -57,44 +63,42 @@ export default function NewApiKeyModal() {
|
||||
<div className="p-6 space-y-6 flex h-full w-full">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</p>
|
||||
<p className="text-red-400 text-sm">Error: {error}</p>
|
||||
)}
|
||||
{apiKey && (
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={`${apiKey.secret}`}
|
||||
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
|
||||
access and configure this AnythingLLM instance.
|
||||
</p>
|
||||
<a
|
||||
href={paths.apiDocs()}
|
||||
target="_blank"
|
||||
className="text-blue-600 dark:text-blue-300 hover:underline"
|
||||
className="text-blue-400 hover:underline"
|
||||
>
|
||||
Read the API documentation →
|
||||
</a>
|
||||
</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 ? (
|
||||
<>
|
||||
<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"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -104,7 +108,7 @@ export default function NewApiKeyModal() {
|
||||
onClick={copyApiKey}
|
||||
type="button"
|
||||
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"}
|
||||
</button>
|
@ -1,47 +1,48 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
||||
import Sidebar, {
|
||||
SidebarMobileHeader,
|
||||
} from "../../../components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import { PlusCircle } from "react-feather";
|
||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
||||
import Admin from "../../../models/admin";
|
||||
import ApiKeyRow from "./ApiKeyRow";
|
||||
import NewApiKeyModal, { NewApiKeyModalId } from "./NewApiKeyModal";
|
||||
import paths from "../../../utils/paths";
|
||||
import { userFromStorage } from "../../../utils/request";
|
||||
import System from "../../../models/system";
|
||||
|
||||
export default function AdminApiKeys() {
|
||||
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 />}
|
||||
<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-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 />}
|
||||
<div className="flex flex-col w-full px-1 md:px-8">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<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-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
API Keys
|
||||
</p>
|
||||
<p className="text-2xl font-semibold text-white">API Keys</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
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
|
||||
</button>
|
||||
</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
|
||||
this AnythingLLM instance.
|
||||
</p>
|
||||
<a
|
||||
href={paths.apiDocs()}
|
||||
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 →
|
||||
</a>
|
||||
@ -55,12 +56,14 @@ export default function AdminApiKeys() {
|
||||
}
|
||||
|
||||
function ApiKeysContainer() {
|
||||
const darkMode = usePrefersDarkMode();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
useEffect(() => {
|
||||
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);
|
||||
setLoading(false);
|
||||
}
|
||||
@ -72,8 +75,8 @@ function ApiKeysContainer() {
|
||||
<Skeleton.default
|
||||
height="80vh"
|
||||
width="100%"
|
||||
baseColor={darkMode ? "#2a3a53" : null}
|
||||
highlightColor={darkMode ? "#395073" : null}
|
||||
highlightColor="#3D4147"
|
||||
baseColor="#2C2F35"
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||
containerClassName="flex w-full"
|
||||
@ -82,10 +85,10 @@ function ApiKeysContainer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
||||
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
|
||||
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||
API Key
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
@ -95,7 +98,7 @@ function ApiKeysContainer() {
|
||||
Created
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||
Actions
|
||||
{" "}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
@ -1,27 +1,30 @@
|
||||
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 Admin from "../../../models/admin";
|
||||
import AnythingLLMLight from "../../../media/logo/anything-llm-light.png";
|
||||
import AnythingLLMDark from "../../../media/logo/anything-llm-dark.png";
|
||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
||||
import AnythingLLM from "../../../media/logo/anything-llm.png";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
import System from "../../../models/system";
|
||||
import EditingChatBubble from "../../../components/EditingChatBubble";
|
||||
import showToast from "../../../utils/toast";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
|
||||
export default function Appearance() {
|
||||
const { logo: _initLogo } = useLogo();
|
||||
const [logo, setLogo] = useState("");
|
||||
const prefersDarkMode = usePrefersDarkMode();
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function setInitLogo() {
|
||||
async function logoInit() {
|
||||
setLogo(_initLogo || "");
|
||||
const _isDefaultLogo = await System.isDefaultLogo();
|
||||
setIsDefaultLogo(_isDefaultLogo);
|
||||
}
|
||||
setInitLogo();
|
||||
logoInit();
|
||||
}, [_initLogo]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -36,29 +39,36 @@ export default function Appearance() {
|
||||
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 Admin.uploadLogo(formData);
|
||||
const { success, error } = await System.uploadLogo(formData);
|
||||
if (!success) {
|
||||
showToast(`Failed to upload logo: ${error}`, "error");
|
||||
setLogo(_initLogo);
|
||||
return;
|
||||
}
|
||||
|
||||
const logoURL = await System.fetchLogo();
|
||||
setLogo(logoURL);
|
||||
showToast("Image uploaded successfully.", "success");
|
||||
setIsDefaultLogo(false);
|
||||
};
|
||||
|
||||
const handleRemoveLogo = async () => {
|
||||
const { success, error } = await Admin.removeCustomLogo();
|
||||
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;
|
||||
}
|
||||
|
||||
const logoURL = await System.fetchLogo();
|
||||
setLogo(logoURL);
|
||||
showToast("Image successfully removed.", "success");
|
||||
};
|
||||
|
||||
@ -89,7 +99,7 @@ export default function Appearance() {
|
||||
};
|
||||
|
||||
const handleMessageSave = async () => {
|
||||
const { success, error } = await Admin.setWelcomeMessages(messages);
|
||||
const { success, error } = await System.setWelcomeMessages(messages);
|
||||
if (!success) {
|
||||
showToast(`Failed to update welcome messages: ${error}`, "error");
|
||||
return;
|
||||
@ -99,29 +109,31 @@ export default function Appearance() {
|
||||
};
|
||||
|
||||
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 />}
|
||||
<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-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 />}
|
||||
<div className="px-1 md:px-8">
|
||||
<div className="mb-6">
|
||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
Appearance Settings
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-base text-slate-600 dark:text-slate-200">
|
||||
<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">
|
||||
Appearance Settings
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-base text-white text-opacity-60">
|
||||
Customize the appearance settings of your platform.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<div className="my-6">
|
||||
<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
|
||||
</h2>
|
||||
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
||||
Change the logo that appears in the sidebar.
|
||||
<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">
|
||||
@ -129,33 +141,44 @@ export default function Appearance() {
|
||||
src={logo}
|
||||
alt="Uploaded Logo"
|
||||
className="w-48 h-48 object-contain mr-6"
|
||||
onError={(e) =>
|
||||
(e.target.src = prefersDarkMode
|
||||
? AnythingLLMLight
|
||||
: AnythingLLMDark)
|
||||
}
|
||||
hidden={isDefaultLogo}
|
||||
onError={(e) => (e.target.src = AnythingLLM)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4">
|
||||
<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">
|
||||
Upload Image
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
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"
|
||||
<div className="flex flex-row gap-x-8">
|
||||
<label
|
||||
className="mt-5 transition-all duration-300 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"
|
||||
>
|
||||
Remove Custom Logo
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Upload your logo. Recommended size: 800x200.
|
||||
</div>
|
||||
<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>
|
||||
@ -164,11 +187,11 @@ export default function Appearance() {
|
||||
<h2 className="leading-tight font-medium text-black dark:text-white">
|
||||
Custom Messages
|
||||
</h2>
|
||||
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
||||
Change the default messages that are displayed to the users.
|
||||
<p className="text-sm font-base text-white/60">
|
||||
Customize the automatic messages displayed to your users.
|
||||
</p>
|
||||
</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) => (
|
||||
<div key={index} className="flex flex-col gap-y-2">
|
||||
{message.user && (
|
||||
@ -191,18 +214,24 @@ export default function Appearance() {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-4 mt-4 justify-between">
|
||||
<div className="flex gap-4 mt-12 justify-between pb-7">
|
||||
<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")}
|
||||
>
|
||||
+ 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
|
||||
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")}
|
||||
>
|
||||
+ User Message
|
||||
<div className="flex items-center">
|
||||
<Plus className="w-5 h-5 m-2" weight="fill" /> New User
|
||||
Message
|
||||
</div>
|
||||
</button>
|
||||
</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";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { FullScreenLoader } from "../../components/Preloader";
|
||||
import UserMenu from "../../components/UserMenu";
|
||||
|
||||
export default function Main() {
|
||||
const { loading, requiresAuth, mode } = usePasswordModal();
|
||||
@ -16,9 +17,11 @@ export default function Main() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
||||
{!isMobile && <Sidebar />}
|
||||
<DefaultChatContainer />
|
||||
</div>
|
||||
<UserMenu>
|
||||
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||
{!isMobile && <Sidebar />}
|
||||
<DefaultChatContainer />
|
||||
</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 (
|
||||
<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 />}
|
||||
<WorkspaceChatContainer loading={loading} workspace={workspace} />
|
||||
</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_TOKEN = "anythingllm_authToken";
|
||||
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: () => {
|
||||
return "/";
|
||||
},
|
||||
login: () => {
|
||||
return "/login";
|
||||
},
|
||||
onboarding: () => {
|
||||
return "/onboarding";
|
||||
},
|
||||
github: () => {
|
||||
return "https://github.com/Mintplex-Labs/anything-llm";
|
||||
},
|
||||
@ -33,6 +39,26 @@ export default {
|
||||
apiDocs: () => {
|
||||
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: {
|
||||
system: () => {
|
||||
return `/admin/system-preferences`;
|
||||
@ -49,11 +75,5 @@ export default {
|
||||
chats: () => {
|
||||
return "/admin/workspace-chats";
|
||||
},
|
||||
appearance: () => {
|
||||
return "/admin/appearance";
|
||||
},
|
||||
apiKeys: () => {
|
||||
return "/admin/api-keys";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -3,11 +3,52 @@ export default {
|
||||
content: ["./src/**/*.{js,jsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
rotate: {
|
||||
'270': '270deg',
|
||||
'360': '360deg',
|
||||
},
|
||||
colors: {
|
||||
'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: [],
|
||||
}
|
||||
|
||||
|
@ -447,6 +447,11 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
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":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.6.3.tgz"
|
||||
@ -1642,6 +1647,11 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "4.6.2"
|
||||
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.development
|
||||
storage/assets/*
|
||||
!storage/assets/anything-llm-dark.png
|
||||
!storage/assets/anything-llm-light.png
|
||||
!storage/assets/anything-llm.png
|
||||
storage/documents/*
|
||||
storage/vector-cache/*.json
|
||||
storage/exports
|
||||
|
@ -32,7 +32,7 @@ const {
|
||||
validFilename,
|
||||
renameLogoFile,
|
||||
removeCustomLogo,
|
||||
DARK_LOGO_FILENAME,
|
||||
LOGO_FILENAME,
|
||||
} = require("../utils/files/logo");
|
||||
const { Telemetry } = require("../models/telemetry");
|
||||
const { WelcomeMessages } = require("../models/welcomeMessages");
|
||||
@ -317,7 +317,7 @@ function systemEndpoints(app) {
|
||||
updateENV(
|
||||
{
|
||||
AuthToken: "",
|
||||
JWTSecret: process.env.JWT_SECRET ?? v4(),
|
||||
JWTSecret: process.env.JWT_SECRET || v4(),
|
||||
},
|
||||
true
|
||||
);
|
||||
@ -325,12 +325,27 @@ function systemEndpoints(app) {
|
||||
await Telemetry.sendTelemetry("enabled_multi_user_mode");
|
||||
response.status(200).json({ success: !!user, error });
|
||||
} catch (e) {
|
||||
await User.delete({});
|
||||
await SystemSettings.updateSettings({
|
||||
multi_user_mode: false,
|
||||
});
|
||||
|
||||
console.log(e.message, e);
|
||||
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) => {
|
||||
try {
|
||||
const { filename, error } = await exportData();
|
||||
@ -341,34 +356,32 @@ function systemEndpoints(app) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/system/data-exports/:filename",
|
||||
[validatedRequest],
|
||||
(request, response) => {
|
||||
const exportLocation = __dirname + "/../storage/exports/";
|
||||
const sanitized = path
|
||||
.normalize(request.params.filename)
|
||||
.replace(/^(\.\.(\/|\\|$))+/, "");
|
||||
const finalDestination = path.join(exportLocation, sanitized);
|
||||
app.get("/system/data-exports/:filename", (request, response) => {
|
||||
const exportLocation = __dirname + "/../storage/exports/";
|
||||
const sanitized = path
|
||||
.normalize(request.params.filename)
|
||||
.replace(/^(\.\.(\/|\\|$))+/, "");
|
||||
const finalDestination = path.join(exportLocation, sanitized);
|
||||
|
||||
if (!fs.existsSync(finalDestination)) {
|
||||
response.status(404).json({
|
||||
error: 404,
|
||||
msg: `File ${request.params.filename} does not exist in exports.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
response.download(finalDestination, request.params.filename, (err) => {
|
||||
if (err) {
|
||||
response.send({
|
||||
error: err,
|
||||
msg: "Problem downloading the file",
|
||||
});
|
||||
}
|
||||
if (!fs.existsSync(finalDestination)) {
|
||||
response.status(404).json({
|
||||
error: 404,
|
||||
msg: `File ${request.params.filename} does not exist in exports.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
response.download(finalDestination, request.params.filename, (err) => {
|
||||
if (err) {
|
||||
response.send({
|
||||
error: err,
|
||||
msg: "Problem downloading the file",
|
||||
});
|
||||
}
|
||||
// delete on download because endpoint is not authenticated.
|
||||
fs.rmSync(finalDestination);
|
||||
});
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/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 {
|
||||
const defaultFilename = getDefaultFilename(request.params.mode);
|
||||
const defaultFilename = getDefaultFilename();
|
||||
const logoPath = await determineLogoFilepath(defaultFilename);
|
||||
const { buffer, size, mime } = fetchLogo(logoPath);
|
||||
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(
|
||||
"/system/remove-logo",
|
||||
[validatedRequest],
|
||||
@ -458,7 +482,7 @@ function systemEndpoints(app) {
|
||||
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
||||
await removeCustomLogo(currentLogoFilename);
|
||||
const { success, error } = await SystemSettings.updateSettings({
|
||||
logo_filename: DARK_LOGO_FILENAME,
|
||||
logo_filename: LOGO_FILENAME,
|
||||
});
|
||||
|
||||
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 {
|
||||
if (response.locals.multiUserMode) {
|
||||
return response.sendStatus(401).end();
|
||||
}
|
||||
|
||||
const apiKey = await ApiKey.get({});
|
||||
const apiKeys = await ApiKey.where({});
|
||||
return response.status(200).json({
|
||||
apiKey,
|
||||
apiKeys,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
@ -575,7 +599,6 @@ function systemEndpoints(app) {
|
||||
return response.sendStatus(401).end();
|
||||
}
|
||||
|
||||
await ApiKey.delete();
|
||||
const { apiKey, error } = await ApiKey.create();
|
||||
return response.status(200).json({
|
||||
apiKey,
|
||||
|
@ -54,7 +54,7 @@ const User = {
|
||||
|
||||
delete: async function (clause = {}) {
|
||||
try {
|
||||
await prisma.users.delete({ where: clause });
|
||||
await prisma.users.deleteMany({ where: clause });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
|
@ -29,6 +29,7 @@ const WorkspaceChats = {
|
||||
where: {
|
||||
workspaceId,
|
||||
user_id: userId,
|
||||
include: true,
|
||||
},
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
orderBy: {
|
||||
@ -48,6 +49,7 @@ const WorkspaceChats = {
|
||||
const chats = await prisma.workspace_chats.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
include: true,
|
||||
},
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
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