diff --git a/frontend/package.json b/frontend/package.json index eb3af3cff0298805de7b08ffb59189823eab177e..75bbf868e63b4e210c85806a656f0ec3f98da648 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/public/fonts/AvenirNext.ttf b/frontend/public/fonts/AvenirNext.ttf deleted file mode 100644 index 271ee1bc569832dd5c7d7d7f830a2ae1b5052283..0000000000000000000000000000000000000000 Binary files a/frontend/public/fonts/AvenirNext.ttf and /dev/null differ diff --git a/frontend/public/fonts/PlusJakartaSans.ttf b/frontend/public/fonts/PlusJakartaSans.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bdd498505bc2000b995f4978fa539968a0ed5fbe Binary files /dev/null and b/frontend/public/fonts/PlusJakartaSans.ttf differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b6cfc7eb17f75b0499c6ca76bd2e28f7b56e919f..2b48a3636427358eeca01f63b406328b42188f00 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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> diff --git a/frontend/src/components/AdminSidebar/index.jsx b/frontend/src/components/AdminSidebar/index.jsx deleted file mode 100644 index a583cb326a514b02857c442e2a3a70711c9099bc..0000000000000000000000000000000000000000 --- a/frontend/src/components/AdminSidebar/index.jsx +++ /dev/null @@ -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> - ); -}; diff --git a/frontend/src/components/ChatBubble/index.jsx b/frontend/src/components/ChatBubble/index.jsx index 7ae52cb63303887524cd5629e9dbd0785afeaf0b..610f9037d57a36cdf58b39487bc0608c579a82c9 100644 --- a/frontend/src/components/ChatBubble/index.jsx +++ b/frontend/src/components/ChatBubble/index.jsx @@ -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> ); diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx index 1e993a4d545fb35432b13091f3a18b8d900dc9f6..bfd0fbdc6d8347a8ee0bf5ddddd0836d0a473e4a 100644 --- a/frontend/src/components/DefaultChat/index.jsx +++ b/frontend/src/components/DefaultChat/index.jsx @@ -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 + 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`} + > + 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 w-full mt-2 justify-start ${ - popMsg ? "chat__message" : "" - }`} + className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`} > - <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 + 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 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 w-full mt-2 justify-start ${ - popMsg ? "chat__message" : "" - }`} + className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`} > - <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 + 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 w-full mt-2 justify-end ${ - popMsg ? "chat__message" : "" - }`} + className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`} > - <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 + 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 w-full mt-2 justify-start ${ - popMsg ? "chat__message" : "" - }`} + className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`} > - <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 + 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 w-full mt-2 justify-end ${ - popMsg ? "chat__message" : "" - }`} + className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`} > - <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 + 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 w-full mt-2 justify-start ${ - popMsg ? "chat__message" : "" - }`} + className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`} > - <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 + 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 w-full mt-2 justify-end ${ - popMsg ? "chat__message" : "" - }`} + className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`} > - <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 + 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 w-full mt-2 justify-start ${ - popMsg ? "chat__message" : "" - }`} + className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`} > - <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" - > - <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" - > - <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> + <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 diff --git a/frontend/src/components/EditingChatBubble/index.jsx b/frontend/src/components/EditingChatBubble/index.jsx index 7d738ee0139e70eb0021a3276691ec2ed7c25340..1cbe44305801f3452ce7732b45e0632657ae6a50 100644 --- a/frontend/src/components/EditingChatBubble/index.jsx +++ b/frontend/src/components/EditingChatBubble/index.jsx @@ -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> ); } diff --git a/frontend/src/components/Icons/Discord.jsx b/frontend/src/components/Icons/Discord.jsx deleted file mode 100644 index ebc55ed1ea508908a9eb54e63fea54ee45d7a005..0000000000000000000000000000000000000000 --- a/frontend/src/components/Icons/Discord.jsx +++ /dev/null @@ -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> - ); -} diff --git a/frontend/src/components/LLMProviderOption/index.jsx b/frontend/src/components/LLMProviderOption/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3c3ed446a1b66abee96d94ae8d58bc836ab67cd1 --- /dev/null +++ b/frontend/src/components/LLMProviderOption/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/Modals/Settings/ApiKey/index.jsx b/frontend/src/components/Modals/LegacySettings/ApiKey/index.jsx similarity index 100% rename from frontend/src/components/Modals/Settings/ApiKey/index.jsx rename to frontend/src/components/Modals/LegacySettings/ApiKey/index.jsx diff --git a/frontend/src/components/Modals/Settings/Appearance/index.jsx b/frontend/src/components/Modals/LegacySettings/Appearance/index.jsx similarity index 96% rename from frontend/src/components/Modals/Settings/Appearance/index.jsx rename to frontend/src/components/Modals/LegacySettings/Appearance/index.jsx index 92f210d5983d3a1710768efcc087ab49770a9499..4ce1078bfa4a1a6f7d187215ec16d19a8f2093fe 100644 --- a/frontend/src/components/Modals/Settings/Appearance/index.jsx +++ b/frontend/src/components/Modals/LegacySettings/Appearance/index.jsx @@ -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"> diff --git a/frontend/src/components/Modals/Settings/ExportImport/index.jsx b/frontend/src/components/Modals/LegacySettings/ExportImport/index.jsx similarity index 100% rename from frontend/src/components/Modals/Settings/ExportImport/index.jsx rename to frontend/src/components/Modals/LegacySettings/ExportImport/index.jsx diff --git a/frontend/src/components/Modals/Settings/LLMSelection/index.jsx b/frontend/src/components/Modals/LegacySettings/LLMSelection/index.jsx similarity index 98% rename from frontend/src/components/Modals/Settings/LLMSelection/index.jsx rename to frontend/src/components/Modals/LegacySettings/LLMSelection/index.jsx index 9d930a7b48255f57a898e6a7cfe5b024ee0c1d0c..7fd07516d47b2865b153f3531469b66d5dbd5f1f 100644 --- a/frontend/src/components/Modals/Settings/LLMSelection/index.jsx +++ b/frontend/src/components/Modals/LegacySettings/LLMSelection/index.jsx @@ -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} diff --git a/frontend/src/components/Modals/Settings/MultiUserMode/index.jsx b/frontend/src/components/Modals/LegacySettings/MultiUserMode/index.jsx similarity index 100% rename from frontend/src/components/Modals/Settings/MultiUserMode/index.jsx rename to frontend/src/components/Modals/LegacySettings/MultiUserMode/index.jsx diff --git a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx b/frontend/src/components/Modals/LegacySettings/PasswordProtection/index.jsx similarity index 100% rename from frontend/src/components/Modals/Settings/PasswordProtection/index.jsx rename to frontend/src/components/Modals/LegacySettings/PasswordProtection/index.jsx diff --git a/frontend/src/components/Modals/Settings/VectorDbs/index.jsx b/frontend/src/components/Modals/LegacySettings/VectorDbs/index.jsx similarity index 100% rename from frontend/src/components/Modals/Settings/VectorDbs/index.jsx rename to frontend/src/components/Modals/LegacySettings/VectorDbs/index.jsx diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/LegacySettings/index.jsx similarity index 100% rename from frontend/src/components/Modals/Settings/index.jsx rename to frontend/src/components/Modals/LegacySettings/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/ConfirmationModal/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/ConfirmationModal/index.jsx deleted file mode 100644 index b5d00a219701864034f568f386d23ca5e65f29bf..0000000000000000000000000000000000000000 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/ConfirmationModal/index.jsx +++ /dev/null @@ -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> - ); -} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ef8bed17d5c4118aa06ea115236ca575074d1fb8 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..abf4cef9cda050adbaa62e4aeacffadce85984f1 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx @@ -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> + )} + </> + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx index b838d0b107fab9b623cd580dce59bd95f2d1b4c9..099dba87f777eb3f9bcbc11ac58fa0d1311d6885 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx @@ -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." - ) - ) + 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; + }); + }; + + const isFolderCompletelySelected = (folder) => { + if (folder.items.length === 0) { return false; - document?.getElementById(meta?.id)?.remove(); - await System.deleteDocument(name, meta); + } + return folder.items.every((file) => selectedItems[file.id]); }; - 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 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 - 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> ); } diff --git a/frontend/src/components/Modals/MangeWorkspace/Upload/FileUploadProgress/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/FileUploadProgress/index.jsx similarity index 63% rename from frontend/src/components/Modals/MangeWorkspace/Upload/FileUploadProgress/index.jsx rename to frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/FileUploadProgress/index.jsx index 8c47581c4f3b9e46f2c8e424a5e2c8dff562c6d9..7a1d83a31c46f633d305e3395638cd3e76001b8a 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Upload/FileUploadProgress/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/FileUploadProgress/index.jsx @@ -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); diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..eac081b7f4ebb8a8d56de4833b94945480399912 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4468ee27b2b0eb2245bc19fd3361b0a7a0e0bce6 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8b1381a141b64c406f721af5c814553010bcd9e0 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx index 1034f3e2e9b0c4d0daf4d70751a8c40d1e0aef41..5e9942060553f1c739bf9c33540b34b27e871938 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx @@ -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"; + +const COST_PER_TOKEN = 0.0004; -export default function DocumentSettings({ workspace }) { - const { slug } = useParams(); +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 = + + const documentsInWorkspace = 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); + + // 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 changesToSend = { + adds: movedItems.map((item) => `${item.folderName}/${item.name}`), + }; - const changes = docChanges(); - await Workspace.modifyEmbeddings(workspace.slug, changes) + 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 newMovedItems = []; - const toggleSelection = (filepath) => { - const isFolder = !filepath.includes("/"); - const parent = isFolder ? filepath : filepath.split("/")[0]; - - 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)]); + 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); + } } - }; - 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> - </> - ); - } + 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> - <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 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> - </> + <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> ); } diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx index 95c66fff8073cb6b4868d091eaeff371f953d6e5..286349e0869b8a40ee76f4209c0c196ef50ea98e 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx @@ -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)} - /> + ) : ( + <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> - <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"> + <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 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> + <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> - <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 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> - <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)} - /> </div> </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 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> diff --git a/frontend/src/components/Modals/MangeWorkspace/Upload/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Upload/index.jsx deleted file mode 100644 index a4d0420661b10e0ec09c06da877f0e0d98ee4589..0000000000000000000000000000000000000000 --- a/frontend/src/components/Modals/MangeWorkspace/Upload/index.jsx +++ /dev/null @@ -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> - </> - ); -} diff --git a/frontend/src/components/Modals/MangeWorkspace/index.jsx b/frontend/src/components/Modals/MangeWorkspace/index.jsx index c35675ad14fb2561deb2d9822e48d4a75262d67a..5ca08915b30506913dee33f6c78797386e69be10 100644 --- a/frontend/src/components/Modals/MangeWorkspace/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/index.jsx @@ -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); }; diff --git a/frontend/src/components/Modals/NewWorkspace.jsx b/frontend/src/components/Modals/NewWorkspace.jsx index 07cb4586088667610514d9222fb8ee85725d48e5..bb97c1a8343f87776353e20c2cbd2917bd46f5a8 100644 --- a/frontend/src/components/Modals/NewWorkspace.jsx +++ b/frontend/src/components/Modals/NewWorkspace.jsx @@ -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> diff --git a/frontend/src/components/Modals/Password/MultiUserAuth.jsx b/frontend/src/components/Modals/Password/MultiUserAuth.jsx index f088266d335fe4c0c368349390e9960036507856..de086fc08eed3a1f2ddac03a8e9348d49aad435d 100644 --- a/frontend/src/components/Modals/Password/MultiUserAuth.jsx +++ b/frontend/src/components/Modals/Password/MultiUserAuth.jsx @@ -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> diff --git a/frontend/src/components/Modals/Password/SingleUserAuth.jsx b/frontend/src/components/Modals/Password/SingleUserAuth.jsx index 1327a278764cbe2bdbe4a00980ad1ee05163eb60..8135f8f941e4e93e0d6c7f4ebb808ded65f54b44 100644 --- a/frontend/src/components/Modals/Password/SingleUserAuth.jsx +++ b/frontend/src/components/Modals/Password/SingleUserAuth.jsx @@ -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> diff --git a/frontend/src/components/Modals/Password/index.jsx b/frontend/src/components/Modals/Password/index.jsx index 096906e8df79ad5a1cce48620ea858188db3aee3..e989868517275777220bdb887cb321b9d73b26c9 100644 --- a/frontend/src/components/Modals/Password/index.jsx +++ b/frontend/src/components/Modals/Password/index.jsx @@ -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> diff --git a/frontend/src/components/Preloader.jsx b/frontend/src/components/Preloader.jsx index 728f41bfc4c81f6875525421304a8c17562e0d3f..066d5552b1eb086d667355db853c0c04e8926074 100644 --- a/frontend/src/components/Preloader.jsx +++ b/frontend/src/components/Preloader.jsx @@ -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> ); } diff --git a/frontend/src/components/PrivateRoute/index.jsx b/frontend/src/components/PrivateRoute/index.jsx index 0829dfd5ed656dfd44e356c735500cc32aa3d7ac..1c403f81886c7c95d0c47eacd6ec7dc4dd3714c4 100644 --- a/frontend/src/components/PrivateRoute/index.jsx +++ b/frontend/src/components/PrivateRoute/index.jsx @@ -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()} /> + ); } diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..08bb846146024a748d09097a08652b9b7cb4c85b --- /dev/null +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -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> + ); +}; diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx index ce7adccfe5354b2803d6b9e33f65e19d53639a66..fd8d04513480fd13449967ffd2b0b1c73b8de913 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx @@ -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} + /> )} </> ); diff --git a/frontend/src/components/Sidebar/SettingsOverlay/index.jsx b/frontend/src/components/Sidebar/SettingsOverlay/index.jsx deleted file mode 100644 index f71f8921db53e11044a0924bcd3e1857eb4efde7..0000000000000000000000000000000000000000 --- a/frontend/src/components/Sidebar/SettingsOverlay/index.jsx +++ /dev/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 }; -} diff --git a/frontend/src/components/Sidebar/index.jsx b/frontend/src/components/Sidebar/index.jsx index 438df1ad360d837613d5835ee99aacf3071bab74..dc4fe2c35b7036f99e145e4d5b7b0f91241b3487 100644 --- a/frontend/src/components/Sidebar/index.jsx +++ b/frontend/src/components/Sidebar/index.jsx @@ -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> diff --git a/frontend/src/components/UserIcon/index.jsx b/frontend/src/components/UserIcon/index.jsx index a98b119256899fd3be3b7f526f4d4db41b731013..7ea0b26fc766cec7519ebe4a8ded55df5a6520ca 100644 --- a/frontend/src/components/UserIcon/index.jsx +++ b/frontend/src/components/UserIcon/index.jsx @@ -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 = "") { diff --git a/frontend/src/components/UserMenu/index.jsx b/frontend/src/components/UserMenu/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..95537bbbb11c72df49a024cc39eb3092df70f47c --- /dev/null +++ b/frontend/src/components/UserMenu/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/VectorDBOption/index.jsx b/frontend/src/components/VectorDBOption/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0dbe97c1932dad2193ffd946eba6d7e33f62a74b --- /dev/null +++ b/frontend/src/components/VectorDBOption/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx index 91aab2edf13068181788d814050046c26320a62e..ddcb6414c8ffb37ffa6afe2b43654ed796ba0484 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx @@ -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}`; +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index 5869a81f795f72156436fa911b52eea5e46c4935..8759a2ce7ffdb3cac8afa4caa66abeeb8423a4be 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -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> - ); - } + return ( + <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} + /> - 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> + {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> - </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} /> + {role === "assistant" && <Citations sources={sources} />} </div> </div> ); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx index 3ef308e2f799c4ddfd3e1aceebb0b49788f4ace9..c69c94e7c39040987f848cf738e5466039f26486 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx @@ -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> diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index 201cbc7ddf5e18d89d78e3fca4b214c6a8b9a980..e420fb3134b66a0b57eb5613aefec2b5f91cf1a5 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -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> ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx index 3c3f6e0f5e5efd67a9acc9fcd2bbc797013192da..556cc67128a3337ed964d3ae3115776891741544 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx @@ -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> ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index 627afe6e2f899bf5be1d87be8bf0d47509dd68a6..14f133e842af68eb7f91464a9cfe6f868a11a761 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -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} diff --git a/frontend/src/components/WorkspaceChat/LoadingChat/index.jsx b/frontend/src/components/WorkspaceChat/LoadingChat/index.jsx index f2844de7a50045b4a2ac3626c5e59ef6297294db..c93aed1d57e3bdee2e5b0757c912e824e6787d62 100644 --- a/frontend/src/components/WorkspaceChat/LoadingChat/index.jsx +++ b/frontend/src/components/WorkspaceChat/LoadingChat/index.jsx @@ -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" diff --git a/frontend/src/hooks/useLogo.js b/frontend/src/hooks/useLogo.js index 622e19b59c7366da060ba237ca1a80c2b4eb1c13..e4ecd5e877e96966349d1a1b0d3357c3e8d5918e 100644 --- a/frontend/src/hooks/useLogo.js +++ b/frontend/src/hooks/useLogo.js @@ -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 }; } diff --git a/frontend/src/index.css b/frontend/src/index.css index a0294126fe7aa714514605ad8122539ce35c338c..a6cb2aec2f5e5eda3f162e8c76a094b487c55d9f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; +} diff --git a/frontend/src/media/logo/anything-llm-dark.png b/frontend/src/media/logo/anything-llm-old.png similarity index 100% rename from frontend/src/media/logo/anything-llm-dark.png rename to frontend/src/media/logo/anything-llm-old.png diff --git a/frontend/src/media/logo/anything-llm-light.png b/frontend/src/media/logo/anything-llm.png similarity index 100% rename from frontend/src/media/logo/anything-llm-light.png rename to frontend/src/media/logo/anything-llm.png diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js index 1c78f07a6ff320e10da803220586699b5778b8cd..a2a8746fb7d391cecf758dedc4ccd8fae00e1a44 100644 --- a/frontend/src/models/admin.js +++ b/frontend/src/models/admin.js @@ -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 () { diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 6e74eb2a51800b0db027b7ce156b8f0bfbc536d6..1b2e34e3131d06dbe7877a0df02c61001ac3f600 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -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(), }) diff --git a/frontend/src/pages/Admin/Chats/ChatRow/index.jsx b/frontend/src/pages/Admin/Chats/ChatRow/index.jsx index ca0d5c78129b4b0408989b9a4dc750804a5afca2..9132b6cd2416976f0e15eaef8fe26e9d71b62989 100644 --- a/frontend/src/pages/Admin/Chats/ChatRow/index.jsx +++ b/frontend/src/pages/Admin/Chats/ChatRow/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Chats/index.jsx b/frontend/src/pages/Admin/Chats/index.jsx index 8c439bb70cdcc7eb9d34d1c4a14a9c0b29f79fcc..ffd2ed1b3aa94f3e088bb2a2ca55f656b23c14e0 100644 --- a/frontend/src/pages/Admin/Chats/index.jsx +++ b/frontend/src/pages/Admin/Chats/index.jsx @@ -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 diff --git a/frontend/src/pages/Admin/Invitations/InviteRow/index.jsx b/frontend/src/pages/Admin/Invitations/InviteRow/index.jsx index 4c6a643e9b3aae882f81cd6549afdde593c2b3d6..68408d7e75970481572d0a5f4a0bc586b62cecc5 100644 --- a/frontend/src/pages/Admin/Invitations/InviteRow/index.jsx +++ b/frontend/src/pages/Admin/Invitations/InviteRow/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx b/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx index 6da534e216b0e61926fc6bf85e0a3677d3964056..6ce4b7b352a2f26d44a70dbf3878c2e33181f1a0 100644 --- a/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx +++ b/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Invitations/index.jsx b/frontend/src/pages/Admin/Invitations/index.jsx index 651275905b6a87f73eab09e32274eb71eda6dedc..d709bd8c2d2720b1a79b14ce4f06816dbaaf5174 100644 --- a/frontend/src/pages/Admin/Invitations/index.jsx +++ b/frontend/src/pages/Admin/Invitations/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/System/index.jsx b/frontend/src/pages/Admin/System/index.jsx index b16a17c26fcc5d59d8ba41a4fadc305807bacf6e..d9ea370e76a05a19c4a17c646447720971b6ec82 100644 --- a/frontend/src/pages/Admin/System/index.jsx +++ b/frontend/src/pages/Admin/System/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index 1c0195b7b05b4681e71a1545b829c2e6dcbe73ed..0628fda9932811568df343e5c39643caa75093e7 100644 --- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx index 0654a6421b9c3029079b8a108559966a23ee8eee..6ae54dcbcb3217c1d130abbc116e140085beec5b 100644 --- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Users/UserRow/index.jsx b/frontend/src/pages/Admin/Users/UserRow/index.jsx index df7fbc4ec310768f43d82577ccd02c4aea736624..c4dac62cf2dca73a73e4ed72e5fbfdbf549a6417 100644 --- a/frontend/src/pages/Admin/Users/UserRow/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/index.jsx @@ -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 && ( <> diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx index b7873bcbb445ca224e27b0a631d799c90b58368f..29eb0340913edca3485b5e8890b2ec4f844ccf3e 100644 --- a/frontend/src/pages/Admin/Users/index.jsx +++ b/frontend/src/pages/Admin/Users/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx index bc38c1910376379f6af4fd7ab27a43c9d715c171..83b2b035923e5346076738822940f7171ba5fa05 100644 --- a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx index c363dbed311e2515042042d107df50acee750353..092ac5854cec1d343306c99fe36748206d20d7ef 100644 --- a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx index 762e2089bac53c6a580408d5507b2812b1b4a09e..6b181d4cd6f30fa9861f2223489082474f85b6f3 100644 --- a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Workspaces/index.jsx b/frontend/src/pages/Admin/Workspaces/index.jsx index ff3cb45a22dcef97e82ee2a45738fa831c7daf43..e7cbb2d096ee9d10c5d62e21b3f7337e580560d9 100644 --- a/frontend/src/pages/Admin/Workspaces/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx b/frontend/src/pages/GeneralSettings/ApiKeys/ApiKeyRow/index.jsx similarity index 64% rename from frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx rename to frontend/src/pages/GeneralSettings/ApiKeys/ApiKeyRow/index.jsx index b6645f3bda33566709bb48f50df2f0c86d87b7e9..29657f3f34376b49c921ca1d327ab6300a4d291b 100644 --- a/frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx +++ b/frontend/src/pages/GeneralSettings/ApiKeys/ApiKeyRow/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx b/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx similarity index 64% rename from frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx rename to frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx index ae59eff71616541ff6db66548125aa05d40ab2ff..7e131f6838a24037f174fbd2ece305d3357c49cf 100644 --- a/frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx +++ b/frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/ApiKeys/index.jsx b/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx similarity index 62% rename from frontend/src/pages/Admin/ApiKeys/index.jsx rename to frontend/src/pages/GeneralSettings/ApiKeys/index.jsx index f685b6c680133691de74bbdc9b9b7eb3094c2c30..769d0730fa47edd919ec1be6a3752f9dd2284ec7 100644 --- a/frontend/src/pages/Admin/ApiKeys/index.jsx +++ b/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx @@ -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> diff --git a/frontend/src/pages/Admin/Appearance/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/index.jsx similarity index 54% rename from frontend/src/pages/Admin/Appearance/index.jsx rename to frontend/src/pages/GeneralSettings/Appearance/index.jsx index 08e61f67c458ff5a02fc0cce49122b9e1ff5311e..6964c128cf8562596f3def88b334466f68ed7671 100644 --- a/frontend/src/pages/Admin/Appearance/index.jsx +++ b/frontend/src/pages/GeneralSettings/Appearance/index.jsx @@ -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> diff --git a/frontend/src/pages/GeneralSettings/ExportImport/index.jsx b/frontend/src/pages/GeneralSettings/ExportImport/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..74c83ff61963ef71b5c8e70e347ff928db8788a3 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/ExportImport/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..645d5bb8ff53be8978ae09f908705be5c364e101 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/GeneralSettings/Security/index.jsx b/frontend/src/pages/GeneralSettings/Security/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f0ecaf7ef47e3a6a5ed6e48016989bca745ca357 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/Security/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c7620a5ee236da9e7aa309488e6d946192d16981 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/Login/index.jsx b/frontend/src/pages/Login/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d7d80926b2352d87ec399cabf2d84205670c486f --- /dev/null +++ b/frontend/src/pages/Login/index.jsx @@ -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} />; +} diff --git a/frontend/src/pages/Main/index.jsx b/frontend/src/pages/Main/index.jsx index 0a1e508f6877364a2cfe54f501aa82b0c0318c21..5d79c5cab70b8bfec0196dc136f36249125e9793 100644 --- a/frontend/src/pages/Main/index.jsx +++ b/frontend/src/pages/Main/index.jsx @@ -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> ); } diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/AppearanceSetup/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/AppearanceSetup/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7fcd46c13fd84decf1453cfb4cdc5573f45767a2 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/AppearanceSetup/index.jsx @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/CreateFirstWorkspace/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/CreateFirstWorkspace/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3b860f07bdeebbe902e35706554a035fbc797416 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/CreateFirstWorkspace/index.jsx @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..70d957858ba756b280eb26fabfeac59af9797eb8 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/MultiUserSetup/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/MultiUserSetup/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..99c66b129557e582fb6d8cc5643a87e00697956b --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/MultiUserSetup/index.jsx @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/PasswordProtection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/PasswordProtection/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1baca6d23eed5bda07db8f00db1f12203cf0a23f --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/PasswordProtection/index.jsx @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserModeSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserModeSelection/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1d981b35fe0e39e3e1eab504343991299443644b --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserModeSelection/index.jsx @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/VectorDatabaseConnection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/VectorDatabaseConnection/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..47fbb870c194e0bcd65ca3f354addf8b9858a8d5 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/VectorDatabaseConnection/index.jsx @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0cdf05fae0c47d61f67ad77f91ab0907663ab121 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/index.jsx b/frontend/src/pages/OnboardingFlow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a8a85380bb0fc780f2e995f6169c203c9af4e2e4 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/WorkspaceChat/index.jsx b/frontend/src/pages/WorkspaceChat/index.jsx index 5b466bda33aa4bfa619625fb7593ab1b0d5889df..5f8985f49c93b869eb338ec81127a1fb5f4313f3 100644 --- a/frontend/src/pages/WorkspaceChat/index.jsx +++ b/frontend/src/pages/WorkspaceChat/index.jsx @@ -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> diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 602460fd2878fac0bd308266dca4cdb8fe920e15..e86aa8aecfc2365e9b414c0c42459735512ce939 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -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"; diff --git a/frontend/src/utils/directories.js b/frontend/src/utils/directories.js new file mode 100644 index 0000000000000000000000000000000000000000..53a45b773a1d0de2b3edc5868eb5b128a7105111 --- /dev/null +++ b/frontend/src/utils/directories.js @@ -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; + } +} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index 4e1ab138df6d417fa57a86b86594772806e2011e..f914f63b79c8da59e648d58a21f1384773a67da3 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -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"; - }, }, }; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index e0d23bb3f1bb1928656598c2a70f21dfa54de30a..e6dbbb94b02c0f59f32eaeef16024a69e1602dd0 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -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: [], } - diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c4199eb396bd25a52efd697e48de3508556e9d48..0c5e8749e76d0753ebd4bb0a2a880d364fd7dc3a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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" diff --git a/server/.gitignore b/server/.gitignore index e063c4a046b5971b6683218d279985f55aeb99b8..be4af591de699562d8ea3b21aba2ef9b2c55591f 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -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 diff --git a/server/endpoints/system.js b/server/endpoints/system.js index eed85839ac4fd9194332c12507a398d20664f6e2..915d31d19a65b3d2962c6c2f0df20375a8b14e29 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -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); - - 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", - }); - } + 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", + }); + } + // 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, diff --git a/server/models/user.js b/server/models/user.js index 3bf7e07a1ab75d4620ad4b4a490699555d0d685b..613aa95ed3ab65a270487a0a744e7697c892994d 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -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); diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js index 3f8e67962cc55e4e267f4bb576700e1729d2e36d..686dada988e51c8d4c79e0a3537754aa59127bbd 100644 --- a/server/models/workspaceChats.js +++ b/server/models/workspaceChats.js @@ -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: { diff --git a/server/storage/anythingllm.db.bak b/server/storage/anythingllm.db.bak new file mode 100644 index 0000000000000000000000000000000000000000..1fb2571e29fd64b66bfce26ee8b12cf557fff088 Binary files /dev/null and b/server/storage/anythingllm.db.bak differ diff --git a/server/storage/assets/anything-llm-dark.png b/server/storage/assets/anything-llm-dark.png deleted file mode 100644 index a294843869eface3065ca61c413528b3bfca668d..0000000000000000000000000000000000000000 Binary files a/server/storage/assets/anything-llm-dark.png and /dev/null differ diff --git a/server/storage/assets/anything-llm-light.png b/server/storage/assets/anything-llm.png similarity index 100% rename from server/storage/assets/anything-llm-light.png rename to server/storage/assets/anything-llm.png diff --git a/server/swagger/index.js b/server/swagger/index.js index 3f2529f8f6acca6f8261dca327aac474ecae602f..9ffdb2b97442927d6d8ccd59fe55a404f4eeb2d0 100644 --- a/server/swagger/index.js +++ b/server/swagger/index.js @@ -21,8 +21,8 @@ function waitForElm(selector) { // Force change the Swagger logo in the header waitForElm('img[alt="Swagger UI"]').then((elm) => { if (window.SWAGGER_DOCS_ENV === 'development') { - elm.src = 'http://localhost:3000/public/anything-llm-light.png' + elm.src = 'http://localhost:3000/public/anything-llm.png' } else { - elm.src = `${window.location.origin}/anything-llm-light.png` + elm.src = `${window.location.origin}/anything-llm.png` } }); \ No newline at end of file diff --git a/server/utils/files/logo.js b/server/utils/files/logo.js index cf509d9ab7c0019df47ef5e7253ec3d970817310..434f8d72cbe22ded49f6b69dc93f9ca29deb0857 100644 --- a/server/utils/files/logo.js +++ b/server/utils/files/logo.js @@ -3,18 +3,17 @@ const fs = require("fs"); const { getType } = require("mime"); const { v4 } = require("uuid"); const { SystemSettings } = require("../../models/systemSettings"); -const LIGHT_LOGO_FILENAME = "anything-llm-light.png"; -const DARK_LOGO_FILENAME = "anything-llm-dark.png"; +const LOGO_FILENAME = "anything-llm.png"; function validFilename(newFilename = "") { - return ![DARK_LOGO_FILENAME, LIGHT_LOGO_FILENAME].includes(newFilename); + return ![LOGO_FILENAME].includes(newFilename); } -function getDefaultFilename(mode = "dark") { - return mode === "light" ? DARK_LOGO_FILENAME : LIGHT_LOGO_FILENAME; +function getDefaultFilename() { + return LOGO_FILENAME; } -async function determineLogoFilepath(defaultFilename = DARK_LOGO_FILENAME) { +async function determineLogoFilepath(defaultFilename = LOGO_FILENAME) { const currentLogoFilename = await SystemSettings.currentLogoFilename(); const basePath = path.join(__dirname, "../../storage/assets"); const defaultFilepath = path.join(basePath, defaultFilename); @@ -53,7 +52,7 @@ async function renameLogoFile(originalFilename = null) { return newFilename; } -async function removeCustomLogo(logoFilename = DARK_LOGO_FILENAME) { +async function removeCustomLogo(logoFilename = LOGO_FILENAME) { if (!logoFilename || !validFilename(logoFilename)) return false; const logoPath = path.join(__dirname, `../../storage/assets/${logoFilename}`); if (fs.existsSync(logoPath)) fs.unlinkSync(logoPath); @@ -67,6 +66,5 @@ module.exports = { validFilename, getDefaultFilename, determineLogoFilepath, - LIGHT_LOGO_FILENAME, - DARK_LOGO_FILENAME, + LOGO_FILENAME, }; diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 3fcbb9a2ac3bc0c2958b22dfbf6d8f12fa23821a..88b07989284051183dd82893a8eabff182e55e83 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -118,10 +118,7 @@ function supportedLLM(input = "") { } function validOpenAIModel(input = "") { - const validModels = [ - "gpt-4", - "gpt-3.5-turbo", - ]; + const validModels = ["gpt-4", "gpt-3.5-turbo"]; return validModels.includes(input) ? null : `Invalid Model type. Must be one of ${validModels.join(", ")}.`;