[FEAT] Settings pages UI updates ()

* WIP main sidebar designs

* hover states and active states for main sidebar

* main and settings sidebar UI updates & improve performance using Link instead of <a>

* update borders to match rest of UI in all pages

* update borders of all containers to match rest of UI

* remove unneeded conditional

* custom messages component redesign and appearance settings layout changes

* improve UX of custom logo file uploader component to match designs

* fix sizing on custom logo upload field

* WIP footer customization new UI

* implement new UI for custom footer
icon selection

* update workspace chats to match new settings UI

* update system preferences to match new settings UI

* update export workspace chats button border

* update invitations settings page to match new settings UI

* update instance workspaces settings page to match new settings UI

* update instance workspaces to match new settings UI

* update api keys settings to match new settings UI

* update LLM preferences settings to match new settings UI

* update embedding preferences settings to match new settings UI

* update vector db preferences settings to match new settings UI

* align all buttons in settings pages

* update ui for data connectors to match rest of settings ui

* update UI for embed chat

* updated ui for logging page

* fix duplicate attributes left from merge conflicts

* fix dynamic class to use ternary

* remove transition classes where it is not needed
This commit is contained in:
Sean Hatfield 2024-03-12 10:45:03 -07:00 committed by GitHub
parent 0f31e43fd4
commit d9fce5f65e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 569 additions and 555 deletions
frontend/src
components
EditingChatBubble
SettingsSidebar
Sidebar
ActiveWorkspaces
index.jsx
pages
Admin
Invitations
Logging
System
Users
Workspaces
GeneralSettings
ApiKeys
Appearance
CustomLogo
CustomMessages
FooterCustomization
SupportEmail
index.jsx
Chats
DataConnectors
Connectors
Github
Youtube
index.jsx
EmbedChats
EmbedConfigs
EmbeddingPreference
LLMPreference
VectorDatabase

View file

