AnythingLLM UI overhaul (#278)

* v2 Login screen (#254)

* adding gradients for modal and sidebar

* adding font setup

* redesigned login screen for MultiUserAuth

* completed multi user mode login screen

* linting

* login screen for single user auth redesign complete

* created reusable gradient for login screen

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>

* v2 sidebar (#262)

* adding gradients for modal and sidebar

* adding font setup

* redesigned login screen for MultiUserAuth

* completed multi user mode login screen

* linting

* login screen for single user auth redesign complete

* WIP sidebar redesign

* created reusable gradient for login screen

* remove dark mode items

* update new workspace button

* completed sidebar for desktop view

* add interactivity states

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>

* remove duplicated pkg

* v2 settings (#264)

* adding gradients for modal and sidebar

* adding font setup

* redesigned login screen for MultiUserAuth

* completed multi user mode login screen

* linting

* login screen for single user auth redesign complete

* WIP sidebar redesign

* created reusable gradient for login screen

* remove dark mode items

* update new workspace button

* completed sidebar for desktop view

* WIP added colors/gradients to admin settings

* WIP fix discord logo import

* WIP settings redesign - added routes for general settings and restyled components

* WIP settings for LLM Preference, VectorDB, ExportImport

* settings menu UI complete WIP functionality

* settings fully functional/removed dark mode logo

* linting

* removing unneeded dependency

* Fix admin sidebar visibility
Fix API Keys location and work with single/mum
Fix Appearance location - WIP on funcitonality

* update api key page

* fix permissions for appearance

* Single user mode fixes

* fix multi user mode enabled

* fix import export

* Rename AdminSidebar to SettingsSidebar

* Fix mobile sidebar links

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>

* V2 user logout (#265)

* Add user logout button

* hide other 3 dot button

* wrap admin routes

* V2 workspace modal (#267)

Update new workspace modal
remove duplicate tailwind colors

* v2 Settings modal styles (#266)

* EditUserModal styles complete

* workspaces modals styles complete

* create invite link modal styles complete

* create new api key modal styles complete

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>

* v2 Chats Redesign (#270)

* fix default message for new workspace

* prompt input box ui redesign complete

* ui tweak to prompt input

* WIP chat msg redesign

* chat container and historical chat messages redesign

* manage workspace modal appears when clicking upload a document on empty workspace

* fixed loading skeleton styles

* citations redesign complete

* restyle pending chat and prompt reply components

* default chat messages styles updated

* linting

* update how chats are returned

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>

* Onboarding modal flow for first time setup (#274)

* WIP onboarding modal flow

* onboarding flow complete and private route redirection for onboarding setep

* redirect to home on onboarding complete

* add onboarding redirect using paths.onboarding()

* Apply changes to auth flow, onboarding determination, and flows

* remove formref

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>

* v2 document picker (#275)

* remove unneeded comments

* WIP document picker UI

* WIP basic UI complete for document picker tab and settings tab

* linting

* settings menu complete, document row WIP

* WIP document picker loading from localFiles

* WIP file picker logic

* refactoring document picker to work with backend

* WIP refactoring document picker

* WIP refactor document picker to work with backend

* file uploading with dropzone working

* WIP deleting file when not embedded

* WIP embeddings

* WIP embedding with temp button and hardcoded paths

* WIP placeholder for WorkspaceDirectory component

* WIP WorkspaceDirectory

* WIP

* sort workspaceDocs and availibleDocs complete

* added directories util

* add and remove document from ws working

* v2 document picker complete

* reference modal ui bug fixes

* truncate function bug fix

* ManageWorkspace modal bug fixes

* blocking mobile users modal for workspace settings

* mobile ui fixes

* linting

* ui padding fixes

* citation bug fixes

* code review changes

* debounce handlers

* change tempFile object to array

* selection count fix

* Convert workspace modal to div
Memo workspace settings
update conditional rendering of workspace settings

* Show no documents

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>

* mobile sidebar styles

* padding on Mobile view
mobile sidebar items

* UI touchup

* suggestion implementations

* CSS fixes and animation perfomance change to GPU accelerated and 60fps

* change will-change

* remove transitions from onboarding modals, simplify on-change handlers

* Swap onboarding to memoized components and debounce onchange handlers

* remove console log

* remove Avenir font

---------

Co-authored-by: Sean Hatfield <seanhatfield5@gmail.com>
This commit is contained in:
Timothy Carambat 2023-10-23 13:10:34 -07:00 committed by GitHub
parent d1fbe94a33
commit 708068a09e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 5527 additions and 2669 deletions

View File

@ -12,8 +12,10 @@
"dependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@metamask/jazzicon": "^2.0.0",
"@phosphor-icons/react": "^2.0.13",
"buffer": "^6.0.3",
"he": "^1.2.0",
"lodash.debounce": "^4.0.8",
"markdown-it": "^13.0.1",
"pluralize": "^8.0.0",
"react": "^18.2.0",

Binary file not shown.

Binary file not shown.

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -8,6 +8,12 @@ import { isMobile } from "react-device-detect";
import { SidebarMobileHeader } from "../Sidebar";
import ChatBubble from "../ChatBubble";
import System from "../../models/system";
import Jazzicon from "../UserIcon";
import { userFromStorage } from "../../utils/request";
import {
AI_BACKGROUND_COLOR,
USER_BACKGROUND_COLOR,
} from "../../utils/constants";
export default function DefaultChatContainer() {
const [mockMsgs, setMockMessages] = useState([]);
@ -30,201 +36,265 @@ export default function DefaultChatContainer() {
const MESSAGES = [
<React.Fragment>
<div
className={`flex w-full mt-2 justify-start ${
popMsg ? "chat__message" : ""
}`}
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR} md:mt-0 mt-[40px]`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
Welcome to AnythingLLM, AnythingLLM is an open-source AI tool by
Mintplex Labs that turns <i>anything</i> into a trained chatbot you
can query and chat with. AnythingLLM is a BYOK (bring-your-own-keys)
software so there is no subscription, fee, or charges for this
software outside of the services you want to use with it.
</p>
</div>
</div>
</React.Fragment>,
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
<React.Fragment>
<div
className={`flex w-full mt-2 justify-start ${
popMsg ? "chat__message" : ""
}`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
AnythingLLM is the easiest way to put powerful AI products like
OpenAi, GPT-4, LangChain, PineconeDB, ChromaDB, and other services
together in a neat package with no fuss to increase your
productivity by 100x.
</p>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex w-full mt-2 justify-start ${
popMsg ? "chat__message" : ""
}`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
AnythingLLM can run totally locally on your machine with little
overhead you wont even notice it's there! No GPU needed. Cloud and
on-premises installation is available as well.
<br />
The AI tooling ecosystem gets more powerful everyday. AnythingLLM
makes it easy to use.
</p>
<a
href={paths.github()}
target="_blank"
className="mt-4 w-fit flex flex-grow gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900 dark:bg-stone-900"
>
<GitMerge className="h-4 w-4" />
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
Create an issue on Github
</p>
</a>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex w-full mt-2 justify-end ${
popMsg ? "chat__message" : ""
}`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
How do I get started?!
</p>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex w-full mt-2 justify-start ${
popMsg ? "chat__message" : ""
}`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
It's simple. All collections are organized into buckets we call{" "}
<b>"Workspaces"</b>. Workspaces are buckets of files, documents,
images, PDFs, and other files which will be transformed into
something LLM's can understand and use in conversation.
<br />
<br />
You can add and remove files at anytime.
</p>
<button
onClick={showNewWsModal}
className="mt-4 w-fit flex flex-grow gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900 dark:bg-stone-900"
>
<Plus className="h-4 w-4" />
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
Create your first workspace
</p>
</button>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex w-full mt-2 justify-end ${
popMsg ? "chat__message" : ""
}`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
Is this like an AI dropbox or something? What about chatting? It is
a chatbot isn't it?
</p>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex w-full mt-2 justify-start ${
popMsg ? "chat__message" : ""
}`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
AnythingLLM is more than a smarter Dropbox.
<br />
<br />
AnythingLLM offers two ways of talking with your data:
<br />
<br />
<i>Query:</i> Your chats will return data or inferences found with
the documents in your workspace it has access to. Adding more
documents to the Workspace make it smarter!
<br />
<br />
<i>Conversational:</i> Your documents + your on-going chat history
both contribute to the LLM knowledge at the same time. Great for
appending real-time text-based info or corrections and
misunderstandings the LLM might have.
<br />
<br />
You can toggle between either mode <i>in the middle of chatting!</i>
</p>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex w-full mt-2 justify-end ${
popMsg ? "chat__message" : ""
}`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
Wow, this sounds amazing, let me try it out already!
</p>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex w-full mt-2 justify-start ${
popMsg ? "chat__message" : ""
}`}
>
<div className="p-4 max-w-full md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-b-2xl rounded-tr-2xl rounded-tl-sm">
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
Have Fun!
</p>
<div className="flex flex-col md:flex-row items-start md:items-center gap-1 md:gap-4">
<a
href={paths.github()}
target="_blank"
className="mt-4 w-fit flex flex-grow gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900 dark:bg-stone-900"
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
<GitHub className="h-4 w-4" />
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
Star on GitHub
</p>
</a>
<a
href={paths.mailToMintplex()}
className="mt-4 w-fit flex flex-grow gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900 dark:bg-stone-900"
Welcome to AnythingLLM, AnythingLLM is an open-source AI tool by
Mintplex Labs that turns anything into a trained chatbot you can
query and chat with. AnythingLLM is a BYOK (bring-your-own-keys)
software so there is no subscription, fee, or charges for this
software outside of the services you want to use with it.
</span>
</div>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
<Mail className="h-4 w-4" />
<p className="text-slate-800 dark:text-slate-200 text-sm md:text-lg leading-loose">
Contact Mintplex Labs
</p>
</a>
AnythingLLM is the easiest way to put powerful AI products like
OpenAi, GPT-4, LangChain, PineconeDB, ChromaDB, and other services
together in a neat package with no fuss to increase your
productivity by 100x.
</span>
</div>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
<div>
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
AnythingLLM can run totally locally on your machine with little
overhead you wont even notice it's there! No GPU needed. Cloud
and on-premises installation is available as well.
<br />
The AI tooling ecosystem gets more powerful everyday.
AnythingLLM makes it easy to use.
</span>
<a
href={paths.github()}
target="_blank"
className="mt-5 w-fit transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
<GitMerge className="h-4 w-4" />
<p>Create an issue on Github</p>
</a>
</div>
</div>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
size={36}
user={{ uid: userFromStorage()?.username }}
role={"user"}
/>
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
How do I get started?!
</span>
</div>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
<div>
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
It's simple. All collections are organized into buckets we call{" "}
"Workspaces". Workspaces are buckets of files, documents,
images, PDFs, and other files which will be transformed into
something LLM's can understand and use in conversation.
<br />
<br />
You can add and remove files at anytime.
</span>
<button
onClick={showNewWsModal}
className="mt-5 w-fit transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
<Plus className="h-4 w-4" />
<p>Create your first workspace</p>
</button>
</div>
</div>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
size={36}
user={{ uid: userFromStorage()?.username }}
role={"user"}
/>
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
Is this like an AI dropbox or something? What about chatting? It
is a chatbot isn't it?
</span>
</div>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
AnythingLLM is more than a smarter Dropbox.
<br />
<br />
AnythingLLM offers two ways of talking with your data:
<br />
<br />
<i>Query:</i> Your chats will return data or inferences found with
the documents in your workspace it has access to. Adding more
documents to the Workspace make it smarter!
<br />
<br />
<i>Conversational:</i> Your documents + your on-going chat history
both contribute to the LLM knowledge at the same time. Great for
appending real-time text-based info or corrections and
misunderstandings the LLM might have.
<br />
<br />
You can toggle between either mode{" "}
<i>in the middle of chatting!</i>
</span>
</div>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex justify-center items-end w-full ${USER_BACKGROUND_COLOR}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
size={36}
user={{ uid: userFromStorage()?.username }}
role={"user"}
/>
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
Wow, this sounds amazing, let me try it out already!
</span>
</div>
</div>
</div>
</React.Fragment>,
<React.Fragment>
<div
className={`flex justify-center items-end w-full ${AI_BACKGROUND_COLOR}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
<div>
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
>
Have Fun!
</span>
<div className="flex flex-col md:flex-row items-start md:items-center gap-1 md:gap-4">
<a
href={paths.github()}
target="_blank"
className="mt-5 w-fit transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
<GitHub className="h-4 w-4" />
<p>Star on GitHub</p>
</a>
<a
href={paths.mailToMintplex()}
className="mt-5 w-fit transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
<Mail className="h-4 w-4" />
<p>Contact Mintplex Labs</p>
</a>
</div>
</div>
</div>
</div>
</div>
@ -259,7 +329,7 @@ export default function DefaultChatContainer() {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full md:min-w-[82%] h-full overflow-y-scroll"
>
{isMobile && <SidebarMobileHeader />}
{fetchedMessages.length === 0

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -0,0 +1,37 @@
export default function LLMProviderOption({
name,
link,
description,
value,
image,
checked = false,
onClick,
}) {
return (
<div onClick={() => onClick(value)}>
<input
type="checkbox"
value={value}
className="peer hidden"
checked={checked}
readOnly={true}
formNoValidate={true}
/>
<label className="transition-all duration-300 inline-flex flex-col h-full w-60 cursor-pointer items-start justify-between rounded-2xl bg-preference-gradient border-2 border-transparent shadow-md px-5 py-4 text-white hover:bg-selected-preference-gradient hover:border-white/60 peer-checked:border-white peer-checked:border-opacity-90 peer-checked:bg-selected-preference-gradient">
<div className="flex items-center">
<img src={image} alt={name} className="h-10 w-10 rounded" />
<div className="ml-4 text-sm font-semibold">{name}</div>
</div>
<div className="mt-2 text-xs font-base text-white tracking-wide">
{description}
</div>
<a
href={`https://${link}`}
className="mt-2 text-xs text-white font-medium underline"
>
{link}
</a>
</label>
</div>
);
}

View File

@ -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">

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
)}
</>
);
}

View File

@ -1,171 +1,146 @@
import React, { useState } from "react";
import {
FileMinus,
FilePlus,
Folder,
FolderMinus,
FolderPlus,
Zap,
} from "react-feather";
import { nFormatter } from "../../../../../utils/numbers";
import System from "../../../../../models/system";
import UploadFile from "../UploadFile";
import PreLoader from "../../../../Preloader";
import { useEffect, useState } from "react";
import FolderRow from "./FolderRow";
import pluralize from "pluralize";
export default function Directory({
files,
parent = null,
nested = 0,
toggleSelection,
isSelected,
loading,
setLoading,
fileTypes,
workspace,
fetchKeys,
selectedItems,
setSelectedItems,
setHighlightWorkspace,
moveToWorkspace,
setLoadingMessage,
loadingMessage,
}) {
const [isExpanded, toggleExpanded] = useState(false);
const [showDetails, toggleDetails] = useState(false);
const [showZap, setShowZap] = useState(false);
const handleDelete = async (name, meta) => {
if (
!window.confirm(
"Are you sure you want to delete this document?\nThis will require you to re-upload and re-embed it.\nThis document will be removed from any workspace that is currently referencing it.\nThis action is not reversible."
)
)
return false;
document?.getElementById(meta?.id)?.remove();
await System.deleteDocument(name, meta);
const [amountSelected, setAmountSelected] = useState(0);
const toggleSelection = (item) => {
setSelectedItems((prevSelectedItems) => {
const newSelectedItems = { ...prevSelectedItems };
if (item.type === "folder") {
const isCurrentlySelected = isFolderCompletelySelected(item);
if (isCurrentlySelected) {
item.items.forEach((file) => delete newSelectedItems[file.id]);
} else {
item.items.forEach((file) => (newSelectedItems[file.id] = true));
}
} else {
if (newSelectedItems[item.id]) {
delete newSelectedItems[item.id];
} else {
newSelectedItems[item.id] = true;
}
}
return newSelectedItems;
});
};
if (files.type === "folder") {
return (
<div style={{ marginLeft: nested }} className="mb-2">
<div
className={`flex items-center hover:bg-gray-100 gap-x-2 text-gray-800 dark:text-stone-200 dark:hover:bg-stone-800 px-2 rounded-lg`}
>
{files.items.some((files) => files.type === "folder") ? (
<Folder className="w-6 h-6" />
) : (
<button onClick={() => toggleSelection(files.name)}>
{isSelected(files.name) ? (
<FolderMinus className="w-6 h-6 stroke-red-800 hover:fill-red-500" />
) : (
<FolderPlus className="w-6 h-6 hover:stroke-green-800 hover:fill-green-500" />
)}
</button>
)}
const isFolderCompletelySelected = (folder) => {
if (folder.items.length === 0) {
return false;
}
return folder.items.every((file) => selectedItems[file.id]);
};
const isSelected = (id, item) => {
if (item && item.type === "folder") {
return isFolderCompletelySelected(item);
}
return !!selectedItems[id];
};
useEffect(() => {
setAmountSelected(Object.keys(selectedItems).length);
}, [selectedItems]);
return (
<div className="px-8 pb-8">
<div className="flex flex-col gap-y-6">
<div className="flex items-center justify-between w-[560px] px-5">
<h3 className="text-white text-base font-bold">My Documents</h3>
</div>
<div className="relative w-[560px] h-[310px] bg-zinc-900 rounded-2xl">
<div className="rounded-t-2xl text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 shadow-lg bg-zinc-900 sticky top-0 z-10">
<p className="col-span-4">Name</p>
<p className="col-span-2">Date</p>
<p className="col-span-2">Size</p>
<p className="col-span-2">Kind</p>
<p className="col-span-2">Cached</p>
</div>
<div
className="flex gap-x-2 items-center cursor-pointer w-full"
onClick={() => toggleExpanded(!isExpanded)}
className="overflow-y-auto pb-9"
style={{ height: "calc(100% - 40px)" }}
>
<h2 className="text-base md:text-2xl">{files.name}</h2>
{files.items.some((files) => files.type === "folder") ? (
<p className="text-xs italic">{files.items.length} folders</p>
{loading ? (
<div className="w-full h-full flex items-center justify-center flex-col gap-y-5">
<PreLoader />
<p className="text-white/80 text-sm font-semibold animate-pulse text-center w-1/3">
{loadingMessage}
</p>
</div>
) : !!files.items ? (
files.items.map(
(item, index) =>
item.type === "folder" && (
<FolderRow
key={index}
item={item}
selected={isSelected(
item.id,
item.type === "folder" ? item : null
)}
fetchKeys={fetchKeys}
onRowClick={() => toggleSelection(item)}
toggleSelection={toggleSelection}
isSelected={isSelected}
setLoading={setLoading}
setLoadingMessage={setLoadingMessage}
/>
)
)
) : (
<p className="text-xs italic">
{files.items.length} documents |{" "}
{nFormatter(
files.items.reduce((a, b) => a + b.token_count_estimate, 0)
)}{" "}
tokens
</p>
<div className="w-full h-full flex items-center justify-center">
<p className="text-white text-opacity-40 text-sm font-medium">
No Documents
</p>
</div>
)}
</div>
</div>
{isExpanded &&
files.items.map((item) => (
<Directory
key={item.name}
parent={files.name}
files={item}
nested={nested + 20}
toggleSelection={toggleSelection}
isSelected={isSelected}
/>
))}
</div>
);
}
const { name, type: _type, ...meta } = files;
return (
<div className="ml-[20px] my-2" id={meta.id}>
<div className="flex items-center">
{meta?.cached && (
<button
type="button"
onClick={() => setShowZap(true)}
className="rounded-full p-1 hover:bg-stone-500 hover:bg-opacity-75"
>
<Zap className="h-4 w-4 stroke-yellow-500 fill-yellow-400" />
</button>
)}
{showZap && (
<dialog
open={true}
style={{ zIndex: 100 }}
className="fixed top-0 flex bg-black bg-opacity-50 w-[100vw] h-full items-center justify-center "
>
<div className="w-fit px-10 py-4 w-[25%] rounded-lg bg-white shadow dark:bg-stone-700 text-black dark:text-slate-200">
<div className="flex flex-col w-full">
<p className="font-semibold text-xl flex items-center gap-x-1 justify-left">
What does{" "}
<Zap className="h-4 w-4 stroke-yellow-500 fill-yellow-400" />{" "}
mean?
</p>
<p className="text-base mt-4">
This symbol indicates that you have embed this document before
and will not have to pay to re-embed this document.
</p>
<div className="flex w-full justify-center items-center mt-4">
<button
onClick={() => setShowZap(false)}
className="border border-gray-800 text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:border-slate-200 dark:hover:bg-stone-900"
>
Close
</button>
{amountSelected !== 0 && (
<div className="absolute bottom-0 left-0 w-full flex justify-center items-center h-9 bg-white rounded-b-2xl">
<div className="flex gap-x-5">
<div
onMouseEnter={() => setHighlightWorkspace(true)}
onMouseLeave={() => setHighlightWorkspace(false)}
onClick={moveToWorkspace}
className="text-sm font-semibold h-7 px-2.5 rounded-lg transition-all duration-300 hover:text-white hover:bg-neutral-800/80 cursor-pointer flex items-center"
>
Move {amountSelected} {pluralize("file", amountSelected)} to
workspace
</div>
</div>
</div>
</dialog>
)}
)}
</div>
<div
className={`flex items-center gap-x-2 text-gray-800 dark:text-stone-200 hover:bg-gray-100 dark:hover:bg-stone-800 px-2 rounded-lg`}
>
<button onClick={() => toggleSelection(`${parent}/${name}`)}>
{isSelected(`${parent}/${name}`) ? (
<FileMinus className="w-6 h-6 stroke-red-800 hover:fill-red-500" />
) : (
<FilePlus className="w-6 h-6 hover:stroke-green-800 hover:fill-green-500" />
)}
</button>
<div
className="w-full items-center flex cursor-pointer"
onClick={() => toggleDetails(!showDetails)}
>
<h3 className="text-sm">{name}</h3>
<br />
</div>
</div>
<UploadFile
fileTypes={fileTypes}
workspace={workspace}
fetchKeys={fetchKeys}
/>
</div>
{showDetails && (
<div className="w-full flex flex-col">
<div className="ml-[20px] flex flex-col gap-y-1 my-1 p-2 rounded-md bg-slate-200 font-mono text-sm overflow-x-scroll">
{Object.entries(meta).map(([key, value], i) => {
if (key === "cached") return null;
return (
<p key={i} className="whitespace-pre">
{key}: {value}
</p>
);
})}
</div>
<div
onClick={() => handleDelete(`${parent}/${name}`, meta)}
className="flex items-center justify-end w-full"
>
<button className="text-sm text-slate-400 dark:text-stone-500 hover:text-red-500">
Purge Document
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -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);

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -1,41 +1,74 @@
import React, { useState, useEffect } from "react";
import System from "../../../../models/system";
import { ArrowsDownUp } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
import Workspace from "../../../../models/workspace";
import paths from "../../../../utils/paths";
import { useParams } from "react-router-dom";
import System from "../../../../models/system";
import Directory from "./Directory";
import ConfirmationModal from "./ConfirmationModal";
import { AlertTriangle } from "react-feather";
import showToast from "../../../../utils/toast";
import WorkspaceDirectory from "./WorkspaceDirectory";
export default function DocumentSettings({ workspace }) {
const { slug } = useParams();
const COST_PER_TOKEN = 0.0004;
export default function DocumentSettings({ workspace, fileTypes }) {
const [highlightWorkspace, setHighlightWorkspace] = useState(false);
const [availableDocs, setAvailableDocs] = useState([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const [directories, setDirectories] = useState(null);
const [originalDocuments, setOriginalDocuments] = useState([]);
const [selectedFiles, setSelectFiles] = useState([]);
const [hasFiles, setHasFiles] = useState(true);
const [canDelete, setCanDelete] = useState(false);
const [workspaceDocs, setWorkspaceDocs] = useState([]);
const [selectedItems, setSelectedItems] = useState({});
const [hasChanges, setHasChanges] = useState(false);
const [movedItems, setMovedItems] = useState([]);
const [embeddingsCost, setEmbeddingsCost] = useState(0);
const [loadingMessage, setLoadingMessage] = useState("");
async function fetchKeys(refetchWorkspace = false) {
setLoading(true);
const localFiles = await System.localFiles();
const currentWorkspace = refetchWorkspace
? await Workspace.bySlug(slug ?? workspace.slug)
? await Workspace.bySlug(workspace.slug)
: workspace;
const originalDocs =
currentWorkspace.documents.map((doc) => doc.docpath) || [];
const hasAnyFiles = localFiles.items.some(
(folder) => folder?.items?.length > 0
);
const canDelete = await System.getCanDeleteWorkspaces();
setCanDelete(canDelete);
setDirectories(localFiles);
setOriginalDocuments([...originalDocs]);
setSelectFiles([...originalDocs]);
setHasFiles(hasAnyFiles);
const documentsInWorkspace =
currentWorkspace.documents.map((doc) => doc.docpath) || [];
// Documents that are not in the workspace
const availableDocs = {
...localFiles,
items: localFiles.items.map((folder) => {
if (folder.items && folder.type === "folder") {
return {
...folder,
items: folder.items.filter(
(file) =>
file.type === "file" &&
!documentsInWorkspace.includes(`${folder.name}/${file.name}`)
),
};
} else {
return folder;
}
}),
};
// Documents that are already in the workspace
const workspaceDocs = {
...localFiles,
items: localFiles.items.map((folder) => {
if (folder.items && folder.type === "folder") {
return {
...folder,
items: folder.items.filter(
(file) =>
file.type === "file" &&
documentsInWorkspace.includes(`${folder.name}/${file.name}`)
),
};
} else {
return folder;
}
}),
};
setAvailableDocs(availableDocs);
setWorkspaceDocs(workspaceDocs);
setLoading(false);
}
@ -43,56 +76,20 @@ export default function DocumentSettings({ workspace }) {
fetchKeys();
}, []);
const deleteWorkspace = async () => {
if (
!window.confirm(
`You are about to delete your entire ${workspace.name} workspace. This will remove all vector embeddings on your vector database.\n\nThe original source files will remain untouched. This action is irreversible.`
)
)
return false;
await Workspace.delete(workspace.slug);
workspace.slug === slug
? (window.location = paths.home())
: window.location.reload();
};
const docChanges = () => {
const changes = {
adds: [],
deletes: [],
};
selectedFiles.map((doc) => {
const inOriginal = !!originalDocuments.find((oDoc) => oDoc === doc);
if (!inOriginal) {
changes.adds.push(doc);
}
});
originalDocuments.map((doc) => {
const selected = !!selectedFiles.find((oDoc) => oDoc === doc);
if (!selected) {
changes.deletes.push(doc);
}
});
return changes;
};
const confirmChanges = (e) => {
e.preventDefault();
const changes = docChanges();
changes.adds.length > 0 ? setShowConfirmation(true) : updateWorkspace(e);
};
const updateWorkspace = async (e) => {
e.preventDefault();
setSaving(true);
setLoading(true);
showToast("Updating workspace...", "info", { autoClose: false });
setShowConfirmation(false);
setLoadingMessage("This may take a while for large documents");
const changes = docChanges();
await Workspace.modifyEmbeddings(workspace.slug, changes)
const changesToSend = {
adds: movedItems.map((item) => `${item.folderName}/${item.name}`),
};
setSelectedItems({});
setHasChanges(false);
setHighlightWorkspace(false);
await Workspace.modifyEmbeddings(workspace.slug, changesToSend)
.then((res) => {
if (res && res.workspace) {
showToast("Workspace updated successfully.", "success", {
@ -108,122 +105,110 @@ export default function DocumentSettings({ workspace }) {
});
});
setSaving(false);
setMovedItems([]);
await fetchKeys(true);
setLoading(false);
setLoadingMessage("");
};
const isSelected = (filepath) => {
const isFolder = !filepath.includes("/");
return isFolder
? selectedFiles.some((doc) => doc.includes(filepath.split("/")[0]))
: selectedFiles.some((doc) => doc.includes(filepath));
};
const moveSelectedItemsToWorkspace = () => {
setHighlightWorkspace(false);
setHasChanges(true);
const toggleSelection = (filepath) => {
const isFolder = !filepath.includes("/");
const parent = isFolder ? filepath : filepath.split("/")[0];
const newMovedItems = [];
if (isSelected(filepath)) {
const updatedDocs = isFolder
? selectedFiles.filter((doc) => !doc.includes(parent))
: selectedFiles.filter((doc) => !doc.includes(filepath));
setSelectFiles([...new Set(updatedDocs)]);
} else {
var newDocs = [];
var parentDirs = directories.items.find((item) => item.name === parent);
if (isFolder && parentDirs) {
const folderItems = parentDirs.items;
newDocs = folderItems.map((item) => parent + "/" + item.name);
} else {
newDocs = [filepath];
for (const itemId of Object.keys(selectedItems)) {
for (const folder of availableDocs.items) {
const foundItem = folder.items.find((file) => file.id === itemId);
if (foundItem) {
newMovedItems.push({ ...foundItem, folderName: folder.name });
break;
}
}
const combined = [...selectedFiles, ...newDocs];
setSelectFiles([...new Set(combined)]);
}
};
if (loading) {
return (
<>
<div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll">
<div className="flex flex-col gap-y-1 w-full">
<p className="text-slate-200 dark:text-stone-300 text-center">
loading workspace files
</p>
</div>
</div>
<div className="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"></div>
</>
);
}
let totalTokenCount = 0;
newMovedItems.forEach((item) => {
const { cached, token_count_estimate } = item;
if (!cached) {
totalTokenCount += token_count_estimate;
}
});
const dollarAmount = (totalTokenCount / 1000) * COST_PER_TOKEN;
setEmbeddingsCost(dollarAmount);
setMovedItems([...movedItems, ...newMovedItems]);
let newAvailableDocs = JSON.parse(JSON.stringify(availableDocs));
let newWorkspaceDocs = JSON.parse(JSON.stringify(workspaceDocs));
for (const itemId of Object.keys(selectedItems)) {
let foundItem = null;
let foundFolderIndex = null;
newAvailableDocs.items = newAvailableDocs.items.map(
(folder, folderIndex) => {
const remainingItems = folder.items.filter((file) => {
const match = file.id === itemId;
if (match) {
foundItem = { ...file };
foundFolderIndex = folderIndex;
}
return !match;
});
return {
...folder,
items: remainingItems,
};
}
);
if (foundItem) {
newWorkspaceDocs.items[foundFolderIndex].items.push(foundItem);
}
}
setAvailableDocs(newAvailableDocs);
setWorkspaceDocs(newWorkspaceDocs);
setSelectedItems({});
};
return (
<>
{showConfirmation && (
<ConfirmationModal
directories={directories}
hideConfirm={() => setShowConfirmation(false)}
additions={docChanges().adds}
updateWorkspace={updateWorkspace}
/>
)}
<div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll">
<div className="flex flex-col gap-y-1 w-full">
{!hasFiles && (
<div className="mb-4 w-full gap-x-2 rounded-lg h-10 border bg-orange-200 border-orange-800 dark:bg-orange-300 text-orange-800 flex items-center justify-center">
<AlertTriangle className="h-6 w-6" />
<p className="text-sm">
You don't have any files uploaded. Upload a file via the "Upload
Docs" tab.
</p>
</div>
)}
<div className="flex flex-col mb-2">
<p className="text-gray-800 dark:text-stone-200 text-base ">
Select folders to add or remove from workspace.
</p>
<p className="text-gray-800 dark:text-stone-400 text-xs italic">
{selectedFiles.length} documents in workspace selected.
</p>
</div>
<div className="w-full h-auto border border-slate-200 dark:border-stone-600 rounded-lg px-4 py-2">
{!!directories && (
<Directory
files={directories}
toggleSelection={toggleSelection}
isSelected={isSelected}
/>
)}
</div>
</div>
<div className="flex gap-x-6 justify-center">
<Directory
files={availableDocs}
loading={loading}
loadingMessage={loadingMessage}
setLoading={setLoading}
fileTypes={fileTypes}
workspace={workspace}
fetchKeys={fetchKeys}
selectedItems={selectedItems}
setSelectedItems={setSelectedItems}
updateWorkspace={updateWorkspace}
highlightWorkspace={highlightWorkspace}
setHighlightWorkspace={setHighlightWorkspace}
moveToWorkspace={moveSelectedItemsToWorkspace}
setLoadingMessage={setLoadingMessage}
/>
<div className="flex items-center">
<ArrowsDownUp className="text-white text-base font-bold rotate-90 w-11 h-11" />
</div>
<div
className={`flex items-center ${
canDelete ? "justify-between" : "justify-end"
} p-4 md:p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600`}
>
<button
hidden={!canDelete}
onClick={deleteWorkspace}
type="button"
className="border border-transparent text-gray-500 bg-white hover:bg-red-100 rounded-lg whitespace-nowrap text-sm font-medium px-5 py-2.5 hover:text-red-900 focus:z-10 dark:bg-transparent dark:text-gray-300 dark:hover:text-white dark:hover:bg-red-600"
>
Delete Workspace
</button>
<div className="flex items-center">
<button
disabled={saving}
onClick={confirmChanges}
type="submit"
className="text-slate-200 bg-black-900 px-4 py-2 rounded-lg hover:bg-gray-900 whitespace-nowrap text-sm"
>
{saving ? "Saving..." : "Confirm Changes"}
</button>
</div>
</div>
</>
<WorkspaceDirectory
workspace={workspace}
files={workspaceDocs}
highlightWorkspace={highlightWorkspace}
loading={loading}
loadingMessage={loadingMessage}
setLoadingMessage={setLoadingMessage}
setLoading={setLoading}
fetchKeys={fetchKeys}
hasChanges={hasChanges}
saveChanges={updateWorkspace}
embeddingCosts={embeddingsCost}
movedItems={movedItems}
/>
</div>
);
}

View File

@ -2,6 +2,9 @@ import React, { useState, useRef, useEffect } from "react";
import Workspace from "../../../../models/workspace";
import paths from "../../../../utils/paths";
import { chatPrompt } from "../../../../utils/chat";
import System from "../../../../models/system";
import PreLoader from "../../../Preloader";
import { useParams } from "react-router-dom";
// Ensure that a type is correct before sending the body
// to the backend.
@ -20,11 +23,14 @@ function castToType(key, value) {
}
export default function WorkspaceSettings({ workspace }) {
const { slug } = useParams();
const formEl = useRef(null);
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
const [totalVectors, setTotalVectors] = useState(null);
const [canDelete, setCanDelete] = useState(false);
useEffect(() => {
function setTimer() {
@ -43,6 +49,17 @@ export default function WorkspaceSettings({ workspace }) {
setTimer();
}, [success, error]);
useEffect(() => {
async function fetchKeys() {
const canDelete = await System.getCanDeleteWorkspaces();
setCanDelete(canDelete);
const totalVectors = await System.totalIndexes();
setTotalVectors(totalVectors);
}
fetchKeys();
}, []);
const handleUpdate = async (e) => {
setError(null);
setSuccess(null);
@ -61,6 +78,7 @@ export default function WorkspaceSettings({ workspace }) {
setError(message);
}
setSaving(false);
setHasChanges(false);
};
const deleteWorkspace = async () => {
@ -78,172 +96,189 @@ export default function WorkspaceSettings({ workspace }) {
return (
<form ref={formEl} onSubmit={handleUpdate}>
<div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll">
<div className="flex flex-col gap-y-1 w-full">
<div className="flex flex-col mb-2">
<p className="text-gray-800 dark:text-stone-200 text-base ">
Edit your workspace's settings
</p>
<div className="-mt-12 px-12 pb-6 flex flex-col h-full w-full max-h-[80vh] overflow-y-scroll">
<div className="flex flex-col gap-y-1 min-w-[900px]">
<div className="text-white text-opacity-60 text-sm font-bold uppercase py-6 border-b-2 border-white/10">
Workspace Settings
</div>
<div className="w-full flex flex-col gap-y-4">
<div>
<input
type="text"
disabled={true}
defaultValue={workspace?.slug}
className="bg-gray-50 border disabled:bg-gray-400 disabled:text-gray-700 disabled:border-gray-400 disabled:dark:bg-stone-800 disabled:dark:border-stone-900 disabled:dark:text-stone-600 disabled:cursor-not-allowed border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
required={true}
autoComplete="off"
/>
<div className="flex flex-row w-full py-6 border-b-2 border-white/10">
<div className="w-1/2">
<h3 className="text-white text-sm font-semibold">
Vector database identifier
</h3>
<p className="text-white text-opacity-60 text-sm font-medium">
{workspace?.slug}
</p>
</div>
<div>
<div className="flex flex-col gap-y-1 mb-4">
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Workspace Name
</label>
<p className="text-xs text-gray-600 dark:text-stone-400">
This will only change the display name of your workspace.
<div className="w-1/2">
<h3 className="text-white text-sm font-semibold">
Number of vectors
</h3>
<p className="text-white text-opacity-60 text-xs font-medium my-[2px]">
Total number of vectors in your vector database.
</p>
{totalVectors !== null ? (
<p className="text-white text-opacity-60 text-sm font-medium">
{totalVectors}
</p>
</div>
<input
name="name"
type="text"
minLength={2}
maxLength={80}
defaultValue={workspace?.name}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="My Workspace"
required={true}
autoComplete="off"
onChange={() => setHasChanges(true)}
/>
</div>
<div>
<div className="flex flex-col gap-y-1 mb-4">
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
LLM Temperature
</label>
<p className="text-xs text-gray-600 dark:text-stone-400">
This setting controls how "random" or dynamic your chat
responses will be.
<br />
The higher the number (2.0 maximum) the more random and
incoherent.
<br />
Recommended: 0.7
</p>
</div>
<input
name="openAiTemp"
type="number"
min={0.0}
max={2.0}
step={0.1}
onWheel={(e) => e.target.blur()}
defaultValue={workspace?.openAiTemp ?? 0.7}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="0.7"
required={true}
autoComplete="off"
onChange={() => setHasChanges(true)}
/>
</div>
<div>
<div className="flex flex-col gap-y-1 mb-4">
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Prompt
</label>
<p className="text-xs text-gray-600 dark:text-stone-400">
The prompt that will be used on this workspace. Define the
context and instructions for the AI to generate a response.
You should to provide a carefully crafted prompt so the AI can
generate a relevant and accurate response.
</p>
</div>
<textarea
name="openAiPrompt"
maxLength={500}
rows={5}
defaultValue={chatPrompt(workspace)}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed."
required={true}
wrap="soft"
autoComplete="off"
onChange={() => setHasChanges(true)}
/>
</div>
<div>
<div className="flex flex-col gap-y-1 mb-4">
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Chat History
</label>
<p className="text-xs text-gray-600 dark:text-stone-400">
The number of previous chats that will be included in the
response's short-term memory.
<br />
Recommend 20. Anything more than 45 is likely to lead to
continuous chat failures depending on message size.
</p>
</div>
<input
name="openAiHistory"
type="number"
min={1}
max={45}
step={1}
onWheel={(e) => e.target.blur()}
defaultValue={workspace?.openAiHistory ?? 20}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="20"
required={true}
autoComplete="off"
onChange={() => setHasChanges(true)}
/>
) : (
<PreLoader size="4" />
)}
</div>
</div>
</div>
<div className="flex flex-col gap-y-1 w-full mt-7">
<div className="flex">
<div className="flex flex-col gap-y-4 w-1/2">
<div className="w-3/4 flex flex-col gap-y-4">
<div>
<div className="flex flex-col">
<label
htmlFor="name"
className="block text-sm font-medium text-white"
>
Workspace Name
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
This will only change the display name of your workspace.
</p>
</div>
<input
name="name"
type="text"
minLength={2}
maxLength={80}
defaultValue={workspace?.name}
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="My Workspace"
required={true}
autoComplete="off"
onChange={() => setHasChanges(true)}
/>
</div>
{error && (
<p className="text-red-600 dark:text-red-400 text-sm">
Error: {error}
</p>
)}
{success && (
<p className="text-green-600 dark:text-green-400 text-sm">
Success: {success}
</p>
)}
<div>
<div className="flex flex-col">
<label
htmlFor="name"
className="block text-sm font-medium text-white"
>
LLM Temperature
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
This setting controls how "random" or dynamic your chat
responses will be.
<br />
The higher the number (2.0 maximum) the more random and
incoherent.
<br />
<i>Recommended: 0.7</i>
</p>
</div>
<input
name="openAiTemp"
type="number"
min={0.0}
max={2.0}
step={0.1}
onWheel={(e) => e.target.blur()}
defaultValue={workspace?.openAiTemp ?? 0.7}
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="0.7"
required={true}
autoComplete="off"
onChange={() => setHasChanges(true)}
/>
</div>
<div>
<div className="flex flex-col gap-y-1 mb-4">
<label
htmlFor="name"
className="block mb-2 text-sm font-medium text-white"
>
Chat History
</label>
<p className="text-white text-opacity-60 text-xs font-medium">
The number of previous chats that will be included in the
response's short-term memory.
<i>Recommend 20. </i>
Anything more than 45 is likely to lead to continuous chat
failures depending on message size.
</p>
</div>
<input
name="openAiHistory"
type="number"
min={1}
max={45}
step={1}
onWheel={(e) => e.target.blur()}
defaultValue={workspace?.openAiHistory ?? 20}
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="20"
required={true}
autoComplete="off"
onChange={() => setHasChanges(true)}
/>
</div>
</div>
</div>
<div className="w-1/2">
<div className="w-3/4">
<div className="flex flex-col">
<label
htmlFor="name"
className="block text-sm font-medium text-white"
>
Prompt
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The prompt that will be used on this workspace. Define the
context and instructions for the AI to generate a response.
You should to provide a carefully crafted prompt so the AI
can generate a relevant and accurate response.
</p>
</div>
<textarea
name="openAiPrompt"
maxLength={500}
rows={5}
defaultValue={chatPrompt(workspace)}
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed."
required={true}
wrap="soft"
autoComplete="off"
onChange={() => setHasChanges(true)}
/>
</div>
</div>
</div>
<div className="text-center">
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
{success && (
<p className="text-green-400 text-sm">Success: {success}</p>
)}
</div>
</div>
</div>
<div className="flex items-center justify-between p-2 md:p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
<button
onClick={deleteWorkspace}
type="button"
className="border border-transparent text-gray-500 bg-white hover:bg-red-100 rounded-lg whitespace-nowrap text-sm font-medium px-5 py-2.5 hover:text-red-900 focus:z-10 dark:bg-transparent dark:text-gray-300 dark:hover:text-white dark:hover:bg-red-600"
>
Delete Workspace
</button>
<div className="flex items-center justify-between p-2 md:p-6 space-x-2 border-t rounded-b border-gray-600">
{canDelete && (
<button
onClick={deleteWorkspace}
type="button"
className="transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-transparent text-white hover:text-white hover:bg-red-600"
>
Delete Workspace
</button>
)}
{hasChanges && (
<button
type="submit"
className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 whitespace-nowrap text-sm font-medium px-2 md:px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
className="transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
{saving ? "Updating..." : "Update workspace"}
</button>

View File

@ -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>
</>
);
}

View File

@ -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);
};

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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()} />
);
}

View File

@ -0,0 +1,394 @@
import React, { useEffect, useRef, useState } from "react";
// import IndexCount from "../Sidebar/IndexCount";
// import LLMStatus from "../Sidebar/LLMStatus";
import paths from "../../utils/paths";
import useLogo from "../../hooks/useLogo";
import {
DiscordLogo,
EnvelopeSimple,
SquaresFour,
Users,
BookOpen,
ChatCenteredText,
Eye,
Key,
ChatText,
Database,
DownloadSimple,
Lock,
GithubLogo,
DotsThree,
House,
X,
List,
} from "@phosphor-icons/react";
import useUser from "../../hooks/useUser";
import { USER_BACKGROUND_COLOR } from "../../utils/constants";
export default function SettingsSidebar() {
const { logo } = useLogo();
const sidebarRef = useRef(null);
const { user } = useUser();
return (
<>
<div
ref={sidebarRef}
style={{ height: "calc(100% - 32px)" }}
className="transition-all duration-500 relative m-[16px] rounded-[26px] bg-sidebar border-4 border-accent min-w-[250px] p-[18px]"
>
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
{/* Header Information */}
<div className="flex w-full items-center justify-between">
<div className="flex shrink-0 max-w-[65%] items-center justify-start ml-2">
<img
src={logo}
alt="Logo"
className="rounded max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
<div className="flex gap-x-2 items-center text-slate-500">
<a
href={paths.home()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<X className="h-4 w-4" />
</a>
</div>
</div>
<div className="text-white text-opacity-60 text-sm font-medium uppercase mt-4 mb-0 ml-2">
Settings
</div>
{/* Primary Body */}
<div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
<div className="h-auto sidebar-items">
<div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll">
{/* Admin Settings */}
{user?.role === "admin" && (
<>
<Option
href={paths.admin.system()}
btnText="System Preferences"
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.admin.invites()}
btnText="Invitation"
icon={
<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />
}
/>
<Option
href={paths.admin.users()}
btnText="Users"
icon={<Users className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.admin.workspaces()}
btnText="Workspaces"
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.admin.chats()}
btnText="Workspace Chat"
icon={
<ChatCenteredText className="h-5 w-5 flex-shrink-0" />
}
/>
</>
)}
{/* General Settings */}
<Option
href={paths.general.appearance()}
btnText="Appearance"
icon={<Eye className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.apiKeys()}
btnText="API Keys"
icon={<Key className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.llmPreference()}
btnText="LLM Preference"
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.vectorDatabase()}
btnText="Vector Database"
icon={<Database className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.exportImport()}
btnText="Export or Import"
icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.security()}
btnText="Security"
icon={<Lock className="h-5 w-5 flex-shrink-0" />}
/>
</div>
</div>
<div>
{/* <div className="flex flex-col gap-y-2">
<div className="w-full flex items-center justify-between">
<LLMStatus />
<IndexCount />
</div>
</div> */}
{/* Footer */}
<div className="flex justify-center mt-2">
<div className="flex space-x-4">
<a
href={paths.github()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<GithubLogo weight="fill" className="h-5 w-5 " />
</a>
<a
href={paths.docs()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<BookOpen weight="fill" className="h-5 w-5 " />
</a>
<a
href={paths.discord()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<DiscordLogo
weight="fill"
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
/>
</a>
<button className="invisible transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border">
<DotsThree className="h-5 w-5 group-hover:stroke-slate-200" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
}
export function SidebarMobileHeader() {
const { logo } = useLogo();
const { user } = useUser();
const sidebarRef = useRef(null);
const [showSidebar, setShowSidebar] = useState(false);
const [showBgOverlay, setShowBgOverlay] = useState(false);
useEffect(() => {
function handleBg() {
if (showSidebar) {
setTimeout(() => {
setShowBgOverlay(true);
}, 300);
} else {
setShowBgOverlay(false);
}
}
handleBg();
}, [showSidebar]);
return (
<>
<div className="fixed top-0 left-0 right-0 z-10 flex justify-between items-center px-4 py-2 bg-sidebar text-slate-200 shadow-lg h-16">
<button
onClick={() => setShowSidebar(true)}
className="rounded-md p-2 flex items-center justify-center text-slate-200"
>
<List className="h-6 w-6" />
</button>
<div className="flex items-center justify-center flex-grow">
<img
src={logo}
alt="Logo"
className="block mx-auto h-6 w-auto"
style={{ maxHeight: "40px", objectFit: "contain" }}
/>
</div>
<div className="w-12"></div>
</div>
<div
style={{
transform: showSidebar ? `translateX(0vw)` : `translateX(-100vw)`,
}}
className={`z-99 fixed top-0 left-0 transition-all duration-500 w-[100vw] h-[100vh]`}
>
<div
className={`${
showBgOverlay
? "transition-all opacity-1"
: "transition-none opacity-0"
} duration-500 fixed top-0 left-0 ${USER_BACKGROUND_COLOR} bg-opacity-75 w-screen h-screen`}
onClick={() => setShowSidebar(false)}
/>
<div
ref={sidebarRef}
className="h-[100vh] fixed top-0 left-0 rounded-r-[26px] bg-sidebar w-[80%] p-[18px] "
>
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
{/* Header Information */}
<div className="flex w-full items-center justify-between gap-x-4">
<div className="flex shrink-1 w-fit items-center justify-start">
<img
src={logo}
alt="Logo"
className="rounded w-full max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
<div className="flex gap-x-2 items-center text-slate-500 shrink-0">
<a
href={paths.home()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<House className="h-4 w-4" />
</a>
</div>
</div>
{/* Primary Body */}
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-hidden ">
<div className="h-auto md:sidebar-items md:dark:sidebar-items">
<div
style={{ height: "calc(100vw - -3rem)" }}
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
>
{user?.role === "admin" && (
<>
<Option
href={paths.admin.system()}
btnText="System Preferences"
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.admin.invites()}
btnText="Invitation"
icon={
<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />
}
/>
<Option
href={paths.admin.users()}
btnText="Users"
icon={<Users className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.admin.workspaces()}
btnText="Workspaces"
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.admin.chats()}
btnText="Workspace Chat"
icon={
<ChatCenteredText className="h-5 w-5 flex-shrink-0" />
}
/>
</>
)}
{/* General Settings */}
<Option
href={paths.general.appearance()}
btnText="Appearance"
icon={<Eye className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.apiKeys()}
btnText="API Keys"
icon={<Key className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.llmPreference()}
btnText="LLM Preference"
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.vectorDatabase()}
btnText="Vector Database"
icon={<Database className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.exportImport()}
btnText="Export or Import"
icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.general.security()}
btnText="Security"
icon={<Lock className="h-5 w-5 flex-shrink-0" />}
/>
</div>
</div>
<div>
{/* Footer */}
<div className="flex justify-center mt-2">
<div className="flex space-x-4">
<a
href={paths.github()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<GithubLogo weight="fill" className="h-5 w-5 " />
</a>
<a
href={paths.docs()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<BookOpen weight="fill" className="h-5 w-5 " />
</a>
<a
href={paths.discord()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<DiscordLogo
weight="fill"
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
/>
</a>
{/* <button className="invisible transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border">
<DotsThree className="h-5 w-5 group-hover:stroke-slate-200" />
</button> */}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
}
const Option = ({ btnText, icon, href }) => {
const isActive = window.location.pathname === href;
return (
<div className="flex gap-x-2 items-center justify-between text-white">
<a
href={href}
className={`
transition-all duration-[200ms]
flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 rounded justify-start items-center border
${
isActive
? "bg-menu-item-selected-gradient border-slate-100 border-opacity-50 font-medium"
: "hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent"
}
`}
>
{React.cloneElement(icon, { weight: isActive ? "fill" : "regular" })}
<p className="text-sm leading-loose text-opacity-60 whitespace-nowrap overflow-hidden ">
{btnText}
</p>
</a>
</div>
);
};

View File

@ -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}
/>
)}
</>
);

View File

@ -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 };
}

View File

@ -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>

View File

@ -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 = "") {

View File

@ -0,0 +1,74 @@
import { useState } from "react";
import { isMobile } from "react-device-detect";
import paths from "../../utils/paths";
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "../../utils/constants";
import { Person, SignOut } from "@phosphor-icons/react";
import { userFromStorage } from "../../utils/request";
export default function UserMenu({ children }) {
if (isMobile) return <>{children}</>;
return (
<div className="w-auto h-auto">
<UserButton />
{children}
</div>
);
}
function useLoginMode() {
const user = !!window.localStorage.getItem(AUTH_USER);
const token = !!window.localStorage.getItem(AUTH_TOKEN);
if (user && token) return "multi";
if (!user && token) return "single";
return null;
}
function userDisplay() {
const user = userFromStorage();
return user?.username?.slice(0, 2) || "AA";
}
function UserButton() {
const [showMenu, setShowMenu] = useState(false);
const mode = useLoginMode();
if (mode === null) return null;
return (
<div className="absolute top-9 right-10 w-fit h-fit z-99">
<button
onClick={() => setShowMenu(!showMenu)}
type="button"
className="uppercase transition-all duration-300 w-[35px] h-[35px] text-base font-semibold rounded-full flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient justify-center text-white p-2 hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
{mode === "multi" ? userDisplay() : <Person size={14} />}
</button>
{showMenu && (
<div className="w-fit rounded-lg absolute top-12 right-0 bg-sidebar p-4 flex items-center-justify-center">
<div className="flex flex-col gap-y-2">
<a
href={paths.mailToMintplex()}
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
>
Support
</a>
<button
onClick={() => {
window.localStorage.removeItem(AUTH_USER);
window.localStorage.removeItem(AUTH_TOKEN);
window.localStorage.removeItem(AUTH_TIMESTAMP);
window.location.replace(paths.home());
}}
type="button"
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
>
Sign out
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,39 @@
import React from "react";
export default function VectorDBOption({
name,
link,
description,
value,
image,
checked = false,
onClick,
}) {
return (
<div onClick={() => onClick(value)}>
<input
type="checkbox"
value={value}
className="peer hidden"
checked={checked}
readOnly={true}
formNoValidate={true}
/>
<label className="transition-all duration-300 inline-flex flex-col h-full w-60 cursor-pointer items-start justify-between rounded-2xl bg-preference-gradient border-2 border-transparent shadow-md px-5 py-4 text-white hover:bg-selected-preference-gradient hover:text-underline hover:border-white/60 peer-checked:border-white peer-checked:border-opacity-90 peer-checked:bg-selected-preference-gradient">
<div className="flex items-center">
<img src={image} alt={name} className="h-10 w-10 rounded" />
<div className="ml-4 text-sm font-semibold">{name}</div>
</div>
<div className="mt-2 text-xs font-base text-white tracking-wide">
{description}
</div>
<a
href={`https://${link}`}
className="mt-2 text-xs text-white font-medium underline"
>
{link}
</a>
</label>
</div>
);
}

View File

@ -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}`;
}

View File

@ -4,49 +4,50 @@ import Jazzicon from "../../../../UserIcon";
import renderMarkdown from "../../../../../utils/chat/markdown";
import { userFromStorage } from "../../../../../utils/request";
import Citations from "../Citation";
import {
AI_BACKGROUND_COLOR,
USER_BACKGROUND_COLOR,
} from "../../../../../utils/constants";
const HistoricalMessage = forwardRef(
({ message, role, workspace, sources = [], error = false }, ref) => {
if (role === "user") {
return (
<div className="flex justify-end mb-4 items-start">
<div className="mr-2 py-1 px-4 w-fit md:max-w-[75%] bg-slate-200 dark:bg-amber-800 rounded-b-2xl rounded-tl-2xl rounded-tr-sm">
<span
className={`inline-block p-2 rounded-lg whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base`}
>
{message}
</span>
</div>
<Jazzicon size={30} user={{ uid: userFromStorage()?.username }} />
</div>
);
}
if (error) {
return (
<div className="flex justify-start mb-4 items-end">
<Jazzicon size={30} user={{ uid: workspace.slug }} />
<div className="ml-2 max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm">
<span
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
>
<AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could not
respond to message.
</span>
</div>
</div>
);
}
return (
<div ref={ref} className="flex justify-start items-end mb-4">
<Jazzicon size={30} user={{ uid: workspace.slug }} />
<div className="ml-2 py-3 px-4 overflow-x-scroll w-fit md:max-w-[75%] bg-orange-100 dark:bg-stone-700 rounded-t-2xl rounded-br-2xl rounded-bl-sm">
<span
className="no-scroll whitespace-pre-line text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base flex flex-col gap-y-1"
dangerouslySetInnerHTML={{ __html: renderMarkdown(message) }}
/>
<Citations sources={sources} />
<div
ref={ref}
className={`flex justify-center items-end w-full ${
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
}`}
>
<div
className={`py-10 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
size={36}
user={{
uid:
role === "user"
? userFromStorage()?.username
: workspace.slug,
}}
role={role}
/>
{error ? (
<span
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
>
<AlertTriangle className="h-4 w-4 mb-1 inline-block" /> Could
not respond to message.
</span>
) : (
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
dangerouslySetInnerHTML={{ __html: renderMarkdown(message) }}
/>
)}
</div>
{role === "assistant" && <Citations sources={sources} />}
</div>
</div>
);

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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}

View File

@ -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"

View File

@ -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 };
}

View File

@ -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;
}

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -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 () {

View File

@ -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(),
})

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 && (
<>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 &rarr;
</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>

View File

@ -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 &rarr;
</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>

View File

@ -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>

View File

@ -0,0 +1,190 @@
import { useEffect, useRef, useState } from "react";
import Sidebar, {
SidebarMobileHeader,
} from "../../../components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import Admin from "../../../models/admin";
import showToast from "../../../utils/toast";
import { CloudArrowUp, DownloadSimple } from "@phosphor-icons/react";
import System from "../../../models/system";
import { API_BASE } from "../../../utils/constants";
export default function GeneralExportImport() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
>
{isMobile && <SidebarMobileHeader />}
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
Export or Import
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
Have multiple AnythingLLM instances or simply want to backup or
re-import data from another instance? You can do so here.
</p>
</div>
<div className="text-white text-sm font-medium py-4">
This will not automatically sync your vector database embeddings.
</div>
<ImportData />
<ExportData />
</div>
</div>
</div>
);
}
function ImportData() {
const inputRef = useRef(null);
const [loading, setLoading] = useState(false);
const [file, setFile] = useState(null);
const [result, setResult] = useState(null);
const startInput = () => inputRef?.current?.click();
const handleUpload = async (e) => {
setLoading(true);
e.preventDefault();
setFile(null);
setResult(null);
const file = e.target.files?.[0];
if (!file) {
showToast("Invalid file upload", "error");
return false;
}
setFile(file);
setLoading(true);
const formData = new FormData();
formData.append("file", file, file.name);
const { success, error } = await System.importData(formData);
if (!success) {
showToast(`Failed to import data: ${error}`, "error");
} else {
setResult(true);
showToast(`Successfully imported ${file.name}`, "success");
}
setLoading(false);
setFile(null);
};
return (
<div
onClick={startInput}
className="max-w-[600px] py-4 bg-zinc-900/50 rounded-2xl border-2 border-dashed border-white border-opacity-60 justify-center items-center inline-flex transition-all duration-300 hover:opacity-60 cursor-pointer"
>
<div className="flex flex-col items-center justify-center">
{loading ? (
<div className="flex items-center justify-center gap-2 animate-pulse">
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
Importing
</div>
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-white border-t-transparent " />
</div>
) : !!result ? (
<div className="flex items-center justify-center gap-2">
<CloudArrowUp className="w-8 h-8 text-green-400" />
<div className="text-green-400 text-opacity-80 text-sm font-semibold py-1">
Import Successful
</div>
</div>
) : (
<>
<input
ref={inputRef}
onChange={handleUpload}
name="import"
type="file"
multiple="false"
accept=".zip"
hidden={true}
/>
<div className="flex flex-col items-center justify-center">
<CloudArrowUp className="w-8 h-8 text-white/80" />
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
Import AnythingLLM Data
</div>
<div className="text-white text-opacity-60 text-xs font-medium py-1">
This must be an export from an AnythingLLM instance.
</div>
</div>
</>
)}
</div>
</div>
);
}
function ExportData() {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const exportData = async function () {
setLoading(true);
const { filename, error } = await System.dataExport();
setLoading(false);
if (!filename) {
showToast(`Failed to export data: ${error}`, "error");
} else {
setResult(filename);
const link = document.createElement("a");
link.href = `${API_BASE}/system/data-exports/${filename}`;
link.target = "_blank";
document.body.appendChild(link);
}
};
if (loading) {
return (
<button
onClick={exportData}
className="transition-all max-w-[600px] bg-white rounded-lg justify-center items-center my-8 text-zinc-900 border-transparent border-2 cursor-not-allowed animate-pulse"
>
<div className="flex items-center justify-center gap-2">
<div className="duration-300 text-center text-sm font-bold py-3">
Exporting
</div>
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-zinc-900 border-t-transparent " />
</div>
</button>
);
}
if (!!result) {
return (
<a
target="_blank"
href={`${API_BASE}/system/data-exports/${result}`}
className="transition-all max-w-[600px] bg-green-100 hover:bg-zinc-900/50 hover:text-white hover:border-white rounded-lg justify-center items-center my-8 text-zinc-900 border-transparent border-2 cursor-pointer"
>
<div className="flex items-center justify-center gap-2">
<div className="duration-300 text-center text-sm font-bold py-3">
Download Data Export
</div>
<DownloadSimple className="w-6 h-6" />
</div>
</a>
);
}
return (
<button
onClick={exportData}
className="transition-all max-w-[600px] bg-white rounded-lg justify-center items-center my-8 cursor-pointer text-zinc-900 border-transparent border-2 hover:bg-zinc-900/50 hover:text-white hover:border-white"
>
<div className="duration-300 text-center text-sm font-bold py-3">
Export AnythingLLM Data
</div>
</button>
);
}

View File

@ -0,0 +1,256 @@
import React, { useEffect, useState } from "react";
import Sidebar, {
SidebarMobileHeader,
} from "../../../components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import System from "../../../models/system";
import showToast from "../../../utils/toast";
import OpenAiLogo from "../../../media/llmprovider/openai.png";
import AzureOpenAiLogo from "../../../media/llmprovider/azure.png";
import AnthropicLogo from "../../../media/llmprovider/anthropic.png";
import PreLoader from "../../../components/Preloader";
import LLMProviderOption from "../../../components/LLMProviderOption";
export default function GeneralLLMPreference() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [llmChoice, setLLMChoice] = useState("openai");
const [settings, setSettings] = useState(null);
const [loading, setLoading] = useState(true);
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
const data = {};
const form = new FormData(e.target);
for (var [key, value] of form.entries()) data[key] = value;
const { error } = await System.updateSystem(data);
if (error) {
showToast(`Failed to save LLM settings: ${error}`, "error");
} else {
showToast("LLM preferences saved successfully.", "success");
}
setSaving(false);
setHasChanges(!!error ? true : false);
};
const updateLLMChoice = (selection) => {
setLLMChoice(selection);
setHasChanges(true);
};
useEffect(() => {
async function fetchKeys() {
const _settings = await System.keys();
setSettings(_settings);
setLLMChoice(_settings?.LLMProvider);
setLoading(false);
}
fetchKeys();
}, []);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll animate-pulse"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
</div>
</div>
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
>
{isMobile && <SidebarMobileHeader />}
<form
onSubmit={handleSubmit}
onChange={() => setHasChanges(true)}
className="flex w-full"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
LLM Preference
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
These are the credentials and settings for your preferred LLM
chat & embedding provider. Its important these keys are
current and correct or else AnythingLLM will not function
properly.
</p>
</div>
<div className="text-white text-sm font-medium py-4">
LLM Providers
</div>
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]">
<input hidden={true} name="LLMProvider" value={llmChoice} />
<LLMProviderOption
name="OpenAI"
value="openai"
link="openai.com"
description="The standard option for most non-commercial use. Provides both chat and embedding."
checked={llmChoice === "openai"}
image={OpenAiLogo}
onClick={updateLLMChoice}
/>
<LLMProviderOption
name="Azure OpenAI"
value="azure"
link="azure.microsoft.com"
description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding."
checked={llmChoice === "azure"}
image={AzureOpenAiLogo}
onClick={updateLLMChoice}
/>
<LLMProviderOption
name="Anthropic Claude 2"
value="anthropic-claude-2"
link="anthropic.com"
description="[COMING SOON] A friendly AI Assistant hosted by Anthropic. Provides chat services only!"
checked={llmChoice === "anthropic-claude-2"}
image={AnthropicLogo}
/>
</div>
<div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
{llmChoice === "openai" && (
<>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="text"
name="OpenAiKey"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="OpenAI API Key"
defaultValue={settings?.OpenAiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
</label>
<select
name="OpenAiModelPref"
defaultValue={settings?.OpenAiModelPref}
required={true}
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{["gpt-3.5-turbo", "gpt-4"].map((model) => {
return (
<option key={model} value={model}>
{model}
</option>
);
})}
</select>
</div>
</>
)}
{llmChoice === "azure" && (
<>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Azure Service Endpoint
</label>
<input
type="url"
name="AzureOpenAiEndpoint"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="https://my-azure.openai.azure.com"
defaultValue={settings?.AzureOpenAiEndpoint}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="password"
name="AzureOpenAiKey"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Azure OpenAI API Key"
defaultValue={
settings?.AzureOpenAiKey ? "*".repeat(20) : ""
}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Deployment Name
</label>
<input
type="text"
name="AzureOpenAiModelPref"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Azure OpenAI chat model deployment name"
defaultValue={settings?.AzureOpenAiModelPref}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Embedding Model Deployment Name
</label>
<input
type="text"
name="AzureOpenAiEmbeddingModelPref"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Azure OpenAI embedding model deployment name"
defaultValue={settings?.AzureOpenAiEmbeddingModelPref}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
</>
)}
{llmChoice === "anthropic-claude-2" && (
<div className="w-full h-40 items-center justify-center flex">
<p className="text-gray-800 dark:text-slate-400">
This provider is unavailable and cannot be used in
AnythingLLM currently.
</p>
</div>
)}
</div>
</div>
</form>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,337 @@
import { useEffect, useState } from "react";
import Sidebar, {
SidebarMobileHeader,
} from "../../../components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import showToast from "../../../utils/toast";
import System from "../../../models/system";
import paths from "../../../utils/paths";
import {
AUTH_TIMESTAMP,
AUTH_TOKEN,
AUTH_USER,
} from "../../../utils/constants";
import PreLoader from "../../../components/Preloader";
export default function GeneralSecurity() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
>
{isMobile && <SidebarMobileHeader />}
<MultiUserMode />
<PasswordProtection />
</div>
</div>
);
}
function MultiUserMode() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [useMultiUserMode, setUseMultiUserMode] = useState(false);
const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false);
const [loading, setLoading] = useState(true);
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
if (useMultiUserMode) {
const form = new FormData(e.target);
const data = {
username: form.get("username"),
password: form.get("password"),
};
const { success, error } = await System.setupMultiUser(data);
if (success) {
showToast("Multi-User mode enabled successfully.", "success");
setSaving(false);
setTimeout(() => {
window.localStorage.removeItem(AUTH_USER);
window.localStorage.removeItem(AUTH_TOKEN);
window.localStorage.removeItem(AUTH_TIMESTAMP);
window.location = paths.admin.users();
}, 2_000);
return;
}
showToast(`Failed to enable Multi-User mode: ${error}`, "error");
setSaving(false);
return;
}
};
useEffect(() => {
async function fetchIsMultiUserMode() {
setLoading(true);
const multiUserModeEnabled = await System.isMultiUserMode();
setMultiUserModeEnabled(multiUserModeEnabled);
setLoading(false);
}
fetchIsMultiUserMode();
}, []);
if (loading) {
return (
<div className="h-1/2 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] md:min-w-[82%] p-[18px] h-full overflow-y-scroll">
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
</div>
</div>
);
}
return (
<form
onSubmit={handleSubmit}
onChange={() => setHasChanges(true)}
className="flex w-full"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Multi-User Mode</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
Set up your instance to support your team by activating Multi-User
Mode.
</p>
</div>
<div className="relative w-full max-h-full">
<div className="relative rounded-lg">
<div className="flex items-start justify-between px-6 py-4"></div>
<div className="space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div className="">
<label className="mb-2.5 block font-medium text-white">
{multiUserModeEnabled
? "Multi-User Mode is Enabled"
: "Enable Multi-User Mode"}
</label>
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
onClick={() => setUseMultiUserMode(!useMultiUserMode)}
checked={useMultiUserMode}
className="peer sr-only pointer-events-none"
/>
<div
hidden={multiUserModeEnabled}
className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"
></div>
</label>
</div>
{useMultiUserMode && (
<div className="w-full flex flex-col gap-y-2 my-5">
<div className="w-80">
<label
htmlFor="username"
className="block mb-3 font-medium text-white"
>
Admin account username
</label>
<input
name="username"
type="text"
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
placeholder="Your admin username"
minLength={2}
required={true}
autoComplete="off"
disabled={multiUserModeEnabled}
defaultValue={multiUserModeEnabled ? "********" : ""}
/>
</div>
<div className="mt-4 w-80">
<label
htmlFor="password"
className="block mb-3 font-medium text-white"
>
Admin account password
</label>
<input
name="password"
type="text"
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
placeholder="Your admin password"
minLength={8}
required={true}
autoComplete="off"
defaultValue={multiUserModeEnabled ? "********" : ""}
/>
</div>
</div>
)}
</div>
</div>
<div className="flex items-center justify-between space-x-14">
<p className="text-white/80 text-xs rounded-lg w-96">
By default, you will be the only admin. As an admin you will
need to create accounts for all new users or admins. Do not lose
your password as only an Admin user can reset passwords.
</p>
</div>
</div>
</div>
</div>
</form>
);
}
function PasswordProtection() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false);
const [usePassword, setUsePassword] = useState(false);
const [loading, setLoading] = useState(true);
const handleSubmit = async (e) => {
e.preventDefault();
if (multiUserModeEnabled) return false;
setSaving(true);
const form = new FormData(e.target);
const data = {
usePassword,
newPassword: form.get("password"),
};
const { success, error } = await System.updateSystemPassword(data);
if (success) {
showToast("Your page will refresh in a few seconds.", "success");
setSaving(false);
setTimeout(() => {
window.localStorage.removeItem(AUTH_USER);
window.localStorage.removeItem(AUTH_TOKEN);
window.localStorage.removeItem(AUTH_TIMESTAMP);
window.location.reload();
}, 3_000);
return;
} else {
showToast(`Failed to update password: ${error}`, "error");
setSaving(false);
}
};
useEffect(() => {
async function fetchIsMultiUserMode() {
setLoading(true);
const multiUserModeEnabled = await System.isMultiUserMode();
const settings = await System.keys();
setMultiUserModeEnabled(multiUserModeEnabled);
setUsePassword(settings?.RequiresAuth);
setLoading(false);
}
fetchIsMultiUserMode();
}, []);
if (loading) {
return (
<div className="h-1/2 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] md:min-w-[82%] p-[18px] h-full overflow-y-scroll">
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
</div>
</div>
);
}
if (multiUserModeEnabled) return null;
return (
<form
onSubmit={handleSubmit}
onChange={() => setHasChanges(true)}
className="flex w-full"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
Password Protection
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
Protect your AnythingLLM instance with a password. If you forget
this there is no recovery method so ensure you save this password.
</p>
</div>
<div className="relative w-full max-h-full">
<div className="relative rounded-lg">
<div className="flex items-start justify-between px-6 py-4"></div>
<div className="space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div className="">
<label className="mb-2.5 block font-medium text-white">
Password Protect Instance
</label>
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
onClick={() => setUsePassword(!usePassword)}
checked={usePassword}
className="peer sr-only pointer-events-none"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
</label>
</div>
{usePassword && (
<div className="w-full flex flex-col gap-y-2 my-5">
<div className="mt-4 w-80">
<label
htmlFor="password"
className="block mb-3 font-medium text-white"
>
Instance password
</label>
<input
name="password"
type="text"
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
placeholder="Your Instance Password"
minLength={8}
required={true}
autoComplete="off"
defaultValue={usePassword ? "********" : ""}
/>
</div>
</div>
)}
</div>
</div>
<div className="flex items-center justify-between space-x-14">
<p className="text-white/80 text-xs rounded-lg w-96">
By default, you will be the only admin. As an admin you will
need to create accounts for all new users or admins. Do not lose
your password as only an Admin user can reset passwords.
</p>
</div>
</div>
</div>
</div>
</form>
);
}

View File

@ -0,0 +1,339 @@
import React, { useState, useEffect } from "react";
import Sidebar, {
SidebarMobileHeader,
} from "../../../components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import System from "../../../models/system";
import showToast from "../../../utils/toast";
import ChromaLogo from "../../../media/vectordbs/chroma.png";
import PineconeLogo from "../../../media/vectordbs/pinecone.png";
import LanceDbLogo from "../../../media/vectordbs/lancedb.png";
import WeaviateLogo from "../../../media/vectordbs/weaviate.png";
import QDrantLogo from "../../../media/vectordbs/qdrant.png";
import PreLoader from "../../../components/Preloader";
import VectorDBOption from "../../../components/VectorDBOption";
export default function GeneralVectorDatabase() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [vectorDB, setVectorDB] = useState("lancedb");
const [settings, setSettings] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchKeys() {
const _settings = await System.keys();
setSettings(_settings);
setVectorDB(_settings?.VectorDB || "lancedb");
setLoading(false);
}
fetchKeys();
}, []);
const updateVectorChoice = (selection) => {
setHasChanges(true);
setVectorDB(selection);
};
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
const data = {};
const form = new FormData(e.target);
for (var [key, value] of form.entries()) data[key] = value;
const { error } = await System.updateSystem(data);
if (error) {
showToast(`Failed to save settings: ${error}`, "error");
} else {
showToast("Settings saved successfully.", "success");
}
setSaving(false);
setHasChanges(!!error ? true : false);
};
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll animate-pulse"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
</div>
</div>
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
>
{isMobile && <SidebarMobileHeader />}
<form
onSubmit={handleSubmit}
onChange={() => setHasChanges(true)}
className="flex w-full"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
Vector Database
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
These are the credentials and settings for how your
AnythingLLM instance will function. It's important these keys
are current and correct.
</p>
</div>
<div className="text-white text-sm font-medium py-4">
Select your preferred vector database provider
</div>
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]">
<input hidden={true} name="VectorDB" value={vectorDB} />
<VectorDBOption
name="Chroma"
value="chroma"
link="trychroma.com"
description="Open source vector database you can host yourself or on the cloud."
checked={vectorDB === "chroma"}
image={ChromaLogo}
onClick={updateVectorChoice}
/>
<VectorDBOption
name="Pinecone"
value="pinecone"
link="pinecone.io"
description="100% cloud-based vector database for enterprise use cases."
checked={vectorDB === "pinecone"}
image={PineconeLogo}
onClick={updateVectorChoice}
/>
<VectorDBOption
name="QDrant"
value="qdrant"
link="qdrant.tech"
description="Open source local and distributed cloud vector database."
checked={vectorDB === "qdrant"}
image={QDrantLogo}
onClick={updateVectorChoice}
/>
<VectorDBOption
name="Weaviate"
value="weaviate"
link="weaviate.io"
description="Open source local and cloud hosted multi-modal vector database."
checked={vectorDB === "weaviate"}
image={WeaviateLogo}
onClick={updateVectorChoice}
/>
<VectorDBOption
name="LanceDB"
value="lancedb"
link="lancedb.com"
description="100% local vector DB that runs on the same instance as AnythingLLM."
checked={vectorDB === "lancedb"}
image={LanceDbLogo}
onClick={updateVectorChoice}
/>
</div>
<div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
{vectorDB === "pinecone" && (
<>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Pinecone DB API Key
</label>
<input
type="password"
name="PineConeKey"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Pinecone API Key"
defaultValue={
settings?.PineConeKey ? "*".repeat(20) : ""
}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Pinecone Index Environment
</label>
<input
type="text"
name="PineConeEnvironment"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="us-gcp-west-1"
defaultValue={settings?.PineConeEnvironment}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Pinecone Index Name
</label>
<input
type="text"
name="PineConeIndex"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="my-index"
defaultValue={settings?.PineConeIndex}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
</>
)}
{vectorDB === "chroma" && (
<>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chroma Endpoint
</label>
<input
type="url"
name="ChromaEndpoint"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://localhost:8000"
defaultValue={settings?.ChromaEndpoint}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Header
</label>
<input
name="ChromaApiHeader"
autoComplete="off"
type="text"
defaultValue={settings?.ChromaApiHeader}
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="X-Api-Key"
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
name="ChromaApiKey"
autoComplete="off"
type="password"
defaultValue={
settings?.ChromaApiKey ? "*".repeat(20) : ""
}
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="sk-myApiKeyToAccessMyChromaInstance"
/>
</div>
</>
)}
{vectorDB === "lancedb" && (
<div className="w-full h-40 items-center justify-center flex">
<p className="text-sm font-base text-white text-opacity-60">
There is no configuration needed for LanceDB.
</p>
</div>
)}
{vectorDB === "qdrant" && (
<>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
QDrant API Endpoint
</label>
<input
type="url"
name="QdrantEndpoint"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://localhost:6633"
defaultValue={settings?.QdrantEndpoint}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="password"
name="QdrantApiKey"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="wOeqxsYP4....1244sba"
defaultValue={settings?.QdrantApiKey}
autoComplete="off"
spellCheck={false}
/>
</div>
</>
)}
{vectorDB === "weaviate" && (
<>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Weaviate Endpoint
</label>
<input
type="url"
name="WeaviateEndpoint"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://localhost:8080"
defaultValue={settings?.WeaviateEndpoint}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="password"
name="WeaviateApiKey"
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="sk-123Abcweaviate"
defaultValue={settings?.WeaviateApiKey}
autoComplete="off"
spellCheck={false}
/>
</div>
</>
)}
</div>
</div>
</form>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,11 @@
import React from "react";
import PasswordModal, {
usePasswordModal,
} from "../../components/Modals/Password";
import { FullScreenLoader } from "../../components/Preloader";
export default function Login() {
const { loading, mode } = usePasswordModal();
if (loading) return <FullScreenLoader />;
return <PasswordModal mode={mode} />;
}

