diff --git a/frontend/src/components/Modals/Settings/Keys/index.jsx b/frontend/src/components/Modals/Settings/Keys/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..77e1a2f4b6eb921b40fa11e959c0d5ea11dd90a0 --- /dev/null +++ b/frontend/src/components/Modals/Settings/Keys/index.jsx @@ -0,0 +1,280 @@ +import React, { useState, useEffect } from "react"; +import { AlertCircle, Loader, X } from "react-feather"; +import System from "../../../../models/system"; + +const noop = () => false; +export default function SystemKeys({ hideModal = noop }) { + const [loading, setLoading] = useState(true); + const [settings, setSettings] = useState({}); + + function validSettings(settings) { + return ( + settings?.OpenAiKey && + !!settings?.OpenAiModelPref && + !!settings?.VectorDB && + (settings?.VectorDB === "chroma" ? !!settings?.ChromaEndpoint : true) && + (settings?.VectorDB === "pinecone" + ? !!settings?.PineConeKey && + !!settings?.PineConeEnvironment && + !!settings?.PineConeIndex + : true) + ); + } + useEffect(() => { + async function fetchKeys() { + const settings = await System.keys(); + setSettings(settings); + setLoading(false); + } + fetchKeys(); + }, []); + + return ( + <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 px-6 py-4"> + <p className="text-gray-800 dark:text-stone-200 text-base "> + These are the credentials and settings for how your AnythingLLM + instance will function. Its important these keys are current and + correct. + </p> + </div> + <div className="p-6 space-y-6 flex h-full w-full"> + {loading ? ( + <div className="w-full h-full flex items-center justify-center"> + <p className="text-gray-800 dark:text-gray-200 text-base"> + loading system settings + </p> + </div> + ) : ( + <div className="w-full flex flex-col gap-y-4"> + {!validSettings(settings) && ( + <div className="bg-orange-300 p-4 rounded-lg border border-orange-600 text-orange-700 w-full items-center flex gap-x-2"> + <AlertCircle className="h-8 w-8" /> + <p className="text-sm md:text-base "> + Ensure all fields are green before attempting to use + AnythingLLM or it may not function as expected! + </p> + </div> + )} + <ShowKey + name="OpenAI API Key" + env="OpenAiKey" + value={settings?.OpenAiKey ? "*".repeat(20) : ""} + valid={settings?.OpenAiKey} + allowDebug={settings?.CanDebug} + /> + <ShowKey + name="OpenAI Model for chats" + env="OpenAiModelPref" + value={settings?.OpenAiModelPref} + valid={!!settings?.OpenAiModelPref} + allowDebug={settings?.CanDebug} + /> + <div className="h-[2px] w-full bg-gray-200 dark:bg-stone-600" /> + <ShowKey + name="Vector DB Choice" + env="VectorDB" + value={settings?.VectorDB} + valid={!!settings?.VectorDB} + allowDebug={settings?.CanDebug} + /> + {settings?.VectorDB === "pinecone" && ( + <> + <ShowKey + name="Pinecone DB API Key" + env="PineConeKey" + value={settings?.PineConeKey ? "*".repeat(20) : ""} + valid={!!settings?.PineConeKey} + allowDebug={settings?.CanDebug} + /> + <ShowKey + name="Pinecone DB Environment" + env="PineConeEnvironment" + value={settings?.PineConeEnvironment} + valid={!!settings?.PineConeEnvironment} + allowDebug={settings?.CanDebug} + /> + <ShowKey + name="Pinecone DB Index" + env="PineConeIndex" + value={settings?.PineConeIndex} + valid={!!settings?.PineConeIndex} + allowDebug={settings?.CanDebug} + /> + </> + )} + {settings?.VectorDB === "chroma" && ( + <> + <ShowKey + name="Chroma Endpoint" + env="ChromaEndpoint" + value={settings?.ChromaEndpoint} + valid={!!settings?.ChromaEndpoint} + allowDebug={settings?.CanDebug} + /> + </> + )} + </div> + )} + </div> + <div className="flex 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-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" + > + Close + </button> + </div> + </div> + </div> + ); +} + +function ShowKey({ name, env, value, valid, allowDebug = true }) { + const [isValid, setIsValid] = useState(valid); + const [debug, setDebug] = useState(false); + const [saving, setSaving] = useState(false); + 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 { newValues, error } = await System.updateSystem(data); + if (!!error) { + alert(error); + setSaving(false); + setIsValid(false); + return; + } + + setSaving(false); + setDebug(false); + setIsValid(true); + }; + + if (!isValid) { + return ( + <form onSubmit={handleSubmit}> + <div> + <label + htmlFor="error" + className="block mb-2 text-sm font-medium text-red-700 dark:text-red-500" + > + {name} + </label> + <input + type="text" + id="error" + name={env} + disabled={!debug} + className="bg-red-50 border border-red-500 text-red-900 placeholder-red-700 text-sm rounded-lg focus:ring-red-500 dark:bg-gray-700 focus:border-red-500 block w-full p-2.5 dark:text-red-500 dark:placeholder-red-500 dark:border-red-500" + placeholder={name} + defaultValue={value} + required={true} + autoComplete="off" + /> + <div className="flex items-center justify-between"> + <p className="mt-2 text-sm text-red-600 dark:text-red-500"> + Need setup in .env file. + </p> + {allowDebug && ( + <> + {debug ? ( + <div className="flex items-center gap-x-2 mt-2"> + {saving ? ( + <> + <Loader className="animate-spin h-4 w-4 text-slate-300 dark:text-slate-500" /> + </> + ) : ( + <> + <button + type="button" + onClick={() => setDebug(false)} + className="text-xs text-slate-300 dark:text-slate-500" + > + Cancel + </button> + <button + type="submit" + className="text-xs text-blue-300 dark:text-blue-500" + > + Save + </button> + </> + )} + </div> + ) : ( + <button + type="button" + onClick={() => setDebug(true)} + className="mt-2 text-xs text-slate-300 dark:text-slate-500" + > + Debug + </button> + )} + </> + )} + </div> + </div> + </form> + ); + } + + return ( + <form onSubmit={handleSubmit}> + <div className="mb-6"> + <label + htmlFor="success" + className="block mb-2 text-sm font-medium text-gray-800 dark:text-slate-200" + > + {name} + </label> + <input + type="text" + id="success" + name={env} + disabled={!debug} + className="border border-white text-green-900 dark:text-green-400 placeholder-green-700 dark:placeholder-green-500 text-sm rounded-lg focus:ring-green-500 focus:border-green-500 block w-full p-2.5 dark:bg-gray-700 dark:border-green-500" + defaultValue={value} + required={true} + autoComplete="off" + /> + {allowDebug && ( + <div className="flex items-center justify-end"> + {debug ? ( + <div className="flex items-center gap-x-2 mt-2"> + {saving ? ( + <> + <Loader className="animate-spin h-4 w-4 text-slate-300 dark:text-slate-500" /> + </> + ) : ( + <> + <button + onClick={() => setDebug(false)} + className="text-xs text-slate-300 dark:text-slate-500" + > + Cancel + </button> + <button className="text-xs text-blue-300 dark:text-blue-500"> + Save + </button> + </> + )} + </div> + ) : ( + <button + onClick={() => setDebug(true)} + className="mt-2 text-xs text-slate-300 dark:text-slate-500" + > + Debug + </button> + )} + </div> + )} + </div> + </form> + ); +} diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d7623c1138d17285030ef8af449bea2e78393799 --- /dev/null +++ b/frontend/src/components/Modals/Settings/index.jsx @@ -0,0 +1,97 @@ +import React, { useState } from "react"; +import { Key, X } from "react-feather"; +import SystemKeys from "./Keys"; + +const TABS = { + keys: SystemKeys, +}; + +const noop = () => false; +export default function SystemSettingsModal({ hideModal = noop }) { + const [selectedTab, setSelectedTab] = useState("keys"); + const Component = TABS[selectedTab || "keys"]; + + 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"> + System Settings + </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" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <SettingTabs selectedTab={selectedTab} changeTab={setSelectedTab} /> + </div> + <Component hideModal={hideModal} /> + </div> + </div> + </div> + ); +} + +function SettingTabs({ 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"> + <SettingTab + active={selectedTab === "keys"} + displayName="Keys" + tabName="keys" + icon={<Key className="h-4 w-4 flex-shrink-0" />} + onClick={changeTab} + /> + </ul> + </div> + ); +} + +function SettingTab({ + 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 function useSystemSettingsModal() { + const [showing, setShowing] = useState(false); + const showModal = () => { + setShowing(true); + }; + const hideModal = () => { + setShowing(false); + }; + + return { showing, showModal, hideModal }; +} diff --git a/frontend/src/components/Sidebar/index.jsx b/frontend/src/components/Sidebar/index.jsx index fc50fe045be99b9cba28901caa0bc9d3a84bf0cf..880e54fde66810843c34f6328d0146cddb912025 100644 --- a/frontend/src/components/Sidebar/index.jsx +++ b/frontend/src/components/Sidebar/index.jsx @@ -7,10 +7,13 @@ import { Key, Menu, Plus, + Tool, } from "react-feather"; import IndexCount from "./IndexCount"; import LLMStatus from "./LLMStatus"; -import KeysModal, { useKeysModal } from "../Modals/Keys"; +import SystemSettingsModal, { + useSystemSettingsModal, +} from "../Modals/Settings"; import NewWorkspaceModal, { useNewWorkspaceModal, } from "../Modals/NewWorkspace"; @@ -20,10 +23,10 @@ import paths from "../../utils/paths"; export default function Sidebar() { const sidebarRef = useRef(null); const { - showing: showingKeyModal, - showModal: showKeyModal, - hideModal: hideKeyModal, - } = useKeysModal(); + showing: showingSystemSettingsModal, + showModal: showSystemSettingsModal, + hideModal: hideSystemSettingsModal, + } = useSystemSettingsModal(); const { showing: showingNewWsModal, showModal: showNewWsModal, @@ -45,10 +48,10 @@ export default function Sidebar() { </p> <div className="flex gap-x-2 items-center text-slate-500"> <button - onClick={showKeyModal} + onClick={showSystemSettingsModal} 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" > - <Key className="h-4 w-4 " /> + <Tool className="h-4 w-4 " /> </button> </div> </div> @@ -126,7 +129,9 @@ export default function Sidebar() { </div> </div> </div> - {showingKeyModal && <KeysModal hideModal={hideKeyModal} />} + {showingSystemSettingsModal && ( + <SystemSettingsModal hideModal={hideSystemSettingsModal} /> + )} {showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />} </> ); @@ -137,10 +142,10 @@ export function SidebarMobileHeader() { const [showBgOverlay, setShowBgOverlay] = useState(false); const sidebarRef = useRef(null); const { - showing: showingKeyModal, - showModal: showKeyModal, - hideModal: hideKeyModal, - } = useKeysModal(); + showing: showingSystemSettingsModal, + showModal: showSystemSettingsModal, + hideModal: hideSystemSettingsModal, + } = useSystemSettingsModal(); const { showing: showingNewWsModal, showModal: showNewWsModal, @@ -199,10 +204,10 @@ export function SidebarMobileHeader() { </p> <div className="flex gap-x-2 items-center text-slate-500"> <button - onClick={showKeyModal} + onClick={showSystemSettingsModal} 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" > - <Key className="h-4 w-4 " /> + <Tool className="h-4 w-4 " /> </button> </div> </div> @@ -283,7 +288,9 @@ export function SidebarMobileHeader() { </div> </div> </div> - {showingKeyModal && <KeysModal hideModal={hideKeyModal} />} + {showingSystemSettingsModal && ( + <SystemSettingsModal hideModal={hideSystemSettingsModal} /> + )} {showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />} </div> </>