@ -13,49 +13,52 @@ export default function EditingChatBubble({
const isUser = type === "user";
return (
<div
className={`relative flex w-full mt-2 items-start ${
isUser ? "justify-end" : "justify-start"
}`}
>
<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>
<p className={`text-xs text-[#D3D4D4] ${isUser ? "text-right" : ""}`}>
{isUser ? "User" : "AnythingLLM Chat Assistant"}
</p>
<div
className={`p-4 max-w-full md:w-[290px] ${
isUser ? "bg-sky-400 text-black" : "bg-white text-black"
} ${
isUser
? "rounded-tr-[40px] rounded-tl-[40px] rounded-bl-[40px]"
: "rounded-br-[40px] rounded-tl-[40px] rounded-tr-[40px]"
}
className={`relative flex w-full mt-2 items-start ${
isUser ? "justify-end" : "justify-start"
}`}
onDoubleClick={() => setIsEditing(true)}
>
{isEditing ? (
<input
value={tempMessage}
onChange={(e) => setTempMessage(e.target.value)}
onBlur={() => {
handleMessageChange(index, type, tempMessage);
setIsEditing(false);
}}
autoFocus
className="w-full"
/>
) : (
tempMessage && (
<p className="text-black font-[500] md:font-semibold text-sm md:text-base break-words">
{tempMessage}
</p>
)
)}
<button
className={`transition-all duration-300 absolute z-10 text-white rounded-full hover:bg-neutral-700 hover:border-white border-transparent border shadow-lg ${
isUser ? "right-0 mr-2" : "ml-2"
}`}
style={{ top: "6px", [isUser ? "right" : "left"]: "290px" }}
onClick={() => removeMessage(index)}
>
<X className="m-0.5" size={20} />
</button>
<div
className={`p-2 max-w-full md:w-[290px] text-black rounded-[8px] ${
isUser ? "bg-[#41444C] text-white" : "bg-[#2E3036] text-white"
}
}`}
onDoubleClick={() => setIsEditing(true)}
>
{isEditing ? (
<input
value={tempMessage}
onChange={(e) => setTempMessage(e.target.value)}
onBlur={() => {
handleMessageChange(index, type, tempMessage);
setIsEditing(false);
}}
autoFocus
className={`w-full ${
isUser ? "bg-[#41444C] text-white" : "bg-[#2E3036] text-white"
}`}
/>
) : (
tempMessage && (
<p className=" font-[500] md:font-semibold text-sm md:text-base break-words">
{tempMessage}
</p>
)
)}
</div>
</div>
</div>
);

View file

@ -149,7 +149,9 @@ export default function SettingsSidebar() {
<SidebarOptions user={user} />
</div>
</div>
<Footer />
<div className="mb-2">
<Footer />
</div>
</div>
</div>
</div>

View file

@ -92,11 +92,11 @@ export default function ActiveWorkspaces() {
className={`
transition-all duration-[200ms]
flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-[4px] text-white justify-start items-center
hover:bg-workspace-item-selected-gradient border-outline
hover:bg-workspace-item-selected-gradient hover:font-bold border-2 border-outline
${
isActive
? "bg-workspace-item-selected-gradient font-medium border-none"
: "border-[1px]"
? "bg-workspace-item-selected-gradient font-bold"
: ""
}`}
>
<div className="flex flex-row justify-between w-full">

View file

@ -38,10 +38,10 @@ export default function Sidebar() {
<div
ref={sidebarRef}
style={{ height: "calc(100% - 76px)" }}
className="transition-all pt-[11px] px-[10px] duration-500 relative m-[16px] rounded-[16px] bg-sidebar border-2 border-outline min-w-[250px]"
className="relative m-[16px] rounded-[16px] bg-sidebar border-2 border-outline min-w-[250px] p-[10px]"
>
<div className="flex flex-col h-full overflow-x-hidden">
<div className="flex-grow flex flex-col w-[235px]">
<div className="flex-grow flex flex-col min-w-[235px]">
<div className="flex flex-col gap-y-2 pb-8 overflow-y-scroll no-scroll">
<div className="flex gap-x-2 items-center justify-between">
{(!user || user?.role !== "default") && (
@ -144,9 +144,11 @@ export function SidebarMobileHeader() {
style={{ objectFit: "contain" }}
/>
</div>
<div className="flex gap-x-2 items-center text-slate-500 shrink-0">
<SettingsButton />
</div>
{(!user || user?.role !== "default") && (
<div className="flex gap-x-2 items-center text-slate-500 shink-0">
<SettingsButton />
</div>
)}
</div>
{/* Primary Body */}

View file

@ -13,25 +13,29 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminInvites() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">Invitations</p>
<p className="text-lg leading-6 font-bold text-white">
Invitations
</p>
<button
onClick={openModal}
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"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<EnvelopeSimple className="h-4 w-4" /> Create Invite Link
<EnvelopeSimple className="h-4 w-4" />
Create Invite Link
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] 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>
@ -50,6 +54,7 @@ function InvitationsContainer() {
const darkMode = usePrefersDarkMode();
const [loading, setLoading] = useState(true);
const [invites, setInvites] = useState([]);
useEffect(() => {
async function fetchInvites() {
const _invites = await Admin.invites();
@ -74,13 +79,13 @@ function InvitationsContainer() {
}
return (
<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">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] 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">
Status
</th>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
<th scope="col" className="px-6 py-3">
Accepted By
</th>
<th scope="col" className="px-6 py-3">

View file

@ -30,20 +30,22 @@ export default function AdminLogs() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">Event Logs</p>
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Event Logs
</p>
<button
onClick={handleResetLogs}
className="px-4 py-1 rounded-lg text-slate-200/50 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
Clear event logs
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
View all actions and events happening on this instance for
monitoring.
</p>
@ -95,10 +97,10 @@ function LogsContainer() {
return (
<>
<table className="md:w-5/6 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">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] 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">
Event Type
</th>
<th scope="col" className="px-6 py-3">
@ -116,7 +118,7 @@ function LogsContainer() {
{!!logs && logs.map((log) => <LogRow key={log.id} log={log} />)}
</tbody>
</table>
<div className="flex w-full justify-between items-center">
<div className="flex w-full justify-between items-center mt-6">
<button
onClick={handlePrevious}
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"

View file

@ -12,6 +12,7 @@ export default function AdminSystem() {
enabled: false,
limit: 10,
});
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
@ -43,46 +44,35 @@ export default function AdminSystem() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form
onSubmit={handleSubmit}
onChange={() => setHasChanges(true)}
className="flex w-full"
className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16"
>
<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">
System Preferences
</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 overall settings and configurations of your
instance.
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
System Preferences
</p>
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the overall settings and configurations of your
instance.
</p>
</div>
<div className="my-5">
<div className="flex flex-col gap-y-2 mb-2.5">
<label className="leading-tight font-semibold text-white">
Users can delete workspaces
</label>
<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>
<label className="relative inline-flex cursor-pointer items-center">
<div className="mt-6 mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Users can delete workspaces
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Allow non-admin users to delete workspaces that they are a part
of. This would delete the workspace for everyone.
</p>
<label className="relative inline-flex cursor-pointer items-center mt-2">
<input
type="checkbox"
name="users_can_delete_workspaces"
@ -94,42 +84,44 @@ export default function AdminSystem() {
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
</div>
</div>
<div className="my-4">
<div className="flex flex-col gap-y-2 mb-2.5">
<label className="leading-tight font-medium text-black dark:text-white">
Limit messages per user per day
<div className="mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Limit messages per user per day
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
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.
</p>
<div className="mt-2">
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
name="limit_user_messages"
value="yes"
checked={messageLimit.enabled}
onChange={(e) => {
setMessageLimit({
...messageLimit,
enabled: e.target.checked,
});
}}
className="peer sr-only"
/>
<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>
<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.
</p>
</div>
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
name="limit_user_messages"
value="yes"
checked={messageLimit.enabled}
onChange={(e) => {
setMessageLimit({
...messageLimit,
enabled: e.target.checked,
});
}}
className="peer sr-only"
/>
<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>
{messageLimit.enabled && (
<div className="mb-4">
<label className=" block flex items-center gap-x-1 font-medium text-black dark:text-white">
<div className="mt-4">
<label className="block text-sm font-medium text-white">
Message limit per day
</label>
<div className="relative">
<div className="relative mt-2">
<input
type="number"
name="message_limit"
@ -143,12 +135,24 @@ export default function AdminSystem() {
value={messageLimit.limit}
min={1}
max={300}
className="w-1/3 my-2 rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-gray-800 dark:text-slate-200 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
className="w-1/3 rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-gray-800 dark:text-slate-200 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
</div>
</div>
)}
</div>
{hasChanges && (
<div className="flex justify-start">
<button
type="submit"
disabled={saving}
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
</div>
)}
</form>
</div>
</div>

View file

@ -13,25 +13,26 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminUsers() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">Users</p>
<p className="text-lg leading-6 font-bold text-white">Users</p>
<button
onClick={openModal}
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"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<UserPlus className="h-4 w-4" /> Add user
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] 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.
@ -51,6 +52,7 @@ function UsersContainer() {
const { user: currUser } = useUser();
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const _users = await Admin.users();
@ -75,8 +77,8 @@ function UsersContainer() {
}
return (
<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">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Username
@ -120,7 +122,7 @@ const ROLE_HINT = {
export function RoleHintDisplay({ role }) {
return (
<div className="flex flex-col gap-y-1 py-1 pb-4">
<p className="text-white/60 font-semibold text-sm">Permissions</p>
<p className="text-sm font-medium text-white">Permissions</p>
<ul className="flex flex-col gap-y-1 list-disc px-4">
{ROLE_HINT[role ?? "default"].map((hints, i) => {
return (

View file

@ -13,27 +13,28 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminWorkspaces() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">
Instance workspaces
<p className="text-lg leading-6 font-bold text-white">
Instance Workspaces
</p>
<button
onClick={openModal}
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"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<BookOpen className="h-4 w-4" /> New Workspace
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] 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>
@ -80,8 +81,8 @@ function WorkspacesContainer() {
}
return (
<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">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Name

View file

@ -15,25 +15,26 @@ import { useModal } from "@/hooks/useModal";
export default function AdminApiKeys() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">API Keys</p>
<p className="text-lg leading-6 font-bold text-white">API Keys</p>
<button
onClick={openModal}
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"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<PlusCircle className="h-4 w-4" /> Generate New API Key
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
API keys allow the holder to programmatically access and manage
this AnythingLLM instance.
</p>
@ -41,7 +42,7 @@ export default function AdminApiKeys() {
href={paths.apiDocs()}
target="_blank"
rel="noreferrer"
className="text-sm font-base text-blue-300 hover:underline"
className="text-xs leading-[18px] font-base text-blue-300 hover:underline"
>
Read the API documentation &rarr;
</a>
@ -59,11 +60,11 @@ export default function AdminApiKeys() {
function ApiKeysContainer() {
const [loading, setLoading] = useState(true);
const [apiKeys, setApiKeys] = useState([]);
useEffect(() => {
async function fetchExistingKeys() {
const user = userFromStorage();
const Model = !!user ? Admin : System;
const { apiKeys: foundKeys } = await Model.getApiKeys();
setApiKeys(foundKeys);
setLoading(false);
@ -86,8 +87,8 @@ function ApiKeysContainer() {
}
return (
<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">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
API Key

View file

@ -1,7 +1,7 @@
import useLogo from "@/hooks/useLogo";
import System from "@/models/system";
import showToast from "@/utils/toast";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import AnythingLLM from "@/media/logo/anything-llm.png";
import { Plus } from "@phosphor-icons/react";
@ -9,6 +9,7 @@ export default function CustomLogo() {
const { logo: _initLogo, setLogo: _setLogo } = useLogo();
const [logo, setLogo] = useState("");
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
const fileInputRef = useRef(null);
useEffect(() => {
async function logoInit() {
@ -62,61 +63,88 @@ export default function CustomLogo() {
showToast("Image successfully removed.", "success");
};
const triggerFileInputClick = () => {
fileInputRef.current?.click();
};
return (
<div className="my-6">
<div className="flex flex-col gap-y-2">
<h2 className="leading-tight font-medium text-white">Custom Logo</h2>
<p className="text-sm font-base text-white/60">
<div className="mt-6 mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Custom Logo
</h2>
<p className="text-xs leading-[18px] 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 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"
{isDefaultLogo ? (
<div className="flex md:flex-row flex-col items-center">
<div className="flex flex-row gap-x-8">
<label
className="mt-3 transition-all duration-300 hover:opacity-60"
hidden={!isDefaultLogo}
>
<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
<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>
</div>
</label>
{!isDefaultLogo && (
<button
onClick={handleRemoveLogo}
className="text-white text-base font-medium hover:text-opacity-60"
>
Delete
</button>
)}
</label>
</div>
</div>
</div>
) : (
<div className="flex md:flex-row flex-col items-center relative">
<div className="group w-80 h-[130px] mt-3 overflow-hidden">
<img
src={logo}
alt="Uploaded Logo"
className="w-full h-full object-cover border-2 border-white/20 border-dashed p-1 rounded-2xl"
/>
<div className="absolute w-80 top-0 left-0 right-0 bottom-0 flex flex-col gap-y-3 justify-center items-center rounded-2xl mt-3 bg-black bg-opacity-80 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-in-out border-2 border-transparent hover:border-white">
<button
onClick={triggerFileInputClick}
className="text-white text-base font-medium hover:text-opacity-60 mx-2"
>
Replace
</button>
<input
id="logo-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
ref={fileInputRef}
/>
<button
onClick={handleRemoveLogo}
className="text-white text-base font-medium hover:text-opacity-60 mx-2"
>
Remove
</button>
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -53,16 +53,16 @@ export default function CustomMessages() {
};
return (
<div className="mb-6">
<div className="flex flex-col gap-y-2">
<h2 className="leading-tight font-medium text-white">
<div className="mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Custom Messages
</h2>
<p className="text-sm font-base text-white/60">
<p className="text-xs leading-[18px] 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 bg-zinc-900 rounded-lg px-6 pt-4 max-w-[700px]">
<div className="mt-3 flex flex-col gap-y-6 bg-[#1C1E21] rounded-lg pr-[31px] pl-[12px] pt-4 max-w-[700px]">
{messages.map((message, index) => (
<div key={index} className="flex flex-col gap-y-2">
{message.user && (
@ -85,27 +85,34 @@ export default function CustomMessages() {
)}
</div>
))}
<div className="flex gap-4 mt-12 justify-between pb-7">
<div className="flex gap-4 mt-12 justify-between pb-[15px]">
<button
className="self-end text-white hover:text-white/60 transition"
onClick={() => addMessage("response")}
>
<div className="flex items-center justify-start">
<Plus className="w-5 h-5 m-2" weight="fill" /> New System Message
<div className="flex items-center justify-start text-sm font-normal -ml-2">
<Plus className="m-2" size={16} weight="bold" />
<span className="leading-5">
New <span className="font-bold italic mr-1">system</span>{" "}
message
</span>
</div>
</button>
<button
className="self-end text-sky-400 hover:text-sky-400/60 transition"
className="self-end text-white hover:text-white/60 transition"
onClick={() => addMessage("user")}
>
<div className="flex items-center">
<Plus className="w-5 h-5 m-2" weight="fill" /> New User Message
<div className="flex items-center justify-start text-sm font-normal">
<Plus className="m-2" size={16} weight="bold" />
<span className="leading-5">
New <span className="font-bold italic mr-1">user</span> message
</span>
</div>
</button>
</div>
</div>
{hasChanges && (
<div className="flex justify-center py-6">
<div className="flex justify-start pt-6">
<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"
onClick={handleMessageSave}

View file

@ -1,11 +1,20 @@
import { ICON_COMPONENTS } from "@/components/Footer";
import React, { useEffect, useRef, useState } from "react";
import { Plus, X } from "@phosphor-icons/react";
export default function NewIconForm({ handleSubmit, showing }) {
const [selectedIcon, setSelectedIcon] = useState("Info");
export default function NewIconForm({ icon, url, onSave, onRemove }) {
const [selectedIcon, setSelectedIcon] = useState(icon || "Plus");
const [selectedUrl, setSelectedUrl] = useState(url || "");
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isEdited, setIsEdited] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
setSelectedIcon(icon || "Plus");
setSelectedUrl(url || "");
setIsEdited(false);
}, [icon, url]);
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
@ -17,82 +26,90 @@ export default function NewIconForm({ handleSubmit, showing }) {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [dropdownRef]);
if (!showing) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (selectedIcon !== "Plus" && selectedUrl) {
onSave(selectedIcon, selectedUrl);
setIsEdited(false);
}
};
const handleRemove = () => {
onRemove();
setSelectedIcon("Plus");
setSelectedUrl("");
setIsEdited(false);
};
const handleIconChange = (iconName) => {
setSelectedIcon(iconName);
setIsDropdownOpen(false);
setIsEdited(true);
};
const handleUrlChange = (e) => {
setSelectedUrl(e.target.value);
setIsEdited(true);
};
return (
<form onSubmit={handleSubmit} className="flex justify-start">
<div className="mt-6 mb-6 flex flex-col bg-zinc-900 rounded-lg px-6 py-4">
<div className="flex gap-x-4 items-center">
<div
className="relative flex flex-col items-center gap-y-4"
ref={dropdownRef}
>
<input type="hidden" name="icon" value={selectedIcon} />
<label className="text-sm font-medium text-white">Icon</label>
<form onSubmit={handleSubmit} className="flex items-center gap-x-1.5">
<div className="relative" ref={dropdownRef}>
<div
className="h-[34px] w-[34px] bg-[#1C1E21] rounded-full flex items-center justify-center cursor-pointer"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
{React.createElement(ICON_COMPONENTS[selectedIcon] || Plus, {
className: "h-5 w-5 text-white",
weight: selectedIcon === "Plus" ? "bold" : "fill",
})}
</div>
{isDropdownOpen && (
<div className="absolute z-10 grid grid-cols-4 bg-[#41444C] mt-2 rounded-md w-[150px] h-[78px] overflow-y-auto border border-white/20 shadow-lg">
{Object.keys(ICON_COMPONENTS).map((iconName) => (
<button
key={iconName}
type="button"
className="flex justify-center items-center border border-transparent hover:bg-[#1C1E21] hover:border-slate-100 rounded-full p-2"
onClick={() => handleIconChange(iconName)}
>
{React.createElement(ICON_COMPONENTS[iconName], {
className: "h-5 w-5 text-white",
weight: "fill",
})}
</button>
))}
</div>
)}
</div>
<input
type="url"
value={selectedUrl}
onChange={handleUrlChange}
placeholder="https://example.com"
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-[300px] h-[32px]"
required
/>
{selectedIcon !== "Plus" && (
<>
{isEdited ? (
<button
type="submit"
className="text-sky-400 px-2 py-2 rounded-md text-sm font-bold hover:text-sky-500"
>
Save
</button>
) : (
<button
type="button"
className={`${
isDropdownOpen
? "bg-menu-item-selected-gradient border-slate-100/50"
: ""
}border-transparent 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`}
onClick={(e) => {
e.preventDefault();
setIsDropdownOpen(!isDropdownOpen);
}}
onClick={handleRemove}
className="hover:text-red-500 text-white/80 px-2 py-2 rounded-md text-sm font-bold"
>
{React.createElement(ICON_COMPONENTS[selectedIcon], {
className: "h-5 w-5 text-white",
weight: "fill",
})}
<X size={20} />
</button>
{isDropdownOpen && (
<div className="absolute z-10 grid grid-cols-4 gap-4 bg-zinc-800 -mt-20 ml-44 p-1 rounded-md w-56 h-28 overflow-y-auto border border-slate-100/10">
{Object.keys(ICON_COMPONENTS).map((iconName) => (
<button
key={iconName}
type="button"
className="flex justify-center items-center border border-transparent hover:bg-menu-item-selected-gradient hover:border-slate-100 rounded-full"
onClick={() => {
setSelectedIcon(iconName);
setIsDropdownOpen(false);
}}
>
{React.createElement(ICON_COMPONENTS[iconName], {
className: "h-5 w-5 text-white m-2.5",
weight: "fill",
})}
</button>
))}
</div>
)}
</div>
<div className="flex flex-col gap-y-4">
<label className="text-sm font-medium text-white">Link</label>
<input
type="url"
name="url"
required={true}
placeholder="https://example.com"
className="bg-sidebar text-white placeholder:text-white/20 rounded-md p-2"
/>
</div>
{selectedIcon !== "" && (
<div className="flex flex-col gap-y-4">
<label className="text-sm font-medium text-white invisible">
Submit
</label>
<div className="flex justify-center">
<button
type="submit"
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"
>
Save
</button>
</div>
</div>
)}
</div>
</div>
</>
)}
</form>
);
}

View file

@ -1,36 +1,37 @@
import React, { useState, useEffect } from "react";
import showToast from "@/utils/toast";
import { Plus, X } from "@phosphor-icons/react";
import { ICON_COMPONENTS, MAX_ICONS } from "@/components/Footer";
import { safeJsonParse } from "@/utils/request";
import NewIconForm from "./NewIconForm";
import Admin from "@/models/admin";
import System from "@/models/system";
export default function FooterCustomization() {
const [loading, setLoading] = useState(true);
const [footerIcons, setFooterIcons] = useState([]);
const [showForm, setShowForm] = useState(false);
const [footerIcons, setFooterIcons] = useState(Array(3).fill(null));
useEffect(() => {
async function fetchFooterIcons() {
const settings = (await Admin.systemPreferences())?.settings;
if (settings && settings.footer_data) {
setFooterIcons(safeJsonParse(settings.footer_data, []));
const parsedIcons = safeJsonParse(settings.footer_data, []);
setFooterIcons((prevIcons) => {
const updatedIcons = [...prevIcons];
parsedIcons.forEach((icon, index) => {
updatedIcons[index] = icon;
});
return updatedIcons;
});
}
setLoading(false);
}
fetchFooterIcons();
}, []);
const removeFooterIcon = async (index) => {
const updatedIcons = footerIcons.filter((_, i) => i !== index);
const updateFooterIcons = async (updatedIcons) => {
const { success, error } = await Admin.updateSystemPreferences({
footer_data: JSON.stringify(updatedIcons),
footer_data: JSON.stringify(updatedIcons.filter((icon) => icon !== null)),
});
if (!success) {
showToast(`Failed to remove footer icon - ${error}`, "error", {
showToast(`Failed to update footer icons - ${error}`, "error", {
clear: true,
});
return;
@ -38,103 +39,44 @@ export default function FooterCustomization() {
window.localStorage.removeItem(System.cacheKeys.footerIcons);
setFooterIcons(updatedIcons);
showToast("Successfully removed footer icon.", "success", { clear: true });
showToast("Successfully updated footer icons.", "success", { clear: true });
};
const onSubmit = async (e) => {
e.preventDefault();
const form = new FormData(e.target);
const icon = form.get("icon");
const url = form.get("url");
const newIcon = { icon, url };
setFooterIcons([...footerIcons, newIcon]);
const { success, error } = await Admin.updateSystemPreferences({
footer_data: JSON.stringify([...footerIcons, newIcon]),
});
if (!success) {
showToast(`Failed to add footer icon - ${error}`, "error", {
clear: true,
});
return;
}
window.localStorage.removeItem(System.cacheKeys.footerIcons);
setShowForm(false);
showToast("Successfully added footer icon.", "success", { clear: true });
const handleRemoveIcon = (index) => {
const updatedIcons = [...footerIcons];
updatedIcons[index] = null;
updateFooterIcons(updatedIcons);
};
return (
<div className="mb-6">
<div className="flex flex-col gap-y-2">
<h2 className="leading-tight font-medium text-white">
<div className="mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Custom Footer Icons
</h2>
<p className="text-sm font-base text-white/60">
<p className="text-xs leading-[18px] font-base text-white/60">
Customize the footer icons displayed on the bottom of the sidebar.
</p>
</div>
<CurrentIcons footerIcons={footerIcons} remove={removeFooterIcon} />
<NewIconForm
handleSubmit={onSubmit}
showing={footerIcons.length < MAX_ICONS && showForm}
/>
<div hidden={!(!showForm && footerIcons.length < MAX_ICONS) || loading}>
<div className="flex gap-2 mt-6">
<button
onClick={() => setShowForm(true)}
className="flex gap-x-2 items-center justify-center text-white text-sm hover:text-sky-400 transition-all duration-300"
>
Add new footer icon
<Plus className="" size={24} weight="fill" />
</button>
</div>
<div className="mt-3 flex gap-x-3 font-bold text-white text-sm">
<div>Icon</div>
<div>Link</div>
</div>
<div className="mt-2 flex flex-col gap-y-[10px]">
{footerIcons.map((icon, index) => (
<NewIconForm
key={index}
icon={icon?.icon}
url={icon?.url}
onSave={(newIcon, newUrl) => {
const updatedIcons = [...footerIcons];
updatedIcons[index] = { icon: newIcon, url: newUrl };
updateFooterIcons(updatedIcons);
}}
onRemove={() => handleRemoveIcon(index)}
/>
))}
</div>
</div>
);
}
function CurrentIcons({ footerIcons, remove }) {
if (footerIcons.length === 0) return null;
return (
<div className="flex flex-col w-fit gap-y-2 mt-4">
{footerIcons.map((icon, index) => (
<div
key={index}
className="flex items-center justify-between bg-zinc-900 p-2 rounded-md gap-x-4"
>
<div className="flex items-center gap-x-2">
<IconPreview symbol={icon.icon} disabled={true} />
<span className="text-white/60">{icon.url}</span>
</div>
<button
type="button"
className="transition-all duration-300 text-neutral-700 bg-transparent rounded-full hover:bg-zinc-600 hover:border-zinc-600 hover:text-white border-transparent border shadow-lg mr-2"
onClick={() => remove(index)}
>
<X className="m-[1px]" size={20} />
</button>
</div>
))}
</div>
);
}
const IconPreview = ({ symbol, disabled = false }) => {
const IconComponent = ICON_COMPONENTS.hasOwnProperty(symbol)
? ICON_COMPONENTS[symbol]
: ICON_COMPONENTS.Info;
return (
<button
type="button"
disabled={disabled}
className="disabled:pointer-events-none border-transparent 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 mx-1"
>
<IconComponent className="h-5 w-5 text-white" weight="fill" />
</button>
);
};

View file

@ -53,9 +53,11 @@ export default function SupportEmail() {
if (loading || !user?.role) return null;
return (
<form className="mb-6" onSubmit={updateSupportEmail}>
<div className="flex flex-col gap-y-2">
<h2 className="leading-tight font-medium text-white">Support Email</h2>
<p className="text-sm font-base text-white/60">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Support Email
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Set the support email address that shows up in the user menu while
logged into this instance.
</p>
@ -64,7 +66,7 @@ export default function SupportEmail() {
<input
name="supportEmail"
type="email"
className="bg-zinc-900 mt-4 text-white placeholder:text-white/20 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 max-w-[275px]"
className="bg-zinc-900 mt-3 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 max-w-[275px] placeholder:text-white/20"
placeholder="support@mycompany.com"
required={true}
autoComplete="off"

View file

@ -11,16 +11,16 @@ export default function Appearance() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
Appearance
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Customize the appearance settings of your platform.
</p>
</div>

View file

@ -7,7 +7,7 @@ import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow";
import showToast from "@/utils/toast";
import System from "@/models/system";
import { CaretDown } from "@phosphor-icons/react";
import { CaretDown, Download } from "@phosphor-icons/react";
import { saveAs } from "file-saver";
const exportOptions = {
@ -47,11 +47,9 @@ const exportOptions = {
export default function WorkspaceChats() {
const [showMenu, setShowMenu] = useState(false);
const [exportType, setExportType] = useState("jsonl");
const menuRef = useRef();
const openMenuButton = useRef();
const handleDumpChats = async () => {
const handleDumpChats = async (exportType) => {
const chats = await System.exportChats(exportType);
if (!!chats) {
const { name, mimeType, fileExtension, filenameFunc } =
@ -90,56 +88,48 @@ export default function WorkspaceChats() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Workspace Chats
</p>
<div className="flex gap-x-1 relative">
<button
onClick={handleDumpChats}
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"
>
Export as {exportOptions[exportType].name}
</button>
<div className="relative">
<button
ref={openMenuButton}
onClick={toggleMenu}
className={`transition-all duration-300 border border-slate-200 p-1 rounded-lg text-slate-200 text-sm items-center flex hover:bg-slate-200 hover:text-slate-800 ${
showMenu ? "bg-slate-200 text-slate-800" : ""
}`}
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<CaretDown weight="bold" className="h-4 w-4" />
<Download size={18} weight="bold" />
Export
<CaretDown size={18} weight="bold" />
</button>
<div
ref={menuRef}
className={`${
showMenu ? "slide-down" : "slide-up hidden"
} z-20 w-fit rounded-lg absolute top-full right-0 bg-sidebar p-4 flex items-center justify-center mt-2`}
} z-20 w-fit rounded-lg absolute top-full right-0 bg-[#2C2F36] mt-2 shadow-md`}
>
<div className="flex flex-col gap-y-2">
{Object.entries(exportOptions)
.filter(([type, _]) => type !== exportType)
.map(([key, data]) => (
<button
key={key}
onClick={() => {
setExportType(key);
setShowMenu(false);
}}
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
>
{data.name}
</button>
))}
<div className="py-2">
{Object.entries(exportOptions).map(([key, data]) => (
<button
key={key}
onClick={() => {
handleDumpChats(key);
setShowMenu(false);
}}
className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]"
>
{data.name}
</button>
))}
</div>
</div>
</div>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] 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>
@ -195,8 +185,8 @@ function ChatsContainer() {
return (
<>
<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">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Id
@ -228,7 +218,7 @@ function ChatsContainer() {
))}
</tbody>
</table>
<div className="flex w-full justify-between items-center">
<div className="flex w-full justify-between items-center mt-6">
<button
onClick={handlePrevious}
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"