View File

@ -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>
);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,109 @@
import React, { useState } from "react";
import { X } from "react-feather";
import LLMSelection from "./Steps/LLMSelection";
import VectorDatabaseConnection from "./Steps/VectorDatabaseConnection";
import AppearanceSetup from "./Steps/AppearanceSetup";
import UserModeSelection from "./Steps/UserModeSelection";
import PasswordProtection from "./Steps/PasswordProtection";
import MultiUserSetup from "./Steps/MultiUserSetup";
import CreateFirstWorkspace from "./Steps/CreateFirstWorkspace";
const DIALOG_ID = "onboarding-modal";
function hideModal() {
document.getElementById(DIALOG_ID)?.close();
}
const STEPS = {
1: {
title: "LLM Preference",
description:
"These are the credentials and settings for your preferred LLM chat & embedding provider.",
component: LLMSelection,
},
2: {
title: "Vector Database",
description:
"These are the credentials and settings for how your AnythingLLM instance will function.",
component: VectorDatabaseConnection,
},
3: {
title: "Appearance",
description: "Customize the appearance of your AnythingLLM instance.",
component: AppearanceSetup,
},
4: {
title: "User Mode Setup",
description: "Choose how many people will be using your instance.",
component: UserModeSelection,
},
5: {
title: "Password Protect",
description:
"Protect your instance with a password. It is important to save this password as it cannot be recovered.",
component: PasswordProtection,
},
6: {
title: "Multi-User Mode",
description:
"Setup your instance to support your team by activating multi-user mode.",
component: MultiUserSetup,
},
7: {
title: "Create Workspace",
description: "To get started, create a new workspace.",
component: CreateFirstWorkspace,
},
};
export const OnboardingModalId = DIALOG_ID;
export default function OnboardingModal() {
const [currentStep, setCurrentStep] = useState(1);
const nextStep = () => {
setCurrentStep((prevStep) => prevStep + 1);
};
const prevStep = () => {
if (currentStep === 1) return hideModal();
setCurrentStep((prevStep) => prevStep - 1);
};
const goToStep = (step) => {
setCurrentStep(step);
};
const { component: StepComponent, ...step } = STEPS[currentStep];
return (
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
<div className="relative max-h-full">
<div className="relative bg-main-gradient rounded-2xl shadow border-2 border-slate-300/10">
<div className="flex items-start justify-between p-8 border-b rounded-t border-gray-500/50">
<div className="flex flex-col gap-2">
<h3 className="text-xl font-semibold text-white">{step.title}</h3>
<p className="text-sm font-base text-white text-opacity-60">
{step.description || ""}
</p>
</div>
<button
onClick={hideModal}
type="button"
className="text-gray-400 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:border-white/60 bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<div className="space-y-6 flex h-full w-full justify-center">
<StepComponent
currentStep={currentStep}
nextStep={nextStep}
prevStep={prevStep}
goToStep={goToStep}
/>
</div>
</div>
</div>
</dialog>
);
}