View file

@ -67,19 +67,19 @@ export default function GithubConnectorSetup() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
<img src={image} alt="Github" className="rounded-lg h-16 w-16" />
<div className="w-full flex flex-col gap-y-1">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
Import GitHub Repository
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Import all files from a public or private Github repository
and have its files be available in your workspace.
</p>
@ -88,7 +88,7 @@ export default function GithubConnectorSetup() {
<form className="w-full" onSubmit={handleSubmit}>
{!accessToken && (
<div className="flex flex-col gap-y-1 py-4 ">
<div className="flex flex-col gap-y-1 py-4">
<div className="flex flex-col w-fit gap-y-2 bg-blue-600/20 rounded-lg px-4 py-2">
<div className="flex items-center gap-x-2">
<Info size={20} className="shrink-0 text-blue-400" />

View file

@ -48,19 +48,19 @@ export default function YouTubeTranscriptConnectorSetup() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
<img src={image} alt="YouTube" className="rounded-lg h-16 w-16" />
<div className="w-full flex flex-col gap-y-1">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
Import YouTube transcription
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
From a youtube link, import the entire transcript of that
video for embedding.
</p>

View file

@ -9,26 +9,31 @@ export default function DataConnectors() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
Data Connectors
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Verified data connectors allow you to add more content to your
AnythingLLM workspaces with no custom code or complexity.
<br />
Guaranteed to work with your AnythingLLM instance.
</p>
</div>
<div className="py-4 w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-full">
<DataConnectorOption slug="github" />
<DataConnectorOption slug="youtube-transcript" />
<div className="text-sm font-medium text-white mt-6 mb-4">
Available Data Connectors
</div>
<div className="w-full">
<div className="py-4 w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-full">
<DataConnectorOption slug="github" />
<DataConnectorOption slug="youtube-transcript" />
</div>
</div>
</div>
</div>

View file

@ -14,14 +14,16 @@ export default function EmbedChats() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">Embed Chats</p>
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Embed Chats
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the recorded chats and messages from any embed that
you have published.
</p>

View file

@ -12,27 +12,28 @@ import Embed from "@/models/embed";
export default function EmbedConfigs() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">
<p className="text-lg leading-6 font-bold text-white">
Embeddable Chat Widgets
</p>
<button
onClick={openModal}
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"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<CodeBlock className="h-4 w-4" /> Create embed
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Embeddable chat widgets are public facing chat interfaces that are
tied to a single workspace. These allow you to build workspaces
that then you can publish to the world.
@ -51,6 +52,7 @@ export default function EmbedConfigs() {
function EmbedContainer() {
const [loading, setLoading] = useState(true);
const [embeds, setEmbeds] = useState([]);
useEffect(() => {
async function fetchUsers() {
const _embeds = await Embed.embeds();
@ -75,8 +77,8 @@ function EmbedContainer() {
}
return (
<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">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Workspace

View file

@ -128,18 +128,11 @@ export default function GeneralEmbeddingPreference() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
<Sidebar />
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient p-[18px] h-full overflow-y-scroll animate-pulse border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
@ -148,30 +141,30 @@ export default function GeneralEmbeddingPreference() {
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form
id="embedding-form"
onSubmit={handleSubmit}
className="flex w-full"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Embedding 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"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
When using an LLM that does not natively support an embedding
engine - you may need to additionally specify credentials to
for embedding text.
@ -181,63 +174,67 @@ export default function GeneralEmbeddingPreference() {
format which AnythingLLM can use to process.
</p>
</div>
<>
<div className="text-white text-sm font-medium py-4">
Embedding Providers
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0 z-20">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search Embedding providers"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
</div>
</div>
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredEmbedders.map((embedder) => {
return (
<EmbedderItem
key={embedder.name}
name={embedder.name}
value={embedder.value}
image={embedder.logo}
description={embedder.description}
checked={selectedEmbedder === embedder.value}
onClick={() => updateChoice(embedder.value)}
/>
);
})}
<div className="text-sm font-medium text-white mt-6 mb-4">
Embedding Providers
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search Embedding providers"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
</div>
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedEmbedder &&
EMBEDDERS.find(
(embedder) => embedder.value === selectedEmbedder
)?.options}
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredEmbedders.map((embedder) => {
return (
<EmbedderItem
key={embedder.name}
name={embedder.name}
value={embedder.value}
image={embedder.logo}
description={embedder.description}
checked={selectedEmbedder === embedder.value}
onClick={() => updateChoice(embedder.value)}
/>
);
})}
</div>
</div>
</>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedEmbedder &&
EMBEDDERS.find(
(embedder) => embedder.value === selectedEmbedder
)?.options}
</div>
</div>
</div>
</form>
</div>
)}
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
</div>
);
}

View file

@ -199,7 +199,7 @@ export default function GeneralLLMPreference() {
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient p-[18px] h-full overflow-y-scroll animate-pulse border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
@ -208,33 +208,33 @@ export default function GeneralLLMPreference() {
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form onSubmit={handleSubmit} className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold 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"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] 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">
<div className="text-sm font-medium text-white mt-6 mb-4">
LLM Providers
</div>
<div className="w-full">

View file

@ -154,18 +154,11 @@ export default function GeneralVectorDatabase() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
<Sidebar />
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline animate-pulse"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
@ -174,42 +167,42 @@ export default function GeneralVectorDatabase() {
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form
id="vectordb-form"
onSubmit={handleSubmit}
className="flex w-full"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 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">
<div className="flex items-center gap-x-4">
<p className="text-lg leading-6 font-bold 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"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] 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 className="text-sm font-medium text-white mt-6 mb-4">
Vector Database Providers
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0 z-20">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
@ -257,6 +250,13 @@ export default function GeneralVectorDatabase() {
</form>
</div>
)}
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
</div>
);
}