View File

@ -0,0 +1,31 @@
import React from "react";
import OnboardingModal, { OnboardingModalId } from "./OnboardingModal";
import useLogo from "../../hooks/useLogo";
export default function OnboardingFlow() {
const { logo } = useLogo();
function showModal() {
document?.getElementById(OnboardingModalId)?.showModal();
}
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex items-center justify-center">
<div className="w-fit p-20 py-24 border-2 border-slate-300/10 rounded-2xl bg-main-gradient shadow-lg">
<div className="text-white text-2xl font-base text-center">
Welcome to
</div>
<img src={logo} alt="logo" className="w-80 mx-auto m-3 mb-11" />
<div className="flex justify-center items-center">
<button
className="border border-slate-200 px-5 py-2.5 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow animate-pulse"
onClick={showModal}
>
Get Started
</button>
</div>
</div>
<OnboardingModal />
</div>
);
}

View File

@ -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>

View File

@ -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";

View File

@ -0,0 +1,34 @@
export function formatDate(dateString) {
const date = isNaN(new Date(dateString).getTime())
? new Date()
: new Date(dateString);
const options = { year: "numeric", month: "short", day: "numeric" };
const formattedDate = date.toLocaleDateString("en-US", options);
return formattedDate;
}
export function getFileExtension(path) {
const match = path.match(/[^\/\\&\?]+\.\w{1,4}(?=([\?&].*$|$))/);
return match ? match[0].split(".").pop() : "file";
}
export function truncate(str, n) {
const fileExtensionPattern = /(\..+)$/;
const extensionMatch = str.match(fileExtensionPattern);
if (str.length <= n) return str;
if (extensionMatch && extensionMatch[1]) {
const extension = extensionMatch[1];
const nameWithoutExtension = str.replace(fileExtensionPattern, "");
const truncationPoint = Math.max(0, n - extension.length - 4);
const truncatedName =
nameWithoutExtension.substr(0, truncationPoint) +
"..." +
nameWithoutExtension.slice(-4);
return truncatedName + extension;
} else {
return str.length > n ? str.substr(0, n - 8) + "..." + str.slice(-4) : str;
}
}

View File

@ -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";
},
},
};

View File

@ -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: [],
}

View File

@ -447,6 +447,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@phosphor-icons/react@^2.0.13":
version "2.0.13"
resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.0.13.tgz#4944b08859d16a6efdbd1e073b5e0ef7e8f55cb9"
integrity sha512-lRjFfCv4pU8vDnPgZ8/QFzYmAJS08Vx+J2/+Ldh217pXaxvaayBZMC/3EinuMwmMylc97+XYCMPdH+y10I+f0g==
"@remix-run/router@1.6.3":
version "1.6.3"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.6.3.tgz"
@ -1642,6 +1647,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"

3
server/.gitignore vendored
View File

@ -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

View File

@ -32,7 +32,7 @@ const {
validFilename,
renameLogoFile,
removeCustomLogo,
DARK_LOGO_FILENAME,
LOGO_FILENAME,
} = require("../utils/files/logo");
const { Telemetry } = require("../models/telemetry");
const { WelcomeMessages } = require("../models/welcomeMessages");
@ -317,7 +317,7 @@ function systemEndpoints(app) {
updateENV(
{
AuthToken: "",
JWTSecret: process.env.JWT_SECRET ?? v4(),
JWTSecret: process.env.JWT_SECRET || v4(),
},
true
);
@ -325,12 +325,27 @@ function systemEndpoints(app) {
await Telemetry.sendTelemetry("enabled_multi_user_mode");
response.status(200).json({ success: !!user, error });
} catch (e) {
await User.delete({});
await SystemSettings.updateSettings({
multi_user_mode: false,
});
console.log(e.message, e);
response.sendStatus(500).end();
}
}
);
app.get("/system/multi-user-mode", async (request, response) => {
try {
const multiUserMode = await SystemSettings.isMultiUserMode();
response.status(200).json({ multiUserMode });
} catch (e) {
console.log(e.message, e);
response.sendStatus(500).end();
}
});
app.get("/system/data-export", [validatedRequest], async (_, response) => {
try {
const { filename, error } = await exportData();
@ -341,34 +356,32 @@ function systemEndpoints(app) {
}
});
app.get(
"/system/data-exports/:filename",
[validatedRequest],
(request, response) => {
const exportLocation = __dirname + "/../storage/exports/";
const sanitized = path
.normalize(request.params.filename)
.replace(/^(\.\.(\/|\\|$))+/, "");
const finalDestination = path.join(exportLocation, sanitized);
app.get("/system/data-exports/:filename", (request, response) => {
const exportLocation = __dirname + "/../storage/exports/";
const sanitized = path
.normalize(request.params.filename)
.replace(/^(\.\.(\/|\\|$))+/, "");
const finalDestination = path.join(exportLocation, sanitized);
if (!fs.existsSync(finalDestination)) {
response.status(404).json({
error: 404,
msg: `File ${request.params.filename} does not exist in exports.`,
});
return;
}
response.download(finalDestination, request.params.filename, (err) => {
if (err) {
response.send({
error: err,
msg: "Problem downloading the file",
});
}
if (!fs.existsSync(finalDestination)) {
response.status(404).json({
error: 404,
msg: `File ${request.params.filename} does not exist in exports.`,
});
return;
}
);
response.download(finalDestination, request.params.filename, (err) => {
if (err) {
response.send({
error: err,
msg: "Problem downloading the file",
});
}
// delete on download because endpoint is not authenticated.
fs.rmSync(finalDestination);
});
});
app.post(
"/system/data-import",
@ -380,9 +393,9 @@ function systemEndpoints(app) {
}
);
app.get("/system/logo/:mode?", async function (request, response) {
app.get("/system/logo", async function (request, response) {
try {
const defaultFilename = getDefaultFilename(request.params.mode);
const defaultFilename = getDefaultFilename();
const logoPath = await determineLogoFilepath(defaultFilename);
const { buffer, size, mime } = fetchLogo(logoPath);
response.writeHead(200, {
@ -443,6 +456,17 @@ function systemEndpoints(app) {
}
);
app.get("/system/is-default-logo", async (request, response) => {
try {
const currentLogoFilename = await SystemSettings.currentLogoFilename();
const isDefaultLogo = currentLogoFilename === LOGO_FILENAME;
response.status(200).json({ isDefaultLogo });
} catch (error) {
console.error("Error processing the logo request:", error);
response.status(500).json({ message: "Internal server error" });
}
});
app.get(
"/system/remove-logo",
[validatedRequest],
@ -458,7 +482,7 @@ function systemEndpoints(app) {
const currentLogoFilename = await SystemSettings.currentLogoFilename();
await removeCustomLogo(currentLogoFilename);
const { success, error } = await SystemSettings.updateSettings({
logo_filename: DARK_LOGO_FILENAME,
logo_filename: LOGO_FILENAME,
});
return response.status(success ? 200 : 500).json({
@ -546,15 +570,15 @@ function systemEndpoints(app) {
}
);
app.get("/system/api-key", [validatedRequest], async (_, response) => {
app.get("/system/api-keys", [validatedRequest], async (_, response) => {
try {
if (response.locals.multiUserMode) {
return response.sendStatus(401).end();
}
const apiKey = await ApiKey.get({});
const apiKeys = await ApiKey.where({});
return response.status(200).json({
apiKey,
apiKeys,
error: null,
});
} catch (error) {
@ -575,7 +599,6 @@ function systemEndpoints(app) {
return response.sendStatus(401).end();
}
await ApiKey.delete();
const { apiKey, error } = await ApiKey.create();
return response.status(200).json({
apiKey,

View File

@ -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);

View File

@ -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: {

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More